Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ dist/
build/
.astro/

# Fetched-at-build artifacts (engine docs, release, discussions) — never hand-copied
# Fetched-at-build artifacts (engine docs, releases, discussions) — never hand-copied
src/content/engine-docs/
src/content/changelog/
data/

# Cloudflare tooling
Expand Down
56 changes: 55 additions & 1 deletion scripts/fetch-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* A failed fetch never breaks the build; the Install page falls back to a
* plain link to the Releases page.
*/
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, rmSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";

Expand Down Expand Up @@ -63,3 +63,57 @@ try {
console.warn(` WARN: release fetch failed (${error.message}); Install page will link to the Releases page`);
}
}

// ---------------------------------------------------------------------------
// #41 — the changelog page. The full release list, newest first, written into
// the `changelog` content collection (gitignored; this script is the only
// writer, exactly like the engine docs). Same failure policy: a failed fetch
// never breaks the build — the page falls back to a plain Releases link.
const CHANGELOG_DIR = join(root, "src/content/changelog");
const LIST_API = "https://api.github.com/repos/CodeGateSoftware/keel/releases?per_page=100";

/** Demote every ATX heading one level, outside code fences, so each release's
* version heading (an H2 from the page template) stays the section's H2. */
function demoteHeadings(markdown) {
let inFence = false;
return markdown
.split("\n")
.map((line) => {
if (/^\s*(```|~~~)/.test(line)) {
inFence = !inFence;
return line;
}
if (!inFence && /^#{1,5} \S/.test(line)) return `#${line}`;
return line;
})
.join("\n");
}

try {
const response = await fetch(LIST_API, { headers });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const releases = (await response.json()).filter((release) => !release.draft);

mkdirSync(CHANGELOG_DIR, { recursive: true });
for (const stale of readdirSync(CHANGELOG_DIR)) {
if (stale.endsWith(".md")) rmSync(join(CHANGELOG_DIR, stale));
}
for (const release of releases) {
const frontmatter = [
"---",
`tag: ${JSON.stringify(release.tag_name)}`,
`name: ${JSON.stringify(release.name ?? release.tag_name)}`,
`publishedAt: ${JSON.stringify(release.published_at ?? "")}`,
`url: ${JSON.stringify(release.html_url)}`,
"---",
"",
].join("\n");
writeFileSync(
join(CHANGELOG_DIR, `${release.tag_name}.md`),
frontmatter + demoteHeadings(release.body ?? "") + "\n",
);
}
console.log(` changelog: ${releases.length} releases -> src/content/changelog/`);
} catch (error) {
console.warn(` WARN: changelog fetch failed (${error.message}); keeping any existing files`);
}
1 change: 1 addition & 0 deletions src/components/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const navItems: { key: Exclude<PageKey, "home">; label: string }[] = [
{ key: "install", label: chrome.nav.install },
{ key: "docs", label: chrome.nav.docs },
{ key: "news", label: chrome.nav.news },
{ key: "changelog", label: chrome.nav.changelog },
{ key: "community", label: chrome.nav.community },
{ key: "compliance", label: chrome.nav.compliance },
{ key: "compare", label: chrome.nav.compare },
Expand Down
106 changes: 106 additions & 0 deletions src/components/pages/ChangelogPage.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
import Base from "../../layouts/Base.astro";
import { getCollection, render } from "astro:content";
import { changelog } from "../../i18n/pages/changelog";
import {
alternatesFor,
formatDate,
localePath,
ENGINE_RELEASES_URL,
type Locale,
} from "../../i18n/config";
import { t } from "../../i18n/ui";

/**
* #41 — every keel release, newest first, rendered from the `changelog`
* content collection that scripts/fetch-release.mjs fills at build time
* (same pattern as the engine docs: fetched markdown, never hand-copied).
* Release notes stay in English — the canonical wording — on every locale.
*/
interface Props {
locale: Locale;
}

const { locale } = Astro.props;
const c = changelog[locale];
const chrome = t(locale);

const entries = (await getCollection("changelog"))
.slice()
.sort((a, b) => b.data.publishedAt.localeCompare(a.data.publishedAt));
const rendered = await Promise.all(
entries.map(async (entry) => ({ entry, Content: (await render(entry)).Content })),
);
const latestTag = entries[0]?.data.tag ?? null;

/** Fragment-safe anchor ids: v0.10.0 → v0-10-0 */
const anchorFor = (tag: string) => tag.replace(/\./g, "-");
---

<Base
locale={locale}
pageKey="changelog"
title={c.title}
description={c.description}
path={localePath(locale, "changelog")}
alternates={alternatesFor("changelog")}
>
<div class="container">
<section class="hero">
<h1>{c.title}</h1>
<p class="lede">{c.intro}</p>
<p class="doc-meta">{c.englishOnly}</p>
</section>

<div class="narrow">
{
rendered.length === 0 ? (
<div class="stale-banner" role="note">
<p>{c.empty}</p>
<p>
<a href={ENGINE_RELEASES_URL}>{chrome.actions.seeReleases} →</a>
</p>
</div>
) : (
<>
<nav class="changelog-toc" aria-label={c.tocLabel}>
<p class="toc-label">{c.tocLabel}</p>
<ul>
{entries.map((entry) => (
<li>
<a href={`#${anchorFor(entry.data.tag)}`}>
{entry.data.tag}
{entry.data.tag === latestTag && (
<span class="latest-chip">{c.latestLabel}</span>
)}
</a>
</li>
))}
</ul>
</nav>

{rendered.map(({ entry, Content }) => (
<article class="release" id={anchorFor(entry.data.tag)}>
<h2>
<a href={`#${anchorFor(entry.data.tag)}`}>{entry.data.tag}</a>
</h2>
<p class="meta">
{entry.data.publishedAt && (
<time datetime={entry.data.publishedAt}>
{formatDate(entry.data.publishedAt)}
</time>
)}
<span class="dot" aria-hidden="true">·</span>
<a href={entry.data.url}>{c.viewRelease} ↗</a>
</p>
<div class="prose">
<Content />
</div>
</article>
))}
</>
)
}
</div>
</div>
</Base>
3 changes: 3 additions & 0 deletions src/components/pages/InstallPage.astro
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ const venvCommands = (os: "mac" | "win"): string => {
}
</p>
<p class="requirements">{c.requirements}</p>
<p class="requirements">
<a href={localePath(locale, "changelog")}>{c.historyLink}</a>
</p>

<div class="download-grid">
{
Expand Down
19 changes: 18 additions & 1 deletion src/content.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineCollection } from "astro:content";
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";

/**
Expand All @@ -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,
};
1 change: 1 addition & 0 deletions src/i18n/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const pageKeys = [
"compliance",
"compare",
"about",
"changelog",
"guides",
] as const;
export type PageKey = (typeof pageKeys)[number];
Expand Down
66 changes: 66 additions & 0 deletions src/i18n/pages/changelog.ts
Original file line number Diff line number Diff line change
@@ -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<ChangelogContent> = {
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.",
},
};
3 changes: 3 additions & 0 deletions src/i18n/pages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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
Expand All @@ -26,6 +27,7 @@
compliance,
compare,
about,
changelog,
};

export type {
Expand All @@ -38,4 +40,5 @@
ComplianceContent,
CompareContent,
AboutContent,
ChangelogContent,

Check warning on line 43 in src/i18n/pages/index.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use `export…from` to re-export `ChangelogContent`.

See more on https://sonarcloud.io/project/issues?id=CodeGateSoftware_keeltrading.com&issues=AaAhjJh4mNh_xRbaHvh_&open=AaAhjJh4mNh_xRbaHvh_&pullRequest=42
};
14 changes: 9 additions & 5 deletions src/i18n/pages/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export interface InstallContent {
versionPrefix: string;
requirements: string;
otherPlatforms: string;
historyLink: string;
cards: PlatformCardCopy[];
thenTitle: string;
allFilesTitle: string;
Expand All @@ -78,14 +79,15 @@ export interface InstallContent {

export const install: LocalizedPage<InstallContent> = {
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.",
downloadTitle: "Download keel",
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",
Expand Down Expand Up @@ -162,15 +164,16 @@ export const install: LocalizedPage<InstallContent> = {
},

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 وقت البناء؛ ومسارُ التثبيت من المصدر في خمس دقائق هنا أيضًا.",
downloadTitle: "تنزيل كيل",
versionPrefix: "أحدث إصدار",
requirements: "يتطلّب Python 3.11 أو أحدث · التنزيل من GitHub Releases — ولا يُنسخ هنا أبدًا",
otherPlatforms: "لينكس وغيره: حزم wheel نفسها متاحةٌ في صفحة الإصدار.",
historyLink: "كلُّ الإصدارات، من الأحدث إلى الأقدم — السجلُّ الكامل للتغييرات",
cards: [
{
name: "macOS",
Expand Down Expand Up @@ -247,15 +250,16 @@ export const install: LocalizedPage<InstallContent> = {
},

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.",
downloadTitle: "Télécharger keel",
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",
Expand Down
Loading
Loading