@jant/core 0.6.8 → 0.6.10
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/bin/commands/uploads/cleanup.js +1 -0
- package/dist/{app-9P4rVCe2.js → app-CGHkOdme.js} +3450 -3121
- package/dist/app-D24n0DoH.js +6 -0
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/{client-CXnEhyyv.js → client-DYrWuaIk.js} +1 -1
- package/dist/client/_assets/{client-auth-CSItbyU8.js → client-auth-B5Re0uCd.js} +187 -167
- package/dist/client/_assets/client-xWDl78yi.css +2 -0
- package/dist/{export-Be082J0n.js → export-DY1v5Iqu.js} +2 -2
- package/dist/{github-sync-D1Cw8mOY.js → github-sync-2_T7nbOv.js} +1 -1
- package/dist/{github-sync-_kPWM4m9.js → github-sync-LefaslGJ.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/node.js +4 -4
- package/package.json +1 -1
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +8 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +64 -12
- package/src/client/components/jant-compose-dialog.ts +12 -0
- package/src/client/components/jant-settings-general.ts +74 -21
- package/src/client/components/settings-types.ts +13 -0
- package/src/client/settings-bridge.ts +3 -0
- package/src/client/tiptap/__tests__/link-toolbar.test.ts +41 -0
- package/src/client/tiptap/__tests__/mark-exit.test.ts +99 -0
- package/src/client/tiptap/bubble-menu.ts +37 -4
- package/src/client/tiptap/link-toolbar.ts +63 -1
- package/src/db/migrations/0026_absent_rhodey.sql +14 -0
- package/src/db/migrations/meta/0026_snapshot.json +2511 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/migrations/pg/0024_high_violations.sql +14 -0
- package/src/db/migrations/pg/meta/0024_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/_journal.json +7 -0
- package/src/db/pg/schema.ts +36 -0
- package/src/db/schema.ts +36 -0
- package/src/i18n/__tests__/middleware.test.ts +46 -0
- package/src/i18n/locales/settings/en.po +282 -27
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +282 -27
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +282 -27
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +17 -8
- package/src/i18n/supported-locales.ts +5 -4
- package/src/lib/__tests__/feed.test.ts +5 -1
- package/src/lib/feed.ts +6 -3
- package/src/lib/ids.ts +1 -0
- package/src/lib/resolve-config.ts +1 -0
- package/src/lib/upload.ts +14 -0
- package/src/routes/api/__tests__/settings.test.ts +1 -4
- package/src/routes/api/__tests__/upload.test.ts +2 -0
- package/src/routes/api/internal/__tests__/uploads.test.ts +19 -1
- package/src/routes/api/settings.ts +2 -1
- package/src/routes/auth/__tests__/setup.test.ts +14 -0
- package/src/routes/dash/__tests__/settings-avatar.test.ts +35 -17
- package/src/routes/dash/settings.tsx +22 -4
- package/src/routes/feed/__tests__/feed.test.ts +58 -19
- package/src/routes/feed/feed.ts +37 -28
- package/src/routes/pages/featured.tsx +17 -0
- package/src/routes/pages/latest.tsx +25 -0
- package/src/services/__tests__/media.test.ts +191 -30
- package/src/services/__tests__/settings.test.ts +55 -0
- package/src/services/bootstrap.ts +7 -0
- package/src/services/export-theme/layouts/_default/baseof.html +2 -1
- package/src/services/media.ts +169 -42
- package/src/services/post.ts +1 -1
- package/src/services/settings.ts +49 -15
- package/src/services/upload-session.ts +13 -3
- package/src/styles/tokens.css +21 -4
- package/src/styles/ui.css +44 -1
- package/src/types/bindings.ts +1 -0
- package/src/types/config.ts +13 -0
- package/src/ui/__tests__/color-themes.test.ts +2 -2
- package/src/ui/color-themes.ts +32 -0
- package/src/ui/dash/appearance/ColorThemeContent.tsx +264 -29
- package/src/ui/dash/settings/GeneralContent.tsx +54 -4
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +3 -2
- package/src/ui/layouts/BaseLayout.tsx +3 -2
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +17 -4
- package/dist/app-DaxS_Cz-.js +0 -6
- package/dist/client/_assets/client-C6peCkkD.css +0 -2
|
@@ -15,6 +15,8 @@ import { createMediaService } from "../../../services/media.js";
|
|
|
15
15
|
import { DEFAULT_APP_PORT } from "../../../lib/env.js";
|
|
16
16
|
import { resolveConfig } from "../../../lib/resolve-config.js";
|
|
17
17
|
import { feedRoutes } from "../feed.js";
|
|
18
|
+
import { latestRoutes } from "../../pages/latest.js";
|
|
19
|
+
import { featuredRoutes } from "../../pages/featured.js";
|
|
18
20
|
import type { Database } from "../../../db/index.js";
|
|
19
21
|
|
|
20
22
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
@@ -58,6 +60,9 @@ function createFeedTestApp(envOverrides: Partial<Bindings> = {}) {
|
|
|
58
60
|
});
|
|
59
61
|
|
|
60
62
|
app.route("/feed", feedRoutes);
|
|
63
|
+
// The canonical latest/featured feeds live in their page route groups.
|
|
64
|
+
app.route("/latest", latestRoutes);
|
|
65
|
+
app.route("/featured", featuredRoutes);
|
|
61
66
|
|
|
62
67
|
return { app, services, db: db as unknown as Database };
|
|
63
68
|
}
|
|
@@ -166,7 +171,7 @@ describe("Atom Feed Routes", () => {
|
|
|
166
171
|
});
|
|
167
172
|
});
|
|
168
173
|
|
|
169
|
-
describe("/feed
|
|
174
|
+
describe("/latest/feed — latest public posts", () => {
|
|
170
175
|
it("returns public published posts and excludes hidden, private, and draft posts", async () => {
|
|
171
176
|
const { app, services } = createFeedTestApp();
|
|
172
177
|
|
|
@@ -204,7 +209,7 @@ describe("Atom Feed Routes", () => {
|
|
|
204
209
|
status: "draft",
|
|
205
210
|
});
|
|
206
211
|
|
|
207
|
-
const res = await app.request("/feed
|
|
212
|
+
const res = await app.request("/latest/feed");
|
|
208
213
|
expect(res.status).toBe(200);
|
|
209
214
|
|
|
210
215
|
const xml = await res.text();
|
|
@@ -237,7 +242,7 @@ describe("Atom Feed Routes", () => {
|
|
|
237
242
|
status: "published",
|
|
238
243
|
});
|
|
239
244
|
|
|
240
|
-
const res = await app.request("/feed
|
|
245
|
+
const res = await app.request("/latest/feed?format=note");
|
|
241
246
|
expect(res.status).toBe(200);
|
|
242
247
|
|
|
243
248
|
const xml = await res.text();
|
|
@@ -262,7 +267,7 @@ describe("Atom Feed Routes", () => {
|
|
|
262
267
|
status: "published",
|
|
263
268
|
});
|
|
264
269
|
|
|
265
|
-
const res = await app.request("/feed
|
|
270
|
+
const res = await app.request("/latest/feed?format=invalid");
|
|
266
271
|
expect(res.status).toBe(200);
|
|
267
272
|
|
|
268
273
|
const xml = await res.text();
|
|
@@ -273,14 +278,14 @@ describe("Atom Feed Routes", () => {
|
|
|
273
278
|
it("returns Atom content type", async () => {
|
|
274
279
|
const { app } = createFeedTestApp();
|
|
275
280
|
|
|
276
|
-
const res = await app.request("/feed
|
|
281
|
+
const res = await app.request("/latest/feed");
|
|
277
282
|
expect(res.headers.get("Content-Type")).toBe(
|
|
278
283
|
"application/atom+xml; charset=utf-8",
|
|
279
284
|
);
|
|
280
285
|
});
|
|
281
286
|
});
|
|
282
287
|
|
|
283
|
-
describe("/feed
|
|
288
|
+
describe("/featured/feed — featured posts", () => {
|
|
284
289
|
it("returns only featured posts", async () => {
|
|
285
290
|
const { app, services } = createFeedTestApp();
|
|
286
291
|
|
|
@@ -298,7 +303,7 @@ describe("Atom Feed Routes", () => {
|
|
|
298
303
|
featured: true,
|
|
299
304
|
});
|
|
300
305
|
|
|
301
|
-
const res = await app.request("/feed
|
|
306
|
+
const res = await app.request("/featured/feed");
|
|
302
307
|
expect(res.status).toBe(200);
|
|
303
308
|
|
|
304
309
|
const xml = await res.text();
|
|
@@ -307,6 +312,24 @@ describe("Atom Feed Routes", () => {
|
|
|
307
312
|
});
|
|
308
313
|
});
|
|
309
314
|
|
|
315
|
+
describe("legacy feed path redirects", () => {
|
|
316
|
+
it("redirects /feed/latest to /latest/feed, preserving ?format=", async () => {
|
|
317
|
+
const { app } = createFeedTestApp();
|
|
318
|
+
|
|
319
|
+
const res = await app.request("/feed/latest?format=note");
|
|
320
|
+
expect(res.status).toBe(308);
|
|
321
|
+
expect(res.headers.get("Location")).toBe("/latest/feed?format=note");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("redirects /feed/featured to /featured/feed", async () => {
|
|
325
|
+
const { app } = createFeedTestApp();
|
|
326
|
+
|
|
327
|
+
const res = await app.request("/feed/featured");
|
|
328
|
+
expect(res.status).toBe(308);
|
|
329
|
+
expect(res.headers.get("Location")).toBe("/featured/feed");
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
310
333
|
describe("legacy atom.xml redirects", () => {
|
|
311
334
|
it("redirects /feed/atom.xml to /feed", async () => {
|
|
312
335
|
const { app } = createFeedTestApp();
|
|
@@ -316,38 +339,54 @@ describe("Atom Feed Routes", () => {
|
|
|
316
339
|
expect(res.headers.get("Location")).toBe("/feed");
|
|
317
340
|
});
|
|
318
341
|
|
|
319
|
-
it("redirects /feed/latest/atom.xml to /feed
|
|
342
|
+
it("redirects /feed/latest/atom.xml to /latest/feed", async () => {
|
|
320
343
|
const { app } = createFeedTestApp();
|
|
321
344
|
|
|
322
345
|
const res = await app.request("/feed/latest/atom.xml");
|
|
323
346
|
expect(res.status).toBe(308);
|
|
324
|
-
expect(res.headers.get("Location")).toBe("/feed
|
|
347
|
+
expect(res.headers.get("Location")).toBe("/latest/feed");
|
|
325
348
|
});
|
|
326
349
|
|
|
327
|
-
it("redirects /feed/featured/atom.xml to /feed
|
|
350
|
+
it("redirects /feed/featured/atom.xml to /featured/feed", async () => {
|
|
328
351
|
const { app } = createFeedTestApp();
|
|
329
352
|
|
|
330
353
|
const res = await app.request("/feed/featured/atom.xml");
|
|
331
354
|
expect(res.status).toBe(308);
|
|
332
|
-
expect(res.headers.get("Location")).toBe("/feed
|
|
355
|
+
expect(res.headers.get("Location")).toBe("/featured/feed");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("redirects /latest/feed/atom.xml to /latest/feed", async () => {
|
|
359
|
+
const { app } = createFeedTestApp();
|
|
360
|
+
|
|
361
|
+
const res = await app.request("/latest/feed/atom.xml");
|
|
362
|
+
expect(res.status).toBe(308);
|
|
363
|
+
expect(res.headers.get("Location")).toBe("/latest/feed");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("redirects /featured/feed/atom.xml to /featured/feed", async () => {
|
|
367
|
+
const { app } = createFeedTestApp();
|
|
368
|
+
|
|
369
|
+
const res = await app.request("/featured/feed/atom.xml");
|
|
370
|
+
expect(res.status).toBe(308);
|
|
371
|
+
expect(res.headers.get("Location")).toBe("/featured/feed");
|
|
333
372
|
});
|
|
334
373
|
});
|
|
335
374
|
|
|
336
375
|
describe("legacy feed aliases", () => {
|
|
337
|
-
it("redirects /feed/all to /feed
|
|
376
|
+
it("redirects /feed/all to /latest/feed", async () => {
|
|
338
377
|
const { app } = createFeedTestApp();
|
|
339
378
|
|
|
340
379
|
const res = await app.request("/feed/all?format=note");
|
|
341
380
|
expect(res.status).toBe(308);
|
|
342
|
-
expect(res.headers.get("Location")).toBe("/feed
|
|
381
|
+
expect(res.headers.get("Location")).toBe("/latest/feed?format=note");
|
|
343
382
|
});
|
|
344
383
|
|
|
345
|
-
it("redirects /feed/all/atom.xml to /feed
|
|
384
|
+
it("redirects /feed/all/atom.xml to /latest/feed", async () => {
|
|
346
385
|
const { app } = createFeedTestApp();
|
|
347
386
|
|
|
348
387
|
const res = await app.request("/feed/all/atom.xml?format=link");
|
|
349
388
|
expect(res.status).toBe(308);
|
|
350
|
-
expect(res.headers.get("Location")).toBe("/feed
|
|
389
|
+
expect(res.headers.get("Location")).toBe("/latest/feed");
|
|
351
390
|
});
|
|
352
391
|
});
|
|
353
392
|
|
|
@@ -388,7 +427,7 @@ describe("Atom Feed Routes", () => {
|
|
|
388
427
|
});
|
|
389
428
|
}
|
|
390
429
|
|
|
391
|
-
const res = await app.request("/feed
|
|
430
|
+
const res = await app.request("/latest/feed");
|
|
392
431
|
expect(res.status).toBe(200);
|
|
393
432
|
|
|
394
433
|
const xml = await res.text();
|
|
@@ -413,7 +452,7 @@ describe("Atom Feed Routes", () => {
|
|
|
413
452
|
});
|
|
414
453
|
}
|
|
415
454
|
|
|
416
|
-
const res = await app.request("/feed
|
|
455
|
+
const res = await app.request("/latest/feed");
|
|
417
456
|
expect(res.status).toBe(200);
|
|
418
457
|
|
|
419
458
|
const xml = await res.text();
|
|
@@ -436,7 +475,7 @@ describe("Atom Feed Routes", () => {
|
|
|
436
475
|
});
|
|
437
476
|
}
|
|
438
477
|
|
|
439
|
-
const res = await app.request("/feed
|
|
478
|
+
const res = await app.request("/featured/feed");
|
|
440
479
|
expect(res.status).toBe(200);
|
|
441
480
|
|
|
442
481
|
const xml = await res.text();
|
|
@@ -463,7 +502,7 @@ describe("Atom Feed Routes", () => {
|
|
|
463
502
|
replyToId: root.id,
|
|
464
503
|
});
|
|
465
504
|
|
|
466
|
-
const res = await app.request("/feed
|
|
505
|
+
const res = await app.request("/latest/feed");
|
|
467
506
|
expect(res.status).toBe(200);
|
|
468
507
|
|
|
469
508
|
const xml = await res.text();
|
package/src/routes/feed/feed.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Atom Feed Routes
|
|
3
3
|
*
|
|
4
|
-
* Feed hierarchy:
|
|
5
|
-
*
|
|
6
|
-
* - /feed
|
|
7
|
-
* - /feed
|
|
8
|
-
* - /
|
|
4
|
+
* Feed hierarchy (resource-first: a feed is a sub-resource of the page it
|
|
5
|
+
* represents, so it lives at `{page}/feed`):
|
|
6
|
+
* - /feed — site main feed (latest or featured, site-configurable; feed of `/`)
|
|
7
|
+
* - /latest/feed — latest public posts (handled in pages/latest)
|
|
8
|
+
* - /featured/feed — featured posts only (handled in pages/featured)
|
|
9
|
+
* - /archive/feed — full archive incl. Hidden-from-Latest (handled in pages/archive)
|
|
10
|
+
* - /{slug}/feed — single-collection feed (handled in page routes)
|
|
9
11
|
* - /collections/{slug}/feed — combined collection feed (handled in collection routes)
|
|
12
|
+
*
|
|
13
|
+
* Legacy: /feed/latest and /feed/featured 308-redirect to the canonical
|
|
14
|
+
* /latest/feed and /featured/feed. Kept indefinitely for old subscribers.
|
|
10
15
|
*/
|
|
11
16
|
|
|
12
17
|
import { msg } from "@lingui/core/macro";
|
|
@@ -255,11 +260,15 @@ async function buildFeaturedFeedData(
|
|
|
255
260
|
/**
|
|
256
261
|
* Build FeedData from the Hono context.
|
|
257
262
|
*
|
|
263
|
+
* Exported so the canonical latest/featured feeds (served from the
|
|
264
|
+
* `/latest/feed` and `/featured/feed` page route groups) can reuse the
|
|
265
|
+
* same feed-building logic.
|
|
266
|
+
*
|
|
258
267
|
* @param c - Hono context
|
|
259
268
|
* @param opts - Filter options for the feed
|
|
260
269
|
* @returns Feed data ready for rendering
|
|
261
270
|
*/
|
|
262
|
-
async function buildFeedData(
|
|
271
|
+
export async function buildFeedData(
|
|
263
272
|
c: Context<Env>,
|
|
264
273
|
opts: FeedOptions,
|
|
265
274
|
): Promise<FeedData> {
|
|
@@ -311,7 +320,7 @@ async function buildFeedData(
|
|
|
311
320
|
* Parse and validate the `format` query parameter.
|
|
312
321
|
* Returns a valid Format or undefined if missing/invalid.
|
|
313
322
|
*/
|
|
314
|
-
function parseFormatQuery(c: Context<Env>): Format | undefined {
|
|
323
|
+
export function parseFormatQuery(c: Context<Env>): Format | undefined {
|
|
315
324
|
const raw = c.req.query("format");
|
|
316
325
|
if (raw && (FORMATS as readonly string[]).includes(raw)) {
|
|
317
326
|
return raw as Format;
|
|
@@ -319,7 +328,7 @@ function parseFormatQuery(c: Context<Env>): Format | undefined {
|
|
|
319
328
|
return undefined;
|
|
320
329
|
}
|
|
321
330
|
|
|
322
|
-
function renderFeed(xml: string) {
|
|
331
|
+
export function renderFeed(xml: string) {
|
|
323
332
|
return new Response(xml, {
|
|
324
333
|
headers: {
|
|
325
334
|
"Content-Type": "application/atom+xml; charset=utf-8",
|
|
@@ -335,24 +344,24 @@ feedRoutes.get("/", async (c) => {
|
|
|
335
344
|
return renderFeed(defaultFeedRenderer(feedData));
|
|
336
345
|
});
|
|
337
346
|
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
return
|
|
347
|
+
// Legacy — /feed/latest moved to the canonical /latest/feed. Kept
|
|
348
|
+
// indefinitely as a 308 so old subscribers don't break; preserves the
|
|
349
|
+
// ?format= query string.
|
|
350
|
+
feedRoutes.get("/latest", (c) => {
|
|
351
|
+
const sitePathPrefix = c.var.appConfig.sitePathPrefix;
|
|
352
|
+
const qs = c.req.url.includes("?")
|
|
353
|
+
? c.req.url.slice(c.req.url.indexOf("?"))
|
|
354
|
+
: "";
|
|
355
|
+
return c.redirect(
|
|
356
|
+
`${toPublicPath("/latest/feed", sitePathPrefix)}${qs}`,
|
|
357
|
+
308,
|
|
358
|
+
);
|
|
347
359
|
});
|
|
348
360
|
|
|
349
|
-
//
|
|
350
|
-
feedRoutes.get("/featured",
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
selfPath: "/feed/featured",
|
|
354
|
-
});
|
|
355
|
-
return renderFeed(defaultFeedRenderer(feedData));
|
|
361
|
+
// Legacy — /feed/featured moved to the canonical /featured/feed.
|
|
362
|
+
feedRoutes.get("/featured", (c) => {
|
|
363
|
+
const sitePathPrefix = c.var.appConfig.sitePathPrefix;
|
|
364
|
+
return c.redirect(toPublicPath("/featured/feed", sitePathPrefix), 308);
|
|
356
365
|
});
|
|
357
366
|
|
|
358
367
|
// Legacy aliases
|
|
@@ -362,7 +371,7 @@ feedRoutes.get("/all", (c) => {
|
|
|
362
371
|
? c.req.url.slice(c.req.url.indexOf("?"))
|
|
363
372
|
: "";
|
|
364
373
|
return c.redirect(
|
|
365
|
-
`${toPublicPath("/feed
|
|
374
|
+
`${toPublicPath("/latest/feed", sitePathPrefix)}${qs}`,
|
|
366
375
|
308,
|
|
367
376
|
);
|
|
368
377
|
});
|
|
@@ -374,13 +383,13 @@ feedRoutes.get("/atom.xml", (c) => {
|
|
|
374
383
|
});
|
|
375
384
|
feedRoutes.get("/latest/atom.xml", (c) => {
|
|
376
385
|
const sitePathPrefix = c.var.appConfig.sitePathPrefix;
|
|
377
|
-
return c.redirect(toPublicPath("/feed
|
|
386
|
+
return c.redirect(toPublicPath("/latest/feed", sitePathPrefix), 308);
|
|
378
387
|
});
|
|
379
388
|
feedRoutes.get("/featured/atom.xml", (c) => {
|
|
380
389
|
const sitePathPrefix = c.var.appConfig.sitePathPrefix;
|
|
381
|
-
return c.redirect(toPublicPath("/feed
|
|
390
|
+
return c.redirect(toPublicPath("/featured/feed", sitePathPrefix), 308);
|
|
382
391
|
});
|
|
383
392
|
feedRoutes.get("/all/atom.xml", (c) => {
|
|
384
393
|
const sitePathPrefix = c.var.appConfig.sitePathPrefix;
|
|
385
|
-
return c.redirect(toPublicPath("/feed
|
|
394
|
+
return c.redirect(toPublicPath("/latest/feed", sitePathPrefix), 308);
|
|
386
395
|
});
|
|
@@ -15,6 +15,8 @@ import { buildPageTitle } from "../../lib/page-title.js";
|
|
|
15
15
|
import { renderPublicPage } from "../../lib/render.js";
|
|
16
16
|
import { assembleFeaturedTimeline } from "../../lib/timeline.js";
|
|
17
17
|
import { toPublicPath } from "../../lib/url.js";
|
|
18
|
+
import { defaultFeedRenderer } from "../../lib/feed.js";
|
|
19
|
+
import { buildFeedData, renderFeed } from "../feed/feed.js";
|
|
18
20
|
import { FeaturedPage } from "../../ui/pages/FeaturedPage.js";
|
|
19
21
|
|
|
20
22
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
@@ -58,3 +60,18 @@ featuredRoutes.get("/", async (c) => {
|
|
|
58
60
|
),
|
|
59
61
|
});
|
|
60
62
|
});
|
|
63
|
+
|
|
64
|
+
// Atom — /featured/feed (canonical featured feed)
|
|
65
|
+
featuredRoutes.get("/feed", async (c) => {
|
|
66
|
+
const feedData = await buildFeedData(c, {
|
|
67
|
+
kind: "featured",
|
|
68
|
+
selfPath: "/featured/feed",
|
|
69
|
+
});
|
|
70
|
+
return renderFeed(defaultFeedRenderer(feedData));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Legacy atom.xml suffix → canonical /featured/feed
|
|
74
|
+
featuredRoutes.get("/feed/atom.xml", (c) => {
|
|
75
|
+
const sitePathPrefix = c.var.appConfig.sitePathPrefix;
|
|
76
|
+
return c.redirect(toPublicPath("/featured/feed", sitePathPrefix), 308);
|
|
77
|
+
});
|
|
@@ -18,6 +18,8 @@ import { buildPageTitle } from "../../lib/page-title.js";
|
|
|
18
18
|
import { renderPublicPage } from "../../lib/render.js";
|
|
19
19
|
import { assembleTimeline } from "../../lib/timeline.js";
|
|
20
20
|
import { toPublicPath } from "../../lib/url.js";
|
|
21
|
+
import { defaultFeedRenderer } from "../../lib/feed.js";
|
|
22
|
+
import { buildFeedData, parseFormatQuery, renderFeed } from "../feed/feed.js";
|
|
21
23
|
import { HomePage } from "../../ui/pages/HomePage.js";
|
|
22
24
|
|
|
23
25
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
@@ -65,3 +67,26 @@ latestRoutes.get("/", async (c) => {
|
|
|
65
67
|
),
|
|
66
68
|
});
|
|
67
69
|
});
|
|
70
|
+
|
|
71
|
+
// Atom — /latest/feed (canonical latest feed; accepts ?format=note|link|quote)
|
|
72
|
+
latestRoutes.get("/feed", async (c) => {
|
|
73
|
+
const format = parseFormatQuery(c);
|
|
74
|
+
const feedData = await buildFeedData(c, {
|
|
75
|
+
kind: "latest",
|
|
76
|
+
selfPath: "/latest/feed",
|
|
77
|
+
format,
|
|
78
|
+
});
|
|
79
|
+
return renderFeed(defaultFeedRenderer(feedData));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Legacy atom.xml suffix → canonical /latest/feed (preserves ?format=)
|
|
83
|
+
latestRoutes.get("/feed/atom.xml", (c) => {
|
|
84
|
+
const sitePathPrefix = c.var.appConfig.sitePathPrefix;
|
|
85
|
+
const qs = c.req.url.includes("?")
|
|
86
|
+
? c.req.url.slice(c.req.url.indexOf("?"))
|
|
87
|
+
: "";
|
|
88
|
+
return c.redirect(
|
|
89
|
+
`${toPublicPath("/latest/feed", sitePathPrefix)}${qs}`,
|
|
90
|
+
308,
|
|
91
|
+
);
|
|
92
|
+
});
|