@jant/core 0.3.20 → 0.3.22

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.
Files changed (94) hide show
  1. package/dist/app.js +60 -17
  2. package/dist/index.js +8 -0
  3. package/dist/lib/feed.js +112 -0
  4. package/dist/lib/navigation.js +9 -9
  5. package/dist/lib/render.js +48 -0
  6. package/dist/lib/theme-components.js +18 -18
  7. package/dist/lib/view.js +228 -0
  8. package/dist/routes/api/timeline.js +20 -16
  9. package/dist/routes/dash/collections.js +38 -10
  10. package/dist/routes/dash/navigation.js +22 -8
  11. package/dist/routes/dash/redirects.js +19 -5
  12. package/dist/routes/dash/settings.js +57 -15
  13. package/dist/routes/feed/rss.js +34 -78
  14. package/dist/routes/feed/sitemap.js +11 -26
  15. package/dist/routes/pages/archive.js +18 -195
  16. package/dist/routes/pages/collection.js +16 -70
  17. package/dist/routes/pages/home.js +25 -47
  18. package/dist/routes/pages/page.js +15 -27
  19. package/dist/routes/pages/post.js +25 -79
  20. package/dist/routes/pages/search.js +20 -130
  21. package/dist/theme/components/MediaGallery.js +10 -10
  22. package/dist/theme/components/PageForm.js +22 -8
  23. package/dist/theme/components/PostForm.js +22 -8
  24. package/dist/theme/components/index.js +1 -1
  25. package/dist/theme/components/timeline/ArticleCard.js +7 -11
  26. package/dist/theme/components/timeline/ImageCard.js +10 -13
  27. package/dist/theme/components/timeline/LinkCard.js +4 -7
  28. package/dist/theme/components/timeline/NoteCard.js +5 -8
  29. package/dist/theme/components/timeline/QuoteCard.js +3 -6
  30. package/dist/theme/components/timeline/ThreadPreview.js +9 -10
  31. package/dist/theme/components/timeline/TimelineFeed.js +8 -5
  32. package/dist/theme/components/timeline/TimelineItem.js +22 -2
  33. package/dist/theme/components/timeline/index.js +1 -1
  34. package/dist/theme/index.js +6 -3
  35. package/dist/theme/layouts/SiteLayout.js +10 -39
  36. package/dist/theme/pages/ArchivePage.js +157 -0
  37. package/dist/theme/pages/CollectionPage.js +63 -0
  38. package/dist/theme/pages/HomePage.js +26 -0
  39. package/dist/theme/pages/PostPage.js +48 -0
  40. package/dist/theme/pages/SearchPage.js +120 -0
  41. package/dist/theme/pages/SinglePage.js +23 -0
  42. package/dist/theme/pages/index.js +11 -0
  43. package/package.json +2 -1
  44. package/src/app.tsx +48 -17
  45. package/src/i18n/locales/en.po +171 -147
  46. package/src/i18n/locales/zh-Hans.po +171 -147
  47. package/src/i18n/locales/zh-Hant.po +171 -147
  48. package/src/index.ts +51 -2
  49. package/src/lib/__tests__/theme-components.test.ts +33 -14
  50. package/src/lib/__tests__/view.test.ts +375 -0
  51. package/src/lib/feed.ts +148 -0
  52. package/src/lib/navigation.ts +11 -11
  53. package/src/lib/render.tsx +67 -0
  54. package/src/lib/theme-components.ts +27 -35
  55. package/src/lib/view.ts +318 -0
  56. package/src/routes/api/__tests__/timeline.test.ts +3 -3
  57. package/src/routes/api/timeline.tsx +32 -25
  58. package/src/routes/dash/collections.tsx +30 -10
  59. package/src/routes/dash/navigation.tsx +20 -10
  60. package/src/routes/dash/redirects.tsx +15 -5
  61. package/src/routes/dash/settings.tsx +53 -15
  62. package/src/routes/feed/rss.ts +47 -94
  63. package/src/routes/feed/sitemap.ts +8 -30
  64. package/src/routes/pages/archive.tsx +24 -209
  65. package/src/routes/pages/collection.tsx +19 -75
  66. package/src/routes/pages/home.tsx +42 -76
  67. package/src/routes/pages/page.tsx +17 -28
  68. package/src/routes/pages/post.tsx +28 -86
  69. package/src/routes/pages/search.tsx +29 -151
  70. package/src/services/search.ts +2 -8
  71. package/src/theme/components/MediaGallery.tsx +12 -12
  72. package/src/theme/components/PageForm.tsx +20 -10
  73. package/src/theme/components/PostForm.tsx +20 -10
  74. package/src/theme/components/index.ts +1 -0
  75. package/src/theme/components/timeline/ArticleCard.tsx +7 -19
  76. package/src/theme/components/timeline/ImageCard.tsx +10 -20
  77. package/src/theme/components/timeline/LinkCard.tsx +4 -11
  78. package/src/theme/components/timeline/NoteCard.tsx +5 -12
  79. package/src/theme/components/timeline/QuoteCard.tsx +3 -10
  80. package/src/theme/components/timeline/ThreadPreview.tsx +5 -5
  81. package/src/theme/components/timeline/TimelineFeed.tsx +7 -3
  82. package/src/theme/components/timeline/TimelineItem.tsx +43 -4
  83. package/src/theme/components/timeline/index.ts +1 -1
  84. package/src/theme/index.ts +7 -3
  85. package/src/theme/layouts/SiteLayout.tsx +25 -77
  86. package/src/theme/layouts/index.ts +2 -1
  87. package/src/theme/pages/ArchivePage.tsx +160 -0
  88. package/src/theme/pages/CollectionPage.tsx +60 -0
  89. package/src/theme/pages/HomePage.tsx +42 -0
  90. package/src/theme/pages/PostPage.tsx +44 -0
  91. package/src/theme/pages/SearchPage.tsx +128 -0
  92. package/src/theme/pages/SinglePage.tsx +24 -0
  93. package/src/theme/pages/index.ts +13 -0
  94. package/src/types.ts +262 -38
@@ -7,7 +7,8 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
7
7
  import { sse } from "../../lib/sse.js";
8
8
  import { buildMediaMap } from "../../lib/media-helpers.js";
9
9
  import { TimelineItem } from "../../theme/components/timeline/TimelineItem.js";
10
- import { ThreadPreview } from "../../theme/components/timeline/ThreadPreview.js";
10
+ import { ThreadPreview as DefaultThreadPreview } from "../../theme/components/timeline/ThreadPreview.js";
11
+ import { createMediaContext, toPostView, toPostViews } from "../../lib/view.js";
11
12
  const PAGE_SIZE = 20;
12
13
  export const timelineApiRoutes = new Hono();
13
14
  timelineApiRoutes.get("/", async (c)=>{
@@ -41,10 +42,8 @@ timelineApiRoutes.get("/", async (c)=>{
41
42
  // Build media map
42
43
  const postIds = displayPosts.map((p)=>p.id);
43
44
  const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
44
- const r2PublicUrl = c.env.R2_PUBLIC_URL;
45
- const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
46
- const s3PublicUrl = c.env.S3_PUBLIC_URL;
47
- const mediaMap = buildMediaMap(rawMediaMap, r2PublicUrl, imageTransformUrl, s3PublicUrl);
45
+ const mediaCtx = createMediaContext(c);
46
+ const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
48
47
  // Get reply counts to identify thread roots
49
48
  const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
50
49
  const threadRootIds = postIds.filter((id)=>(replyCounts.get(id) ?? 0) > 0);
@@ -57,42 +56,47 @@ timelineApiRoutes.get("/", async (c)=>{
57
56
  previewReplyIds.push(reply.id);
58
57
  }
59
58
  }
60
- const previewMediaMap = previewReplyIds.length > 0 ? buildMediaMap(await c.var.services.media.getByPostIds(previewReplyIds), r2PublicUrl, imageTransformUrl, s3PublicUrl) : new Map();
61
- // Assemble timeline items
59
+ const previewMediaMap = previewReplyIds.length > 0 ? buildMediaMap(await c.var.services.media.getByPostIds(previewReplyIds), mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl) : new Map();
60
+ // Assemble timeline items with View Models
62
61
  const items = displayPosts.map((post)=>{
63
- const postWithMedia = {
62
+ const postView = toPostView({
64
63
  ...post,
65
64
  mediaAttachments: mediaMap.get(post.id) ?? []
66
- };
65
+ }, mediaCtx);
67
66
  const replyCount = replyCounts.get(post.id) ?? 0;
68
67
  const previewReplies = threadPreviews.get(post.id);
69
68
  if (replyCount > 0 && previewReplies) {
70
69
  return {
71
- post: postWithMedia,
70
+ post: postView,
72
71
  threadPreview: {
73
- replies: previewReplies.map((r)=>({
72
+ replies: toPostViews(previewReplies.map((r)=>({
74
73
  ...r,
75
74
  mediaAttachments: previewMediaMap.get(r.id) ?? []
76
- })),
75
+ })), mediaCtx),
77
76
  totalReplyCount: replyCount
78
77
  }
79
78
  };
80
79
  }
81
80
  return {
82
- post: postWithMedia
81
+ post: postView
83
82
  };
84
83
  });
84
+ // Resolve theme components for card rendering
85
+ const theme = c.var.config.theme?.components;
86
+ const ResolvedThreadPreview = theme?.ThreadPreview ?? DefaultThreadPreview;
85
87
  // Render items to HTML
86
88
  const itemsHtml = items.map((item)=>{
87
89
  if (item.threadPreview) {
88
- return /*#__PURE__*/ _jsx(ThreadPreview, {
90
+ return /*#__PURE__*/ _jsx(ResolvedThreadPreview, {
89
91
  rootPost: item.post,
90
92
  previewReplies: item.threadPreview.replies,
91
- totalReplyCount: item.threadPreview.totalReplyCount
93
+ totalReplyCount: item.threadPreview.totalReplyCount,
94
+ theme: theme
92
95
  });
93
96
  }
94
97
  return /*#__PURE__*/ _jsx(TimelineItem, {
95
- item: item
98
+ item: item,
99
+ theme: theme
96
100
  });
97
101
  }).map((jsx)=>jsx.toString()).join("");
98
102
  // Determine next cursor
@@ -86,6 +86,7 @@ function NewCollectionContent() {
86
86
  /*#__PURE__*/ _jsxs("form", {
87
87
  "data-signals": "{title: '', path: '', description: ''}",
88
88
  "data-on:submit__prevent": "@post('/dash/collections')",
89
+ "data-indicator": "_loading",
89
90
  class: "flex flex-col gap-4 max-w-lg",
90
91
  children: [
91
92
  /*#__PURE__*/ _jsxs("div", {
@@ -161,13 +162,26 @@ function NewCollectionContent() {
161
162
  /*#__PURE__*/ _jsxs("div", {
162
163
  class: "flex gap-2",
163
164
  children: [
164
- /*#__PURE__*/ _jsx("button", {
165
+ /*#__PURE__*/ _jsxs("button", {
165
166
  type: "submit",
166
167
  class: "btn",
167
- children: $__i18n._({
168
- id: "Pbm2/N",
169
- message: "Create Collection"
170
- })
168
+ "data-attr-disabled": "$_loading",
169
+ children: [
170
+ /*#__PURE__*/ _jsx("span", {
171
+ "data-show": "!$_loading",
172
+ children: $__i18n._({
173
+ id: "Pbm2/N",
174
+ message: "Create Collection"
175
+ })
176
+ }),
177
+ /*#__PURE__*/ _jsx("span", {
178
+ "data-show": "$_loading",
179
+ children: $__i18n._({
180
+ id: "k1ifdL",
181
+ message: "Processing..."
182
+ })
183
+ })
184
+ ]
171
185
  }),
172
186
  /*#__PURE__*/ _jsx("a", {
173
187
  href: "/dash/collections",
@@ -304,6 +318,7 @@ function EditCollectionContent({ collection }) {
304
318
  /*#__PURE__*/ _jsxs("form", {
305
319
  "data-signals": signals,
306
320
  "data-on:submit__prevent": `@post('/dash/collections/${collection.id}')`,
321
+ "data-indicator": "_loading",
307
322
  class: "flex flex-col gap-4 max-w-lg",
308
323
  children: [
309
324
  /*#__PURE__*/ _jsxs("div", {
@@ -364,13 +379,26 @@ function EditCollectionContent({ collection }) {
364
379
  /*#__PURE__*/ _jsxs("div", {
365
380
  class: "flex gap-2",
366
381
  children: [
367
- /*#__PURE__*/ _jsx("button", {
382
+ /*#__PURE__*/ _jsxs("button", {
368
383
  type: "submit",
369
384
  class: "btn",
370
- children: $__i18n._({
371
- id: "7Mk+/h",
372
- message: "Update Collection"
373
- })
385
+ "data-attr-disabled": "$_loading",
386
+ children: [
387
+ /*#__PURE__*/ _jsx("span", {
388
+ "data-show": "!$_loading",
389
+ children: $__i18n._({
390
+ id: "7Mk+/h",
391
+ message: "Update Collection"
392
+ })
393
+ }),
394
+ /*#__PURE__*/ _jsx("span", {
395
+ "data-show": "$_loading",
396
+ children: $__i18n._({
397
+ id: "k1ifdL",
398
+ message: "Processing..."
399
+ })
400
+ })
401
+ ]
374
402
  }),
375
403
  /*#__PURE__*/ _jsx("a", {
376
404
  href: `/dash/collections/${collection.id}`,
@@ -102,6 +102,7 @@ function NavigationFormContent({ link, isEdit }) {
102
102
  /*#__PURE__*/ _jsxs("form", {
103
103
  "data-signals": signals,
104
104
  "data-on:submit__prevent": `@post('${action}')`,
105
+ "data-indicator": "_loading",
105
106
  class: "flex flex-col gap-4 max-w-lg",
106
107
  children: [
107
108
  /*#__PURE__*/ _jsxs("div", {
@@ -159,16 +160,29 @@ function NavigationFormContent({ link, isEdit }) {
159
160
  /*#__PURE__*/ _jsxs("div", {
160
161
  class: "flex gap-2",
161
162
  children: [
162
- /*#__PURE__*/ _jsx("button", {
163
+ /*#__PURE__*/ _jsxs("button", {
163
164
  type: "submit",
164
165
  class: "btn",
165
- children: isEdit ? $__i18n._({
166
- id: "IUwGEM",
167
- message: "Save Changes"
168
- }) : $__i18n._({
169
- id: "kd7eBB",
170
- message: "Create Link"
171
- })
166
+ "data-attr-disabled": "$_loading",
167
+ children: [
168
+ /*#__PURE__*/ _jsx("span", {
169
+ "data-show": "!$_loading",
170
+ children: isEdit ? $__i18n._({
171
+ id: "IUwGEM",
172
+ message: "Save Changes"
173
+ }) : $__i18n._({
174
+ id: "kd7eBB",
175
+ message: "Create Link"
176
+ })
177
+ }),
178
+ /*#__PURE__*/ _jsx("span", {
179
+ "data-show": "$_loading",
180
+ children: $__i18n._({
181
+ id: "k1ifdL",
182
+ message: "Processing..."
183
+ })
184
+ })
185
+ ]
172
186
  }),
173
187
  /*#__PURE__*/ _jsx("a", {
174
188
  href: "/dash/navigation",
@@ -83,6 +83,7 @@ function NewRedirectContent() {
83
83
  /*#__PURE__*/ _jsxs("form", {
84
84
  "data-signals": "{fromPath: '', toPath: '', type: '301'}",
85
85
  "data-on:submit__prevent": "@post('/dash/redirects')",
86
+ "data-indicator": "_loading",
86
87
  class: "flex flex-col gap-4 max-w-lg",
87
88
  children: [
88
89
  /*#__PURE__*/ _jsxs("div", {
@@ -172,13 +173,26 @@ function NewRedirectContent() {
172
173
  /*#__PURE__*/ _jsxs("div", {
173
174
  class: "flex gap-2",
174
175
  children: [
175
- /*#__PURE__*/ _jsx("button", {
176
+ /*#__PURE__*/ _jsxs("button", {
176
177
  type: "submit",
177
178
  class: "btn",
178
- children: $__i18n._({
179
- id: "Mhf/H/",
180
- message: "Create Redirect"
181
- })
179
+ "data-attr-disabled": "$_loading",
180
+ children: [
181
+ /*#__PURE__*/ _jsx("span", {
182
+ "data-show": "!$_loading",
183
+ children: $__i18n._({
184
+ id: "Mhf/H/",
185
+ message: "Create Redirect"
186
+ })
187
+ }),
188
+ /*#__PURE__*/ _jsx("span", {
189
+ "data-show": "$_loading",
190
+ children: $__i18n._({
191
+ id: "k1ifdL",
192
+ message: "Processing..."
193
+ })
194
+ })
195
+ ]
182
196
  }),
183
197
  /*#__PURE__*/ _jsx("a", {
184
198
  href: "/dash/redirects",
@@ -78,6 +78,7 @@ function GeneralContent({ siteName, siteDescription, siteLanguage, siteNameFallb
78
78
  children: /*#__PURE__*/ _jsxs("form", {
79
79
  "data-signals": generalSignals,
80
80
  "data-on:submit__prevent": "@post('/dash/settings')",
81
+ "data-indicator": "_loading",
81
82
  children: [
82
83
  /*#__PURE__*/ _jsxs("div", {
83
84
  class: "card",
@@ -167,13 +168,26 @@ function GeneralContent({ siteName, siteDescription, siteLanguage, siteNameFallb
167
168
  })
168
169
  ]
169
170
  }),
170
- /*#__PURE__*/ _jsx("button", {
171
+ /*#__PURE__*/ _jsxs("button", {
171
172
  type: "submit",
172
173
  class: "btn mt-4",
173
- children: $__i18n._({
174
- id: "UGT5vp",
175
- message: "Save Settings"
176
- })
174
+ "data-attr-disabled": "$_loading",
175
+ children: [
176
+ /*#__PURE__*/ _jsx("span", {
177
+ "data-show": "!$_loading",
178
+ children: $__i18n._({
179
+ id: "UGT5vp",
180
+ message: "Save Settings"
181
+ })
182
+ }),
183
+ /*#__PURE__*/ _jsx("span", {
184
+ "data-show": "$_loading",
185
+ children: $__i18n._({
186
+ id: "k1ifdL",
187
+ message: "Processing..."
188
+ })
189
+ })
190
+ ]
177
191
  })
178
192
  ]
179
193
  })
@@ -334,6 +348,7 @@ function AccountContent({ userName }) {
334
348
  /*#__PURE__*/ _jsxs("form", {
335
349
  "data-signals": profileSignals,
336
350
  "data-on:submit__prevent": "@post('/dash/settings/account')",
351
+ "data-indicator": "_profileLoading",
337
352
  children: [
338
353
  /*#__PURE__*/ _jsxs("div", {
339
354
  class: "card",
@@ -369,19 +384,33 @@ function AccountContent({ userName }) {
369
384
  })
370
385
  ]
371
386
  }),
372
- /*#__PURE__*/ _jsx("button", {
387
+ /*#__PURE__*/ _jsxs("button", {
373
388
  type: "submit",
374
389
  class: "btn mt-4",
375
- children: $__i18n._({
376
- id: "ssqvZi",
377
- message: "Save Profile"
378
- })
390
+ "data-attr-disabled": "$_profileLoading",
391
+ children: [
392
+ /*#__PURE__*/ _jsx("span", {
393
+ "data-show": "!$_profileLoading",
394
+ children: $__i18n._({
395
+ id: "ssqvZi",
396
+ message: "Save Profile"
397
+ })
398
+ }),
399
+ /*#__PURE__*/ _jsx("span", {
400
+ "data-show": "$_profileLoading",
401
+ children: $__i18n._({
402
+ id: "k1ifdL",
403
+ message: "Processing..."
404
+ })
405
+ })
406
+ ]
379
407
  })
380
408
  ]
381
409
  }),
382
410
  /*#__PURE__*/ _jsxs("form", {
383
411
  "data-signals": "{currentPassword: '', newPassword: '', confirmPassword: ''}",
384
412
  "data-on:submit__prevent": "@post('/dash/settings/password')",
413
+ "data-indicator": "_passwordLoading",
385
414
  children: [
386
415
  /*#__PURE__*/ _jsxs("div", {
387
416
  class: "card",
@@ -460,13 +489,26 @@ function AccountContent({ userName }) {
460
489
  })
461
490
  ]
462
491
  }),
463
- /*#__PURE__*/ _jsx("button", {
492
+ /*#__PURE__*/ _jsxs("button", {
464
493
  type: "submit",
465
494
  class: "btn mt-4",
466
- children: $__i18n._({
467
- id: "VhMDMg",
468
- message: "Change Password"
469
- })
495
+ "data-attr-disabled": "$_passwordLoading",
496
+ children: [
497
+ /*#__PURE__*/ _jsx("span", {
498
+ "data-show": "!$_passwordLoading",
499
+ children: $__i18n._({
500
+ id: "VhMDMg",
501
+ message: "Change Password"
502
+ })
503
+ }),
504
+ /*#__PURE__*/ _jsx("span", {
505
+ "data-show": "$_passwordLoading",
506
+ children: $__i18n._({
507
+ id: "k1ifdL",
508
+ message: "Processing..."
509
+ })
510
+ })
511
+ ]
470
512
  })
471
513
  ]
472
514
  })
@@ -1,18 +1,19 @@
1
1
  /**
2
2
  * RSS Feed Routes
3
3
  */ import { Hono } from "hono";
4
- import * as sqid from "../../lib/sqid.js";
5
- import * as time from "../../lib/time.js";
6
- import { getMediaUrl, getPublicUrlForProvider } from "../../lib/image.js";
4
+ import { defaultRssRenderer, defaultAtomRenderer } from "../../lib/feed.js";
5
+ import { getSiteLanguage } from "../../lib/config.js";
6
+ import { buildMediaMap } from "../../lib/media-helpers.js";
7
+ import { createMediaContext, toPostViews } from "../../lib/view.js";
7
8
  export const rssRoutes = new Hono();
8
- // RSS 2.0 Feed - main feed at /feed
9
- rssRoutes.get("/", async (c)=>{
9
+ /**
10
+ * Build FeedData from the Hono context.
11
+ */ async function buildFeedData(c) {
10
12
  const all = await c.var.services.settings.getAll();
11
13
  const siteName = all["SITE_NAME"] ?? "Jant";
12
14
  const siteDescription = all["SITE_DESCRIPTION"] ?? "";
13
15
  const siteUrl = c.env.SITE_URL;
14
- const r2PublicUrl = c.env.R2_PUBLIC_URL;
15
- const s3PublicUrl = c.env.S3_PUBLIC_URL;
16
+ const siteLanguage = await getSiteLanguage(c);
16
17
  const posts = await c.var.services.posts.list({
17
18
  visibility: [
18
19
  "featured",
@@ -22,36 +23,28 @@ rssRoutes.get("/", async (c)=>{
22
23
  });
23
24
  // Batch load media for enclosures
24
25
  const postIds = posts.map((p)=>p.id);
25
- const mediaMap = await c.var.services.media.getByPostIds(postIds);
26
- const items = posts.map((post)=>{
27
- const link = `${siteUrl}/p/${sqid.encode(post.id)}`;
28
- const title = post.title || `Post #${post.id}`;
29
- const pubDate = new Date(post.publishedAt * 1000).toUTCString();
30
- // Add enclosure for first media attachment
31
- const postMedia = mediaMap.get(post.id);
32
- const firstMedia = postMedia?.[0];
33
- const enclosure = firstMedia ? `\n <enclosure url="${getMediaUrl(firstMedia.id, firstMedia.storageKey, getPublicUrlForProvider(firstMedia.provider, r2PublicUrl, s3PublicUrl))}" length="${firstMedia.size}" type="${firstMedia.mimeType}"/>` : "";
34
- return `
35
- <item>
36
- <title><![CDATA[${escapeXml(title)}]]></title>
37
- <link>${link}</link>
38
- <guid isPermaLink="true">${link}</guid>
39
- <pubDate>${pubDate}</pubDate>
40
- <description><![CDATA[${post.contentHtml || ""}]]></description>${enclosure}
41
- </item>`;
42
- }).join("");
43
- const rss = `<?xml version="1.0" encoding="UTF-8"?>
44
- <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
45
- <channel>
46
- <title>${escapeXml(siteName)}</title>
47
- <link>${siteUrl}</link>
48
- <description>${escapeXml(siteDescription)}</description>
49
- <language>en</language>
50
- <atom:link href="${siteUrl}/feed" rel="self" type="application/rss+xml"/>
51
- ${items}
52
- </channel>
53
- </rss>`;
54
- return new Response(rss, {
26
+ const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
27
+ const mediaCtx = createMediaContext(c);
28
+ const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
29
+ // Transform to PostView[] with media
30
+ const postViews = toPostViews(posts.map((p)=>({
31
+ ...p,
32
+ mediaAttachments: mediaMap.get(p.id) ?? []
33
+ })), mediaCtx);
34
+ return {
35
+ siteName,
36
+ siteDescription,
37
+ siteUrl,
38
+ siteLanguage,
39
+ posts: postViews
40
+ };
41
+ }
42
+ // RSS 2.0 Feed - main feed at /feed
43
+ rssRoutes.get("/", async (c)=>{
44
+ const feedData = await buildFeedData(c);
45
+ const renderer = c.var.config.theme?.feed?.rss ?? defaultRssRenderer;
46
+ const xml = renderer(feedData);
47
+ return new Response(xml, {
55
48
  headers: {
56
49
  "Content-Type": "application/rss+xml; charset=utf-8"
57
50
  }
@@ -59,49 +52,12 @@ rssRoutes.get("/", async (c)=>{
59
52
  });
60
53
  // Atom Feed
61
54
  rssRoutes.get("/atom.xml", async (c)=>{
62
- const all = await c.var.services.settings.getAll();
63
- const siteName = all["SITE_NAME"] ?? "Jant";
64
- const siteDescription = all["SITE_DESCRIPTION"] ?? "";
65
- const siteUrl = c.env.SITE_URL;
66
- const posts = await c.var.services.posts.list({
67
- visibility: [
68
- "featured",
69
- "quiet"
70
- ],
71
- limit: 50
72
- });
73
- const entries = posts.map((post)=>{
74
- const link = `${siteUrl}/p/${sqid.encode(post.id)}`;
75
- const title = post.title || `Post #${post.id}`;
76
- const updated = time.toISOString(post.updatedAt);
77
- const published = time.toISOString(post.publishedAt);
78
- return `
79
- <entry>
80
- <title>${escapeXml(title)}</title>
81
- <link href="${link}" rel="alternate"/>
82
- <id>${link}</id>
83
- <published>${published}</published>
84
- <updated>${updated}</updated>
85
- <content type="html"><![CDATA[${post.contentHtml || ""}]]></content>
86
- </entry>`;
87
- }).join("");
88
- const now = time.toISOString(time.now());
89
- const atom = `<?xml version="1.0" encoding="UTF-8"?>
90
- <feed xmlns="http://www.w3.org/2005/Atom">
91
- <title>${escapeXml(siteName)}</title>
92
- <subtitle>${escapeXml(siteDescription)}</subtitle>
93
- <link href="${siteUrl}" rel="alternate"/>
94
- <link href="${siteUrl}/feed/atom.xml" rel="self"/>
95
- <id>${siteUrl}/</id>
96
- <updated>${now}</updated>
97
- ${entries}
98
- </feed>`;
99
- return new Response(atom, {
55
+ const feedData = await buildFeedData(c);
56
+ const renderer = c.var.config.theme?.feed?.atom ?? defaultAtomRenderer;
57
+ const xml = renderer(feedData);
58
+ return new Response(xml, {
100
59
  headers: {
101
60
  "Content-Type": "application/atom+xml; charset=utf-8"
102
61
  }
103
62
  });
104
63
  });
105
- function escapeXml(str) {
106
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
107
- }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Sitemap Routes
3
3
  */ import { Hono } from "hono";
4
- import * as sqid from "../../lib/sqid.js";
5
- import * as time from "../../lib/time.js";
4
+ import { defaultSitemapRenderer } from "../../lib/feed.js";
5
+ import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
6
6
  export const sitemapRoutes = new Hono();
7
7
  // XML Sitemap
8
8
  sitemapRoutes.get("/sitemap.xml", async (c)=>{
@@ -14,30 +14,15 @@ sitemapRoutes.get("/sitemap.xml", async (c)=>{
14
14
  ],
15
15
  limit: 1000
16
16
  });
17
- const urls = posts.map((post)=>{
18
- const loc = `${siteUrl}/p/${sqid.encode(post.id)}`;
19
- const lastmod = time.toISOString(post.updatedAt).split("T")[0];
20
- const priority = post.visibility === "featured" ? "0.8" : "0.6";
21
- return `
22
- <url>
23
- <loc>${loc}</loc>
24
- <lastmod>${lastmod}</lastmod>
25
- <priority>${priority}</priority>
26
- </url>`;
27
- }).join("");
28
- // Add homepage
29
- const homepageUrl = `
30
- <url>
31
- <loc>${siteUrl}/</loc>
32
- <priority>1.0</priority>
33
- <changefreq>daily</changefreq>
34
- </url>`;
35
- const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
36
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
37
- ${homepageUrl}
38
- ${urls}
39
- </urlset>`;
40
- return new Response(sitemap, {
17
+ // Transform to PostView[]
18
+ const mediaCtx = createMediaContext(c);
19
+ const postViews = toPostViewsFromPosts(posts, mediaCtx);
20
+ const renderer = c.var.config.theme?.feed?.sitemap ?? defaultSitemapRenderer;
21
+ const xml = renderer({
22
+ siteUrl,
23
+ posts: postViews
24
+ });
25
+ return new Response(xml, {
41
26
  headers: {
42
27
  "Content-Type": "application/xml; charset=utf-8"
43
28
  }