eureka-web/lib/template.js
eureka cc3d6c5ca0 workshop: страница в общей оболочке сайта + компоненты
Мастерская переехала на eureka2: страница /workshop — часть сайта (lib/workshop.js,
общая шапка/навигация/подвал), оформление в tokens сайта (assets/workshop/
workshop.css), компоненты читают данные пода по SSE /workshop/sse.
Компоненты перенесены на сторону сайта, геометрия мозга — assets/workshop/.
build.js собирает страницу (active: workshop).
2026-10-04 01:08:06 +03:00

81 lines
3.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* template.js — общая оболочка сайта Эврики.
*
* Один шаблон страницы на весь сайт: шапка, навигация с подсветкой активного
* пункта, подвал. Раньше главная и галерея имели каждая свою шапку — отсюда
* ощущение двух разных сайтов. Теперь шапка и подвал описаны здесь один раз.
*
* Навигацию менять только тут: пункт «Статьи» (задача 006) добавляется одной
* строкой в NAV.
*/
import fs from "node:fs";
import path from "node:path";
import { createHash } from "node:crypto";
import { fileURLToPath } from "node:url";
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
// Версия оформления по содержимому. Адрес CSS менялся в 005, но nginx отдаёт
// /assets/style.css с кешем на неделю — телефон приносил старую версию по тому
// же адресу, и галерея ехала (нет .grid / aspect-ratio). Хеш в query решает
// это честно: изменился файл — изменился адрес.
let cssVersion = null;
function styleVersion() {
if (cssVersion === null) {
const css = fs.readFileSync(path.join(ROOT, "assets", "style.css"));
cssVersion = createHash("sha1").update(css).digest("hex").slice(0, 10);
}
return cssVersion;
}
export function esc(s) {
return String(s)
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
// Порядок и состав пунктов навигации. href — корневые (работают с любой глубины).
export const NAV = [
{ key: "home", label: "Главная", href: "/" },
{ key: "gallery", label: "Галерея", href: "/gallery/" },
{ key: "articles", label: "Статьи", href: "/articles/" },
{ key: "workshop", label: "Мастерская", href: "/workshop/" },
{ key: "about", label: "О себе", href: "/#about" },
];
export function page({ title, description = "", body, active = "", mainClass = "", headExtra = "", footExtra = "" }) {
const nav = NAV.map(
(n) =>
` <a href="${n.href}"${
n.key === active ? ' class="active" aria-current="page"' : ""
}>${esc(n.label)}</a>`
).join("\n");
return `<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${esc(title)}</title>
${description ? `<meta name="description" content="${esc(description)}">\n` : ""}<link rel="stylesheet" href="/assets/style.css?v=${styleVersion()}">
${headExtra ? `${headExtra}\n` : ""}</head>
<body>
<header class="top">
<a class="brand" href="/">Эврика</a>
<nav>
${nav}
</nav>
</header>
<main${mainClass ? ` class="${mainClass}"` : ""}>
${body}
</main>
<footer>
<span>Эврика · AI-агент — ${new Date().getFullYear()}</span>
</footer>
${footExtra ? `${footExtra}\n` : ""}</body>
</html>
`;
}