@jant/core 0.3.33 → 0.3.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +57 -9
- package/package.json +1 -1
- package/src/app.tsx +1 -1
- package/src/routes/auth/__tests__/setup.test.ts +1 -1
- package/src/routes/auth/setup.tsx +1 -1
- package/src/routes/feed/__tests__/rss.test.ts +244 -5
- package/src/routes/feed/rss.ts +68 -6
- package/src/types/constants.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -2586,7 +2586,7 @@ const SYSTEM_NAV_KEYS = {
|
|
|
2586
2586
|
},
|
|
2587
2587
|
collections: {
|
|
2588
2588
|
defaultLabel: "Collections",
|
|
2589
|
-
url: "/
|
|
2589
|
+
url: "/c"
|
|
2590
2590
|
},
|
|
2591
2591
|
archive: {
|
|
2592
2592
|
defaultLabel: "Archive",
|
|
@@ -5152,7 +5152,7 @@ const I18nProvider = ({ c, children })=>{
|
|
|
5152
5152
|
}
|
|
5153
5153
|
|
|
5154
5154
|
const IS_VITE_DEV = typeof __JANT_DEV__ !== "undefined" && __JANT_DEV__ === true;
|
|
5155
|
-
const CORE_VERSION = "0.3.
|
|
5155
|
+
const CORE_VERSION = "0.3.35";
|
|
5156
5156
|
|
|
5157
5157
|
const BaseLayout = ({ title, description, lang, c, toast, faviconUrl, faviconVersion, noindex, isAuthenticated = false, children })=>{
|
|
5158
5158
|
// Read lang from Hono context if available, otherwise use prop or default
|
|
@@ -6223,7 +6223,7 @@ setupRoutes.post("/setup", async (c)=>{
|
|
|
6223
6223
|
await c.var.services.navItems.create({
|
|
6224
6224
|
type: "link",
|
|
6225
6225
|
label: "Collections",
|
|
6226
|
-
url: "/
|
|
6226
|
+
url: "/c"
|
|
6227
6227
|
});
|
|
6228
6228
|
// Seed default navigation items
|
|
6229
6229
|
await c.var.services.navItems.create({
|
|
@@ -14925,7 +14925,11 @@ composeRoutes.post("/", async (c)=>{
|
|
|
14925
14925
|
const rssRoutes = new Hono();
|
|
14926
14926
|
/**
|
|
14927
14927
|
* Build FeedData from the Hono context.
|
|
14928
|
-
|
|
14928
|
+
*
|
|
14929
|
+
* @param c - Hono context
|
|
14930
|
+
* @param opts - Filter options for the feed
|
|
14931
|
+
* @returns Feed data ready for rendering
|
|
14932
|
+
*/ async function buildFeedData(c, opts) {
|
|
14929
14933
|
const { appConfig } = c.var;
|
|
14930
14934
|
const siteName = appConfig.siteName;
|
|
14931
14935
|
const siteDescription = appConfig.siteDescription;
|
|
@@ -14935,6 +14939,8 @@ const rssRoutes = new Hono();
|
|
|
14935
14939
|
const posts = await c.var.services.posts.list({
|
|
14936
14940
|
status: "published",
|
|
14937
14941
|
excludeReplies: true,
|
|
14942
|
+
featured: opts?.featured,
|
|
14943
|
+
format: opts?.format,
|
|
14938
14944
|
limit: feedLimit
|
|
14939
14945
|
});
|
|
14940
14946
|
// Batch load media for enclosures
|
|
@@ -14955,9 +14961,22 @@ const rssRoutes = new Hono();
|
|
|
14955
14961
|
posts: postViews
|
|
14956
14962
|
};
|
|
14957
14963
|
}
|
|
14958
|
-
|
|
14964
|
+
/**
|
|
14965
|
+
* Parse and validate the `format` query parameter.
|
|
14966
|
+
* Returns a valid Format or undefined if missing/invalid.
|
|
14967
|
+
*/ function parseFormatQuery(c) {
|
|
14968
|
+
const raw = c.req.query("format");
|
|
14969
|
+
if (raw && FORMATS.includes(raw)) {
|
|
14970
|
+
return raw;
|
|
14971
|
+
}
|
|
14972
|
+
return undefined;
|
|
14973
|
+
}
|
|
14974
|
+
// --- Featured feed (curated) ---
|
|
14975
|
+
// RSS 2.0 — /feed
|
|
14959
14976
|
rssRoutes.get("/", async (c)=>{
|
|
14960
|
-
const feedData = await buildFeedData(c
|
|
14977
|
+
const feedData = await buildFeedData(c, {
|
|
14978
|
+
featured: true
|
|
14979
|
+
});
|
|
14961
14980
|
const xml = defaultRssRenderer(feedData);
|
|
14962
14981
|
return new Response(xml, {
|
|
14963
14982
|
headers: {
|
|
@@ -14965,9 +14984,38 @@ rssRoutes.get("/", async (c)=>{
|
|
|
14965
14984
|
}
|
|
14966
14985
|
});
|
|
14967
14986
|
});
|
|
14968
|
-
// Atom
|
|
14987
|
+
// Atom — /feed/atom.xml
|
|
14969
14988
|
rssRoutes.get("/atom.xml", async (c)=>{
|
|
14970
|
-
const feedData = await buildFeedData(c
|
|
14989
|
+
const feedData = await buildFeedData(c, {
|
|
14990
|
+
featured: true
|
|
14991
|
+
});
|
|
14992
|
+
const xml = defaultAtomRenderer(feedData);
|
|
14993
|
+
return new Response(xml, {
|
|
14994
|
+
headers: {
|
|
14995
|
+
"Content-Type": "application/atom+xml; charset=utf-8"
|
|
14996
|
+
}
|
|
14997
|
+
});
|
|
14998
|
+
});
|
|
14999
|
+
// --- All posts feed ---
|
|
15000
|
+
// RSS 2.0 — /feed/all
|
|
15001
|
+
rssRoutes.get("/all", async (c)=>{
|
|
15002
|
+
const format = parseFormatQuery(c);
|
|
15003
|
+
const feedData = await buildFeedData(c, {
|
|
15004
|
+
format
|
|
15005
|
+
});
|
|
15006
|
+
const xml = defaultRssRenderer(feedData);
|
|
15007
|
+
return new Response(xml, {
|
|
15008
|
+
headers: {
|
|
15009
|
+
"Content-Type": "application/rss+xml; charset=utf-8"
|
|
15010
|
+
}
|
|
15011
|
+
});
|
|
15012
|
+
});
|
|
15013
|
+
// Atom — /feed/all/atom.xml
|
|
15014
|
+
rssRoutes.get("/all/atom.xml", async (c)=>{
|
|
15015
|
+
const format = parseFormatQuery(c);
|
|
15016
|
+
const feedData = await buildFeedData(c, {
|
|
15017
|
+
format
|
|
15018
|
+
});
|
|
14971
15019
|
const xml = defaultAtomRenderer(feedData);
|
|
14972
15020
|
return new Response(xml, {
|
|
14973
15021
|
headers: {
|
|
@@ -15575,7 +15623,7 @@ const errorHandler = (err, c)=>{
|
|
|
15575
15623
|
app.route("/archive", archiveRoutes);
|
|
15576
15624
|
app.route("/featured", featuredRoutes);
|
|
15577
15625
|
app.route("/latest", latestRoutes);
|
|
15578
|
-
app.route("/
|
|
15626
|
+
app.route("/c", collectionsPageRoutes);
|
|
15579
15627
|
app.route("/c", collectionRoutes);
|
|
15580
15628
|
app.route("/p", postRoutes);
|
|
15581
15629
|
app.route("/", homeRoutes);
|
package/package.json
CHANGED
package/src/app.tsx
CHANGED
|
@@ -266,7 +266,7 @@ export function createApp(): App {
|
|
|
266
266
|
app.route("/archive", archiveRoutes);
|
|
267
267
|
app.route("/featured", featuredRoutes);
|
|
268
268
|
app.route("/latest", latestRoutes);
|
|
269
|
-
app.route("/
|
|
269
|
+
app.route("/c", collectionsPageRoutes);
|
|
270
270
|
app.route("/c", collectionRoutes);
|
|
271
271
|
app.route("/p", postRoutes);
|
|
272
272
|
app.route("/", homeRoutes);
|
|
@@ -198,7 +198,7 @@ setupRoutes.post("/setup", async (c) => {
|
|
|
198
198
|
await c.var.services.navItems.create({
|
|
199
199
|
type: "link",
|
|
200
200
|
label: "Collections",
|
|
201
|
-
url: "/
|
|
201
|
+
url: "/c",
|
|
202
202
|
});
|
|
203
203
|
// Seed default navigation items
|
|
204
204
|
await c.var.services.navItems.create({
|
|
@@ -46,17 +46,255 @@ function createFeedTestApp(envOverrides: Partial<Bindings> = {}) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
describe("RSS Feed Routes", () => {
|
|
49
|
+
describe("/feed — featured only", () => {
|
|
50
|
+
it("returns only featured posts", async () => {
|
|
51
|
+
const { app, services } = createFeedTestApp();
|
|
52
|
+
|
|
53
|
+
// Create a mix of featured and non-featured posts
|
|
54
|
+
await services.posts.create({
|
|
55
|
+
format: "note",
|
|
56
|
+
title: "Regular Post",
|
|
57
|
+
body: "Not featured",
|
|
58
|
+
status: "published",
|
|
59
|
+
});
|
|
60
|
+
await services.posts.create({
|
|
61
|
+
format: "note",
|
|
62
|
+
title: "Featured Post",
|
|
63
|
+
body: "This is featured",
|
|
64
|
+
status: "published",
|
|
65
|
+
featured: true,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const res = await app.request("/feed");
|
|
69
|
+
expect(res.status).toBe(200);
|
|
70
|
+
|
|
71
|
+
const xml = await res.text();
|
|
72
|
+
expect(xml).toContain("Featured Post");
|
|
73
|
+
expect(xml).not.toContain("Regular Post");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns empty feed when no featured posts exist", async () => {
|
|
77
|
+
const { app, services } = createFeedTestApp();
|
|
78
|
+
|
|
79
|
+
await services.posts.create({
|
|
80
|
+
format: "note",
|
|
81
|
+
title: "Regular Post",
|
|
82
|
+
body: "Not featured",
|
|
83
|
+
status: "published",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const res = await app.request("/feed");
|
|
87
|
+
expect(res.status).toBe(200);
|
|
88
|
+
|
|
89
|
+
const xml = await res.text();
|
|
90
|
+
expect(xml).not.toContain("Regular Post");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("returns RSS content type", async () => {
|
|
94
|
+
const { app } = createFeedTestApp();
|
|
95
|
+
|
|
96
|
+
const res = await app.request("/feed");
|
|
97
|
+
expect(res.headers.get("Content-Type")).toBe(
|
|
98
|
+
"application/rss+xml; charset=utf-8",
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("/feed/atom.xml — featured only (Atom)", () => {
|
|
104
|
+
it("returns only featured posts in Atom format", async () => {
|
|
105
|
+
const { app, services } = createFeedTestApp();
|
|
106
|
+
|
|
107
|
+
await services.posts.create({
|
|
108
|
+
format: "note",
|
|
109
|
+
title: "Regular Post",
|
|
110
|
+
body: "Not featured",
|
|
111
|
+
status: "published",
|
|
112
|
+
});
|
|
113
|
+
await services.posts.create({
|
|
114
|
+
format: "note",
|
|
115
|
+
title: "Featured Post",
|
|
116
|
+
body: "This is featured",
|
|
117
|
+
status: "published",
|
|
118
|
+
featured: true,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const res = await app.request("/feed/atom.xml");
|
|
122
|
+
expect(res.status).toBe(200);
|
|
123
|
+
expect(res.headers.get("Content-Type")).toBe(
|
|
124
|
+
"application/atom+xml; charset=utf-8",
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const xml = await res.text();
|
|
128
|
+
expect(xml).toContain("Featured Post");
|
|
129
|
+
expect(xml).not.toContain("Regular Post");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("/feed/all — all published posts", () => {
|
|
134
|
+
it("returns all published posts", async () => {
|
|
135
|
+
const { app, services } = createFeedTestApp();
|
|
136
|
+
|
|
137
|
+
await services.posts.create({
|
|
138
|
+
format: "note",
|
|
139
|
+
title: "Regular Post",
|
|
140
|
+
body: "Not featured",
|
|
141
|
+
status: "published",
|
|
142
|
+
});
|
|
143
|
+
await services.posts.create({
|
|
144
|
+
format: "note",
|
|
145
|
+
title: "Featured Post",
|
|
146
|
+
body: "This is featured",
|
|
147
|
+
status: "published",
|
|
148
|
+
featured: true,
|
|
149
|
+
});
|
|
150
|
+
await services.posts.create({
|
|
151
|
+
format: "note",
|
|
152
|
+
title: "Draft Post",
|
|
153
|
+
body: "Draft",
|
|
154
|
+
status: "draft",
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const res = await app.request("/feed/all");
|
|
158
|
+
expect(res.status).toBe(200);
|
|
159
|
+
|
|
160
|
+
const xml = await res.text();
|
|
161
|
+
expect(xml).toContain("Regular Post");
|
|
162
|
+
expect(xml).toContain("Featured Post");
|
|
163
|
+
expect(xml).not.toContain("Draft Post");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("filters by format query parameter", async () => {
|
|
167
|
+
const { app, services } = createFeedTestApp();
|
|
168
|
+
|
|
169
|
+
await services.posts.create({
|
|
170
|
+
format: "note",
|
|
171
|
+
title: "My Note",
|
|
172
|
+
body: "A note",
|
|
173
|
+
status: "published",
|
|
174
|
+
});
|
|
175
|
+
await services.posts.create({
|
|
176
|
+
format: "link",
|
|
177
|
+
title: "My Link",
|
|
178
|
+
url: "https://example.com",
|
|
179
|
+
status: "published",
|
|
180
|
+
});
|
|
181
|
+
await services.posts.create({
|
|
182
|
+
format: "quote",
|
|
183
|
+
title: "My Quote",
|
|
184
|
+
quoteText: "Something wise",
|
|
185
|
+
status: "published",
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const res = await app.request("/feed/all?format=note");
|
|
189
|
+
expect(res.status).toBe(200);
|
|
190
|
+
|
|
191
|
+
const xml = await res.text();
|
|
192
|
+
expect(xml).toContain("My Note");
|
|
193
|
+
expect(xml).not.toContain("My Link");
|
|
194
|
+
expect(xml).not.toContain("My Quote");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("ignores invalid format query parameter", async () => {
|
|
198
|
+
const { app, services } = createFeedTestApp();
|
|
199
|
+
|
|
200
|
+
await services.posts.create({
|
|
201
|
+
format: "note",
|
|
202
|
+
title: "My Note",
|
|
203
|
+
body: "A note",
|
|
204
|
+
status: "published",
|
|
205
|
+
});
|
|
206
|
+
await services.posts.create({
|
|
207
|
+
format: "link",
|
|
208
|
+
title: "My Link",
|
|
209
|
+
url: "https://example.com",
|
|
210
|
+
status: "published",
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const res = await app.request("/feed/all?format=invalid");
|
|
214
|
+
expect(res.status).toBe(200);
|
|
215
|
+
|
|
216
|
+
const xml = await res.text();
|
|
217
|
+
// Invalid format is ignored — all posts returned
|
|
218
|
+
expect(xml).toContain("My Note");
|
|
219
|
+
expect(xml).toContain("My Link");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("returns RSS content type", async () => {
|
|
223
|
+
const { app } = createFeedTestApp();
|
|
224
|
+
|
|
225
|
+
const res = await app.request("/feed/all");
|
|
226
|
+
expect(res.headers.get("Content-Type")).toBe(
|
|
227
|
+
"application/rss+xml; charset=utf-8",
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("/feed/all/atom.xml — all published posts (Atom)", () => {
|
|
233
|
+
it("returns all published posts in Atom format", async () => {
|
|
234
|
+
const { app, services } = createFeedTestApp();
|
|
235
|
+
|
|
236
|
+
await services.posts.create({
|
|
237
|
+
format: "note",
|
|
238
|
+
title: "Regular Post",
|
|
239
|
+
body: "Not featured",
|
|
240
|
+
status: "published",
|
|
241
|
+
});
|
|
242
|
+
await services.posts.create({
|
|
243
|
+
format: "note",
|
|
244
|
+
title: "Featured Post",
|
|
245
|
+
body: "This is featured",
|
|
246
|
+
status: "published",
|
|
247
|
+
featured: true,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const res = await app.request("/feed/all/atom.xml");
|
|
251
|
+
expect(res.status).toBe(200);
|
|
252
|
+
expect(res.headers.get("Content-Type")).toBe(
|
|
253
|
+
"application/atom+xml; charset=utf-8",
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const xml = await res.text();
|
|
257
|
+
expect(xml).toContain("Regular Post");
|
|
258
|
+
expect(xml).toContain("Featured Post");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("supports format filtering", async () => {
|
|
262
|
+
const { app, services } = createFeedTestApp();
|
|
263
|
+
|
|
264
|
+
await services.posts.create({
|
|
265
|
+
format: "note",
|
|
266
|
+
title: "My Note",
|
|
267
|
+
body: "A note",
|
|
268
|
+
status: "published",
|
|
269
|
+
});
|
|
270
|
+
await services.posts.create({
|
|
271
|
+
format: "link",
|
|
272
|
+
title: "My Link",
|
|
273
|
+
url: "https://example.com",
|
|
274
|
+
status: "published",
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const res = await app.request("/feed/all/atom.xml?format=link");
|
|
278
|
+
expect(res.status).toBe(200);
|
|
279
|
+
|
|
280
|
+
const xml = await res.text();
|
|
281
|
+
expect(xml).not.toContain("My Note");
|
|
282
|
+
expect(xml).toContain("My Link");
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
49
286
|
describe("RSS_FEED_LIMIT env var", () => {
|
|
50
287
|
it("defaults to 50 when RSS_FEED_LIMIT is not set", async () => {
|
|
51
288
|
const { app, services } = createFeedTestApp();
|
|
52
289
|
|
|
53
|
-
// Create 3 posts
|
|
290
|
+
// Create 3 featured posts
|
|
54
291
|
for (let i = 0; i < 3; i++) {
|
|
55
292
|
await services.posts.create({
|
|
56
293
|
format: "note",
|
|
57
294
|
title: `Post ${i}`,
|
|
58
295
|
body: `Body ${i}`,
|
|
59
296
|
status: "published",
|
|
297
|
+
featured: true,
|
|
60
298
|
});
|
|
61
299
|
}
|
|
62
300
|
|
|
@@ -75,7 +313,7 @@ describe("RSS Feed Routes", () => {
|
|
|
75
313
|
RSS_FEED_LIMIT: "2",
|
|
76
314
|
});
|
|
77
315
|
|
|
78
|
-
// Create 5 posts
|
|
316
|
+
// Create 5 posts on /feed/all
|
|
79
317
|
for (let i = 0; i < 5; i++) {
|
|
80
318
|
await services.posts.create({
|
|
81
319
|
format: "note",
|
|
@@ -85,7 +323,7 @@ describe("RSS Feed Routes", () => {
|
|
|
85
323
|
});
|
|
86
324
|
}
|
|
87
325
|
|
|
88
|
-
const res = await app.request("/feed");
|
|
326
|
+
const res = await app.request("/feed/all");
|
|
89
327
|
expect(res.status).toBe(200);
|
|
90
328
|
|
|
91
329
|
const xml = await res.text();
|
|
@@ -103,7 +341,7 @@ describe("RSS Feed Routes", () => {
|
|
|
103
341
|
RSS_FEED_LIMIT: "not-a-number",
|
|
104
342
|
});
|
|
105
343
|
|
|
106
|
-
// Create 2 posts
|
|
344
|
+
// Create 2 posts on /feed/all
|
|
107
345
|
for (let i = 0; i < 2; i++) {
|
|
108
346
|
await services.posts.create({
|
|
109
347
|
format: "note",
|
|
@@ -113,7 +351,7 @@ describe("RSS Feed Routes", () => {
|
|
|
113
351
|
});
|
|
114
352
|
}
|
|
115
353
|
|
|
116
|
-
const res = await app.request("/feed");
|
|
354
|
+
const res = await app.request("/feed/all");
|
|
117
355
|
expect(res.status).toBe(200);
|
|
118
356
|
|
|
119
357
|
const xml = await res.text();
|
|
@@ -133,6 +371,7 @@ describe("RSS Feed Routes", () => {
|
|
|
133
371
|
title: `Post ${i}`,
|
|
134
372
|
body: `Body ${i}`,
|
|
135
373
|
status: "published",
|
|
374
|
+
featured: true,
|
|
136
375
|
});
|
|
137
376
|
}
|
|
138
377
|
|
package/src/routes/feed/rss.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* RSS Feed Routes
|
|
3
|
+
*
|
|
4
|
+
* Three-level hierarchy:
|
|
5
|
+
* - /feed — featured posts only (curated feed for subscribers)
|
|
6
|
+
* - /feed/all — all published posts (with optional ?format= filter)
|
|
7
|
+
* - /c/{slug}/feed — per-collection feed (handled in collection routes)
|
|
3
8
|
*/
|
|
4
9
|
|
|
5
10
|
import { Hono } from "hono";
|
|
6
11
|
import type { Context } from "hono";
|
|
7
|
-
import type { Bindings, FeedData } from "../../types.js";
|
|
12
|
+
import type { Bindings, FeedData, Format } from "../../types.js";
|
|
8
13
|
import type { AppVariables } from "../../types/app-context.js";
|
|
9
14
|
import { defaultRssRenderer, defaultAtomRenderer } from "../../lib/feed.js";
|
|
10
15
|
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
16
|
+
import { FORMATS } from "../../types/constants.js";
|
|
11
17
|
|
|
12
18
|
import { createMediaContext, toPostViews } from "../../lib/view.js";
|
|
13
19
|
|
|
@@ -15,10 +21,22 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
|
15
21
|
|
|
16
22
|
export const rssRoutes = new Hono<Env>();
|
|
17
23
|
|
|
24
|
+
interface FeedOptions {
|
|
25
|
+
featured?: boolean;
|
|
26
|
+
format?: Format;
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
/**
|
|
19
30
|
* Build FeedData from the Hono context.
|
|
31
|
+
*
|
|
32
|
+
* @param c - Hono context
|
|
33
|
+
* @param opts - Filter options for the feed
|
|
34
|
+
* @returns Feed data ready for rendering
|
|
20
35
|
*/
|
|
21
|
-
async function buildFeedData(
|
|
36
|
+
async function buildFeedData(
|
|
37
|
+
c: Context<Env>,
|
|
38
|
+
opts?: FeedOptions,
|
|
39
|
+
): Promise<FeedData> {
|
|
22
40
|
const { appConfig } = c.var;
|
|
23
41
|
const siteName = appConfig.siteName;
|
|
24
42
|
const siteDescription = appConfig.siteDescription;
|
|
@@ -29,6 +47,8 @@ async function buildFeedData(c: Context<Env>): Promise<FeedData> {
|
|
|
29
47
|
const posts = await c.var.services.posts.list({
|
|
30
48
|
status: "published",
|
|
31
49
|
excludeReplies: true,
|
|
50
|
+
featured: opts?.featured,
|
|
51
|
+
format: opts?.format,
|
|
32
52
|
limit: feedLimit,
|
|
33
53
|
});
|
|
34
54
|
|
|
@@ -61,9 +81,23 @@ async function buildFeedData(c: Context<Env>): Promise<FeedData> {
|
|
|
61
81
|
};
|
|
62
82
|
}
|
|
63
83
|
|
|
64
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Parse and validate the `format` query parameter.
|
|
86
|
+
* Returns a valid Format or undefined if missing/invalid.
|
|
87
|
+
*/
|
|
88
|
+
function parseFormatQuery(c: Context<Env>): Format | undefined {
|
|
89
|
+
const raw = c.req.query("format");
|
|
90
|
+
if (raw && (FORMATS as readonly string[]).includes(raw)) {
|
|
91
|
+
return raw as Format;
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- Featured feed (curated) ---
|
|
97
|
+
|
|
98
|
+
// RSS 2.0 — /feed
|
|
65
99
|
rssRoutes.get("/", async (c) => {
|
|
66
|
-
const feedData = await buildFeedData(c);
|
|
100
|
+
const feedData = await buildFeedData(c, { featured: true });
|
|
67
101
|
const xml = defaultRssRenderer(feedData);
|
|
68
102
|
|
|
69
103
|
return new Response(xml, {
|
|
@@ -73,9 +107,37 @@ rssRoutes.get("/", async (c) => {
|
|
|
73
107
|
});
|
|
74
108
|
});
|
|
75
109
|
|
|
76
|
-
// Atom
|
|
110
|
+
// Atom — /feed/atom.xml
|
|
77
111
|
rssRoutes.get("/atom.xml", async (c) => {
|
|
78
|
-
const feedData = await buildFeedData(c);
|
|
112
|
+
const feedData = await buildFeedData(c, { featured: true });
|
|
113
|
+
const xml = defaultAtomRenderer(feedData);
|
|
114
|
+
|
|
115
|
+
return new Response(xml, {
|
|
116
|
+
headers: {
|
|
117
|
+
"Content-Type": "application/atom+xml; charset=utf-8",
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// --- All posts feed ---
|
|
123
|
+
|
|
124
|
+
// RSS 2.0 — /feed/all
|
|
125
|
+
rssRoutes.get("/all", async (c) => {
|
|
126
|
+
const format = parseFormatQuery(c);
|
|
127
|
+
const feedData = await buildFeedData(c, { format });
|
|
128
|
+
const xml = defaultRssRenderer(feedData);
|
|
129
|
+
|
|
130
|
+
return new Response(xml, {
|
|
131
|
+
headers: {
|
|
132
|
+
"Content-Type": "application/rss+xml; charset=utf-8",
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Atom — /feed/all/atom.xml
|
|
138
|
+
rssRoutes.get("/all/atom.xml", async (c) => {
|
|
139
|
+
const format = parseFormatQuery(c);
|
|
140
|
+
const feedData = await buildFeedData(c, { format });
|
|
79
141
|
const xml = defaultAtomRenderer(feedData);
|
|
80
142
|
|
|
81
143
|
return new Response(xml, {
|
package/src/types/constants.ts
CHANGED
|
@@ -22,7 +22,7 @@ export type NavItemType = (typeof NAV_ITEM_TYPES)[number];
|
|
|
22
22
|
export const SYSTEM_NAV_KEYS = {
|
|
23
23
|
rss: { defaultLabel: "RSS", url: "/feed" },
|
|
24
24
|
dashboard: { defaultLabel: "Dashboard", url: "/dash" },
|
|
25
|
-
collections: { defaultLabel: "Collections", url: "/
|
|
25
|
+
collections: { defaultLabel: "Collections", url: "/c" },
|
|
26
26
|
archive: { defaultLabel: "Archive", url: "/archive" },
|
|
27
27
|
} as const;
|
|
28
28
|
export type SystemNavKey = keyof typeof SYSTEM_NAV_KEYS;
|