File size: 17,460 Bytes
97ee7cb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 | #!/usr/bin/env node
/**
* Postbuild prerender script — injects critical CSS and the user-visible
* server-rendered welcome app into the built HTML.
*/
import { readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createServer } from 'vite';
const __dirname = dirname(fileURLToPath(import.meta.url));
const STATIC_SCRIPT_NONCE = 'wm-static-bootstrap';
// Single source of truth for the brand's cross-site entity links + Organization
// structured data, injected into BOTH pro pages (see PAGES) so the homepage and
// /pro can't drift. Nonce'd to match the static-bootstrap CSP trust (otherwise
// deploy-config.test.mjs would demand a script-src sha256 hash for it).
const WM_SAMEAS = [
'https://github.com/koala73/worldmonitor',
'https://www.npmjs.com/package/worldmonitor',
'https://x.com/worldmonitorai',
'https://x.com/eliehabib',
'https://discord.gg/re63kWKxaz',
'https://www.wired.com/story/world-monitor-elie-habib/',
];
const ORGANIZATION_JSONLD = ` <script type="application/ld+json" nonce="${STATIC_SCRIPT_NONCE}">${JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'World Monitor',
alternateName: 'WorldMonitor',
url: 'https://www.worldmonitor.app/',
logo: 'https://www.worldmonitor.app/favico/apple-touch-icon.png',
description: 'Open-source real-time global intelligence platform aggregating conflicts, military movements, markets, infrastructure, and geopolitical data. Used by 2M+ people across 190+ countries.',
founder: {
'@type': 'Person',
name: 'Elie Habib',
url: 'https://x.com/eliehabib',
sameAs: [
'https://x.com/eliehabib',
'https://github.com/koala73',
'https://www.linkedin.com/in/elie-habib-7047b931',
'https://www.wikidata.org/wiki/Q121365724',
'https://www.crunchbase.com/person/elie-habib-2',
],
},
sameAs: WM_SAMEAS,
contactPoint: {
'@type': 'ContactPoint',
contactType: 'customer support',
email: 'support@worldmonitor.app',
url: 'https://www.worldmonitor.app/pro',
availableLanguage: 'English',
},
address: {
'@type': 'PostalAddress',
addressLocality: 'Dubai',
addressCountry: 'AE',
},
})}</script>`;
const DASHBOARD_SCREENSHOT_BASENAME = 'worldmonitor-7-mar-2026';
const DASHBOARD_SCREENSHOT_ASSETS = [
{ filenamePrefix: DASHBOARD_SCREENSHOT_BASENAME, extension: '.jpg' },
{ filenamePrefix: DASHBOARD_SCREENSHOT_BASENAME + '-640', extension: '.avif' },
{ filenamePrefix: DASHBOARD_SCREENSHOT_BASENAME + '-960', extension: '.avif' },
{ filenamePrefix: DASHBOARD_SCREENSHOT_BASENAME + '-1280', extension: '.avif' },
{ filenamePrefix: DASHBOARD_SCREENSHOT_BASENAME + '-640', extension: '.webp' },
{ filenamePrefix: DASHBOARD_SCREENSHOT_BASENAME + '-960', extension: '.webp' },
{ filenamePrefix: DASHBOARD_SCREENSHOT_BASENAME + '-1280', extension: '.webp' },
];
// This inline critical CSS is UNLAYERED, so it wins the cascade over the
// full Tailwind stylesheet (which lives in @layer utilities) regardless of
// specificity/media -- even after the deferred sheet loads. That means any
// `hidden <bp>:<display>` reveal (e.g. `hidden lg:flex` / `hidden md:block`
// nav rows, `hidden sm:block`) must ALSO be re-shown here in the matching
// @media block, or the
// unlayered `nav .hidden`/`main .hidden` rules keep those elements hidden at
// ALL widths (regressed the pro/welcome desktop nav in #4603; see the
// responsive nav and `main [class~="sm:block"]` reveals below).
const CRITICAL_CSS = [
':root{--font-sans:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--font-display:system-ui,sans-serif;--color-wm-bg:#050505;--color-wm-card:#111;--color-wm-border:#222;--color-wm-green:#4ade80;--color-wm-blue:#60a5fa;--color-wm-text:#f3f4f6;--color-wm-muted:#9ca3af}',
'*,::before,::after{box-sizing:border-box;border:0 solid #222}html{background:#050505;color:#f3f4f6;-webkit-text-size-adjust:100%;tab-size:4}body{margin:0;background:#050505;color:#f3f4f6;font-family:var(--font-sans);line-height:1.5;-webkit-font-smoothing:antialiased}a{color:inherit;text-decoration:none}img,svg{display:block;vertical-align:middle}img{max-width:100%;height:auto}h1,h2,h3,p{margin:0}',
'#root,#root>div{min-height:100vh}.glass-panel{background:rgba(17,17,17,.7);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid #222}.text-glow{text-shadow:0 0 20px rgba(74,222,128,.3)}.border-glow{box-shadow:0 0 20px rgba(74,222,128,.1)}',
'nav{position:fixed;top:0;left:0;right:0;z-index:50;background:rgba(17,17,17,.7);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid #222;border-inline-width:0;border-bottom-width:0}nav>div{max-width:80rem;margin-inline:auto;padding-inline:1rem;height:4rem;display:flex;align-items:center;justify-content:space-between;gap:.75rem}nav a{display:flex;align-items:center;gap:.5rem}nav a[aria-label*="Launch"]{flex-shrink:0;background:#4ade80;color:#050505;padding:.5rem .75rem;border-radius:.25rem;font:700 .75rem/1 ui-monospace,SFMono-Regular,monospace;text-transform:uppercase;letter-spacing:.025em}nav .hidden{display:none}nav [class~=font-display]{font-family:var(--font-display);font-weight:700}nav [class~=text-wm-muted],main [class~=text-wm-muted]{color:#9ca3af}nav [class~=text-wm-green],main [class~=text-wm-green]{color:#4ade80}nav [class~=text-wm-blue]{color:#60a5fa}',
'main>section:first-child{position:relative;overflow:hidden;padding:7rem 1rem 4rem}main>section:first-child>div:first-child{position:absolute;inset:0;background:radial-gradient(circle at 50% 0%,rgba(74,222,128,.10) 0%,transparent 55%);pointer-events:none}main>section:first-child>div:nth-child(2){position:relative;z-index:10;max-width:64rem;margin-inline:auto;text-align:center}main h1{font-family:var(--font-display);font-weight:700;font-size:2.25rem;line-height:1.08;letter-spacing:-.025em}main p{margin:1.5rem auto 0;max-width:42rem;color:#9ca3af;font-size:1rem;line-height:1.5}',
'main [class~=relative]{position:relative}main [class~=absolute]{position:absolute}main [class~=inset-0]{inset:0}main [class~=z-10]{z-index:10}main [class~=pointer-events-none]{pointer-events:none}main [class~=flex]{display:flex}main [class~=inline-flex]{display:inline-flex}main [class~=grid]{display:grid}main [class~=block]{display:block}main .hidden{display:none}main [class~=items-center]{align-items:center}main [class~=items-stretch]{align-items:stretch}main [class~=justify-center]{justify-content:center}main [class~=justify-between]{justify-content:space-between}main [class~=flex-col]{flex-direction:column}main [class~=flex-wrap]{flex-wrap:wrap}main [class~=grid-cols-2]{grid-template-columns:repeat(2,minmax(0,1fr))}',
'main [class~=mx-auto]{margin-inline:auto}main [class~=mt-1]{margin-top:.25rem}main [class~=mt-3]{margin-top:.75rem}main [class~=mt-6]{margin-top:1.5rem}main [class~=mt-8]{margin-top:2rem}main [class~=mt-9]{margin-top:2.25rem}main [class~=mt-10]{margin-top:2.5rem}main [class~=mb-5]{margin-bottom:1.25rem}main [class~=gap-1]{gap:.25rem}main [class~=gap-2]{gap:.5rem}main [class~=gap-3]{gap:.75rem}main [class~=gap-4]{gap:1rem}main [class~=gap-x-6]{column-gap:1.5rem}main [class~=gap-y-3]{row-gap:.75rem}',
'main [class~=w-full]{width:100%}main [class~=max-w-full]{max-width:100%}main [class~=max-w-2xl]{max-width:42rem}main [class~=max-w-3xl]{max-width:48rem}main [class~=max-w-5xl]{max-width:64rem}main [class~=min-w-0]{min-width:0}main [class~=shrink-0]{flex-shrink:0}main [class~=overflow-hidden]{overflow:hidden}',
'main [class~=rounded-full]{border-radius:9999px}main [class~=rounded-sm]{border-radius:.25rem}main [class~=rounded-md]{border-radius:.375rem}main .border{border-style:solid;border-width:1px;border-color:#222}main .border-l{border-left-style:solid;border-left-width:1px}main .border-t{border-top-style:solid;border-top-width:1px}main .border-b{border-bottom-style:solid;border-bottom-width:1px}main [class~=bg-wm-card]{background:#111}main [class~=bg-wm-bg]{background:#050505}main [class~=bg-wm-green]{background:#4ade80;color:#050505}main [class~="bg-[#ff5f57]"]{background:#ff5f57}main [class~="bg-[#febc2e]"]{background:#febc2e}main [class~="bg-[#28c840]"]{background:#28c840}',
'main [class~=px-3]{padding-inline:.75rem}main [class~=px-4]{padding-inline:1rem}main [class~=px-5]{padding-inline:1.25rem}main [class~=py-1]{padding-block:.25rem}main [class~=py-2]{padding-block:.5rem}main [class~=py-3]{padding-block:.75rem}main [class~="py-3.5"]{padding-block:.875rem}main [class~=font-mono]{font-family:var(--font-mono)}main [class~=font-display]{font-family:var(--font-display)}main [class~=font-bold]{font-weight:700}main [class~=uppercase]{text-transform:uppercase}main [class~=text-center]{text-align:center}main [class~=text-left]{text-align:left}',
'main [class~=text-2xl]{font-size:1.5rem;line-height:1.33}main [class~=text-4xl]{font-size:2.25rem;line-height:1.11}main [class~=text-base]{font-size:1rem;line-height:1.5}main [class~=text-sm]{font-size:.875rem;line-height:1.25rem}main [class~=text-xs]{font-size:.75rem;line-height:1rem}main [class~="text-[9px]"]{font-size:9px}main [class~="text-[10px]"]{font-size:10px}main [class~="text-[11px]"]{font-size:11px}main [class~=leading-none]{line-height:1}main [class~=leading-relaxed]{line-height:1.625}main [class~=tracking-tight]{letter-spacing:-.025em}main [class~=tracking-wide]{letter-spacing:.025em}main [class~=tracking-wider]{letter-spacing:.05em}main [class~=tracking-widest]{letter-spacing:.1em}main [class~="tracking-[1px]"]{letter-spacing:1px}main [class~="tracking-[4px]"]{letter-spacing:4px}main [class~=break-words]{overflow-wrap:break-word}',
'main [class~=text-wm-bg]{color:#050505}main [class~=text-wm-border]{color:#222}main [class~=text-wm-muted]{color:#9ca3af}main [class~=text-wm-text]{color:#f3f4f6}main [class~=text-wm-blue]{color:#60a5fa}main [class~=opacity-50]{opacity:.5}main [class~=opacity-60]{opacity:.6}main [class~=backdrop-blur-sm]{backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}main picture{display:block}main picture img{display:block;width:100%}',
'main a[href*="welcome-hero"],main a[href*="moments"]{width:100%;justify-content:center;padding:.875rem 1.25rem;border-radius:.25rem;font:700 .875rem/1.25 var(--font-mono);letter-spacing:.025em;text-transform:uppercase}main a[href*="moments"]{background:transparent;color:#f3f4f6}',
'@media (min-width:640px){nav>div{padding-inline:1.5rem}main>section:first-child{padding-top:8rem;padding-inline:1.5rem}main h1{font-size:3rem;line-height:1.05}main [class~="sm:flex-row"]{flex-direction:row}main [class~="sm:items-center"]{align-items:center}main [class~="sm:w-auto"]{width:auto}main [class~="sm:grid-cols-4"]{grid-template-columns:repeat(4,minmax(0,1fr))}main [class~="sm:max-w-3xl"]{max-width:48rem}main [class~="sm:max-w-none"]{max-width:none}main [class~="sm:px-4"]{padding-inline:1rem}main [class~="sm:px-6"]{padding-inline:1.5rem}main [class~="sm:px-8"]{padding-inline:2rem}main [class~="sm:tracking-wider"]{letter-spacing:.05em}main [class~="sm:block"]{display:block}}',
'@media (min-width:768px){main h1{font-size:4.5rem}main p{font-size:1.125rem;line-height:1.75rem}main [class~="md:text-lg"]{font-size:1.125rem;line-height:1.75rem}nav [class~="md:block"]{display:block}}',
'@media (min-width:1024px){nav [class~="lg:flex"]{display:flex}nav [class~="lg:hidden"]{display:none}}',
].join('');
// Flip each deferred preload to a real stylesheet on load, on error (retry a
// failed fetch as a stylesheet), and after a timeout (covers browsers that
// ignore `rel=preload as=style` and never fire an event) -- without a timeout
// or error arm a failed/unsupported preload leaves JS users on critical-CSS
// only, with the full sheet never applied. Idempotent (guarded rel check) and
// CSP-safe (no inline onload; runs inside the nonce'd bootstrap script).
const DEFERRED_STYLES_SCRIPT = "(function(){var links=document.querySelectorAll('link[data-wm-deferred-style]');for(var i=0;i<links.length;i++){(function(l){function a(){if(l.rel!=='stylesheet'){l.rel='stylesheet';}}l.addEventListener('load',a,{once:true});l.addEventListener('error',a,{once:true});setTimeout(a,3000);})(links[i]);}})();";
function findStylesheetTags(html) {
return [...html.matchAll(/<link\b[^>]*\brel="stylesheet"[^>]*>/gi)]
.map((match) => match[0]);
}
function tagAttribute(tag, name) {
const marker = name + '="';
const start = tag.indexOf(marker);
if (start === -1) return '';
const valueStart = start + marker.length;
const valueEnd = tag.indexOf('"', valueStart);
return valueEnd === -1 ? '' : tag.slice(valueStart, valueEnd);
}
function inlineCriticalCss(html, file) {
const stylesheetTags = findStylesheetTags(html);
if (stylesheetTags.length !== 1) {
console.error("[prerender] ERROR: Expected exactly one stylesheet tag for " + file + ", found " + stylesheetTags.length + ".");
process.exit(1);
}
const stylesheetTag = stylesheetTags[0];
const href = tagAttribute(stylesheetTag, 'href');
if (!href) {
console.error('[prerender] ERROR: Could not parse stylesheet href for ' + file + '.');
process.exit(1);
}
const crossorigin = stylesheetTag.includes(' crossorigin') ? ' crossorigin' : '';
const criticalTags = [
' <style nonce="' + STATIC_SCRIPT_NONCE + '">' + CRITICAL_CSS + '</style>',
' <link rel="preload" as="style" href="' + href + '"' + crossorigin + ' data-wm-deferred-style nonce="' + STATIC_SCRIPT_NONCE + '">',
' <script nonce="' + STATIC_SCRIPT_NONCE + '">' + DEFERRED_STYLES_SCRIPT + '</script>',
' <noscript><link rel="stylesheet" href="' + href + '"' + crossorigin + '></noscript>',
].join('\n');
return html.replace(stylesheetTag, criticalTags);
}
async function renderWelcomeRoot() {
const server = await createServer({
configFile: resolve(__dirname, 'vite.config.ts'),
appType: 'custom',
logLevel: 'error',
server: { hmr: false, middlewareMode: true },
});
try {
const { renderWelcomeApp } = await server.ssrLoadModule('/src/welcome-prerender.tsx');
return rewriteBuiltAssetUrls(await renderWelcomeApp());
} finally {
await server.close();
}
}
function builtAssetHref(filenamePrefix, extension) {
const assetsDir = resolve(__dirname, '../public/pro/assets');
const file = readdirSync(assetsDir).find((candidate) => (
candidate.startsWith(`${filenamePrefix}-`) && candidate.endsWith(extension)
));
if (!file) {
console.error(`[prerender] ERROR: Could not find built asset for ${filenamePrefix}${extension}`);
process.exit(1);
}
return `/pro/assets/${file}`;
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function rewriteBuiltAssetUrls(markup) {
let rewritten = markup;
for (const { filenamePrefix, extension } of DASHBOARD_SCREENSHOT_ASSETS) {
const builtHref = builtAssetHref(filenamePrefix, extension);
const sourceAssetPattern = new RegExp(
`(?:/pro/src/assets/|/@fs/[^"'<>\\s]*/)${escapeRegExp(filenamePrefix + extension)}`,
'g',
);
if (!rewritten.match(sourceAssetPattern)) {
console.error(`[prerender] ERROR: Could not find SSR asset URL for ${filenamePrefix}${extension} in welcome markup.`);
process.exit(1);
}
rewritten = rewritten.replace(sourceAssetPattern, builtHref);
}
// Catch any OTHER dev-only asset URL — a newly added asset import the rewrite
// map above doesn't cover would otherwise ship a broken /pro/src/assets or
// /@fs path into the static HTML and break hydration on the hashed client URL.
const leaked = rewritten.match(/(?:\/pro\/src\/assets\/|\/@fs\/)[^"'<>\s]+/);
if (leaked) {
console.error(`[prerender] ERROR: Unrewritten dev asset URL in welcome markup: ${leaked[0]}. Extend rewriteBuiltAssetUrls() to cover it.`);
process.exit(1);
}
return rewritten;
}
const welcomeContent = await renderWelcomeRoot();
const PAGES = [
{ file: 'index.html', content: '', rootAttributes: '' },
{
file: 'welcome.html',
content: welcomeContent,
rootAttributes: ' data-wm-prerendered="welcome" data-wm-prerender-lang="en"',
},
];
for (const { file, content, rootAttributes } of PAGES) {
// Fail loudly if server-rendered visible content resolved a missing locale key.
if (content.includes('undefined')) {
console.error(`[prerender] ERROR: Prerendered content for ${file} contains literal "undefined". Check the welcome SSR locale keys.`);
process.exit(1);
}
const htmlPath = resolve(__dirname, '../public/pro', file);
let html = readFileSync(htmlPath, 'utf-8');
html = inlineCriticalCss(html, file);
if (!html.includes('</head>')) {
console.error(`[prerender] ERROR: ${file} has no </head> to inject Organization JSON-LD into.`);
process.exit(1);
}
html = html.replace('</head>', `${ORGANIZATION_JSONLD}\n </head>`);
if (!html.includes('<div id="root"></div>')) {
console.error(`[prerender] ERROR: ${file} has no empty <div id="root"></div> to inject into.`);
process.exit(1);
}
html = html.replace('<div id="root"></div>', `<div id="root"${rootAttributes}>${content}</div>`);
writeFileSync(htmlPath, html, 'utf-8');
console.log(`[prerender] Injected critical CSS and visible content into public/pro/${file}`);
}
|