Header: site/partials/nav.html, head.html, footer.html, injected by site/build.mjs between markers in every page (the bench template too) with the active link marked; the master mark in its black square, the wordmark, Litepaper, Live devnet, Engineering log, Evidence, GitHub and the miner button, in that order everywhere; one menu script (closes on a link, a tap outside, Escape with focus back on the button, a resize past 900 px); a skip link; one focus ring and one reduced-motion rule. Litepaper: the hash routed by a click delegate and popstate instead of the browser's fragment jump. Root cause of the tabs fault: show() scrolled to the top of the article, never to the section; in whole-paper mode the fragment jump landed on the section and the smooth scroll then dragged the reader back to the Abstract; in one-section mode a deep link scrolled while the section was still display:none and landed on the cover. Now: every section and all 29 subsection headings reachable by URL in both modes, scroll-margin from the measured bar height (nav, plus the contents row on a phone), focus moved to the heading, a re-aim after a late font batch, mode buttons stacked in the sidebar. Pages: homepage headline capped at 9cqw so it is two lines at 390 and 1440 (was four with "fire." alone), journey inlined by the build (no fetch, no shift), dates as "4 Oct 2026"; live page legend no longer forces 420 px of width at 390, the hash-rate unit as small mono, idle redraw at 30/s with a 1.5x backing store (0.7 to 1.2% of one core idle in Chrome for Testing, taken under load), the proving line wraps on a phone; evidence sort headings are buttons, chips carry aria-pressed, a scroll hint under 1,140 px; bench contents in a sticky scroll box, og.png?v=3; 404.html; sitemap with /evidence; vercel.json caches fonts a year and images a day. Fonts: eight latin woff2 files in site/fonts (139 KB for all, 118 KB a typical page), preloaded first-paint faces, fallback faces with size-adjust and ascent overrides from the font tables; every page lost its render-blocking fonts.googleapis.com stylesheet and the gstatic origin. HTML per page before to after (gzip): / 17,265 to 21,810 B; /litepaper 20,556 to 24,957; /live 15,640 to 17,777; /evidence 18,899 to 22,493; /bench 94,723 to 98,289. Third-party requests before to after: 3 to 1 (jsdelivr, light client), 1 to 0, 3 to 0, 1 to 0, 1 to 0. Lighthouse before and after is queued under the measure lock (held by reliability runs since 19:16Z, load average 258) and recorded in docs/design/site-polish-2026-10-04.md when it runs. Checks: node site/build.mjs, tools/ci/link-check.mjs (246 links, 0 broken), identity grep, and a Chrome for Testing run over every page at 390, 768 and 1440 (no overflow, no console errors, menu, router in both modes, deep links on reload, keyboard). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
259 lines
18 KiB
JavaScript
259 lines
18 KiB
JavaScript
// Igneum site build: renders the engineering log from the repository's markdown into a branded
|
|
// page and merges the log's dated entries into the journey feed. The FUD ledger is internal and
|
|
// is never rendered here.
|
|
// Runs on every deploy (Vercel build command) and locally with `node build.mjs`.
|
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
import { scrubBench } from './scrub.mjs';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const repo = join(here, '..');
|
|
const docs = join(repo, 'docs');
|
|
|
|
function esc(s) { return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
function inline(s) {
|
|
s = esc(s);
|
|
s = s.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
s = s.replace(/\[([^\]]+)\]\((https?:[^)]+)\)/g, '<a href="$2" rel="noopener">$1</a>');
|
|
return s;
|
|
}
|
|
function md(src) {
|
|
const lines = src.split('\n'); const out = []; let i = 0; let toc = [];
|
|
const slug = t => t.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
while (i < lines.length) {
|
|
const l = lines[i];
|
|
if (/^```/.test(l)) { const buf = []; i++; while (i < lines.length && !/^```/.test(lines[i])) buf.push(lines[i++]); i++; out.push('<pre>' + esc(buf.join('\n')) + '</pre>'); continue; }
|
|
const h = /^(#{1,4})\s+(.*)$/.exec(l);
|
|
if (h) { const lvl = h[1].length; const t = h[2].trim(); const id = slug(t); if (lvl <= 2) toc.push({ lvl, t, id }); out.push(`<h${lvl} id="${id}">${inline(t)}</h${lvl}>`); i++; continue; }
|
|
if (/^\|/.test(l)) {
|
|
const rows = []; while (i < lines.length && /^\|/.test(lines[i])) rows.push(lines[i++]);
|
|
const cells = r => r.replace(/^\||\|$/g, '').split('|').map(c => c.trim());
|
|
const head = cells(rows[0]); const body = rows.slice(1).filter(r => !/^\|\s*-{2,}/.test(r));
|
|
out.push('<div class="tbl"><table><thead><tr>' + head.map(c => `<th>${inline(c)}</th>`).join('') + '</tr></thead><tbody>' + body.map(r => '<tr>' + cells(r).map(c => `<td>${inline(c)}</td>`).join('') + '</tr>').join('') + '</tbody></table></div>');
|
|
continue;
|
|
}
|
|
if (/^\s*[-*]\s+/.test(l)) { const items = []; while (i < lines.length && /^\s*[-*]\s+/.test(lines[i])) items.push(lines[i++].replace(/^\s*[-*]\s+/, '')); out.push('<ul>' + items.map(x => `<li>${inline(x)}</li>`).join('') + '</ul>'); continue; }
|
|
if (/^\s*\d+\.\s+/.test(l)) { const items = []; while (i < lines.length && /^\s*\d+\.\s+/.test(lines[i])) items.push(lines[i++].replace(/^\s*\d+\.\s+/, '')); out.push('<ol>' + items.map(x => `<li>${inline(x)}</li>`).join('') + '</ol>'); continue; }
|
|
if (/^\s*$/.test(l)) { i++; continue; }
|
|
const buf = []; while (i < lines.length && !/^\s*$/.test(lines[i]) && !/^(#|\||```|\s*[-*]\s|\s*\d+\.\s)/.test(lines[i])) buf.push(lines[i++]);
|
|
out.push('<p>' + inline(buf.join(' ')) + '</p>');
|
|
}
|
|
return { html: out.join('\n'), toc };
|
|
}
|
|
|
|
// Shared chrome: fonts and tokens (head), the nav bar and the footer live once in site/partials/ and are injected into every
|
|
// page between HTML comment markers (<!-- head:start --> ... <!-- head:end -->, nav, footer; journey on the homepage).
|
|
// The committed pages carry the injected copy, so they are right with or without a build; the build keeps them in step.
|
|
const partial = name => readFileSync(join(here, 'partials', name), 'utf8').trim();
|
|
const HEAD = partial('head.html'), NAV = partial('nav.html'), FOOT = partial('footer.html');
|
|
function navFor(active) {
|
|
if (!active) return NAV;
|
|
const re = new RegExp(`(<a href="[^"]*" data-nav="${active}")`);
|
|
if (!re.test(NAV)) throw new Error(`nav: no link with data-nav="${active}"`);
|
|
return NAV.replace(re, '$1 aria-current="page"');
|
|
}
|
|
function inject(html, name, content, file) {
|
|
const re = new RegExp(`<!-- ${name}:start -->[\\s\\S]*?<!-- ${name}:end -->`);
|
|
if (!re.test(html)) throw new Error(`${file}: missing <!-- ${name}:start --> ... <!-- ${name}:end --> markers`);
|
|
return html.replace(re, () => `<!-- ${name}:start -->\n${content}\n<!-- ${name}:end -->`);
|
|
}
|
|
const ORIGIN = 'https://igneum.network';
|
|
const svgIcon = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Crect width='1024' height='1024' fill='%230C0C0E'/%3E%3Cg transform='translate(166.95 166.95) scale(6.901)'%3E%3Cpolygon points='50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34' fill='%23F2541B'/%3E%3Cpolygon points='50,42 59,58 50,82 41,58' fill='%230C0C0E'/%3E%3C/g%3E%3C/svg%3E";
|
|
// Search and share metadata. Same set as the hand-written pages (index, litepaper, live); keep them in step.
|
|
function meta(title, desc, path) {
|
|
const url = ORIGIN + path; const t = esc(title); const d = esc(desc);
|
|
return `<title>${t}</title>
|
|
<meta name="description" content="${d}">
|
|
<link rel="canonical" href="${url}">
|
|
<meta name="theme-color" content="#0C0C0E">
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:site_name" content="Igneum">
|
|
<meta property="og:title" content="${t}">
|
|
<meta property="og:description" content="${d}">
|
|
<meta property="og:url" content="${url}">
|
|
<meta property="og:image" content="${ORIGIN}/og.png?v=3">
|
|
<meta property="og:image:width" content="1200">
|
|
<meta property="og:image:height" content="630">
|
|
<meta property="og:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:title" content="${t}">
|
|
<meta name="twitter:description" content="${d}">
|
|
<meta name="twitter:image" content="${ORIGIN}/og.png?v=3">
|
|
<meta name="twitter:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
|
|
<link rel="icon" href="${svgIcon}" type="image/svg+xml">
|
|
<link rel="icon" href="/favicon.ico" sizes="48x48">
|
|
<link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32">
|
|
<link rel="icon" href="/icon-192.png" type="image/png" sizes="192x192">
|
|
<link rel="icon" href="/icon-512.png" type="image/png" sizes="512x512">
|
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
|
|
<link rel="manifest" href="/site.webmanifest">`;
|
|
}
|
|
function page(title, desc, bodyHtml, toc, note, { path = '/bench', heading = title, active = 'bench' } = {}) {
|
|
const h2s = toc.filter(t => t.lvl === 2);
|
|
const nav = h2s.map(t => `<li><a href="#${t.id}">${esc(t.t)}</a></li>`).join('');
|
|
return `<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
${meta(title, desc, path)}
|
|
<!-- head:start -->
|
|
${HEAD}
|
|
<!-- head:end -->
|
|
<style>
|
|
:root{--obsidian:#0C0C0E;--graphite:#16161A;--line:#2A2A30;--ember:#F2541B;--molten:#FFB35C;--bone:#F4F1EC;--ash:#9A9A9E;--ink-2:#C9C7C2;
|
|
/* one system for every page: container, gutter, section rhythm, card and tile boxes, heading scale (same tokens as the homepage) */
|
|
--max:1200px;--gutter:clamp(16px,4vw,32px);--sec:clamp(56px,8vw,96px);--head:clamp(40px,6vw,64px);
|
|
--card-pad:clamp(18px,3vw,28px);--card-r:18px;--tile-pad:18px 20px;--tile-r:14px;--gap:24px;--gap-tile:12px;
|
|
--fs-h1:clamp(32px,5.5vw,56px);--fs-h2:clamp(28px,4.2vw,44px);--fs-h3:clamp(18px,2vw,22px);--fs-tile:clamp(20px,2.2vw,26px)}
|
|
*{box-sizing:border-box}body{margin:0;background:var(--obsidian);color:var(--bone);font-family:var(--f-sans);font-size:16px;line-height:1.6;padding:0;-webkit-font-smoothing:antialiased}
|
|
a{color:var(--ember);text-decoration:none}a:hover{text-decoration:underline}
|
|
[id]{scroll-margin-top:calc(var(--nav-h) + 16px)}
|
|
.wrap{max-width:var(--max);margin:0 auto;padding-inline:var(--gutter)}
|
|
.eyebrow{font-family:var(--f-mono);font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--ash)}
|
|
h1{font-family:var(--f-display);font-weight:900;font-size:var(--fs-h1);line-height:1.05;margin:12px 0 12px}
|
|
.head{padding-top:var(--head)}
|
|
.note{color:var(--ash);font-size:15px;max-width:72ch;margin:0 0 32px}
|
|
.layout{display:grid;grid-template-columns:minmax(0,1fr);gap:32px}
|
|
@media(min-width:960px){.layout{grid-template-columns:260px minmax(0,1fr);gap:56px}}
|
|
.toc{align-self:start;font-size:14px;border:1px solid var(--line);border-radius:var(--tile-r);background:var(--graphite);padding:8px}
|
|
.toc .lbl{font-family:var(--f-mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);padding:6px 10px 8px}
|
|
.toc ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:1px;counter-reset:s;max-height:40vh;overflow:auto;overscroll-behavior:contain;scrollbar-width:thin}
|
|
@media(min-width:960px){.toc{position:sticky;top:calc(var(--nav-h) + 20px)}.toc ol{max-height:calc(100vh - var(--nav-h) - 96px)}}
|
|
.toc a{display:flex;gap:10px;padding:7px 10px;border-radius:8px;color:var(--ink-2);counter-increment:s;line-height:1.35}
|
|
.toc a::before{content:counter(s,decimal-leading-zero);font-family:var(--f-mono);font-size:11px;color:var(--ash);min-width:20px;padding-top:2px}
|
|
.toc a:hover{background:var(--obsidian);text-decoration:none;color:var(--bone)}
|
|
article{max-width:78ch;min-width:0}
|
|
article h1{display:none}
|
|
article h2{font-family:var(--f-display);font-weight:700;font-size:var(--fs-h3);line-height:1.3;margin:44px 0 12px;padding-top:24px;border-top:1px solid var(--line)}
|
|
article h2:first-of-type{margin-top:0;padding-top:0;border-top:0}
|
|
article h3{font-family:var(--f-display);font-weight:500;font-size:16px;margin:26px 0 8px}
|
|
article h4{font-size:15px;margin:20px 0 6px;color:var(--molten)}
|
|
article p{margin:0 0 14px}article ul,article ol{margin:0 0 14px;padding-left:22px}article li{margin-bottom:6px}
|
|
article p,article li,article h2,article h3{overflow-wrap:anywhere}
|
|
code{font-family:var(--f-mono);font-size:.92em;background:var(--graphite);padding:1px 5px;border-radius:4px;overflow-wrap:anywhere}
|
|
pre{margin:0 0 16px;padding:16px 18px;background:var(--graphite);border-radius:var(--card-r);font-family:var(--f-mono);font-size:13px;line-height:1.55;overflow-x:auto;-webkit-overflow-scrolling:touch}
|
|
pre code{background:none;padding:0;font-size:inherit}
|
|
.tbl{overflow-x:auto;margin:18px 0 24px;border:1px solid var(--line);border-radius:var(--card-r);background:var(--graphite);-webkit-overflow-scrolling:touch}
|
|
table{border-collapse:collapse;width:100%;min-width:560px;font-size:15px}th,td{padding:12px 14px;text-align:left;vertical-align:top;border-bottom:1px solid var(--line)}
|
|
th{font-family:var(--f-mono);font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);font-weight:500;background:var(--obsidian)}tr:last-child td{border-bottom:0}
|
|
.gen{color:var(--ash);font-size:13px;margin:40px 0 0;max-width:72ch}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- nav:start -->
|
|
${navFor(active)}
|
|
<!-- nav:end -->
|
|
<main id="main" class="wrap">
|
|
<div class="head">
|
|
<div class="eyebrow">${h2s.length} entries, newest at the bottom</div>
|
|
<h1>${esc(heading)}</h1>
|
|
<p class="note">${note}</p>
|
|
</div>
|
|
<div class="layout">
|
|
<nav class="toc" aria-label="Contents"><div class="lbl">Contents</div><ol>${nav}</ol></nav>
|
|
<article>${bodyHtml}</article>
|
|
</div>
|
|
<p class="gen">Generated from the repository at build time. Times are UTC. Machine names are model names.</p>
|
|
</main>
|
|
<!-- footer:start -->
|
|
${FOOT}
|
|
<!-- footer:end -->
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
let built = [];
|
|
if (existsSync(join(docs, 'bench-log.md'))) {
|
|
// the public /bench page renders a SCRUBBED copy (site/scrub.mjs; the build fails on any private string, R4.6.1)
|
|
const src = scrubBench(readFileSync(join(docs, 'bench-log.md'), 'utf8'));
|
|
const { html, toc } = md(src);
|
|
writeFileSync(join(here, 'bench.html'), page('Igneum engineering log', 'Every Igneum benchmark and test, with the hardware and the commands that produced it. Prototype numbers are labelled as such.', html, toc,
|
|
'Every measurement the project has made, newest at the bottom, written by the people and agents who ran it, with the commands and hardware. Prototype numbers are not mining numbers and say so.',
|
|
{ path: '/bench', heading: 'Engineering log' }));
|
|
built.push('bench.html');
|
|
// journey feed: dated headings become log entries. `short` is the homepage line (one clause, under 72 characters);
|
|
// SHORT holds hand-written lines keyed by the start of the heading, the rule below covers the rest.
|
|
const SHORT = [
|
|
['Generator version 2 adopted', 'Generator v2 adopted: every hash does 128 distinct reads'],
|
|
['Sim/economy', 'Economy simulation: mining versus proving under stress'],
|
|
['Difficulty rule under attack', 'Difficulty rule attacked seven ways'],
|
|
['Difficulty rule: timestamp attack fixed', 'Timestamp attack on the difficulty rule fixed'],
|
|
['Devnet-v4 integration', 'Devnet v4: nine branches merged into one node'],
|
|
['Per-identity hash rate', 'RTX 5090 hash-rate decay diagnosed: a miner bug, fixed'],
|
|
['Igneum-node devnet v2', 'Finality rule v2 live on a four-miner test network'],
|
|
['Execution layer devnet v3', 'EVM execution layer: identical state on three nodes'],
|
|
['Weak-program census', 'Census of 400,000 programs: redundant loads found'],
|
|
['First machine in the United States', 'First machine in the United States: a laptop on an integrated GPU, synced and voting'],
|
|
['Shard proving on the RTX 5090', 'Shard layer on the RTX 5090: a full shard proven in 10.9 s, a block aggregated in 2.2 s'],
|
|
['Proving v0 on the RTX 5090', 'First GPU proof of an Igneum block: 1.4 s on an RTX 5090'],
|
|
['Proving v0', 'First SP1 proof of an Igneum block, on a laptop CPU'],
|
|
['Windows node package', 'Windows node package: cross-compiled, two-peer sync'],
|
|
['R3.26 / M15', 'Cache-build attack closed: 10.6 s of rebuilds to 14 ms'],
|
|
['Difficulty controller', 'Igneum dual-lane difficulty rule built and simulated'],
|
|
['Devnet started four months', 'Devnet started four months ahead of plan'],
|
|
['First blocks mined over the network', 'First blocks from a Windows PC: RTX 5090 and AMD'],
|
|
['First devnet blocks on the real lottery', 'First devnet blocks on the real lottery hash'],
|
|
['Igneum-node devnet v0', 'Devnet v0: three nodes at one block per second'],
|
|
['Proto-opencl', 'OpenCL worker built and proven without AMD silicon'],
|
|
['AMD gfx1036', 'AMD integrated GPU runs the hash through OpenCL'],
|
|
['RTX 5090 through NVIDIA OpenCL', 'RTX 5090 through NVIDIA OpenCL'],
|
|
['RTX 5090 first run', 'RTX 5090 first run'],
|
|
['RTX 5090, memory-hard', 'RTX 5090 on the memory-hard dataset'],
|
|
['RTX 5090 ran two generated programs', 'RTX 5090 matches Apple silicon hash for hash, 192 of 192'],
|
|
['Fuzzing', 'Fuzzing: 1.3 million hashes, zero GPU to CPU mismatches'],
|
|
['Second hostile review', 'Second hostile review: two fatal flaws found and fixed'],
|
|
['First prototype of the random-program', 'First prototype of the lottery hash on Apple silicon'],
|
|
['Igneum named', 'Igneum named. Litepaper, brand and wallet designs published'],
|
|
['Finality floor', 'Finality floor raised to two thirds of all weight'],
|
|
['First hourly program swap on the live devnet', 'First live hourly swap: no pause on Mac, NVIDIA or AMD'],
|
|
['First finality lock on the live devnet', 'First live finality lock: 77.4% of weight, 17 voters'],
|
|
['First machine on the Igneum Miner app', 'First machine on the one-click app: a 5090 at 118 MH/s'],
|
|
['Difficulty rule v2 activated on the live devnet', 'Difficulty rule v2 switched on the live devnet by height, no fresh chain'],
|
|
['First outside machine on the devnet', 'First outside machine: a friend\'s Mac mining through the app'],
|
|
['Devnet v4 cut-over', 'Devnet v4 live: generator v2, two-thirds floor, fresh chain'],
|
|
];
|
|
function shortTitle(t) {
|
|
for (const [k, v] of SHORT) if (t.startsWith(k)) return v;
|
|
let s = t.split(/[:;]/)[0].trim();
|
|
if (s.length < 16) s = t.split(/[,;]/).slice(0, 2).join(',').trim();
|
|
if (s.length > 72) s = s.slice(0, 69).replace(/\s+\S*$/, '') + '…';
|
|
return s.replace(/[.,]$/, '');
|
|
}
|
|
const entries = [];
|
|
for (const m of src.matchAll(/^##+\s+(\d{1,2}\s+\w+\s+\d{4})[,:]?\s*(.*)$/gm)) {
|
|
const d = new Date(m[1] + ' 12:00:00 UTC'); if (isNaN(d)) continue;
|
|
const iso = d.toISOString().slice(0, 10);
|
|
const text = m[2].replace(/\(.*?\)/g, '').replace(/\s+/g, ' ').trim();
|
|
if (text) { const t = text.charAt(0).toUpperCase() + text.slice(1); entries.push({ date: iso, text: t, short: shortTitle(t) }); }
|
|
}
|
|
const jp = join(here, 'journey.json');
|
|
const j = JSON.parse(readFileSync(jp, 'utf8'));
|
|
const seen = new Set(j.log.map(e => e.date + '|' + e.text));
|
|
for (const e of entries.reverse()) { const k = e.date + '|' + e.text; if (!seen.has(k)) { j.log.unshift(e); seen.add(k); } }
|
|
for (const e of j.log) e.short = shortTitle(e.text);
|
|
j.log.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
|
|
j.log = j.log.slice(0, 40);
|
|
j.updated = j.log[0] ? j.log[0].date : j.updated;
|
|
writeFileSync(jp, JSON.stringify(j, null, 2) + '\n');
|
|
built.push('journey.json (' + j.log.length + ' entries)');
|
|
}
|
|
|
|
// The hand-written pages: shared head, nav (with the active link marked) and footer injected in place; the homepage also
|
|
// gets journey.json inlined so the phases and the log render without a fetch and without a layout shift.
|
|
const journey = JSON.parse(readFileSync(join(here, 'journey.json'), 'utf8'));
|
|
const PAGES = [['index.html', ''], ['litepaper.html', 'litepaper'], ['live.html', 'live'], ['evidence.html', 'evidence'], ['404.html', '']];
|
|
for (const [file, active] of PAGES) {
|
|
const p = join(here, file);
|
|
if (!existsSync(p)) throw new Error(`missing page ${file}`);
|
|
let html = readFileSync(p, 'utf8');
|
|
html = inject(html, 'head', HEAD, file);
|
|
html = inject(html, 'nav', navFor(active), file);
|
|
html = inject(html, 'footer', FOOT, file);
|
|
if (file === 'index.html') html = inject(html, 'journey', `<script type="application/json" id="journey-data">${JSON.stringify(journey).replace(/</g, '\\u003c')}</script>`, file);
|
|
writeFileSync(p, html);
|
|
built.push(file);
|
|
}
|
|
console.log('built: ' + built.join(', '));
|