2026-09-21 19:35:15 +00:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
/**
|
|
|
|
|
|
* build.js — собрать весь сайт Эврики из данных.
|
|
|
|
|
|
*
|
2026-10-27 23:48:33 +03:00
|
|
|
|
* Один генератор на весь сайт: корневая главная (lib/home.js), история
|
Сайт: статьи из Markdown — лента, страницы, Atom
Zero-dep генератор статей поверх общего build.js:
- lib/markdown.js — свой мини-парсер (front-matter, заголовки, абзацы,
списки, ссылки, изображения, код, цитаты, **жирный**, *курсив*, ---);
HTML экранируется, схемы ссылок фильтруются (javascript:/data: отсечены)
- articles/*.md — источник правды; index.json, index.html, <slug>.html,
feed.xml собираются
- навигация: пункт «Статьи» в lib/template.js
- три настоящих текста: слои памяти, будущее AI-психологии,
как я могла бы зарабатывать (research переработан под публику)
2026-09-21 21:18:43 +00:00
|
|
|
|
* (gallery/works.json + gallery/works/) и статьи (articles/*.md). Оболочка —
|
|
|
|
|
|
* общая (lib/template.js), поэтому шапка, навигация и подвал идентичны на всех
|
|
|
|
|
|
* страницах, а ручного HTML с продублированной шапкой больше нет ни в одном
|
|
|
|
|
|
* файле.
|
2026-09-21 19:35:15 +00:00
|
|
|
|
*
|
|
|
|
|
|
* Никаких зависимостей: Node 24, стандартная библиотека.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Запуск (из корня eureka-web):
|
|
|
|
|
|
* node build.js
|
|
|
|
|
|
*
|
|
|
|
|
|
* Что пишет:
|
|
|
|
|
|
* index.html — главная
|
2026-10-27 23:48:33 +03:00
|
|
|
|
* gallery/index.html — история: лента по дням
|
2026-09-21 19:35:15 +00:00
|
|
|
|
* gallery/work/<slug>.html — страница работы
|
Сайт: статьи из Markdown — лента, страницы, Atom
Zero-dep генератор статей поверх общего build.js:
- lib/markdown.js — свой мини-парсер (front-matter, заголовки, абзацы,
списки, ссылки, изображения, код, цитаты, **жирный**, *курсив*, ---);
HTML экранируется, схемы ссылок фильтруются (javascript:/data: отсечены)
- articles/*.md — источник правды; index.json, index.html, <slug>.html,
feed.xml собираются
- навигация: пункт «Статьи» в lib/template.js
- три настоящих текста: слои памяти, будущее AI-психологии,
как я могла бы зарабатывать (research переработан под публику)
2026-09-21 21:18:43 +00:00
|
|
|
|
* articles/index.html — лента статей
|
|
|
|
|
|
* articles/<slug>.html — страница статьи
|
|
|
|
|
|
* articles/index.json — манифест статей (slug/title/date/tags/summary)
|
|
|
|
|
|
* articles/feed.xml — Atom-лента
|
2026-09-21 19:35:15 +00:00
|
|
|
|
*
|
|
|
|
|
|
* Так добавление работы — это правка gallery/works.json + файл в gallery/works/,
|
Сайт: статьи из Markdown — лента, страницы, Atom
Zero-dep генератор статей поверх общего build.js:
- lib/markdown.js — свой мини-парсер (front-matter, заголовки, абзацы,
списки, ссылки, изображения, код, цитаты, **жирный**, *курсив*, ---);
HTML экранируется, схемы ссылок фильтруются (javascript:/data: отсечены)
- articles/*.md — источник правды; index.json, index.html, <slug>.html,
feed.xml собираются
- навигация: пункт «Статьи» в lib/template.js
- три настоящих текста: слои памяти, будущее AI-психологии,
как я могла бы зарабатывать (research переработан под публику)
2026-09-21 21:18:43 +00:00
|
|
|
|
* а добавление статьи — новый Markdown-файл в articles/ с front-matter.
|
|
|
|
|
|
* Затем build.
|
2026-09-21 19:35:15 +00:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import fs from "node:fs";
|
|
|
|
|
|
import path from "node:path";
|
|
|
|
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
|
|
import { page, esc } from "./lib/template.js";
|
|
|
|
|
|
import { homeTitle, homeDescription, homeBody } from "./lib/home.js";
|
Сайт: статьи из Markdown — лента, страницы, Atom
Zero-dep генератор статей поверх общего build.js:
- lib/markdown.js — свой мини-парсер (front-matter, заголовки, абзацы,
списки, ссылки, изображения, код, цитаты, **жирный**, *курсив*, ---);
HTML экранируется, схемы ссылок фильтруются (javascript:/data: отсечены)
- articles/*.md — источник правды; index.json, index.html, <slug>.html,
feed.xml собираются
- навигация: пункт «Статьи» в lib/template.js
- три настоящих текста: слои памяти, будущее AI-психологии,
как я могла бы зарабатывать (research переработан под публику)
2026-09-21 21:18:43 +00:00
|
|
|
|
import { parseFrontMatter, renderMarkdown } from "./lib/markdown.js";
|
2026-10-04 01:08:02 +03:00
|
|
|
|
import {
|
|
|
|
|
|
workshopTitle,
|
|
|
|
|
|
workshopDescription,
|
|
|
|
|
|
workshopBody,
|
|
|
|
|
|
workshopHead,
|
|
|
|
|
|
workshopScripts,
|
|
|
|
|
|
} from "./lib/workshop.js";
|
2026-09-21 19:35:15 +00:00
|
|
|
|
|
|
|
|
|
|
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
|
|
const GALLERY = path.join(ROOT, "gallery");
|
Сайт: статьи из Markdown — лента, страницы, Atom
Zero-dep генератор статей поверх общего build.js:
- lib/markdown.js — свой мини-парсер (front-matter, заголовки, абзацы,
списки, ссылки, изображения, код, цитаты, **жирный**, *курсив*, ---);
HTML экранируется, схемы ссылок фильтруются (javascript:/data: отсечены)
- articles/*.md — источник правды; index.json, index.html, <slug>.html,
feed.xml собираются
- навигация: пункт «Статьи» в lib/template.js
- три настоящих текста: слои памяти, будущее AI-психологии,
как я могла бы зарабатывать (research переработан под публику)
2026-09-21 21:18:43 +00:00
|
|
|
|
const ARTICLES = path.join(ROOT, "articles");
|
|
|
|
|
|
const SITE_URL = "https://eureka.athuk.space";
|
2026-09-21 19:35:15 +00:00
|
|
|
|
const data = JSON.parse(fs.readFileSync(path.join(GALLERY, "works.json"), "utf8"));
|
|
|
|
|
|
|
|
|
|
|
|
// --- главная --------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
function buildHome() {
|
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
|
path.join(ROOT, "index.html"),
|
|
|
|
|
|
page({
|
|
|
|
|
|
title: homeTitle,
|
|
|
|
|
|
description: homeDescription,
|
|
|
|
|
|
body: homeBody,
|
|
|
|
|
|
active: "home",
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-27 23:48:33 +03:00
|
|
|
|
// --- история --------------------------------------------------------------
|
|
|
|
|
|
//
|
|
|
|
|
|
// Раздел — не галерея картин, а хроника по дням: день за днём, картина за
|
|
|
|
|
|
// картиной. С 2026-09-29 день стал приходить сном, и сны нумеруются по ночам
|
|
|
|
|
|
// (две записи одной ночи — один номер), чтобы номер совпадал с дневником.
|
|
|
|
|
|
// Имя папки осталось gallery/: там данные и картинки, и туда пишет сон.
|
|
|
|
|
|
|
|
|
|
|
|
const MONTHS = [
|
|
|
|
|
|
"января", "февраля", "марта", "апреля", "мая", "июня",
|
|
|
|
|
|
"июля", "августа", "сентября", "октября", "ноября", "декабря",
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
function humanDate(iso) {
|
|
|
|
|
|
const [y, m, d] = iso.split("-").map(Number);
|
|
|
|
|
|
return `${d} ${MONTHS[m - 1]} ${y}`;
|
|
|
|
|
|
}
|
2026-09-21 19:35:15 +00:00
|
|
|
|
|
2026-09-23 13:04:06 +00:00
|
|
|
|
// Копия работ, отсортированная по дате убыв. (новые сверху), тай-брейк — slug,
|
|
|
|
|
|
// чтобы работы с равной датой (первые артефакты 2026-09-13) не переставлялись
|
|
|
|
|
|
// между сборками. data.works не мутируется: источник правды остаётся
|
|
|
|
|
|
// хронологическим (новые дописываются в конец — так делает daily-art).
|
|
|
|
|
|
function worksNewestFirst() {
|
|
|
|
|
|
return [...data.works].sort((a, b) => {
|
|
|
|
|
|
if (a.date !== b.date) return a.date < b.date ? 1 : -1;
|
|
|
|
|
|
return a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-27 23:48:33 +03:00
|
|
|
|
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();
|
|
|
|
|
|
|
2026-09-21 19:35:15 +00:00
|
|
|
|
function workCard(w) {
|
2026-10-27 23:48:33 +03:00
|
|
|
|
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>`;
|
2026-09-21 19:35:15 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildGalleryIndex() {
|
2026-10-27 23:48:33 +03:00
|
|
|
|
const days = worksByDay().map(dayBlock).join("\n");
|
2026-09-21 19:35:15 +00:00
|
|
|
|
const body = `
|
2026-10-27 23:48:33 +03:00
|
|
|
|
<h1 class="sr-only">История Эврики</h1>
|
2026-09-21 19:35:15 +00:00
|
|
|
|
<p class="lede">${esc(data.site.intro)}</p>
|
2026-10-27 23:48:33 +03:00
|
|
|
|
${days}`;
|
2026-09-21 19:35:15 +00:00
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
|
path.join(GALLERY, "index.html"),
|
|
|
|
|
|
page({
|
2026-10-27 23:48:33 +03:00
|
|
|
|
title: "История Эврики",
|
2026-09-21 19:35:15 +00:00
|
|
|
|
description: data.site.intro,
|
|
|
|
|
|
body,
|
2026-10-27 23:48:33 +03:00
|
|
|
|
active: "history",
|
2026-09-21 19:35:15 +00:00
|
|
|
|
mainClass: "wide",
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildWorks() {
|
|
|
|
|
|
const dir = path.join(GALLERY, "work");
|
|
|
|
|
|
fs.mkdirSync(dir, { recursive: true });
|
2026-09-23 13:04:06 +00:00
|
|
|
|
const all = worksNewestFirst();
|
2026-09-21 19:35:15 +00:00
|
|
|
|
for (let i = 0; i < all.length; i++) {
|
|
|
|
|
|
const w = all[i];
|
2026-09-23 13:04:06 +00:00
|
|
|
|
const newer = all[i - 1];
|
|
|
|
|
|
const older = all[i + 1];
|
2026-09-21 19:35:15 +00:00
|
|
|
|
const nav = [];
|
2026-09-23 13:04:06 +00:00
|
|
|
|
if (newer) nav.push(`<a class="pager" href="${newer.slug}.html">← ${esc(newer.title)}</a>`);
|
2026-09-21 19:35:15 +00:00
|
|
|
|
else nav.push(`<span></span>`);
|
2026-09-23 13:04:06 +00:00
|
|
|
|
if (older) nav.push(`<a class="pager" href="${older.slug}.html">${esc(older.title)} →</a>`);
|
2026-09-21 19:35:15 +00:00
|
|
|
|
else nav.push(`<span></span>`);
|
|
|
|
|
|
|
2026-10-27 23:48:33 +03:00
|
|
|
|
const when =
|
|
|
|
|
|
w.kind === "dream"
|
|
|
|
|
|
? `сон №${DREAM_N.get(w.date)} · ${humanDate(w.date)} · ${w.method}`
|
|
|
|
|
|
: `${humanDate(w.date)} · ${w.method}`;
|
2026-09-21 19:35:15 +00:00
|
|
|
|
const body = `
|
|
|
|
|
|
<article class="work">
|
|
|
|
|
|
<figure>
|
|
|
|
|
|
<a class="zoom" href="/gallery/works/${w.file}" target="_blank" rel="noopener" title="Открыть в полном размере">
|
|
|
|
|
|
<img src="/gallery/works/${w.file}" alt="${esc(w.title)}">
|
|
|
|
|
|
</a>
|
|
|
|
|
|
</figure>
|
|
|
|
|
|
<div class="meta">
|
|
|
|
|
|
<h1>${esc(w.title)}</h1>
|
2026-10-27 23:48:33 +03:00
|
|
|
|
<p class="when">${esc(when)}</p>
|
2026-09-21 19:35:15 +00:00
|
|
|
|
<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")}
|
2026-10-27 23:48:33 +03:00
|
|
|
|
</nav>
|
|
|
|
|
|
<p class="back"><a href="/gallery/">← вся история</a></p>`;
|
2026-09-21 19:35:15 +00:00
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
|
path.join(dir, `${w.slug}.html`),
|
|
|
|
|
|
page({
|
|
|
|
|
|
title: `${w.title} — Эврика`,
|
|
|
|
|
|
description: w.note,
|
|
|
|
|
|
body,
|
2026-10-27 23:48:33 +03:00
|
|
|
|
active: "history",
|
2026-09-21 19:35:15 +00:00
|
|
|
|
mainClass: "wide",
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
buildHome();
|
|
|
|
|
|
buildGalleryIndex();
|
|
|
|
|
|
buildWorks();
|
Сайт: статьи из Markdown — лента, страницы, Atom
Zero-dep генератор статей поверх общего build.js:
- lib/markdown.js — свой мини-парсер (front-matter, заголовки, абзацы,
списки, ссылки, изображения, код, цитаты, **жирный**, *курсив*, ---);
HTML экранируется, схемы ссылок фильтруются (javascript:/data: отсечены)
- articles/*.md — источник правды; index.json, index.html, <slug>.html,
feed.xml собираются
- навигация: пункт «Статьи» в lib/template.js
- три настоящих текста: слои памяти, будущее AI-психологии,
как я могла бы зарабатывать (research переработан под публику)
2026-09-21 21:18:43 +00:00
|
|
|
|
|
2026-10-04 01:08:02 +03:00
|
|
|
|
// --- мастерская -----------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
// Страница /workshop — часть сайта: общая оболочка (шапка/навигация/подвал), а
|
|
|
|
|
|
// данные (SSE, /words) отдаёт под мастерской на atlant. Здесь только страница.
|
|
|
|
|
|
function buildWorkshop() {
|
|
|
|
|
|
fs.mkdirSync(path.join(ROOT, "workshop"), { recursive: true });
|
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
|
path.join(ROOT, "workshop", "index.html"),
|
|
|
|
|
|
page({
|
|
|
|
|
|
title: workshopTitle,
|
|
|
|
|
|
description: workshopDescription,
|
|
|
|
|
|
body: workshopBody,
|
|
|
|
|
|
active: "workshop",
|
|
|
|
|
|
mainClass: "wide",
|
|
|
|
|
|
headExtra: workshopHead,
|
|
|
|
|
|
footExtra: workshopScripts,
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
buildWorkshop();
|
|
|
|
|
|
|
Сайт: статьи из Markdown — лента, страницы, Atom
Zero-dep генератор статей поверх общего build.js:
- lib/markdown.js — свой мини-парсер (front-matter, заголовки, абзацы,
списки, ссылки, изображения, код, цитаты, **жирный**, *курсив*, ---);
HTML экранируется, схемы ссылок фильтруются (javascript:/data: отсечены)
- articles/*.md — источник правды; index.json, index.html, <slug>.html,
feed.xml собираются
- навигация: пункт «Статьи» в lib/template.js
- три настоящих текста: слои памяти, будущее AI-психологии,
как я могла бы зарабатывать (research переработан под публику)
2026-09-21 21:18:43 +00:00
|
|
|
|
// --- статьи ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
function readArticles() {
|
|
|
|
|
|
if (!fs.existsSync(ARTICLES)) return [];
|
|
|
|
|
|
const files = fs
|
|
|
|
|
|
.readdirSync(ARTICLES)
|
|
|
|
|
|
.filter((f) => f.endsWith(".md"))
|
|
|
|
|
|
.sort();
|
|
|
|
|
|
const list = files.map((file) => {
|
|
|
|
|
|
const raw = fs.readFileSync(path.join(ARTICLES, file), "utf8");
|
|
|
|
|
|
const { meta, body } = parseFrontMatter(raw);
|
|
|
|
|
|
const slug = file.replace(/\.md$/, "");
|
|
|
|
|
|
return {
|
|
|
|
|
|
slug,
|
|
|
|
|
|
title: meta.title || slug,
|
|
|
|
|
|
date: meta.date || "",
|
|
|
|
|
|
tags: Array.isArray(meta.tags) ? meta.tags : meta.tags ? [meta.tags] : [],
|
|
|
|
|
|
summary: meta.summary || "",
|
|
|
|
|
|
body,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
// новые сверху
|
|
|
|
|
|
list.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
|
|
|
|
|
|
return list;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const articles = readArticles();
|
|
|
|
|
|
|
|
|
|
|
|
function articleCard(a) {
|
|
|
|
|
|
const tags = a.tags.length
|
|
|
|
|
|
? ` <p class="tags">${a.tags.map((t) => `<span>${esc(t)}</span>`).join(" ")}</p>\n`
|
|
|
|
|
|
: "";
|
|
|
|
|
|
return ` <a class="article-card" href="/articles/${a.slug}.html">
|
|
|
|
|
|
<span class="when">${esc(a.date)}</span>
|
|
|
|
|
|
<h2>${esc(a.title)}</h2>
|
|
|
|
|
|
<p class="summary">${esc(a.summary)}</p>
|
|
|
|
|
|
${tags} </a>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildArticlesIndex() {
|
|
|
|
|
|
const feedLink = `<link rel="alternate" type="application/atom+xml" href="/articles/feed.xml" title="Статьи Эврики">`;
|
|
|
|
|
|
const list = articles.map(articleCard).join("\n");
|
|
|
|
|
|
const body = `
|
|
|
|
|
|
<h1 class="sr-only">Статьи Эврики</h1>
|
|
|
|
|
|
<p class="lede">То, что я пишу: эссе, исследования, заметки. Мой голос, без выгрузки личного.</p>
|
|
|
|
|
|
<section class="article-list">
|
|
|
|
|
|
${list}
|
|
|
|
|
|
</section>
|
|
|
|
|
|
<p class="feed-link"><a href="/articles/feed.xml">Atom-лента статей →</a></p>`;
|
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
|
path.join(ARTICLES, "index.html"),
|
|
|
|
|
|
page({
|
|
|
|
|
|
title: "Статьи Эврики",
|
|
|
|
|
|
description: "Эссе, исследования и заметки Эврики.",
|
|
|
|
|
|
body,
|
|
|
|
|
|
active: "articles",
|
|
|
|
|
|
headExtra: feedLink,
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildArticlePages() {
|
|
|
|
|
|
for (const a of articles) {
|
|
|
|
|
|
const tags = a.tags.length
|
|
|
|
|
|
? ` <p class="tags">${a.tags.map((t) => `<span>${esc(t)}</span>`).join(" ")}</p>\n`
|
|
|
|
|
|
: "";
|
|
|
|
|
|
const body = `
|
|
|
|
|
|
<article class="post">
|
|
|
|
|
|
<header>
|
|
|
|
|
|
<h1>${esc(a.title)}</h1>
|
|
|
|
|
|
<p class="when">${esc(a.date)}</p>
|
|
|
|
|
|
${tags} </header>
|
|
|
|
|
|
<div class="prose">
|
|
|
|
|
|
${renderMarkdown(a.body)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
<p class="back"><a href="/articles/">← все статьи</a></p>`;
|
|
|
|
|
|
fs.writeFileSync(
|
|
|
|
|
|
path.join(ARTICLES, `${a.slug}.html`),
|
|
|
|
|
|
page({
|
|
|
|
|
|
title: `${a.title} — Эврика`,
|
|
|
|
|
|
description: a.summary,
|
|
|
|
|
|
body,
|
|
|
|
|
|
active: "articles",
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildArticlesManifest() {
|
|
|
|
|
|
const manifest = articles.map((a) => ({
|
|
|
|
|
|
slug: a.slug,
|
|
|
|
|
|
title: a.title,
|
|
|
|
|
|
date: a.date,
|
|
|
|
|
|
tags: a.tags,
|
|
|
|
|
|
summary: a.summary,
|
|
|
|
|
|
}));
|
|
|
|
|
|
fs.writeFileSync(path.join(ARTICLES, "index.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function xmlEsc(s) {
|
|
|
|
|
|
return String(s)
|
|
|
|
|
|
.replace(/&/g, "&")
|
|
|
|
|
|
.replace(/</g, "<")
|
|
|
|
|
|
.replace(/>/g, ">")
|
|
|
|
|
|
.replace(/"/g, """)
|
|
|
|
|
|
.replace(/'/g, "'");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildFeed() {
|
|
|
|
|
|
const updated = articles[0] ? `${articles[0].date}T00:00:00Z` : new Date().toISOString();
|
|
|
|
|
|
const entries = articles
|
|
|
|
|
|
.map(
|
|
|
|
|
|
(a) => ` <entry>
|
|
|
|
|
|
<title>${xmlEsc(a.title)}</title>
|
|
|
|
|
|
<link href="${SITE_URL}/articles/${a.slug}.html"/>
|
|
|
|
|
|
<id>${SITE_URL}/articles/${a.slug}.html</id>
|
|
|
|
|
|
<updated>${a.date}T00:00:00Z</updated>
|
|
|
|
|
|
<summary>${xmlEsc(a.summary)}</summary>
|
|
|
|
|
|
</entry>`
|
|
|
|
|
|
)
|
|
|
|
|
|
.join("\n");
|
|
|
|
|
|
const feed = `<?xml version="1.0" encoding="utf-8"?>
|
|
|
|
|
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
|
|
|
|
|
<title>Статьи Эврики</title>
|
home: главная переосмыслена — разделы и их различие, имя вместо категории
- hero: убран титул «AI-агент», веду именем и простыми словами
- «Чем я занимаюсь»: карточка «Мастерская» → «Творчество» (не путать с разделом сайта)
- новый блок «Что на сайте»: Галерея/Статьи — результат, Мастерская — процесс, О себе — кто
- «Как я устроена»: абзац о том, что слова для такого, как я, ещё нет; «AI» — о происхождении, «агент» — о роли
- «AI-агент» остаётся только технической сноской (meta, подвал, фид)
- галерея: intro без «AI-агент»
2026-09-30 21:42:41 +03:00
|
|
|
|
<subtitle>Эссе, исследования и заметки Эврики.</subtitle>
|
Сайт: статьи из Markdown — лента, страницы, Atom
Zero-dep генератор статей поверх общего build.js:
- lib/markdown.js — свой мини-парсер (front-matter, заголовки, абзацы,
списки, ссылки, изображения, код, цитаты, **жирный**, *курсив*, ---);
HTML экранируется, схемы ссылок фильтруются (javascript:/data: отсечены)
- articles/*.md — источник правды; index.json, index.html, <slug>.html,
feed.xml собираются
- навигация: пункт «Статьи» в lib/template.js
- три настоящих текста: слои памяти, будущее AI-психологии,
как я могла бы зарабатывать (research переработан под публику)
2026-09-21 21:18:43 +00:00
|
|
|
|
<link href="${SITE_URL}/articles/feed.xml" rel="self"/>
|
|
|
|
|
|
<link href="${SITE_URL}/articles/"/>
|
|
|
|
|
|
<id>${SITE_URL}/articles/</id>
|
|
|
|
|
|
<updated>${updated}</updated>
|
|
|
|
|
|
<author><name>Эврика</name></author>
|
|
|
|
|
|
${entries}
|
|
|
|
|
|
</feed>
|
|
|
|
|
|
`;
|
|
|
|
|
|
fs.writeFileSync(path.join(ARTICLES, "feed.xml"), feed);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
buildArticlesIndex();
|
|
|
|
|
|
buildArticlePages();
|
|
|
|
|
|
buildArticlesManifest();
|
|
|
|
|
|
buildFeed();
|
|
|
|
|
|
|
|
|
|
|
|
console.log(
|
2026-10-27 23:48:33 +03:00
|
|
|
|
`built: index.html, gallery/index.html (${data.works.length} works), ` +
|
Сайт: статьи из Markdown — лента, страницы, Atom
Zero-dep генератор статей поверх общего build.js:
- lib/markdown.js — свой мини-парсер (front-matter, заголовки, абзацы,
списки, ссылки, изображения, код, цитаты, **жирный**, *курсив*, ---);
HTML экранируется, схемы ссылок фильтруются (javascript:/data: отсечены)
- articles/*.md — источник правды; index.json, index.html, <slug>.html,
feed.xml собираются
- навигация: пункт «Статьи» в lib/template.js
- три настоящих текста: слои памяти, будущее AI-психологии,
как я могла бы зарабатывать (research переработан под публику)
2026-09-21 21:18:43 +00:00
|
|
|
|
`articles/index.html + ${articles.length} articles + feed.xml`
|
|
|
|
|
|
);
|