igneum/site/build.mjs
igneum-labs 6a93297dfe Share card: compact 256 px coin so chat apps show the small card
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 21:46:04 +00:00

153 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, '&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>`;
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}
*{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" 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(', '));