Files
worldmonitor/pro-test/prerender.mjs
Elie Habib ef3967d991 feat(pro): realign /pro copy with shipped product — AI-native positioning (#3151)
* feat(pro): realign /pro page copy with shipped product reality

Marketing was selling 2024 positioning (equity research first) while the app
ships a 2026 AI-native product (MCP on Pro, WM Analyst chat, Custom Widget
Builder, 30-item AI digest with multi-channel push). Realigns hero, capabilities
grid, FAQ, and showcase copy to honestly reflect what Pro actually delivers.

Highlights:
- Hero tagline: "The intelligence geopolitical AI layer — ask it, subscribe to
  it, build on it."
- New Pillars block (Ask it / Subscribe to it / Build on it) below hero.
- New Delivery Desk section reframing the 30-min digest as a personal analyst.
- whyUpgrade quadrants rewritten as outcomes (Know first / Ask anything /
  Build anything / Wake up informed).
- twoPath capabilities grid: WM Analyst, Custom Widget Builder, MCP connectors,
  flight search, market watchlist, AES-256 alerts; cuts WSB Ticker / Telegram
  Intel / OREF noise.
- proShowcase: orbital surveillance moved to roadmap with inline Soon badge;
  morningBriefs reframed as Personal Intelligence Desk.
- FAQ q8 corrected (MCP is Pro, not Enterprise-only); adds q9-q13 covering
  Custom Widget Builder, MCP, digest personalization, refresh rate, AES-256.
- Pricing table proF4: "Early access pricing" replaced with roadmap-flagged
  "Priority data refresh (Soon)"; canonical price keys added for downstream
  use (PricingSection already reads tiers.json which has the real numbers).
- Page spine reordered: Hero, Pillars, WhyUpgrade, TwoPath, ProShowcase,
  DeliveryDesk, AudiencePersonas, SocialProof, LivePreview, Pricing, FAQ.

Plan: docs/plans/2026-04-17-001-copy-pro-page-rewrite-plan.md

English-only this PR; other locales ship stale by design (tracked as follow-up).

* chore(pro): rebuild /pro bundle for copy rewrite

Regenerates public/pro/* artifacts so the AI-native copy realignment
(see preceding commit) actually ships through the Vercel deploy.

Per commit 05b8f66ec, forgetting this rebuild is a known repeat bug.

* feat(pro): translate /pro page to 20 locales matching new en.json copy

Brings ar, bg, cs, de, el, es, fr, it, ja, ko, nl, pl, pt, ro, ru, sv, th, tr, vi, zh up to the new AI-native positioning shipped in en.json (PR #3151). Without this, non-English visitors still saw the stale 2024 'equity research + geopolitical + macro analyst' marketing copy.

Product names (WorldMonitor, MCP, Slack, Claude, GPT, Brent, AIS, FRED, ACLED, etc.) kept in English. 'Soon' badge translated per-locale (Próximamente / Bientôt / Bald / 即将推出 / 近日公開 / قريباً …).

Also rebuilds public/pro bundle with updated locale chunks.

* fix(pro): repair static SEO layer (prerender, JSON-LD, OG, noscript)

Three problems shipping to crawlers and non-JS users:

1. pro-test/prerender.mjs read deleted hero keys (en.hero.title1/title2/subtitle/missionLine), so public/pro/index.html literally contained <h1>undefined undefined</h1> and undefined paragraphs. Now reads existing keys (noiseWord, signalWord, valueProps, launchingDate) and surfaces the new pillars + deliveryDesk blocks. Added a guard that fails the build if any interpolation resolves to 'undefined'.

2. pro-test/prerender.mjs only emitted q1-q8. Extended to q1-q13 to match the new FAQ.

3. pro-test/index.html static head still carried 2024 positioning: title/description/keywords, OG/Twitter tags, SoftwareApplication ld+json (description, featureList), FAQ ld+json (q8 said MCP was Enterprise-only, missing q9-q13), and noscript fallback. All updated to AI-native positioning matching the new en.json.

Without this fix, every rebuild reintroduced stale SEO/social cards even though the React app rendered the new copy.

* fix(pro): drop pre-launch messaging — Pro is shipped

The PR repositioned /pro around already-shipped AI-native features, but two strings still said the opposite:

- pricingTable.proHeader = 'Pro (Early Access)' rendered in the comparison table at App.tsx:776
- hero.launchingDate = 'Launching March 2026' got prerendered into public/pro/index.html for crawlers

Both gone, across all 21 locales:

- proHeader → 'Pro ($39.99)' (matches free 'Free ($0)' pattern, surfaces real pricing)
- launchingDate → per-locale 'Live now / Disponible ahora / Disponible maintenant / Jetzt verfügbar / 现已上线 / 現在ご利用可能 / 지금 이용 가능 / متاح الآن / …'

Pro is launched (per commit 7693a4fa4 'cut /pro over from waitlist → Pro-launched messaging'); waitlist phrasing in the hero/SEO contradicted that. Bundle rebuilt.

* fix(pro): match marketing to actual notification capabilities

Two overclaims removed across all 21 locales + static SEO:

1. WhatsApp delivery does not exist. Supported channels in src/services/notification-channels.ts are telegram | slack | email | discord | webhook. Removed WhatsApp from every delivery-channel listing (deliveryDesk channels list, pillars subscribeItDesc, twoPath proF2, proShowcase morningBriefsDesc, OG/Twitter descriptions, JSON-LD featureList, noscript fallback). Kept referral.whatsapp because that's a 'share this page to WhatsApp' button, not a delivery claim.

2. 'AI digest every 30 minutes' was wrong. The 30-min cron in scripts/seed-digest-notifications.mjs is just the scheduler frequency; user-facing cadence is daily | twice_daily | weekly per src/services/notification-channels.ts. Replaced with truthful 'daily, twice-daily, or weekly cadence' across deliveryDesk body, pillars subscribeItDesc, twoPath proF2, proShowcase morningBriefsDesc, faq a11, plus all static SEO copies. 'Real-time alerts' surfaced where appropriate (the realtime mode is real and separate from digest).

Channel list now: Slack, Discord, Telegram, Email, webhook.

* fix(pro): align rendered UI with shipped channels + i18n SoonBadge

Three remaining mismatches with src/services/notification-channels.ts:

1. App.tsx delivery icon strip (proShowcase) still rendered a hardcoded WhatsApp icon and omitted Webhook. Replaced with Slack/Discord/Telegram/Email/Webhook (Plug icon for webhook), matching the actual channel enum.

2. pricingTable.fSlackTgWa value was 'Slack/TG/WA/Email' across all 21 locales, rendered in the comparison table at App.tsx:758. Updated to 'Slack/Discord/TG/Email/Webhook' (per-locale email word). Key name kept (renaming would touch every locale's structure unnecessarily); only the value changed.

3. SoonBadge component hardcoded 'Soon' in English, leaking through localized /pro?lang=… pages. Now uses t('soonBadge'); added a soonBadge i18n key per locale (Próximamente / Bientôt / Bald / Presto / Em breve / Binnenkort / Snart / Wkrótce / Brzy / În curând / Σύντομα / Скоро / Скоро / Yakında / 即将推出 / 近日公開 / 출시 예정 / เร็วๆ นี้ / Sắp ra mắt / قريباً).

Remaining 'WhatsApp' string in bundle is referral.whatsapp — the share button label, which is correct (people share TO WhatsApp; we don't deliver via it).
2026-04-18 08:00:04 +04:00

119 lines
4.9 KiB
JavaScript

#!/usr/bin/env node
/**
* Postbuild prerender script — injects critical SEO content into the built HTML
* so search engines see real content without executing JavaScript.
*
* Reads only keys that exist in pro-test/src/locales/en.json. If you remove a
* key, also remove it here, otherwise the build will inject the literal string
* "undefined" into the page that crawlers index.
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const htmlPath = resolve(__dirname, '../public/pro/index.html');
const en = JSON.parse(readFileSync(resolve(__dirname, 'src/locales/en.json'), 'utf-8'));
const seoContent = `
<div id="seo-prerender" style="position:absolute;left:-9999px;top:-9999px;overflow:hidden;width:1px;height:1px;">
<h1>World Monitor Pro — From ${en.hero.noiseWord} to ${en.hero.signalWord}</h1>
<p>${en.hero.valueProps}</p>
<p>${en.hero.launchingDate}</p>
<h2>Three pillars</h2>
<h3>${en.pillars.askIt}</h3><p>${en.pillars.askItDesc}</p>
<h3>${en.pillars.subscribeIt}</h3><p>${en.pillars.subscribeItDesc}</p>
<h3>${en.pillars.buildOnIt}</h3><p>${en.pillars.buildOnItDesc}</p>
<h2>Plans</h2>
<h3>${en.twoPath.proTitle}</h3>
<p>${en.twoPath.proDesc}</p>
<p>${en.twoPath.proF1}</p>
<p>${en.twoPath.proF2}</p>
<p>${en.twoPath.proF3}</p>
<p>${en.twoPath.proF4}</p>
<p>${en.twoPath.proF5}</p>
<p>${en.twoPath.proF6}</p>
<p>${en.twoPath.proF7}</p>
<p>${en.twoPath.proF8}</p>
<p>${en.twoPath.proF9}</p>
<h3>${en.twoPath.entTitle}</h3>
<p>${en.twoPath.entDesc}</p>
<h2>${en.whyUpgrade.title}</h2>
<h3>${en.whyUpgrade.noiseTitle}</h3><p>${en.whyUpgrade.noiseDesc}</p>
<h3>${en.whyUpgrade.fasterTitle}</h3><p>${en.whyUpgrade.fasterDesc}</p>
<h3>${en.whyUpgrade.controlTitle}</h3><p>${en.whyUpgrade.controlDesc}</p>
<h3>${en.whyUpgrade.deeperTitle}</h3><p>${en.whyUpgrade.deeperDesc}</p>
<h2>${en.proShowcase.title}</h2>
<p>${en.proShowcase.subtitle}</p>
<h3>${en.proShowcase.equityResearch}</h3><p>${en.proShowcase.equityResearchDesc}</p>
<h3>${en.proShowcase.geopoliticalAnalysis}</h3><p>${en.proShowcase.geopoliticalAnalysisDesc}</p>
<h3>${en.proShowcase.economyAnalytics}</h3><p>${en.proShowcase.economyAnalyticsDesc}</p>
<h3>${en.proShowcase.riskMonitoring}</h3><p>${en.proShowcase.riskMonitoringDesc}</p>
<h3>${en.proShowcase.orbitalSurveillance}</h3><p>${en.proShowcase.orbitalSurveillanceDesc}</p>
<h3>${en.proShowcase.morningBriefs}</h3><p>${en.proShowcase.morningBriefsDesc}</p>
<h3>${en.proShowcase.oneKey}</h3><p>${en.proShowcase.oneKeyDesc}</p>
<h2>${en.deliveryDesk.title}</h2>
<p>${en.deliveryDesk.body}</p>
<p>${en.deliveryDesk.closer}</p>
<p>${en.deliveryDesk.channels}</p>
<h2>${en.audience.title}</h2>
<h3>${en.audience.investorsTitle}</h3><p>${en.audience.investorsDesc}</p>
<h3>${en.audience.tradersTitle}</h3><p>${en.audience.tradersDesc}</p>
<h3>${en.audience.researchersTitle}</h3><p>${en.audience.researchersDesc}</p>
<h3>${en.audience.journalistsTitle}</h3><p>${en.audience.journalistsDesc}</p>
<h3>${en.audience.govTitle}</h3><p>${en.audience.govDesc}</p>
<h3>${en.audience.teamsTitle}</h3><p>${en.audience.teamsDesc}</p>
<h2>${en.dataCoverage.title}</h2>
<p>${en.dataCoverage.subtitle}</p>
<h2>${en.apiSection.title}</h2>
<p>${en.apiSection.subtitle}</p>
<h2>${en.enterpriseShowcase.title}</h2>
<p>${en.enterpriseShowcase.subtitle}</p>
<h2>${en.pricingTable.title}</h2>
<p>${en.tiers.priceMonthly} · ${en.tiers.priceAnnual} (${en.tiers.annualSavingsNote})</p>
<h2>${en.faq.title}</h2>
<dl>
<dt>${en.faq.q1}</dt><dd>${en.faq.a1}</dd>
<dt>${en.faq.q2}</dt><dd>${en.faq.a2}</dd>
<dt>${en.faq.q3}</dt><dd>${en.faq.a3}</dd>
<dt>${en.faq.q4}</dt><dd>${en.faq.a4}</dd>
<dt>${en.faq.q5}</dt><dd>${en.faq.a5}</dd>
<dt>${en.faq.q6}</dt><dd>${en.faq.a6}</dd>
<dt>${en.faq.q7}</dt><dd>${en.faq.a7}</dd>
<dt>${en.faq.q8}</dt><dd>${en.faq.a8}</dd>
<dt>${en.faq.q9}</dt><dd>${en.faq.a9}</dd>
<dt>${en.faq.q10}</dt><dd>${en.faq.a10}</dd>
<dt>${en.faq.q11}</dt><dd>${en.faq.a11}</dd>
<dt>${en.faq.q12}</dt><dd>${en.faq.a12}</dd>
<dt>${en.faq.q13}</dt><dd>${en.faq.a13}</dd>
</dl>
<h2>${en.finalCta.title}</h2>
<p>${en.finalCta.subtitle}</p>
</div>`;
// Fail loudly if any key resolved to undefined — this prevents the build from
// silently shipping "undefined" strings to crawlers.
if (seoContent.includes('undefined')) {
console.error('[prerender] ERROR: SEO content contains literal "undefined". Check that all en.json keys referenced in this file exist.');
process.exit(1);
}
let html = readFileSync(htmlPath, 'utf-8');
html = html.replace('<div id="root"></div>', `<div id="root">${seoContent}</div>`);
writeFileSync(htmlPath, html, 'utf-8');
console.log('[prerender] Injected SEO content into public/pro/index.html');