{
diff --git a/src/content.config.ts b/src/content.config.ts
index 3a5c621..dfad9f0 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -1,4 +1,4 @@
-import { defineCollection } from "astro:content";
+import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
/**
@@ -11,6 +11,23 @@ const engineDocs = defineCollection({
loader: glob({ pattern: "*.md", base: "./src/content/engine-docs" }),
});
+/**
+ * #41 — release notes, fetched at build time by scripts/fetch-release.mjs
+ * into src/content/changelog/ (gitignored; the fetch script is the only
+ * writer). One markdown file per release; frontmatter carries the tag,
+ * date and GitHub URL the changelog page renders around the body.
+ */
+const changelog = defineCollection({
+ loader: glob({ pattern: "*.md", base: "./src/content/changelog" }),
+ schema: z.object({
+ tag: z.string(),
+ name: z.string(),
+ publishedAt: z.string(),
+ url: z.string(),
+ }),
+});
+
export const collections = {
"engine-docs": engineDocs,
+ changelog,
};
diff --git a/src/i18n/config.ts b/src/i18n/config.ts
index f896c97..f17de2a 100644
--- a/src/i18n/config.ts
+++ b/src/i18n/config.ts
@@ -29,6 +29,7 @@ export const pageKeys = [
"compliance",
"compare",
"about",
+ "changelog",
"guides",
] as const;
export type PageKey = (typeof pageKeys)[number];
diff --git a/src/i18n/pages/changelog.ts b/src/i18n/pages/changelog.ts
new file mode 100644
index 0000000..9784420
--- /dev/null
+++ b/src/i18n/pages/changelog.ts
@@ -0,0 +1,66 @@
+import type { LocalizedPage } from "../config";
+
+/**
+ * #41 — the changelog page. Release notes are fetched from GitHub Releases at
+ * build time into the `changelog` content collection (scripts/fetch-release.mjs
+ * is the only writer) and stay in English — the canonical wording — on every
+ * locale; ar/fr translate the chrome and say so, mirroring the engine-docs
+ * policy (FR-7/8).
+ *
+ * Sonar: fields are flat single strings on purpose — parallel tri-locale
+ * blocks trip the duplication gate otherwise (see CONTRIBUTING.md).
+ */
+export interface ChangelogContent {
+ title: string;
+ description: string;
+ intro: string;
+ englishOnly: string;
+ tocLabel: string;
+ latestLabel: string;
+ viewRelease: string;
+ empty: string;
+}
+
+export const changelog: LocalizedPage = {
+ en: {
+ rev: "2026-08-20.1",
+ title: "Changelog — every keel release, newest first",
+ description:
+ "Every keel release with its install and configuration notes, newest first, pulled from GitHub Releases at build time.",
+ intro:
+ "Every version of keel, newest first, exactly as published to GitHub Releases. The build fetches the notes; nobody hand-updates this page.",
+ englishOnly: "Release notes are published in English — the text below is the canonical wording.",
+ tocLabel: "On this page",
+ latestLabel: "latest",
+ viewRelease: "View this release on GitHub",
+ empty: "The release list could not be fetched for this build.",
+ },
+ ar: {
+ rev: "2026-08-20.1",
+ translatedFromRev: "2026-08-20.1",
+ title: "سجلُّ التغييرات — كلُّ إصدارات كيل، من الأحدث إلى الأقدم",
+ description:
+ "كلُّ إصدارٍ من كيل مع ملاحظات التثبيت والإعداد، من الأحدث إلى الأقدم، مجلوبةٌ وقتَ البناء من GitHub Releases.",
+ intro:
+ "كلُّ إصدارٍ من كيل، من الأحدث إلى الأقدم، كما نُشر على GitHub Releases تمامًا. البناءُ هو الذي يجلب الملاحظات، ولا أحدَ يُحدِّث هذه الصفحة يدويًّا.",
+ englishOnly: "تُنشر ملاحظات الإصدار بالإنجليزية، والنصُّ أدناه هو الصياغة المعتمدة.",
+ tocLabel: "في هذه الصفحة",
+ latestLabel: "الأحدث",
+ viewRelease: "شاهِد هذا الإصدار على GitHub",
+ empty: "تعذَّر جلبُ قائمة الإصدارات في هذا البناء.",
+ },
+ fr: {
+ rev: "2026-08-20.1",
+ translatedFromRev: "2026-08-20.1",
+ title: "Journal des versions — chaque sortie de keel, de la plus récente à la plus ancienne",
+ description:
+ "Chaque version de keel avec ses notes d'installation et de configuration, de la plus récente à la plus ancienne, importées de GitHub Releases à la construction.",
+ intro:
+ "Chaque version de keel, de la plus récente à la plus ancienne, exactement comme publiée sur GitHub Releases. La construction importe les notes ; personne ne met cette page à jour à la main.",
+ englishOnly: "Les notes de version sont publiées en anglais ; le texte ci-dessous fait foi.",
+ tocLabel: "Sur cette page",
+ latestLabel: "la plus récente",
+ viewRelease: "Voir cette version sur GitHub",
+ empty: "La liste des versions n'a pas pu être importée pour cette construction.",
+ },
+};
diff --git a/src/i18n/pages/index.ts b/src/i18n/pages/index.ts
index 608ede7..467664b 100644
--- a/src/i18n/pages/index.ts
+++ b/src/i18n/pages/index.ts
@@ -8,6 +8,7 @@ import { community, type CommunityContent } from "./community";
import { compliance, type ComplianceContent } from "./compliance";
import { compare, type CompareContent } from "./compare";
import { about, type AboutContent } from "./about";
+import { changelog, type ChangelogContent } from "./changelog";
/**
* FR-8 — the layout reads this registry to stamp every translated page with
@@ -26,6 +27,7 @@ export const pageDicts: Record, LocalizedPage = {
en: {
- rev: "2026-08-20.6",
+ rev: "2026-08-20.7",
title: "Download keel — macOS & Windows",
description:
"Download keel for macOS or Windows. Version and links come from GitHub Releases at build time; the five-minute source path is here too.",
@@ -86,6 +87,7 @@ export const install: LocalizedPage = {
versionPrefix: "Latest release",
requirements: "Requires Python 3.11 or later · downloaded from GitHub Releases — never mirrored here",
otherPlatforms: "Linux and everything else: same wheels from the release page.",
+ historyLink: "Every version, newest first — the full changelog",
cards: [
{
name: "macOS",
@@ -162,8 +164,8 @@ export const install: LocalizedPage = {
},
ar: {
- rev: "2026-08-20.6",
- translatedFromRev: "2026-08-20.6",
+ rev: "2026-08-20.7",
+ translatedFromRev: "2026-08-20.7",
title: "تنزيل كيل — macOS وWindows",
description:
"نزّل كيل لنظام macOS أو Windows. ويأتي رقمُ الإصدار وروابطه من GitHub Releases وقت البناء؛ ومسارُ التثبيت من المصدر في خمس دقائق هنا أيضًا.",
@@ -171,6 +173,7 @@ export const install: LocalizedPage = {
versionPrefix: "أحدث إصدار",
requirements: "يتطلّب Python 3.11 أو أحدث · التنزيل من GitHub Releases — ولا يُنسخ هنا أبدًا",
otherPlatforms: "لينكس وغيره: حزم wheel نفسها متاحةٌ في صفحة الإصدار.",
+ historyLink: "كلُّ الإصدارات، من الأحدث إلى الأقدم — السجلُّ الكامل للتغييرات",
cards: [
{
name: "macOS",
@@ -247,8 +250,8 @@ export const install: LocalizedPage = {
},
fr: {
- rev: "2026-08-20.6",
- translatedFromRev: "2026-08-20.6",
+ rev: "2026-08-20.7",
+ translatedFromRev: "2026-08-20.7",
title: "Télécharger keel — macOS et Windows",
description:
"Téléchargez keel pour macOS ou Windows. Le numéro de version et les liens proviennent de GitHub Releases, récupérés au moment du build ; le parcours en cinq minutes depuis les sources figure également ici.",
@@ -256,6 +259,7 @@ export const install: LocalizedPage = {
versionPrefix: "Dernière version",
requirements: "Nécessite Python 3.11 ou plus · téléchargé depuis GitHub Releases — jamais recopié ici",
otherPlatforms: "Linux et le reste : les mêmes wheels, depuis la page des versions.",
+ historyLink: "Toutes les versions, de la plus récente à la plus ancienne — le journal complet",
cards: [
{
name: "macOS",
diff --git a/src/i18n/ui.ts b/src/i18n/ui.ts
index a11aefb..5010e89 100644
--- a/src/i18n/ui.ts
+++ b/src/i18n/ui.ts
@@ -14,6 +14,7 @@ export const ui = {
install: "Install",
docs: "Docs",
news: "News",
+ changelog: "Changelog",
community: "Community",
compliance: "Compliance",
compare: "Compare",
@@ -100,6 +101,7 @@ export const ui = {
install: "التثبيت",
docs: "الوثائق",
news: "الأخبار",
+ changelog: "سجلُّ التغييرات",
community: "المجتمع",
compliance: "الامتثال",
compare: "مقارنة",
@@ -192,6 +194,7 @@ export const ui = {
install: "Installation",
docs: "Documentation",
news: "Actualités",
+ changelog: "Journal des versions",
community: "Communauté",
compliance: "Conformité",
compare: "Comparatif",
diff --git a/src/pages/ar/changelog.astro b/src/pages/ar/changelog.astro
new file mode 100644
index 0000000..f85593c
--- /dev/null
+++ b/src/pages/ar/changelog.astro
@@ -0,0 +1,4 @@
+---
+import ChangelogPage from "../../components/pages/ChangelogPage.astro";
+---
+
diff --git a/src/pages/en/changelog.astro b/src/pages/en/changelog.astro
new file mode 100644
index 0000000..97c5020
--- /dev/null
+++ b/src/pages/en/changelog.astro
@@ -0,0 +1,4 @@
+---
+import ChangelogPage from "../../components/pages/ChangelogPage.astro";
+---
+
diff --git a/src/pages/fr/changelog.astro b/src/pages/fr/changelog.astro
new file mode 100644
index 0000000..093a026
--- /dev/null
+++ b/src/pages/fr/changelog.astro
@@ -0,0 +1,4 @@
+---
+import ChangelogPage from "../../components/pages/ChangelogPage.astro";
+---
+
diff --git a/src/styles/global.css b/src/styles/global.css
index 15eb931..8500d67 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -1656,3 +1656,75 @@ html[lang="fr"] .site-nav a {
font-size: 0.87rem;
padding-inline: 0.35rem;
}
+
+/* Changelog (#41) — version TOC and per-release articles */
+.changelog-toc {
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ background: var(--surface);
+ padding: 1rem 1.25rem;
+ margin-bottom: 2.5rem;
+}
+
+.changelog-toc .toc-label {
+ font-size: 0.78rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--muted);
+ margin: 0 0 0.6rem;
+}
+
+.changelog-toc ul {
+ list-style: none;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem 1.1rem;
+ margin: 0;
+ padding: 0;
+}
+
+.changelog-toc a {
+ font-family: var(--font-mono);
+ font-size: 0.85rem;
+}
+
+html[dir="rtl"] .changelog-toc a {
+ font-family: inherit;
+}
+
+.latest-chip {
+ margin-inline-start: 0.45rem;
+ font-family: var(--font-sans);
+ font-size: 0.68rem;
+ color: var(--gold-2);
+ border: 1px solid var(--gold-2);
+ border-radius: 999px;
+ padding: 0.05rem 0.45rem;
+ vertical-align: middle;
+}
+
+.release {
+ border-top: 1px solid var(--border);
+ padding-block: 2rem;
+}
+
+.release h2 {
+ font-family: var(--font-mono);
+ font-size: 1.35rem;
+ margin-bottom: 0.25rem;
+}
+
+html[dir="rtl"] .release h2 {
+ font-family: inherit;
+}
+
+.release h2 a {
+ color: inherit;
+ text-decoration: none;
+}
+
+.release .meta {
+ font-size: 0.85rem;
+ color: var(--muted);
+ margin-top: 0;
+}