From c5d6502a3500806ae28c40c714842883149c3065 Mon Sep 17 00:00:00 2001
From: Elmehdi Aitbrahim
Date: Sat, 22 Aug 2026 02:26:33 -0400
Subject: [PATCH 1/2] feat(news): newest-first, 5-per-page pagination,
community-discussions window
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Closes #73. The announcements feed rendered oldest-first (REST creation
order, never sorted) — every feed now sorts descending at the fetch
layer, and the announcements cap rises 10 -> 30 so the archive has
room. News paginates at five per static page: /{locale}/news/ plus
/news/2/… with per-page canonical + hreflang parity, prev/next arrows
and numbered links; the subscribe form, Show and tell, and the new
community section render on page 1 only — archive pages are pure list.
The community window answers 'why isn't #472 on the site': News read
only Announcements (+ Show and tell), and #472 lives in Compliance &
classification. Rather than flooding the archive with support threads,
a third section shows the five newest discussions from every other
category — original language only (D4/D5), deep links to GitHub — so
the community's voice (including #472) is visible without changing
what Announcements means.
---
scripts/fetch-discussions.mjs | 33 +++++--
src/components/pages/NewsPage.astro | 133 ++++++++++++++++++++++++++--
src/i18n/pages/news.ts | 34 +++++--
src/pages/ar/news.astro | 4 -
src/pages/ar/news/[page].astro | 14 +++
src/pages/ar/news/index.astro | 4 +
src/pages/en/news.astro | 4 -
src/pages/en/news/[page].astro | 14 +++
src/pages/en/news/index.astro | 4 +
src/pages/fr/news.astro | 4 -
src/pages/fr/news/[page].astro | 14 +++
src/pages/fr/news/index.astro | 4 +
12 files changed, 235 insertions(+), 31 deletions(-)
delete mode 100644 src/pages/ar/news.astro
create mode 100644 src/pages/ar/news/[page].astro
create mode 100644 src/pages/ar/news/index.astro
delete mode 100644 src/pages/en/news.astro
create mode 100644 src/pages/en/news/[page].astro
create mode 100644 src/pages/en/news/index.astro
delete mode 100644 src/pages/fr/news.astro
create mode 100644 src/pages/fr/news/[page].astro
create mode 100644 src/pages/fr/news/index.astro
diff --git a/scripts/fetch-discussions.mjs b/scripts/fetch-discussions.mjs
index 37dc20e..7ad3949 100644
--- a/scripts/fetch-discussions.mjs
+++ b/scripts/fetch-discussions.mjs
@@ -27,10 +27,22 @@ mkdirSync(DATA_DIR, { recursive: true });
const API =
"https://api.github.com/repos/CodeGateSoftware/keel/discussions?per_page=50&state=open";
-/** One output feed: category slug, destination file, item cap (#13). */
+/**
+ * One output feed: category slug, destination file, item cap. `exclude`
+ * instead of `category` = everything but those slugs (#73: the community's
+ * own threads — Q&A, compliance classification, ideas — get a window
+ * without flooding the announcements archive).
+ */
const FEEDS = [
- { category: "announcements", file: "discussions.json", maxItems: 10 },
+ { category: "announcements", file: "discussions.json", maxItems: 30 },
{ category: "show-and-tell", file: "show-and-tell.json", maxItems: 5 },
+ {
+ category: null,
+ exclude: ["announcements", "show-and-tell"],
+ file: "community.json",
+ maxItems: 5,
+ label: "discussions",
+ },
];
const headers = {
@@ -61,7 +73,12 @@ const categoryUrl = (slug) =>
/** Write one feed's file from the fetched discussions. */
function writeFeed(feed, discussions) {
const items = discussions
- .filter((discussion) => discussion.category?.slug === feed.category)
+ .filter((discussion) =>
+ feed.category
+ ? discussion.category?.slug === feed.category
+ : !feed.exclude.includes(discussion.category?.slug ?? ""),
+ )
+ .sort((a, b) => b.created_at.localeCompare(a.created_at)) // newest first (#73)
.slice(0, feed.maxItems)
.map((discussion) => ({
number: discussion.number,
@@ -76,8 +93,8 @@ function writeFeed(feed, discussions) {
join(DATA_DIR, feed.file),
JSON.stringify(
{
- category: feed.category,
- categoryUrl: categoryUrl(feed.category),
+ category: feed.category ?? feed.label,
+ categoryUrl: feed.category ? categoryUrl(feed.category) : "https://github.com/CodeGateSoftware/keel/discussions",
fetchedAt: new Date().toISOString(),
items,
},
@@ -85,7 +102,7 @@ function writeFeed(feed, discussions) {
2,
) + "\n",
);
- console.log(` discussions: ${items.length} ${feed.category}(s) -> data/${feed.file}`);
+ console.log(` discussions: ${items.length} ${feed.category ?? feed.label}(s) -> data/${feed.file}`);
}
/** Degrade one feed's file: keep last-known data, else an empty stub. */
@@ -99,8 +116,8 @@ function degradeFeed(feed, reason) {
out,
JSON.stringify(
{
- category: feed.category,
- categoryUrl: categoryUrl(feed.category),
+ category: feed.category ?? feed.label,
+ categoryUrl: feed.category ? categoryUrl(feed.category) : "https://github.com/CodeGateSoftware/keel/discussions",
fetchedAt: null,
items: [],
},
diff --git a/src/components/pages/NewsPage.astro b/src/components/pages/NewsPage.astro
index 3f20aa2..f5067b1 100644
--- a/src/components/pages/NewsPage.astro
+++ b/src/components/pages/NewsPage.astro
@@ -7,9 +7,11 @@ import { t } from "../../i18n/ui";
interface Props {
locale: Locale;
+ /** 1-based page number; page 1 renders at /news/, archives at /news/2/… (#73). */
+ page?: number;
}
-const { locale } = Astro.props;
+const { locale, page = 1 } = Astro.props;
const c = news[locale];
const chrome = t(locale);
@@ -48,15 +50,35 @@ const showAndTell: DiscussionsFile =
fetchedAt: null,
items: [],
};
+
+/** Community window (#73) — newest threads from every other category. */
+const community: DiscussionsFile =
+ readDataFile("community.json") ?? {
+ category: "discussions",
+ categoryUrl: "https://github.com/CodeGateSoftware/keel/discussions",
+ fetchedAt: null,
+ items: [],
+ };
+
+/** Pagination (#73) — five announcements per static page. */
+const PAGE_SIZE = 5;
+const totalPages = Math.max(1, Math.ceil(feed.items.length / PAGE_SIZE));
+const current = Math.min(Math.max(1, page), totalPages);
+const pageItems = feed.items.slice((current - 1) * PAGE_SIZE, current * PAGE_SIZE);
+const pageHref = (n: number) => (n <= 1 ? localePath(locale, "news") : `/${locale}/news/${n}/`);
---
1 ? `${c.title} — ${current}` : c.title}
description={c.description}
- path={localePath(locale, "news")}
- alternates={alternatesFor("news")}
+ path={current > 1 ? `/${locale}/news/${current}/` : localePath(locale, "news")}
+ alternates={
+ current > 1
+ ? alternatesFor("news").map((alt) => ({ ...alt, path: `/${alt.locale}/news/${current}/` }))
+ : alternatesFor("news")
+ }
>
@@ -80,7 +102,7 @@ const showAndTell: DiscussionsFile =
) : (
+
+ {
+ current === 1 && (
+
+ )
+ }