diff --git a/frontend/src/index.mempool.html b/frontend/src/index.mempool.html
index ed5f7e0b4..14dc8dcc5 100644
--- a/frontend/src/index.mempool.html
+++ b/frontend/src/index.mempool.html
@@ -3,11 +3,21 @@
+
+
+
mempool - Bitcoin Explorer
+
+
+
+
+
+
+
@@ -21,19 +31,21 @@
+
+
-
+
diff --git a/production/mempool-build-custom.js b/production/mempool-build-custom.js
new file mode 100644
index 000000000..c7e3def89
--- /dev/null
+++ b/production/mempool-build-custom.js
@@ -0,0 +1,128 @@
+import fs from 'fs';
+
+const defaultConfig = {
+ "domains": ["mempool.space"],
+ "theme": "default",
+ "enterprise": "mempool",
+ "branding": {
+ "name": "mempool",
+ "title": "mempool",
+ "site_id": 5,
+ },
+ "meta": {
+ "title": "mempool - Bitcoin Explorer",
+ "description": "Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See the real-time status of your transactions, get network info, and more.",
+ },
+ "unfurls": {
+ "preview": {
+ "src": "https://mempool.space/resources/previews/mempool-space-preview.jpg",
+ "type": "image/jpeg",
+ "width": "2000",
+ "height": "1000"
+ }
+ }
+}
+
+function deepMerge(target, source) {
+ for (const key in source) {
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
+ target[key] = target[key] || {};
+ deepMerge(target[key], source[key]);
+ } else {
+ target[key] = source[key];
+ }
+ }
+ return target;
+}
+
+function addDefaultsToConfig(config) {
+ return deepMerge(structuredClone(defaultConfig), config);
+}
+
+function substitute(indexhtml, config) {
+ let newhtml = indexhtml;
+ // substitute title
+ newhtml = newhtml.replace(/\<\!\-\- TITLE \-\-\>.*\<\!\-\- END TITLE \-\-\>/gis, `${config.meta.title}`);
+
+ // substitute customization script
+ newhtml = newhtml.replace(/\<\!\-\- CUSTOMIZATION \-\-\>.*\<\!\-\- END CUSTOMIZATION \-\-\>/gis, ``);
+
+ // substitute meta tags
+ newhtml = newhtml.replace(/\<\!\-\- META \-\-\>.*\<\!\-\- END META \-\-\>/gis, `
+
+
+
+
+
+
+
+
+
+
+
+ `);
+
+
+ // substitute favicons
+ newhtml = newhtml.replace(/\<\!\-\- FAVICONS -->.*\<\!\-\- END FAVICONS \-\-\>/gis, `
+
+
+
+
+
+
+
+
+
+ `);
+
+ return newhtml;
+}
+
+async function run() {
+ const servicesHost = process.argv[2] || 'http://localhost:9000';
+ const mempoolDir = process.argv[3] || '../';
+
+ console.log('fetching list of custom builds');
+ const customBuilds = await (await fetch(`${servicesHost}/api/v1/services/custom/list`)).json();
+
+ // fetch config for each custom build from `$SERVICES/api/v1/services/custom/config/`
+ const customConfigs = await Promise.all(customBuilds.map(async (build) => {
+ console.log(`fetching config for ${build} `);
+ return addDefaultsToConfig(await (await fetch(`${servicesHost}/api/v1/services/custom/config/${build}`)).json());
+ }));
+
+
+ // for each custom build config:
+ let i = 0;
+ for (const config of customConfigs) {
+ console.log(`generating ${config.enterprise} build (${i + 1}/${customConfigs.length})`);
+ const browserDir = `${mempoolDir}/frontend/dist/mempool/browser`;
+ const locales = fs.readdirSync(browserDir)
+ .filter(file => fs.statSync(`${browserDir}/${file}`).isDirectory())
+ .filter(file => fs.existsSync(`${browserDir}/${file}/index.html`));
+
+ // Process each locale's index.html
+ for (const locale of locales) {
+ const indexPath = `${browserDir}/${locale}/index.html`;
+ const indexContent = fs.readFileSync(indexPath, 'utf-8').toString();
+ const processedHtml = substitute(indexContent, config);
+
+ // Save processed HTML
+ const outputPath = `${browserDir}/${locale}/index.${config.enterprise}.html`;
+ fs.writeFileSync(outputPath, processedHtml);
+ console.log(`updated index.${config.enterprise}.html for locale ${locale}`);
+ }
+
+ console.log(`finished generating ${config.enterprise} build`);
+ i++;
+ }
+
+ console.log('finished updating custom builds');
+}
+
+run();