prometheus/service/layout.js
Local Dev a9ca956c2e prometheus: standalone identity + first-wave Silent Mode listings
Four changes, one coherent pass:

1. Prometheus gets its own flame mark (SVG, not an emoji). Used in the
   topnav brand, in the hero at 88 px, and as the favicon. Replaces the
   🔥 glyph in the service and in site/prometheus/index.html.

2. silentmode.st/prometheus is now a brief introduction with two big
   buttons (Open prometheus.x · Open apps.silentmode.st mirror) and a
   short "what / who / status" block, instead of the full category
   grid that duplicated what prometheus.x already renders. The /prometheus
   page's job on silentmode.st is "point you at the real site," not host
   the content.

3. Footer on the Prometheus service drops the "Silent Mode — a Deviant
   project" byline and reframes the sibling-x links as "Also live in the
   decentralized web:" so Prometheus reads as its own site rather than a
   Silent Mode subpage. Keeps the attribution via the MPL-2.0 + source
   link on the final line. aegis.x and pithos.x added to the sibling row.

4. Four Silent Mode apps seeded as first-wave listings
   (scripts/seed-listings.json + scripts/seed.mjs). Theseus Navigator,
   Ariadne's Thread (Windows + Android), Pithos. Each carries the
   project's own SVG icon from public/icons/, pulled from the project's
   own site. The seed is a direct-DB insert (not through /api/listing)
   with signer_address = chipnet operator and signature = sentinel
   `SM_OPERATOR_SEED`; scan_report is an operator-attested envelope
   signed by the service key so the display pipeline is unchanged. Run
   with `node scripts/seed.mjs`; idempotent (skips existing (publisher,
   slug)).

   Deliberately NOT seeded: Theseus extensions that have no standalone
   form — pdf-editor, docx-editor, shield-blocker, bergamot provider.
   They ship via Theseus's own OTA, not as Prometheus listings. Aegis
   is in the same boat today (a Theseus bundled add-on without a
   standalone binary); its icon is included in public/icons/ for when
   it has its own distribution.

Real file sizes + SHAs come from the current dl.silentmode.st manifest
(theseus 0.3.89, ariadne resolver 0.1.13, ariadne android 0.22,
pithos-desktop 0.3.26). When APS1 lands cross-session, these listings
migrate to real on-chain records signed by each project's own publisher
key and the sentinel values go away.
2026-10-09 01:23:21 +02:00

112 lines
4.2 KiB
JavaScript

/**
* Shared document shell for every rendered page.
*
* A page function returns `{title, description, body}`; `layout()` wraps it in
* the `<html>` document with the right meta, topnav, and footer. The brand mark
* is inlined as SVG so pages do not fetch an image to render a 20-pixel logo.
*/
const BRAND_MARK = `<svg width="18" height="18" viewBox="0 0 64 64" style="vertical-align:-4px;margin-right:6px" aria-hidden="true">
<rect width="64" height="64" rx="12" fill="#0b0e14"/>
<path d="M32 6 C 38 18 46 24 46 36 C 46 48 40 56 32 56 C 24 56 18 48 18 36 C 18 28 24 22 20 14 C 26 18 30 20 32 6 Z" fill="#d6ff3d"/>
<path d="M32 20 C 36 28 40 32 40 40 C 40 48 36 52 32 52 C 28 52 24 48 24 40 C 24 34 28 30 32 20 Z" fill="#0b0e14"/>
<circle cx="32" cy="44" r="3" fill="#d6ff3d"/>
</svg>`;
const NAV = [
{ href: '/browse', label: 'Browse' },
{ href: '/submit', label: 'Submit' },
{ href: '/publish', label: 'How publishing works' },
{ href: '/boundaries', label: 'Boundaries' },
];
/**
* Compose a full HTML document from a page.
*
* @param {{title: string, description: string, body: string, here?: string}} page
* @returns {string} the HTML document, ready to send.
*/
export function layout(page) {
const {
title, description, body, here = '',
ogType = 'website',
ogImage = 'https://prometheus.x/static/favicon.svg',
headExtra = '',
} = page;
// The canonical URL is always on prometheus.x — never the IP+port the server
// actually listens on, and never the SEO mirror at apps.silentmode.st, even
// when the mirror is what served this request. Readers who followed a link
// from search land on the mirror; the canonical sends search engines back to
// the real address and keeps the scorecard on prometheus.x.
const canonical = `https://prometheus.x${here}`;
const navHtml = NAV.map(n => {
const classAttr = n.href === here ? ' class="here"' : '';
return `<a href="${n.href}"${classAttr}>${n.label}</a>`;
}).join('\n ');
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${escapeHtml(title)} — Prometheus</title>
<meta name="description" content="${escapeHtml(description)}">
<link rel="canonical" href="${canonical}">
<meta property="og:type" content="${escapeHtml(ogType)}">
<meta property="og:site_name" content="Prometheus">
<meta property="og:title" content="${escapeHtml(title)} — Prometheus">
<meta property="og:description" content="${escapeHtml(description)}">
<meta property="og:url" content="${canonical}">
<meta property="og:image" content="${escapeHtml(ogImage)}">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="stylesheet" href="/static/style.css?v=1">
${headExtra}
</head>
<body>
<nav class="topnav">
<a class="brand" href="/">${BRAND_MARK}Prometheus<span class="g">.X</span></a>
${navHtml}
</nav>
${body}
<footer>
<div class="fine">
Named for Prometheus, who gave fire to humans without asking the gods.
Distinct from the well-known Prometheus monitoring project — both take
the name from the same myth; neither has anything to do with the other.
</div>
<div class="fine" style="margin-top:1.4rem;color:var(--dim);font-size:12px">
Also live in the decentralized web:
<a href="//theseus.x/">theseus.x</a> ·
<a href="//sirius.x/">sirius.x</a> ·
<a href="//ariadne.x/">ariadne.x</a> ·
<a href="//aegis.x/">aegis.x</a> ·
<a href="//pithos.x/">pithos.x</a> ·
<a href="//hephaestus.x/">hephaestus.x</a>
</div>
<div class="fine" style="color:var(--dim);font-size:11px;margin-top:.6rem">
Source at <a href="https://code.silentmode.st/silentmode/prometheus">code.silentmode.st/silentmode/prometheus</a> · MPL-2.0
</div>
</footer>
</body>
</html>`;
}
/**
* Minimal HTML escape for text that goes into attributes or text nodes.
* Not for anything that must survive as markup; page bodies ship as-is.
*/
export function escapeHtml(s) {
return String(s)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}