mirror of
https://github.com/mempool/mempool.git
synced 2025-01-19 05:34:03 +01:00
refactor unfurler routing & add fallback imgs
This commit is contained in:
parent
2979f286cf
commit
a5db46240e
BIN
frontend/src/resources/previews/dashboard.png
Normal file
BIN
frontend/src/resources/previews/dashboard.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
BIN
frontend/src/resources/previews/lightning.png
Normal file
BIN
frontend/src/resources/previews/lightning.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 288 KiB |
BIN
frontend/src/resources/previews/mining.png
Normal file
BIN
frontend/src/resources/previews/mining.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 112 KiB |
4
unfurler/package-lock.json
generated
4
unfurler/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mempool-unfurl",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mempool-unfurl",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@types/node": "^16.11.41",
|
||||
"express": "^4.18.0",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mempool-unfurl",
|
||||
"version": "0.0.2",
|
||||
"version": "0.1.0",
|
||||
"description": "Renderer for mempool open graph link preview images",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -1,10 +1,12 @@
|
||||
import express from "express";
|
||||
import { Application, Request, Response, NextFunction } from 'express';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import config from './config';
|
||||
import { Cluster } from 'puppeteer-cluster';
|
||||
import ReusablePage from './concurrency/ReusablePage';
|
||||
import { parseLanguageUrl } from './language/lang';
|
||||
import { matchRoute } from './routes';
|
||||
const puppeteerConfig = require('../puppeteer.config.json');
|
||||
|
||||
if (config.PUPPETEER.EXEC_PATH) {
|
||||
@ -17,13 +19,13 @@ class Server {
|
||||
cluster?: Cluster;
|
||||
mempoolHost: string;
|
||||
network: string;
|
||||
defaultImageUrl: string;
|
||||
secureHost = true;
|
||||
|
||||
constructor() {
|
||||
this.app = express();
|
||||
this.mempoolHost = config.MEMPOOL.HTTP_HOST + (config.MEMPOOL.HTTP_PORT ? ':' + config.MEMPOOL.HTTP_PORT : '');
|
||||
this.secureHost = this.mempoolHost.startsWith('https');
|
||||
this.network = config.MEMPOOL.NETWORK || 'bitcoin';
|
||||
this.defaultImageUrl = this.getDefaultImageUrl();
|
||||
this.startServer();
|
||||
}
|
||||
|
||||
@ -113,11 +115,25 @@ class Server {
|
||||
|
||||
async renderPreview(req, res) {
|
||||
try {
|
||||
const path = req.params[0]
|
||||
const img = await this.cluster?.execute({ url: this.mempoolHost + path, path: path, action: 'screenshot' });
|
||||
const rawPath = req.params[0];
|
||||
|
||||
let img = null;
|
||||
|
||||
const { lang, path } = parseLanguageUrl(rawPath);
|
||||
const matchedRoute = matchRoute(this.network, path);
|
||||
|
||||
// don't bother unless the route is definitely renderable
|
||||
if (rawPath.includes('/preview/') && matchedRoute.render) {
|
||||
img = await this.cluster?.execute({ url: this.mempoolHost + rawPath, path: rawPath, action: 'screenshot' });
|
||||
}
|
||||
|
||||
if (!img) {
|
||||
res.status(500).send('failed to render page preview');
|
||||
// proxy fallback image from the frontend
|
||||
if (this.secureHost) {
|
||||
https.get(this.mempoolHost + matchedRoute.fallbackImg, (got) => got.pipe(res));
|
||||
} else {
|
||||
http.get(this.mempoolHost + matchedRoute.fallbackImg, (got) => got.pipe(res));
|
||||
}
|
||||
} else {
|
||||
res.contentType('image/png');
|
||||
res.send(img);
|
||||
@ -137,50 +153,14 @@ class Server {
|
||||
return;
|
||||
}
|
||||
|
||||
let previewSupported = true;
|
||||
let mode = 'mainnet'
|
||||
let ogImageUrl = this.defaultImageUrl;
|
||||
let ogTitle;
|
||||
const { lang, path } = parseLanguageUrl(rawPath);
|
||||
const parts = path.slice(1).split('/');
|
||||
const matchedRoute = matchRoute(this.network, path);
|
||||
let ogImageUrl = this.mempoolHost + (matchedRoute.staticImg || matchedRoute.fallbackImg);
|
||||
let ogTitle = 'The Mempool Open Source Project™';
|
||||
|
||||
// handle network mode modifiers
|
||||
if (['testnet', 'signet'].includes(parts[0])) {
|
||||
mode = parts.shift();
|
||||
}
|
||||
|
||||
// handle supported preview routes
|
||||
switch (parts[0]) {
|
||||
case 'block':
|
||||
ogTitle = `Block: ${parts[1]}`;
|
||||
break;
|
||||
case 'address':
|
||||
ogTitle = `Address: ${parts[1]}`;
|
||||
break;
|
||||
case 'tx':
|
||||
ogTitle = `Transaction: ${parts[1]}`;
|
||||
break;
|
||||
case 'lightning':
|
||||
switch (parts[1]) {
|
||||
case 'node':
|
||||
ogTitle = `Lightning Node: ${parts[2]}`;
|
||||
break;
|
||||
case 'channel':
|
||||
ogTitle = `Lightning Channel: ${parts[2]}`;
|
||||
break;
|
||||
default:
|
||||
previewSupported = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
previewSupported = false;
|
||||
}
|
||||
|
||||
if (previewSupported) {
|
||||
if (matchedRoute.render) {
|
||||
ogImageUrl = `${config.SERVER.HOST}/render/${lang || 'en'}/preview${path}`;
|
||||
ogTitle = `${this.network ? capitalize(this.network) + ' ' : ''}${mode !== 'mainnet' ? capitalize(mode) + ' ' : ''}${ogTitle}`;
|
||||
} else {
|
||||
ogTitle = 'The Mempool Open Source Project™';
|
||||
ogTitle = `${this.network ? capitalize(this.network) + ' ' : ''}${matchedRoute.networkMode !== 'mainnet' ? capitalize(matchedRoute.networkMode) + ' ' : ''}${matchedRoute.title}`;
|
||||
}
|
||||
|
||||
res.send(`
|
||||
@ -192,8 +172,8 @@ class Server {
|
||||
<meta name="description" content="The Mempool Open Source Project™ - our self-hosted explorer for the ${capitalize(this.network)} community."/>
|
||||
<meta property="og:image" content="${ogImageUrl}"/>
|
||||
<meta property="og:image:type" content="image/png"/>
|
||||
<meta property="og:image:width" content="${previewSupported ? 1200 : 1000}"/>
|
||||
<meta property="og:image:height" content="${previewSupported ? 600 : 500}"/>
|
||||
<meta property="og:image:width" content="${matchedRoute.render ? 1200 : 1000}"/>
|
||||
<meta property="og:image:height" content="${matchedRoute.render ? 600 : 500}"/>
|
||||
<meta property="og:title" content="${ogTitle}">
|
||||
<meta property="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:site" content="@mempool">
|
||||
@ -206,17 +186,6 @@ class Server {
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
|
||||
getDefaultImageUrl() {
|
||||
switch (this.network) {
|
||||
case 'liquid':
|
||||
return this.mempoolHost + '/resources/liquid/liquid-network-preview.png';
|
||||
case 'bisq':
|
||||
return this.mempoolHost + '/resources/bisq/bisq-markets-preview.png';
|
||||
default:
|
||||
return this.mempoolHost + '/resources/mempool-space-preview.png';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const server = new Server();
|
||||
|
124
unfurler/src/routes.ts
Normal file
124
unfurler/src/routes.ts
Normal file
@ -0,0 +1,124 @@
|
||||
interface Match {
|
||||
render: boolean;
|
||||
title: string;
|
||||
fallbackImg: string;
|
||||
staticImg?: string;
|
||||
networkMode: string;
|
||||
}
|
||||
|
||||
const routes = {
|
||||
block: {
|
||||
render: true,
|
||||
params: 1,
|
||||
getTitle(path) {
|
||||
return `Block: ${path[0]}`;
|
||||
}
|
||||
},
|
||||
address: {
|
||||
render: true,
|
||||
params: 1,
|
||||
getTitle(path) {
|
||||
return `Address: ${path[0]}`;
|
||||
}
|
||||
},
|
||||
tx: {
|
||||
render: true,
|
||||
params: 1,
|
||||
getTitle(path) {
|
||||
return `Transaction: ${path[0]}`;
|
||||
}
|
||||
},
|
||||
lightning: {
|
||||
title: "Lightning",
|
||||
fallbackImg: '/resources/previews/lightning.png',
|
||||
routes: {
|
||||
node: {
|
||||
render: true,
|
||||
params: 1,
|
||||
getTitle(path) {
|
||||
return `Lightning Node: ${path[0]}`;
|
||||
}
|
||||
},
|
||||
channel: {
|
||||
render: true,
|
||||
params: 1,
|
||||
getTitle(path) {
|
||||
return `Lightning Channel: ${path[0]}`;
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
mining: {
|
||||
title: "Mining",
|
||||
fallbackImg: '/resources/previews/mining.png'
|
||||
}
|
||||
};
|
||||
|
||||
const networks = {
|
||||
bitcoin: {
|
||||
fallbackImg: '/resources/mempool-space-preview.png',
|
||||
staticImg: '/resources/previews/dashboard.png',
|
||||
routes: {
|
||||
...routes // all routes supported
|
||||
}
|
||||
},
|
||||
liquid: {
|
||||
fallbackImg: '/resources/liquid/liquid-network-preview.png',
|
||||
routes: { // only block, address & tx routes supported
|
||||
block: routes.block,
|
||||
address: routes.address,
|
||||
tx: routes.tx
|
||||
}
|
||||
},
|
||||
bisq: {
|
||||
fallbackImg: '/resources/bisq/bisq-markets-preview.png',
|
||||
routes: {} // no routes supported
|
||||
}
|
||||
};
|
||||
|
||||
export function matchRoute(network: string, path: string): Match {
|
||||
const match: Match = {
|
||||
render: false,
|
||||
title: '',
|
||||
fallbackImg: '',
|
||||
networkMode: 'mainnet'
|
||||
}
|
||||
|
||||
const parts = path.slice(1).split('/').filter(p => p.length);
|
||||
|
||||
if (parts[0] === 'preview') {
|
||||
parts.shift();
|
||||
}
|
||||
if (['testnet', 'signet'].includes(parts[0])) {
|
||||
match.networkMode = parts.shift() || 'mainnet';
|
||||
}
|
||||
|
||||
let route = networks[network] || networks.bitcoin;
|
||||
match.fallbackImg = route.fallbackImg;
|
||||
|
||||
// traverse the route tree until we run out of route or tree, or hit a renderable match
|
||||
while (!route.render && route.routes && parts.length && route.routes[parts[0]]) {
|
||||
route = route.routes[parts[0]];
|
||||
parts.shift();
|
||||
if (route.fallbackImg) {
|
||||
match.fallbackImg = route.fallbackImg;
|
||||
}
|
||||
}
|
||||
|
||||
// enough route parts left for title & rendering
|
||||
if (route.render && parts.length >= route.params) {
|
||||
match.render = true;
|
||||
}
|
||||
// only use set a static image for exact matches
|
||||
if (!parts.length && route.staticImg) {
|
||||
match.staticImg = route.staticImg;
|
||||
}
|
||||
// apply the title function if present
|
||||
if (route.getTitle && typeof route.getTitle === 'function') {
|
||||
match.title = route.getTitle(parts);
|
||||
} else {
|
||||
match.title = route.title;
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
Loading…
Reference in New Issue
Block a user