igneum/site/build.mjs
igneum-josh 14ef6b3880 Take the FUD ledger off the public site
The ledger stays in the repository as an internal working document while it carries open items. /ledger now redirects to the homepage. Promotion-rules entry rewritten without any founder location.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 19:18:24 +01:00

125 lines
8.9 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
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>`;
function page(title, desc, bodyHtml, toc, note) {
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">
<title>${esc(title)}</title>
<meta name="description" content="${esc(desc)}">
<meta name="theme-color" content="#0C0C0E">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cpolygon points='50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34' fill='%23F2541B'/%3E%3C/svg%3E">
<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}
*{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-inline:clamp(16px,4vw,32px)}
a{color:var(--ember);text-decoration:none}a:hover{text-decoration:underline}
.wrap{max-width:1180px;margin:0 auto}
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:clamp(28px,5vw,48px);line-height:1.05;margin:36px 0 10px}
.note{color:var(--ash);font-size:15px;max-width:72ch;margin-bottom:28px}
.layout{display:grid;grid-template-columns:minmax(0,1fr);gap:32px;padding-bottom:80px}
@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:6px 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 h2{font-family:'Unbounded',sans-serif;font-weight:700;font-size:clamp(20px,2.6vw,26px);margin:44px 0 12px;padding-top:24px;border-top:1px solid var(--line)}
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}
code{font-family:'IBM Plex Mono',monospace;font-size:.92em;background:var(--graphite);padding:1px 5px;border-radius:4px}
pre{margin:0 0 16px;padding:14px 16px;background:var(--graphite);border-radius:12px;font-family:'IBM Plex Mono',monospace;font-size:13px;line-height:1.55;overflow-x:auto}
.tbl{overflow-x:auto;margin:14px 0 20px;border:1px solid var(--line);border-radius:12px;background:var(--graphite)}
table{border-collapse:collapse;width:100%;min-width:560px;font-size:14px}th,td{padding:10px 12px;text-align:left;vertical-align:top;border-bottom:1px solid var(--line)}
th{font-family:'IBM Plex Mono',monospace;font-size:11px;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:24px 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/igneum" rel="noopener">GitHub</a></nav></header>
<h1>${esc(title)}</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('Engineering log', 'Every Igneum benchmark and test, with the commands that produced it.', 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.'));
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(', '));