// Builds the static site into ./dist. No dependencies: `node build.mjs`. import { mkdir, rm, writeFile, cp, readFile } from 'node:fs/promises'; import { createHash } from 'node:crypto'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { page, eyebrow, btn, SITE } from './src/layout.mjs'; import { LOCALES, DEFAULT_LOCALE, localize, translatePage } from './src/i18n.mjs'; const root = dirname(fileURLToPath(import.meta.url)); const out = join(root, 'dist'); const pages = ['home', 'about', 'portfolio', 'consulting', 'marketing', 'business-setup', 'business-optimization', 'supply-chain', 'cybersecurity', 'education', 'fractional', 'contact', 'book', 'press']; await rm(out, { recursive: true, force: true }); await mkdir(out, { recursive: true }); await cp(join(root, 'static'), out, { recursive: true }); // Fingerprint CSS/JS so a new deploy never reuses a stale cached copy. const hashOf = async (f) => createHash('sha256').update(await readFile(join(root, 'static', f))).digest('hex').slice(0, 10); const assetVersions = Object.fromEntries(await Promise.all(['styles.css', 'site.js', 'home.css', 'home.js', 'fonts.css', 'book.js', 'rtl.css'].map(async (f) => ['/assets/' + f, await hashOf('assets/' + f)]))); const fingerprint = (html) => Object.entries(assetVersions).reduce((h, [u, v]) => h.replaceAll(`"${u}"`, `"${u}?v=${v}"`), html); // Spanish and Arabic pages are the English pages with their text swapped through src/i18n/.json. const dicts = Object.fromEntries(await Promise.all(LOCALES.filter((l) => l !== DEFAULT_LOCALE).map(async (l) => [l, JSON.parse(await readFile(join(root, 'src/i18n', l + '.json'), 'utf8'))]))); const missing = {}; const urls = []; for (const name of pages) { const def = (await import(`./src/pages/${name}.mjs`)).default; for (const locale of LOCALES) { let html = page(def, locale); if (locale !== DEFAULT_LOCALE) { const t = translatePage(html, locale, dicts[locale]); html = t.html; for (const s of t.missing) (missing[locale] ??= new Set()).add(s); } const file = join(out, localize(def.path, locale), 'index.html'); await mkdir(dirname(file), { recursive: true }); await writeFile(file, fingerprint(html)); } if (!def.noindex) urls.push(def.path); console.log('built', def.path); } for (const [l, set] of Object.entries(missing)) { console.warn(`\n${l}: ${set.size} strings without translation (shown in English):`); for (const s of set) console.warn(' ' + JSON.stringify(s)); } if (process.argv.includes('--strict') && Object.keys(missing).length) process.exit(1); await writeFile(join(out, '404.html'), fingerprint(page({ path: '/404/', title: 'Page Not Found | Dylan Mazzei', description: 'This page could not be found.', body: `
${eyebrow('404')}

Page Not Found

${btn('BACK TO HOME', '/')}
`, }))); const today = new Date().toISOString().slice(0, 10); await writeFile(join(out, 'sitemap.xml'), ` ${urls.flatMap((u) => LOCALES.map((l) => ` ${SITE.url}${localize(u, l)}${today}${LOCALES.map((a) => ``).join('')}`)).join('\n')} `); await writeFile(join(out, 'robots.txt'), 'User-agent: *\nAllow: /\nSitemap: https://dylanmazzei.com/sitemap.xml\n'); console.log('done →', out);