Compare commits
3 Commits
master
...
mononaut/a
Author | SHA1 | Date | |
---|---|---|---|
|
bd300578b6 | ||
|
9fc3a9130b | ||
|
601f0ea671 |
@ -7,6 +7,7 @@ class ServicesRoutes {
|
|||||||
public initRoutes(app: Application): void {
|
public initRoutes(app: Application): void {
|
||||||
app
|
app
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'wallet/:walletId', this.$getWallet)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'wallet/:walletId', this.$getWallet)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'services/custom/config', this.$getCustomConfig)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,6 +23,11 @@ class ServicesRoutes {
|
|||||||
handleError(req, res, 500, 'Failed to get wallet');
|
handleError(req, res, 500, 'Failed to get wallet');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serve a blank custom config file by default
|
||||||
|
private async $getCustomConfig(req: Request, res: Response): Promise<void> {
|
||||||
|
res.status(200).contentType('application/javascript').send('');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ServicesRoutes();
|
export default new ServicesRoutes();
|
||||||
|
@ -2,7 +2,7 @@ import { DOCUMENT } from '@angular/common';
|
|||||||
import { Inject, Injectable } from '@angular/core';
|
import { Inject, Injectable } from '@angular/core';
|
||||||
import { ApiService } from '@app/services/api.service';
|
import { ApiService } from '@app/services/api.service';
|
||||||
import { SeoService } from '@app/services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { StateService } from '@app/services/state.service';
|
import { Customization, StateService } from '@app/services/state.service';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
@ -51,6 +51,8 @@ export class EnterpriseService {
|
|||||||
if (this.stateService.env.customize?.branding) {
|
if (this.stateService.env.customize?.branding) {
|
||||||
const info = this.stateService.env.customize?.branding;
|
const info = this.stateService.env.customize?.branding;
|
||||||
this.insertMatomo(info.site_id);
|
this.insertMatomo(info.site_id);
|
||||||
|
this.setFavicons(this.stateService.env.customize);
|
||||||
|
this.seoService.setCustomMeta(this.stateService.env.customize);
|
||||||
this.seoService.setEnterpriseTitle(info.title, true);
|
this.seoService.setEnterpriseTitle(info.title, true);
|
||||||
this.info$.next(info);
|
this.info$.next(info);
|
||||||
} else {
|
} else {
|
||||||
@ -67,6 +69,50 @@ export class EnterpriseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setFavicons(customize: Customization): void {
|
||||||
|
const enterprise = customize.enterprise;
|
||||||
|
const head = this.document.getElementsByTagName('head')[0];
|
||||||
|
|
||||||
|
const faviconLinks = [
|
||||||
|
{
|
||||||
|
rel: 'apple-touch-icon',
|
||||||
|
sizes: '180x180',
|
||||||
|
href: `/resources/${enterprise}/favicons/apple-touch-icon.png`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: 'icon',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '32x32',
|
||||||
|
href: `/resources/${enterprise}/favicons/favicon-32x32.png`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: 'icon',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '16x16',
|
||||||
|
href: `/resources/${enterprise}/favicons/favicon-16x16.png`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: 'manifest',
|
||||||
|
href: `/resources/${enterprise}/favicons/site.webmanifest`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: 'shortcut icon',
|
||||||
|
href: `/resources/${enterprise}/favicons/favicon.ico`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
faviconLinks.forEach(linkInfo => {
|
||||||
|
let link = this.document.querySelector(`link[rel="${linkInfo.rel}"]${linkInfo.sizes ? `[sizes="${linkInfo.sizes}"]` : ''}`) as HTMLLinkElement;
|
||||||
|
if (!link) {
|
||||||
|
link = this.document.createElement('link');
|
||||||
|
head.appendChild(link);
|
||||||
|
}
|
||||||
|
Object.entries(linkInfo).forEach(([attr, value]) => {
|
||||||
|
link.setAttribute(attr, value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
insertMatomo(siteId?: number): void {
|
insertMatomo(siteId?: number): void {
|
||||||
let statsUrl = '//stats.mempool.space/';
|
let statsUrl = '//stats.mempool.space/';
|
||||||
|
|
||||||
|
@ -12,6 +12,9 @@ import { LanguageService } from '@app/services/language.service';
|
|||||||
export class OpenGraphService {
|
export class OpenGraphService {
|
||||||
network = '';
|
network = '';
|
||||||
defaultImageUrl = '';
|
defaultImageUrl = '';
|
||||||
|
defaultImageType = 'image/png';
|
||||||
|
defaultImageWidth = '1000';
|
||||||
|
defaultImageHeight = '500';
|
||||||
previewLoadingEvents = {};
|
previewLoadingEvents = {};
|
||||||
previewLoadingCount = 0;
|
previewLoadingCount = 0;
|
||||||
|
|
||||||
@ -25,12 +28,17 @@ export class OpenGraphService {
|
|||||||
) {
|
) {
|
||||||
// save og:image tag from original template
|
// save og:image tag from original template
|
||||||
const initialOgImageTag = metaService.getTag("property='og:image'");
|
const initialOgImageTag = metaService.getTag("property='og:image'");
|
||||||
this.defaultImageUrl = initialOgImageTag?.content || 'https://mempool.space/resources/previews/mempool-space-preview.jpg';
|
this.defaultImageUrl = (this.stateService.env.customize?.meta?.image?.src ? this.stateService.env.customize.meta.image.src : initialOgImageTag?.content) || 'https://mempool.space/resources/previews/mempool-space-preview.jpg';
|
||||||
|
this.defaultImageType = (this.stateService.env.customize?.meta?.image?.type ? this.stateService.env.customize.meta.image.type : 'image/png');
|
||||||
|
this.defaultImageWidth = (this.stateService.env.customize?.meta?.image?.width ? this.stateService.env.customize.meta.image.width : '1000');
|
||||||
|
this.defaultImageHeight = (this.stateService.env.customize?.meta?.image?.height ? this.stateService.env.customize.meta.image.height : '500');
|
||||||
this.router.events.pipe(
|
this.router.events.pipe(
|
||||||
filter(event => event instanceof NavigationEnd),
|
filter(event => event instanceof NavigationEnd),
|
||||||
map(() => this.activatedRoute),
|
map(() => this.activatedRoute),
|
||||||
map(route => {
|
map(route => {
|
||||||
while (route.firstChild) route = route.firstChild;
|
while (route.firstChild) {
|
||||||
|
route = route.firstChild;
|
||||||
|
}
|
||||||
return route;
|
return route;
|
||||||
}),
|
}),
|
||||||
filter(route => route.outlet === 'primary'),
|
filter(route => route.outlet === 'primary'),
|
||||||
@ -45,7 +53,7 @@ export class OpenGraphService {
|
|||||||
|
|
||||||
// expose routing method to global scope, so we can access it from the unfurler
|
// expose routing method to global scope, so we can access it from the unfurler
|
||||||
window['ogService'] = {
|
window['ogService'] = {
|
||||||
loadPage: (path) => { return this.loadPage(path) }
|
loadPage: (path) => { return this.loadPage(path); }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,9 +70,9 @@ export class OpenGraphService {
|
|||||||
clearOgImage() {
|
clearOgImage() {
|
||||||
this.metaService.updateTag({ property: 'og:image', content: this.defaultImageUrl });
|
this.metaService.updateTag({ property: 'og:image', content: this.defaultImageUrl });
|
||||||
this.metaService.updateTag({ name: 'twitter:image', content: this.defaultImageUrl });
|
this.metaService.updateTag({ name: 'twitter:image', content: this.defaultImageUrl });
|
||||||
this.metaService.updateTag({ property: 'og:image:type', content: 'image/png' });
|
this.metaService.updateTag({ property: 'og:image:type', content: this.defaultImageType });
|
||||||
this.metaService.updateTag({ property: 'og:image:width', content: '1000' });
|
this.metaService.updateTag({ property: 'og:image:width', content: this.defaultImageWidth });
|
||||||
this.metaService.updateTag({ property: 'og:image:height', content: '500' });
|
this.metaService.updateTag({ property: 'og:image:height', content: this.defaultImageHeight });
|
||||||
}
|
}
|
||||||
|
|
||||||
setManualOgImage(imageFilename) {
|
setManualOgImage(imageFilename) {
|
||||||
|
@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { Title, Meta } from '@angular/platform-browser';
|
import { Title, Meta } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||||
import { filter, map, switchMap } from 'rxjs';
|
import { filter, map, switchMap } from 'rxjs';
|
||||||
import { StateService } from '@app/services/state.service';
|
import { Customization, StateService } from '@app/services/state.service';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -23,13 +23,17 @@ export class SeoService {
|
|||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
) {
|
) {
|
||||||
// save original meta tags
|
// save original meta tags
|
||||||
this.baseDescription = metaService.getTag('name=\'description\'')?.content || this.baseDescription;
|
this.baseDescription = this.stateService.env.customize?.meta?.description || metaService.getTag('name=\'description\'')?.content || this.baseDescription;
|
||||||
this.baseTitle = titleService.getTitle()?.split(' - ')?.[0] || this.baseTitle;
|
this.baseTitle = this.stateService.env.customize?.meta?.title || titleService.getTitle()?.split(' - ')?.[0] || this.baseTitle;
|
||||||
try {
|
if (this.stateService.env.customize?.domains?.length) {
|
||||||
const canonicalUrl = new URL(this.canonicalLink?.href || '');
|
this.baseDomain = this.stateService.env.customize.domains[0];
|
||||||
this.baseDomain = canonicalUrl?.host;
|
} else {
|
||||||
} catch (e) {
|
try {
|
||||||
// leave as default
|
const canonicalUrl = new URL(this.canonicalLink?.href || '');
|
||||||
|
this.baseDomain = canonicalUrl?.host;
|
||||||
|
} catch (e) {
|
||||||
|
// leave as default
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||||
@ -70,6 +74,22 @@ export class SeoService {
|
|||||||
this.resetTitle();
|
this.resetTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCustomMeta(customize: Customization) {
|
||||||
|
if (!customize.meta) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.metaService.updateTag({ name: 'description', content: customize.meta.description});
|
||||||
|
this.metaService.updateTag({ name: 'twitter:description', content: customize.meta.description});
|
||||||
|
this.metaService.updateTag({ property: 'og:description', content: customize.meta.description});
|
||||||
|
this.metaService.updateTag({ name: 'twitter:image', content: customize.meta.image.src});
|
||||||
|
this.metaService.updateTag({ property: 'og:image', content: customize.meta.image.src});
|
||||||
|
this.metaService.updateTag({ property: 'og:image:type', content: customize.meta.image.type});
|
||||||
|
this.metaService.updateTag({ property: 'og:image:width', content: customize.meta.image.width});
|
||||||
|
this.metaService.updateTag({ property: 'og:image:height', content: customize.meta.image.height});
|
||||||
|
const domain = customize.domains?.[0] || window.location.hostname;
|
||||||
|
this.metaService.updateTag({ name: 'twitter:domain', content: domain});
|
||||||
|
}
|
||||||
|
|
||||||
setDescription(newDescription: string): void {
|
setDescription(newDescription: string): void {
|
||||||
this.metaService.updateTag({ name: 'description', content: newDescription});
|
this.metaService.updateTag({ name: 'description', content: newDescription});
|
||||||
this.metaService.updateTag({ name: 'twitter:description', content: newDescription});
|
this.metaService.updateTag({ name: 'twitter:description', content: newDescription});
|
||||||
|
@ -22,6 +22,7 @@ export interface MarkBlockState {
|
|||||||
export interface ILoadingIndicators { [name: string]: number; }
|
export interface ILoadingIndicators { [name: string]: number; }
|
||||||
|
|
||||||
export interface Customization {
|
export interface Customization {
|
||||||
|
domains: string[];
|
||||||
theme: string;
|
theme: string;
|
||||||
enterprise?: string;
|
enterprise?: string;
|
||||||
branding: {
|
branding: {
|
||||||
@ -33,6 +34,16 @@ export interface Customization {
|
|||||||
footer_img?: string;
|
footer_img?: string;
|
||||||
rounded_corner: boolean;
|
rounded_corner: boolean;
|
||||||
},
|
},
|
||||||
|
meta: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
image: {
|
||||||
|
src: string;
|
||||||
|
type: string;
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
dashboard: {
|
dashboard: {
|
||||||
widgets: {
|
widgets: {
|
||||||
component: string;
|
component: string;
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>mempool - Bitcoin Explorer</title>
|
<title>mempool - Bitcoin Explorer</title>
|
||||||
<script src="/resources/config.js"></script>
|
<script src="/resources/config.js"></script>
|
||||||
<script src="/resources/customize.js"></script>
|
<script src="/api/v1/services/custom/config"></script>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
|
|
||||||
<meta name="description" content="Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See the real-time status of your transactions, get network info, and more." />
|
<meta name="description" content="Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See the real-time status of your transactions, get network info, and more." />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user