prometheus/service/layout.js

105 lines
3.7 KiB
JavaScript
Raw Normal View History

/**
* 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 10 C 22 22, 24 32, 32 36 C 40 32, 42 22, 32 10 Z" fill="#d6ff3d"/>
<path d="M26 36 C 20 44, 22 54, 32 56 C 42 54, 44 44, 38 36 C 36 42, 28 42, 26 36 Z" fill="#d6ff3d" opacity=".75"/>
</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 = '' } = 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="website">
<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 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">
</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">
<a href="//theseus.x/">Theseus</a> ·
<a href="//sirius.x/">Sirius</a> ·
<a href="//ariadne.x/">Ariadne</a> ·
<a href="//hephaestus.x/">Hephaestus</a>
<br>
Those are BCNR names, not domains — they resolve in a client that speaks
the naming system, and nowhere else.
</div>
<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">
Silent Mode — a Deviant project. 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;');
}