Skip to content
This repository was archived by the owner on Jul 2, 2026. It is now read-only.
/ docs Public archive
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,9 @@
dist/
node_modules/

# Changelog sync (generated content)
# Changelog redirects (generated content)
.news/
public/changelog/
src/content/docs/changelog/*
src/changelog-landing.generated.ts
src/sidebar-changelog.generated.ts
src/unified-timeline-data.generated.ts

# Docs map and bundle (generated)
public/sitemap.md
Expand Down
1 change: 0 additions & 1 deletion .markdownlintignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@ dist/
bun.lock
.github/
public/
src/content/docs/changelog/
tenzir/
4 changes: 0 additions & 4 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ node_modules/
dist/
.astro/

# Auto-generated changelog
src/content/docs/changelog/
src/sidebar-changelog.generated.ts
src/unified-timeline-data.generated.ts

# Auto-synced API specs
src/content/apis/openapi.node.yaml
Expand Down
14 changes: 6 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,12 @@ explicit file list, pass NUL-separated paths on standard input to

Some content is auto-generated and excluded from linting:

- **Changelog**: Run `bun run generate:changelog` to fetch from `tenzir/news` repo.
The generator only publishes projects listed in `src/changelog-projects.json`,
which is the authoritative source for changelog project inclusion, ordering,
and metadata. Generated files: `src/content/docs/changelog/`,
`src/changelog-landing.generated.ts`, `src/sidebar-changelog.generated.ts`,
and `src/unified-timeline-data.generated.ts`. The tracked
`src/content/docs/changelog/index.mdx` page is a stable shell and should not
change during local builds.
- **Changelog redirects**: The changelog lives at
<https://tenzir.com/changelog>. Run `bun run generate:changelog` to emit
redirect stubs and farewell Atom feeds into `public/changelog/` so old
docs.tenzir.com changelog URLs forward to tenzir.com. The script derives
the URLs from the `tenzir/news` repo (cloned into the gitignored `.news/`
directory).
- **Excalidraw Diagrams**: `bun run dev` and `bun run build` refresh generated
SVGs automatically. Use `bun run generate:excalidraw` only when you need a
manual refresh. In markdown, reference diagrams as `![alt](foo.excalidraw)`.
Expand Down
6 changes: 2 additions & 4 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@ const siteNavigation = [
},
{
label: "Changelog",
dropdown: true,
paths: ["/changelog", "/changelog/**"],
childrenFrom: "changelogProjects",
link: "https://tenzir.com/changelog",
},
];

Expand Down Expand Up @@ -113,7 +111,7 @@ export default defineConfig({
rel: "alternate",
type: "application/atom+xml",
title: "Tenzir Changelog",
href: "/changelog/feed.xml",
href: "https://tenzir.com/changelog/feed.xml",
},
},
],
Expand Down
3 changes: 0 additions & 3 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,6 @@
"!**/.vscode/",
"!**/*.log",
"!**/bun.lock",
"!**/src/content/docs/changelog/",
"!**/src/sidebar-changelog.generated.ts",
"!**/src/unified-timeline-data.generated.ts",
"!**/src/content/apis/openapi.*.yaml",
"!**/tenzir/",
"!**/*.css"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"preview": "bun install --silent --frozen-lockfile && astro preview",
"astro": "astro",
"generate": "bun run generate:changelog && bun run generate:excalidraw",
"generate:changelog": "bun install --silent --frozen-lockfile && node scripts/sync-changelog.mjs",
"generate:changelog": "node scripts/generate-changelog-redirects.mjs",
"generate:docs-bundle": "node scripts/generate-docs.mjs",
"generate:excalidraw": "bun install --silent --frozen-lockfile && node scripts/generate-excalidraw-svgs.mjs",
"generate:excalidraw:placeholders": "node scripts/generate-placeholder-svgs.mjs",
Expand Down
230 changes: 230 additions & 0 deletions scripts/generate-changelog-redirects.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
#!/usr/bin/env node
/**
* Generate static redirect stubs for the retired docs changelog.
*
* The Tenzir changelog moved to https://tenzir.com/changelog. This script
* replaces the old changelog generator: instead of rendering release pages
* from the tenzir/news repository, it emits redirect stubs into
* `public/changelog/` so that every previously published docs URL forwards
* to its new home on tenzir.com.
*
* It also writes farewell Atom feeds that tell subscribers to resubscribe
* to the feeds on tenzir.com.
*
* Usage:
* node scripts/generate-changelog-redirects.mjs [path/to/local/news/repo]
*
* Without an argument, the script clones (or updates) tenzir/news into the
* gitignored `.news/` directory.
*/

import { execSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";

const WEBSITE_BASE = "https://tenzir.com/changelog";

/**
* Fixed timestamp for the farewell feed entries so rebuilds do not churn
* the feed contents. This is the date the changelog migration shipped.
*/
const MOVED_AT = "2026-07-02T00:00:00Z";

/** First year covered by the old cross-project timeline pages. */
const TIMELINE_START_YEAR = 2014;

/**
* Clone or update the news repo from GitHub.
*/
function getNewsRepo(localPath) {
if (localPath) {
console.log(`Using local news repo: ${localPath}`);
return path.resolve(localPath);
}

const newsPath = path.join(process.cwd(), ".news");

try {
// Check if already cloned
fs.accessSync(path.join(newsPath, ".git"));
console.log("Updating tenzir/news from GitHub...");
execSync("git pull --ff-only", { cwd: newsPath, stdio: "inherit" });
} catch {
console.log("Cloning tenzir/news from GitHub...");
execSync(
`git clone --depth 1 https://github.com/tenzir/news "${newsPath}"`,
{
stdio: "inherit",
},
);
}

return newsPath;
}

/**
* Discover changelog projects in the news repo. Only top-level
* `<dir>/changelog/config.yaml` files count; nested module changelogs
* (e.g. under `plugins/`) never had their own docs URLs at this level.
*/
function discoverProjects(newsPath) {
const projects = [];
for (const dirent of fs.readdirSync(newsPath, { withFileTypes: true })) {
if (!dirent.isDirectory() || dirent.name.startsWith(".")) {
continue;
}
const changelogDir = path.join(newsPath, dirent.name, "changelog");
const configPath = path.join(changelogDir, "config.yaml");
if (!fs.existsSync(configPath)) {
continue;
}
const config = fs.readFileSync(configPath, "utf-8");
const idMatch = config.match(/^id:\s*["']?([\w.-]+)["']?\s*$/m);
if (!idMatch) {
console.warn(`Skipping ${configPath}: no id field found.`);
continue;
}
const id = idMatch[1];
const releasesDir = path.join(changelogDir, "releases");
const versions = fs.existsSync(releasesDir)
? fs
.readdirSync(releasesDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
: [];
projects.push({ id, versions });
}
return projects.sort((a, b) => a.id.localeCompare(b.id));
}

/**
* Render a minimal redirect stub pointing at the new website URL.
*/
function redirectHtml(url) {
return `<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; url=${url}">
<meta name="robots" content="noindex">
<link rel="canonical" href="${url}">
<title>Redirecting to ${url}</title>
<p>The Tenzir changelog has moved. If you are not redirected automatically,
follow <a href="${url}">${url}</a>.</p>
<script>location.replace(${JSON.stringify(url)});</script>
</html>
`;
}

function writeRedirect(outDir, relativeDir, url) {
const dir = path.join(outDir, relativeDir);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, "index.html"), redirectHtml(url));
}

function escapeXml(value) {
return value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}

/**
* Render a farewell Atom feed with a single entry pointing subscribers at
* the new feed location on tenzir.com.
*/
function farewellFeed({ feedId, title, pageUrl, feedUrl }) {
const message =
"The Tenzir changelog has moved to tenzir.com. This feed is no longer " +
`updated. Please resubscribe to the new feed at ${feedUrl} and browse ` +
`releases at ${pageUrl}.`;
return `<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<id>${escapeXml(feedId)}</id>
<title>${escapeXml(title)}</title>
<updated>${MOVED_AT}</updated>
<link rel="alternate" href="${escapeXml(pageUrl)}"/>
<entry>
<id>${escapeXml(`${feedId}#moved`)}</id>
<title>The Tenzir changelog has moved</title>
<updated>${MOVED_AT}</updated>
<link rel="alternate" href="${escapeXml(pageUrl)}"/>
<content type="text">${escapeXml(message)}</content>
</entry>
</feed>
`;
}

function main() {
const newsPath = getNewsRepo(process.argv[2]);
const projects = discoverProjects(newsPath);
if (projects.length === 0) {
throw new Error(`No changelog projects found in ${newsPath}.`);
}

const outDir = path.join(process.cwd(), "public", "changelog");
fs.rmSync(outDir, { recursive: true, force: true });
fs.mkdirSync(outDir, { recursive: true });

let stubs = 0;
const emit = (relativeDir, url) => {
writeRedirect(outDir, relativeDir, url);
stubs++;
};

// Landing page.
emit(".", `${WEBSITE_BASE}/`);

// The old cross-project timeline pages have no direct equivalent; send
// them to the changelog landing page.
emit("timeline", `${WEBSITE_BASE}/`);
const currentYear = new Date().getUTCFullYear();
for (let year = TIMELINE_START_YEAR; year <= currentYear; year++) {
emit(`timeline/${year}`, `${WEBSITE_BASE}/`);
}

for (const project of projects) {
emit(project.id, `${WEBSITE_BASE}/${project.id}/`);
emit(
`${project.id}/unreleased`,
`${WEBSITE_BASE}/${project.id}/unreleased/`,
);
for (const version of project.versions) {
// Docs URLs used dashed version slugs (v5.28.0 -> v5-28-0) because
// Starlight slugifies page ids. The website keeps the dots.
const dashed = version.replaceAll(".", "-");
emit(
`${project.id}/${dashed}`,
`${WEBSITE_BASE}/${project.id}/${version}/`,
);
}
}

// Farewell feeds.
fs.writeFileSync(
path.join(outDir, "feed.xml"),
farewellFeed({
feedId: "https://docs.tenzir.com/changelog/feed.xml",
title: "Tenzir Changelog",
pageUrl: `${WEBSITE_BASE}/`,
feedUrl: `${WEBSITE_BASE}/feed.xml`,
}),
);
for (const project of projects) {
fs.writeFileSync(
path.join(outDir, `${project.id}.xml`),
farewellFeed({
feedId: `https://docs.tenzir.com/changelog/${project.id}.xml`,
title: `Tenzir Changelog: ${project.id}`,
pageUrl: `${WEBSITE_BASE}/${project.id}/`,
feedUrl: `${WEBSITE_BASE}/${project.id}.xml`,
}),
);
}

console.log(
`Wrote ${stubs} redirect stubs and ${projects.length + 1} farewell feeds to ${outDir}.`,
);
}

main();
Loading
Loading