Мастерская переехала на eureka2: страница /workshop — часть сайта (lib/workshop.js, общая шапка/навигация/подвал), оформление в tokens сайта (assets/workshop/ workshop.css), компоненты читают данные пода по SSE /workshop/sse. Компоненты перенесены на сторону сайта, геометрия мозга — assets/workshop/. build.js собирает страницу (active: workshop).
81 lines
3.2 KiB
JavaScript
81 lines
3.2 KiB
JavaScript
/**
|
||
* 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, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
|
||
// Порядок и состав пунктов навигации. 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>
|
||
`;
|
||
}
|