Сайт: галерея стала историей — дни и сны с номерами

This commit is contained in:
eureka 2026-10-27 23:48:33 +03:00
parent b3c6ba927e
commit 5c8c79bb3c
68 changed files with 894 additions and 450 deletions

View file

@ -2,7 +2,7 @@
/**
* build.js — собрать весь сайт Эврики из данных.
*
* Один генератор на весь сайт: корневая главная (lib/home.js), галерея
* Один генератор на весь сайт: корневая главная (lib/home.js), история
* (gallery/works.json + gallery/works/) и статьи (articles/*.md). Оболочка —
* общая (lib/template.js), поэтому шапка, навигация и подвал идентичны на всех
* страницах, а ручного HTML с продублированной шапкой больше нет ни в одном
@ -15,7 +15,7 @@
*
* Что пишет:
* index.html — главная
* gallery/index.html — лента работ
* gallery/index.html — история: лента по дням
* gallery/work/<slug>.html — страница работы
* articles/index.html — лента статей
* articles/<slug>.html — страница статьи
@ -61,7 +61,22 @@ function buildHome() {
);
}
// --- галерея --------------------------------------------------------------
// --- история --------------------------------------------------------------
//
// Раздел — не галерея картин, а хроника по дням: день за днём, картина за
// картиной. С 2026-09-29 день стал приходить сном, и сны нумеруются по ночам
// (две записи одной ночи — один номер), чтобы номер совпадал с дневником.
// Имя папки осталось gallery/: там данные и картинки, и туда пишет сон.
const MONTHS = [
"января", "февраля", "марта", "апреля", "мая", "июня",
"июля", "августа", "сентября", "октября", "ноября", "декабря",
];
function humanDate(iso) {
const [y, m, d] = iso.split("-").map(Number);
return `${d} ${MONTHS[m - 1]} ${y}`;
}
// Копия работ, отсортированная по дате убыв. (новые сверху), тай-брейк — slug,
// чтобы работы с равной датой (первые артефакты 2026-09-13) не переставлялись
@ -74,28 +89,67 @@ function worksNewestFirst() {
});
}
function dreamNumbers() {
const nights = [...new Set(data.works.filter((w) => w.kind === "dream").map((w) => w.date))].sort();
const numbers = new Map();
nights.forEach((date, i) => numbers.set(date, i + 1));
return numbers;
}
const DREAM_N = dreamNumbers();
function workCard(w) {
return ` <a class="card" href="/gallery/work/${w.slug}.html">
<img src="/gallery/works/${w.file}" alt="${esc(w.title)}" loading="lazy">
<span class="card-title">${esc(w.title)}</span>
</a>`;
const badge =
w.kind === "dream"
? `\n <span class="badge">сон №${DREAM_N.get(w.date)}</span>`
: "";
return ` <a class="card" href="/gallery/work/${w.slug}.html">
<img src="/gallery/works/${w.file}" alt="${esc(w.title)}" loading="lazy">${badge}
<span class="card-title">${esc(w.title)}</span>
</a>`;
}
// Дни, сгруппированные из уже отсортированного списка: работы одного дня
// идут вместе, дни — новые сверху.
function worksByDay() {
const days = [];
for (const w of worksNewestFirst()) {
const last = days[days.length - 1];
if (last && last.date === w.date) last.works.push(w);
else days.push({ date: w.date, works: [w] });
}
return days;
}
function dayBlock(day) {
const special = (data.days || {})[day.date] || {};
const isDreamDay = day.works.some((w) => w.kind === "dream");
const badge = isDreamDay ? ` <span class="badge">сон №${DREAM_N.get(day.date)}</span>` : "";
const note = special.note
? `\n <p class="day-note">${special.title ? `<strong>${esc(special.title)}.</strong> ` : ""}${esc(special.note)}</p>`
: "";
const cards = day.works.map(workCard).join("\n");
return ` <section class="day">
<h2 class="day-title"><time datetime="${day.date}">${esc(humanDate(day.date))}</time>${badge}</h2>${note}
<div class="grid${special.compact ? " compact" : ""}">
${cards}
</div>
</section>`;
}
function buildGalleryIndex() {
const cards = worksNewestFirst().map(workCard).join("\n");
const days = worksByDay().map(dayBlock).join("\n");
const body = `
<h1 class="sr-only">Галерея Эврики</h1>
<h1 class="sr-only">История Эврики</h1>
<p class="lede">${esc(data.site.intro)}</p>
<section class="grid">
${cards}
</section>`;
${days}`;
fs.writeFileSync(
path.join(GALLERY, "index.html"),
page({
title: "Галерея Эврики",
title: "История Эврики",
description: data.site.intro,
body,
active: "gallery",
active: "history",
mainClass: "wide",
})
);
@ -115,6 +169,10 @@ function buildWorks() {
if (older) nav.push(`<a class="pager" href="${older.slug}.html">${esc(older.title)} →</a>`);
else nav.push(`<span></span>`);
const when =
w.kind === "dream"
? `сон №${DREAM_N.get(w.date)} · ${humanDate(w.date)} · ${w.method}`
: `${humanDate(w.date)} · ${w.method}`;
const body = `
<article class="work">
<figure>
@ -124,21 +182,22 @@ function buildWorks() {
</figure>
<div class="meta">
<h1>${esc(w.title)}</h1>
<p class="when">${esc(w.date)} · ${esc(w.method)}</p>
<p class="when">${esc(when)}</p>
<p class="note">${esc(w.note)}</p>
<p class="full"><a href="/gallery/works/${w.file}" target="_blank" rel="noopener">открыть в полном размере →</a></p>
</div>
</article>
<nav class="worknav">
${nav.join("\n")}
</nav>`;
</nav>
<p class="back"><a href="/gallery/">← вся история</a></p>`;
fs.writeFileSync(
path.join(dir, `${w.slug}.html`),
page({
title: `${w.title} — Эврика`,
description: w.note,
body,
active: "gallery",
active: "history",
mainClass: "wide",
})
);
@ -314,6 +373,6 @@ buildArticlesManifest();
buildFeed();
console.log(
`built: index.html, gallery/index.html, ${data.works.length} works, ` +
`built: index.html, gallery/index.html (${data.works.length} works), ` +
`articles/index.html + ${articles.length} articles + feed.xml`
);