Skip to content

Commit 0a9d90a

Browse files
TMHSDigitalclaude
andcommitted
feeds: an Atom feed of new sources per category
The only feeds covered the whole catalog. A reader who only wants audio or fonts had to follow every new source. The build now writes category/<id>/feed.xml: the newest non-deprecated entries in that category by `added`, built by atomFeed with its own id, title, self and alternate links. Each category page advertises its feed in <head> and links it beside the catalog-search link. docs/data.md lists the new files. Closes #214 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LpkeXRUqm7oDedPDk3QWtK
1 parent 750a58d commit 0a9d90a

5 files changed

Lines changed: 28 additions & 7 deletions

File tree

‎docs/data.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ The catalog is published as machine-readable files on every deploy. The metadata
1212
| [`catalog.csv`](https://tmhsdigital.github.io/Free-Game-Dev-Assets/catalog.csv) | One row per entry, for spreadsheets. Lists are joined with `; ` |
1313
| [`feed.xml`](https://tmhsdigital.github.io/Free-Game-Dev-Assets/feed.xml) | Atom feed of newly added sources |
1414
| [`reverified.xml`](https://tmhsdigital.github.io/Free-Game-Dev-Assets/reverified.xml) | Atom feed of entries whose licence was most recently re-read |
15+
| `category/<id>/feed.xml` | Atom feed of new sources in one category, such as [`category/audio/feed.xml`](https://tmhsdigital.github.io/Free-Game-Dev-Assets/category/audio/feed.xml); each category page links its own |
1516
| [`llms.txt`](https://tmhsdigital.github.io/Free-Game-Dev-Assets/llms.txt) | A plain-text summary for language-model tools |
1617

1718
These URLs always serve the latest build. To pin a version, download the same files from a tagged [release](https://github.com/TMHSDigital/Free-Game-Dev-Assets/releases). Releases are tagged `vYYYY.MM.DD`, and the attached `data.json` carries the same string as `version`.

‎site/build.mjs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,11 @@ function main() {
634634
writeOut("catalog.csv", catalogCsv(entries, config.site.siteUrl));
635635
writeOut("feed.xml", atomFeed({ entries, site: config.site, generatedAt: payload.generatedAt, kind: "added" }));
636636
writeOut("reverified.xml", atomFeed({ entries, site: config.site, generatedAt: payload.generatedAt, kind: "verified" }));
637+
// New sources per category, linked from each category page.
638+
for (const [id, meta] of Object.entries(config.categories)) {
639+
if (!entries.some((e) => e.category === id)) continue;
640+
writeOut(path.join("category", id, "feed.xml"), atomFeed({ entries, site: config.site, generatedAt: payload.generatedAt, kind: "added", category: { id, label: meta.label || id } }));
641+
}
637642

638643
// Prerender. The served HTML must be complete without JavaScript: this is a
639644
// catalog whose whole value is that its entries can be found.

‎site/lib/category-page.mjs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export function categoryPageHtml({ cat, meta, entries, site, stamp, total, hasCa
4848
<title>${esc(clipText(title, SEO_TITLE_MAX))}</title>
4949
<meta name="description" content="${esc(description)}" />
5050
${baseHeadHtml({ site, title, description, url, hasCard, root: "../../", modules: PAGE_MODULES.category })}
51+
<link rel="alternate" type="application/atom+xml" title="${esc(`${site.title}: new sources in ${label}`)}" href="${esc(`${url}feed.xml`)}" />
5152
<script type="application/ld+json">${scriptJson(categoryJsonLd({ cat, label, description, entries, site }))}</script>
5253
</head>
5354
<body>
@@ -60,7 +61,7 @@ export function categoryPageHtml({ cat, meta, entries, site, stamp, total, hasCa
6061
</nav>
6162
<h1>${esc(label)}</h1>
6263
<p class="section-blurb">${esc(sources)} · ${esc(blurb)}</p>
63-
<p class="category-search"><a href="../../?cat=${encodeURIComponent(cat)}#catalog">Search and filter on the catalog page</a></p>
64+
<p class="category-search"><a href="../../?cat=${encodeURIComponent(cat)}#catalog">Search and filter on the catalog page</a> · <a href="feed.xml">Feed of new ${esc(label)} sources</a></p>
6465
<div class="entry-list" id="entry-grid">
6566
${grid}
6667
</div>

‎site/lib/exports.mjs‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,17 @@ export const FEEDS = {
114114
* feed.xml) or "verified" (licence re-reads, reverified.xml). Two feeds, so
115115
* a re-verification sweep of old entries cannot bury the new ones.
116116
*/
117-
export function atomFeed({ entries, site, generatedAt, limit = 40, kind = "added" }) {
117+
export function atomFeed({ entries, site, generatedAt, limit = 40, kind = "added", category = null }) {
118118
const spec = FEEDS[kind];
119119
if (!spec) throw new Error(`atomFeed: unknown kind "${kind}"`);
120120
const base = site.siteUrl.replace(/\/+$/, "");
121+
// A category feed (category/<id>/feed.xml): new sources in that category only.
122+
const file = category ? `category/${category.id}/feed.xml` : spec.file;
123+
const home = category ? `${base}/category/${category.id}/` : `${base}/`;
124+
const feedTitle = category ? `${spec.title} in ${category.label}` : spec.title;
125+
const feedId = category ? `${base}/${file}` : `${base}/${kind === "added" ? "" : spec.file}`;
121126
const recent = entries
122-
.filter((e) => e.status !== "deprecated" && e[kind])
127+
.filter((e) => (!category || e.category === category.id) && e.status !== "deprecated" && e[kind])
123128
.sort((a, b) => String(b[kind]).localeCompare(String(a[kind])) || a.name.localeCompare(b.name))
124129
.slice(0, limit);
125130
const items = recent.map((e) => {
@@ -137,13 +142,13 @@ export function atomFeed({ entries, site, generatedAt, limit = 40, kind = "added
137142
return [
138143
'<?xml version="1.0" encoding="UTF-8"?>',
139144
'<feed xmlns="http://www.w3.org/2005/Atom">',
140-
` <id>${esc(base)}/${kind === "added" ? "" : spec.file}</id>`,
141-
` <title>${esc(site.title)}: ${spec.title}</title>`,
145+
` <id>${esc(feedId)}</id>`,
146+
` <title>${esc(site.title)}: ${esc(feedTitle)}</title>`,
142147
` <subtitle>${esc(`${site.tagline} ${spec.what[0].toUpperCase()}${spec.what.slice(1)}.`)}</subtitle>`,
143148
// RFC 4287 4.1.1: a feed-level author covers every entry.
144149
` <author><name>${esc(site.title)}</name><uri>${esc(base)}/</uri></author>`,
145-
` <link href="${esc(base)}/" />`,
146-
` <link rel="self" href="${esc(base)}/${spec.file}" />`,
150+
` <link href="${esc(home)}" />`,
151+
` <link rel="self" href="${esc(base)}/${file}" />`,
147152
` <updated>${esc(generatedAt)}</updated>`,
148153
...items,
149154
"</feed>",

‎site/lib/lib.test.mjs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,15 @@ eq("new-sources feed is by added, newest first", newFeed.indexOf("entry/c/") < n
691691
has("new-sources feed is the default feed.xml", newFeed, '<link rel="self" href="https://s.test/site/feed.xml" />');
692692
has("new-sources feed uses the added date", newFeed, "<updated>2026-09-20T00:00:00Z</updated>");
693693
has("new-sources feed title", newFeed, "<title>T: new sources</title>");
694+
{
695+
const catFeed = atomFeed({ entries: sample, site: { title: "T", tagline: "t", siteUrl: "https://s.test/site/" }, generatedAt: "2026-10-01T00:00:00.000Z", category: { id: sample[0].category, label: "Cat & Co" } });
696+
const others = sample.filter((e) => e.category !== sample[0].category && e.status !== "deprecated");
697+
has("category feed names itself", catFeed, `<link rel="self" href="https://s.test/site/category/${sample[0].category}/feed.xml" />`);
698+
has("category feed links its page", catFeed, `<link href="https://s.test/site/category/${sample[0].category}/" />`);
699+
has("category feed title escapes the label", catFeed, "<title>T: new sources in Cat &amp; Co</title>");
700+
has("category feed keeps its own entries", catFeed, `entry/${sample[0].id}/`);
701+
for (const e of others) lacks(`category feed leaves out ${e.id}`, catFeed, `entry/${e.id}/`);
702+
}
694703

695704
/* review fixes, 2026-10 (#59-#66) ------------------------------------------ */
696705
import { csvField } from "./exports.mjs";

0 commit comments

Comments
 (0)