Measured at 375, 768, 1280 and 1680 with headless Chrome. One token set in all four stylesheets: container 1200 px with a clamp(16px,4vw,32px) gutter, section rhythm clamp(56px,8vw,96px), cards clamp(18px,3vw,28px) padding and 18 px radius, tiles 18 by 20 px padding and 14 px radius, 24 px card gap and 12 px tile gap, headings h1 clamp(32,5.5vw,56) h2 clamp(28,4.2vw,44) h3 clamp(18,2vw,22), tile values clamp(20,2.2vw,26). Home: the live strip spans the full hero width with economics-spec tiles; stat strip and economics tiles share one spec; journey phases and log take the tile and card boxes; inline padding-top overrides removed, consecutive dark sections share one gap; every nav anchor lands the heading 11 px under the sticky bar at every width (padded sections subtract their own padding). Live: head, strip, cards and gaps on the tokens; four tiles a row, two on phones; network name no longer clips. Litepaper: real gutter instead of a fixed 16 px, cover, layout, tables, figures, pull quotes and stat tiles on the tokens, pager stacks on phones. Engineering log: same tokens, long code tokens wrap so phones no longer scroll sideways, duplicate h1 hidden. Copy shortened so no label orphans at any width: stat strip labels, the economics tiles, the strip tile labels, the litepaper stat tiles, the mining-program eyebrow. The light-client card block is untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
160 lines
11 KiB
JavaScript
160 lines
11 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 { 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 };
|
|
}
|
|
|
|
const mark = `<svg viewBox="0 0 100 100" width="26" height="26" aria-hidden="true"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></svg>`;
|
|
const ORIGIN = 'https://igneum.network';
|
|
const svgIcon = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='48' fill='%230C0C0E'/%3E%3Ccircle cx='50' cy='50' r='45' fill='none' stroke='%23F2541B' stroke-width='6'/%3E%3Cg transform='translate(24 24) scale(0.52)'%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">
|
|
<meta property="og:image:width" content="256">
|
|
<meta property="og:image:height" content="256">
|
|
<meta property="og:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
|
|
<meta name="twitter:card" content="summary">
|
|
<meta name="twitter:title" content="${t}">
|
|
<meta name="twitter:description" content="${d}">
|
|
<meta name="twitter:image" content="${ORIGIN}/og.png">
|
|
<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="/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 } = {}) {
|
|
const nav = toc.filter(t => t.lvl === 2).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)}
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Unbounded:wght@500;700;900&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap">
|
|
<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:'IBM Plex Sans',system-ui,sans-serif;font-size:16px;line-height:1.6;padding:0}
|
|
a{color:var(--ember);text-decoration:none}a:hover{text-decoration:underline}
|
|
.wrap{max-width:var(--max);margin:0 auto;padding-inline:var(--gutter)}
|
|
header{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:16px;padding-block:18px;border-bottom:1px solid var(--line)}
|
|
.brand{display:flex;align-items:center;gap:10px;color:var(--bone)}.brand b{font-family:'Unbounded',sans-serif;font-weight:900;letter-spacing:.06em;font-size:18px}
|
|
header nav{display:flex;flex-wrap:wrap;gap:18px;font-size:14px}header nav a{color:var(--ink-2)}
|
|
h1{font-family:'Unbounded',sans-serif;font-weight:900;font-size:var(--fs-h1);line-height:1.05;margin:var(--head) 0 12px}
|
|
.note{color:var(--ash);font-size:15px;max-width:72ch;margin-bottom:32px}
|
|
.layout{display:grid;grid-template-columns:minmax(0,1fr);gap:32px;padding-bottom:var(--sec)}
|
|
@media(min-width:960px){.layout{grid-template-columns:240px minmax(0,1fr);gap:56px}}
|
|
.toc{align-self:start;font-size:14px}@media(min-width:960px){.toc{position:sticky;top:24px}}
|
|
.toc ol{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px}.toc a{display:block;padding:7px 10px;border-radius:8px;color:var(--ink-2)}.toc a:hover{background:var(--graphite);text-decoration:none;color:var(--bone)}
|
|
article{max-width:78ch;min-width:0}
|
|
article h1{display:none}
|
|
article h2{font-family:'Unbounded',sans-serif;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:'Unbounded',sans-serif;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:'IBM Plex Mono',monospace;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:'IBM Plex Mono',monospace;font-size:13px;line-height:1.55;overflow-x:auto}
|
|
.tbl{overflow-x:auto;margin:18px 0 24px;border:1px solid var(--line);border-radius:var(--card-r);background:var(--graphite)}
|
|
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:'IBM Plex Mono',monospace;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}
|
|
footer{border-top:1px solid var(--line);padding-block:32px 48px;font-size:13px;color:var(--ash)}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<header><a class="brand" href="/">${mark}<b>IGNEUM</b></a><nav><a href="/">Home</a><a href="/litepaper">Litepaper</a><a href="/bench">Engineering log</a><a href="https://github.com/igneum-network" rel="noopener">GitHub, code opens Jan 2027</a></nav></header>
|
|
<h1>${esc(heading)}</h1>
|
|
<p class="note">${note}</p>
|
|
<div class="layout">
|
|
<nav class="toc" aria-label="Contents"><ol>${nav}</ol></nav>
|
|
<article>${bodyHtml}</article>
|
|
</div>
|
|
<footer>© 2026 Igneum. Generated from the repository at build time. Nothing on this page is an offer to sell anything.</footer>
|
|
</div>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
let built = [];
|
|
if (existsSync(join(docs, 'bench-log.md'))) {
|
|
const src = 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
|
|
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) entries.push({ date: iso, text: text.charAt(0).toUpperCase() + text.slice(1) });
|
|
}
|
|
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); } }
|
|
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)');
|
|
}
|
|
console.log('built: ' + built.join(', '));
|