#!/usr/bin/env node import fs from 'node:fs'; import path from 'node:path'; const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..'); const distDir = path.join(root, 'dist'); const wwwDir = path.join(distDir, 'www'); const h5Dir = path.join(distDir, 'h5'); const siteContent = readJson(path.join(root, 'content', 'site.json')); const legalContent = readJson(path.join(root, 'content', 'legal.json')); const H5_PRIVACY_URL = `${siteContent.company.h5Website}/privacy.html`; const H5_TERMS_URL = `${siteContent.company.h5Website}/terms.html`; const H5_DELETE_ACCOUNT_URL = `${siteContent.company.h5Website}/delete-account.html`; const H5_CHILD_SAFETY_URL = `${siteContent.company.h5Website}/child-safety.html`; const landingPages = fs .readdirSync(path.join(root, 'content', 'landing-pages')) .filter((file) => file.endsWith('.json')) .map((file) => readJson(path.join(root, 'content', 'landing-pages', file))) .sort((a, b) => a.slug.localeCompare(b.slug)); build(); function build() { fs.rmSync(distDir, { force: true, recursive: true }); ensureDir(path.join(wwwDir, 'assets')); ensureDir(path.join(h5Dir, 'assets')); copyFile(path.join(root, 'src', 'site.css'), path.join(wwwDir, 'assets', 'site.css')); copyFile(path.join(root, 'src', 'site.css'), path.join(h5Dir, 'assets', 'site.css')); writeHtml(path.join(wwwDir, 'index.html'), renderHomePage()); writeHtml( path.join(wwwDir, 'privacy', 'index.html'), renderRedirectPage({ title: 'Privacy Policy Redirect', target: H5_PRIVACY_URL, }), ); writeHtml( path.join(wwwDir, 'terms', 'index.html'), renderRedirectPage({ title: 'Terms of Service Redirect', target: H5_TERMS_URL, }), ); writeHtml( path.join(wwwDir, 'delete-account', 'index.html'), renderRedirectPage({ title: 'Delete Account Redirect', target: H5_DELETE_ACCOUNT_URL, }), ); writeHtml( path.join(wwwDir, 'child-safety', 'index.html'), renderRedirectPage({ title: 'Child Safety Standards Redirect', target: H5_CHILD_SAFETY_URL, }), ); writeText(path.join(wwwDir, 'robots.txt'), 'User-agent: *\nAllow: /\n'); writeText(path.join(wwwDir, 'sitemap.xml'), renderSiteMap()); writeHtml(path.join(h5Dir, 'index.html'), renderLandingIndexPage()); writeHtml( path.join(h5Dir, 'privacy.html'), renderPlainLegalPage('privacy', { canonical: H5_PRIVACY_URL, }), ); writeHtml( path.join(h5Dir, 'terms.html'), renderLegalPage('terms', { canonical: H5_TERMS_URL, homeHref: '/', privacyHref: '/privacy.html', termsHref: '/terms.html', primaryActionHref: 'https://www.haiyihy.com/', primaryActionLabel: 'Official Site', }), ); writeHtml( path.join(h5Dir, 'delete-account.html'), renderPlainLegalPage('deleteAccount', { canonical: H5_DELETE_ACCOUNT_URL, }), ); writeHtml( path.join(h5Dir, 'child-safety.html'), renderPlainLegalPage('childSafety', { canonical: H5_CHILD_SAFETY_URL, }), ); writeHtml(path.join(h5Dir, 'studio', 'index.html'), renderStudioPage()); writeText(path.join(h5Dir, 'data', 'pages.json'), `${JSON.stringify(landingPages, null, 2)}\n`); for (const page of landingPages) { writeHtml(path.join(h5Dir, page.slug, 'index.html'), renderLandingPage(page)); } console.log(`Built public site to ${distDir}`); } function renderHomePage() { const { brand, company, hero } = siteContent; return renderDocument({ title: `${brand.name} | Official Website`, description: brand.description, canonical: `${company.website}/`, hostRoot: '', theme: 'ember', bodyClass: 'corp-body', body: `
${escapeHtml(hero.eyebrow)}

${escapeHtml(hero.title)}

${escapeHtml(hero.summary)}

About us

${escapeHtml(company.legalNameEn)}

${escapeHtml(company.legalNameCn)}

${escapeHtml(company.address)}

`, }); } function renderLegalPage(kind, options = {}) { const { company, brand } = siteContent; const doc = legalContent[kind]; const canonical = options.canonical || `${company.website}/${kind}/`; const homeHref = options.homeHref || '/'; const privacyHref = options.privacyHref || H5_PRIVACY_URL; const termsHref = options.termsHref || H5_TERMS_URL; const primaryActionHref = options.primaryActionHref || 'https://play.google.com/store/apps/details?id=com.org.yumi'; const primaryActionLabel = options.primaryActionLabel || 'Get Yumi'; return renderDocument({ title: `${doc.title} | ${brand.name}`, description: doc.summary, canonical, hostRoot: '', theme: kind === 'privacy' ? 'steel' : 'ink', body: ` ${renderHeader({ brandName: brand.name, subline: doc.title, links: [ { href: homeHref, label: 'Home' }, { href: privacyHref, label: 'Privacy' }, { href: termsHref, label: 'Terms' }, ], action: { href: primaryActionHref, label: primaryActionLabel }, })}
${escapeHtml(kind === 'privacy' ? 'Legal disclosure' : 'Service terms')}

${escapeHtml(doc.title)}

${renderFooter({ company, brandName: brand.name })} `, }); } function renderPlainLegalPage(kind, options = {}) { const { company, brand } = siteContent; const doc = legalContent[kind]; const canonical = options.canonical || `${company.h5Website}/${kind}.html`; return ` ${escapeHtml(doc.title)} | ${escapeHtml(brand.name)}

${escapeHtml(doc.title)}

Effective date: ${escapeHtml(legalContent.effectiveDate)}

Operator: ${escapeHtml(company.legalNameEn)} (${escapeHtml(company.legalNameCn)})

Address: ${escapeHtml(company.address)}

Contact: ${escapeHtml(company.privacyEmail)} / ${escapeHtml(company.phone)}

${kind === 'deleteAccount' ? `

Start a deletion request by email

` : ''}

${escapeHtml(doc.summary)}

${doc.sections.map((section) => renderPlainLegalSection(section)).join('\n')} `; } function renderLandingIndexPage() { const { brand, company } = siteContent; return renderDocument({ title: `${brand.name} H5 | Campaign Pages`, description: 'Index of JSON-driven Yumi landing pages.', canonical: `${company.h5Website}/`, hostRoot: '', theme: 'sapphire', body: ` ${renderHeader({ brandName: brand.name, subline: 'H5 landing pages', links: [ { href: '/', label: 'H5 Home' }, { href: '/studio/', label: 'Studio' }, { href: 'https://www.haiyihy.com/', label: 'Official Site' }, ], action: { href: H5_PRIVACY_URL, label: 'Privacy' }, })}
Operations and campaigns

A structured index of all public H5 destinations.

Each page below is generated from a single JSON file, which keeps campaign updates lightweight and predictable.

${landingPages.map((page) => ` ${escapeHtml(page.eyebrow)}

${escapeHtml(page.title)}

${escapeHtml(page.summary)}

Open page
`).join('')}
${renderFooter({ company, brandName: brand.name })} `, }); } function renderLandingPage(page) { const { brand, company } = siteContent; return renderDocument({ title: `${page.title} | ${brand.name} H5`, description: page.summary, canonical: `${company.h5Website}/${page.slug}/`, hostRoot: '../', theme: page.theme || 'ember', body: ` ${renderHeader({ brandName: brand.name, subline: 'H5 landing page', links: [ { href: '/', label: 'H5 Home' }, { href: '/studio/', label: 'Studio' }, { href: 'https://www.haiyihy.com/', label: 'Official Site' }, ], action: { href: H5_PRIVACY_URL, label: 'Privacy' }, })}
${escapeHtml(page.eyebrow)}

${escapeHtml(page.title)}

${escapeHtml(page.summary)}

${page.stats.map((item) => `
${escapeHtml(item.value)} ยท ${escapeHtml(item.label)}
`).join('')}

${escapeHtml(page.highlight)}

${escapeHtml(page.cta.label)} ${escapeHtml(page.secondaryCta.label)}
${page.sections.map((section) => `

${escapeHtml(section.title)}

${escapeHtml(section.body)}

${renderBullets(section.bullets)}
`).join('')}
${renderFooter({ company, brandName: brand.name })} `, }); } function renderStudioPage() { const { brand, company } = siteContent; return renderDocument({ title: `${brand.name} H5 Studio`, description: 'Editorial cockpit for landing page content files.', canonical: `${company.h5Website}/studio/`, hostRoot: '../', theme: 'forest', body: ` ${renderHeader({ brandName: brand.name, subline: 'Content studio', links: [ { href: '/', label: 'H5 Home' }, { href: 'https://www.haiyihy.com/', label: 'Official Site' }, { href: H5_PRIVACY_URL, label: 'Privacy' }, ], action: { href: '/', label: 'View Pages' }, })}
Editorial cockpit

Manage landing pages from JSON, not from page code.

This page is a publishing reference for operators and developers. It documents the current slugs and shows the exact JSON structure that drives each landing page.

Current management mode is file-driven. Edit JSON under public-site/content/landing-pages, rebuild the site, and redeploy. This keeps operations fast while avoiding app-release coupling.
${landingPages.map((page) => `
${escapeHtml(page.slug)}

${escapeHtml(page.title)}

${escapeHtml(page.summary)}

${escapeHtml(JSON.stringify(page, null, 2))}
`).join('')}
${renderFooter({ company, brandName: brand.name })} `, }); } function renderHeader({ brandName, subline, links, action }) { return ` `; } function renderFooter({ company, brandName }) { return ` `; } function renderDocument({ title, description, canonical, theme, hostRoot, body, bodyClass = '' }) { return ` ${escapeHtml(title)} ${body} `; } function renderParagraphs(paragraphs = []) { return paragraphs.map((paragraph) => `

${escapeHtml(paragraph)}

`).join(''); } function renderBullets(bullets = []) { if (!bullets?.length) { return ''; } return ``; } function renderPlainLegalSection(section) { return `

${escapeHtml(section.title)}

${renderParagraphs(section.paragraphs)} ${renderPlainBullets(section.bullets)} `; } function renderPlainBullets(bullets = []) { if (!bullets?.length) { return ''; } return bullets.map((item) => `

- ${escapeHtml(item)}

`).join(''); } function renderDeleteAccountMailto(email) { const subject = encodeURIComponent('Delete Yumi Account'); return `mailto:${email}?subject=${subject}`; } function renderSiteMap() { const pages = [ 'https://www.haiyihy.com/', 'https://h5.haiyihy.com/', H5_PRIVACY_URL, H5_TERMS_URL, H5_DELETE_ACCOUNT_URL, H5_CHILD_SAFETY_URL, 'https://h5.haiyihy.com/studio/', ...landingPages.map((page) => `https://h5.haiyihy.com/${page.slug}/`), ]; return ` ${pages.map((url) => ` ${url}`).join('\n')} `; } function renderRedirectPage({ title, target }) { return ` ${escapeHtml(title)}

Redirecting to ${escapeHtml(target)}...

`; } function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } function ensureDir(dirPath) { fs.mkdirSync(dirPath, { recursive: true }); } function writeHtml(filePath, content) { ensureDir(path.dirname(filePath)); fs.writeFileSync(filePath, `${content}\n`, 'utf8'); } function writeText(filePath, content) { ensureDir(path.dirname(filePath)); fs.writeFileSync(filePath, content, 'utf8'); } function copyFile(source, target) { ensureDir(path.dirname(target)); fs.copyFileSync(source, target); } function escapeHtml(value = '') { return String(value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function escapeAttr(value = '') { return escapeHtml(value); }