feat(docs): version-based docs, correct Gitea tag links, form guide help

- each release now carries a full docs snapshot (reference + handbook)
  under docs/releases/<v>/; the docs frontend loads a version's snapshot
  for /docs/vX.Y.Z/* and shows the handbook/reference of that version
- version dropdown navigates to /docs/<version> (docs home) instead of
  the release notes page; old /docs/vX.Y.Z redirect removed
- generator: --snapshot writes handbook + reference; build copies
  versioned snapshots (last 7 versions) into /docs/versions/<v>/
- fix Gitea tag links: /admin/tool-evaluator/tags/<v> was 404, correct
  URL is /releases/tag/<v>
- add GuideHelp button to forms/pages linking to the matching handbook
  guide (rating, tool add/edit, costs, compare, watchlist, analytics)
This commit is contained in:
opencode
2026-08-04 07:31:47 +02:00
parent d23990e322
commit 3bb4598d04
53 changed files with 7545 additions and 117 deletions
+53 -9
View File
@@ -196,7 +196,7 @@ function slugify(name) {
return name.replace(/\.md$/, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-");
}
async function buildHandbookIndex() {
async function readHandbookPages() {
let files;
try {
files = (await readdir(handbookDir)).filter(isHandbookFile);
@@ -213,13 +213,35 @@ async function buildHandbookIndex() {
file,
title: frontmatter.title ?? extractTitle(body) ?? basename(file),
order: typeof frontmatter.order === "number" ? frontmatter.order : 999,
body,
});
await writeFile(join(targetDir, "handbook", file), body);
}
pages.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title));
return pages;
}
async function writeHandbookTo(dir) {
const pages = await readHandbookPages();
await mkdir(dir, { recursive: true });
for (const page of pages) {
await writeFile(join(dir, page.file), page.body);
}
await writeFile(
join(dir, "index.json"),
JSON.stringify(
pages.map(({ slug, file, title, order }) => ({ slug, file, title, order })),
null,
2,
),
);
return pages;
}
async function buildHandbookIndex() {
const pages = await writeHandbookTo(join(targetDir, "handbook"));
return pages;
}
// ---------------------------------------------------------------------------
// Search index
// ---------------------------------------------------------------------------
@@ -295,7 +317,8 @@ async function writeSnapshot(version) {
const dir = join(releasesDir, version);
await mkdir(dir, { recursive: true });
await writeFile(join(dir, "reference.json"), JSON.stringify(reference, null, 2));
console.log(`[generate-docs] snapshot ${version} -> ${join(dir, "reference.json")}`);
await writeHandbookTo(join(dir, "handbook"));
console.log(`[generate-docs] snapshot ${version} -> ${dir}/ (reference.json + handbook)`);
}
// ---------------------------------------------------------------------------
@@ -343,27 +366,48 @@ async function main() {
JSON.stringify(handbookPages, null, 2),
);
// 3. Release notes + versioned reference snapshots
// 3. Release notes + versioned docs snapshots (last 7 versions)
const versions = [];
for (const name of releaseNames) {
for (const name of releaseNames.slice(0, 7)) {
const version = parseVersion(name);
const content = await readFile(join(releasesDir, name), "utf8");
await copyFile(join(releasesDir, name), join(targetDir, "releases", name));
const hasSnapshot = await stat(join(releasesDir, version, "reference.json"))
const snapDir = join(releasesDir, version);
const hasReference = await stat(join(snapDir, "reference.json"))
.then(() => true)
.catch(() => false);
if (hasSnapshot) {
const hasHandbook = await stat(join(snapDir, "handbook", "index.json"))
.then(() => true)
.catch(() => false);
if (hasReference) {
await copyFile(
join(releasesDir, version, "reference.json"),
join(snapDir, "reference.json"),
join(targetDir, "versions", `${version}.json`),
);
}
if (hasHandbook) {
await mkdir(join(targetDir, "versions", version, "handbook"), { recursive: true });
await copyFile(
join(snapDir, "handbook", "index.json"),
join(targetDir, "versions", version, "handbook", "index.json"),
);
const pages = JSON.parse(
await readFile(join(snapDir, "handbook", "index.json"), "utf8"),
);
for (const page of pages) {
await copyFile(
join(snapDir, "handbook", page.file),
join(targetDir, "versions", version, "handbook", page.file),
);
}
}
versions.push({
version,
file: `releases/${name}`,
title: extractTitle(content) ?? version,
date: extractDate(content) ?? null,
hasReference: hasSnapshot,
hasReference,
hasHandbook,
});
}