@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
@@ -1,191 +1,16 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
2
  /**
3
3
  * Archive Page Route
4
4
  *
5
5
  * Shows all posts, optionally filtered by type
6
6
  */ import { Hono } from "hono";
7
- import { useLingui as $_useLingui } from "@jant/core/i18n";
8
- import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
9
- import { Pagination } from "../../theme/components/index.js";
10
7
  import { POST_TYPES } from "../../types.js";
11
- import * as sqid from "../../lib/sqid.js";
12
- import * as time from "../../lib/time.js";
8
+ import { ArchivePage as DefaultArchivePage } from "../../theme/pages/ArchivePage.js";
13
9
  import { getNavigationData } from "../../lib/navigation.js";
10
+ import { renderPublicPage } from "../../lib/render.js";
11
+ import { createMediaContext, toArchiveGroups } from "../../lib/view.js";
14
12
  const PAGE_SIZE = 50;
15
13
  export const archiveRoutes = new Hono();
16
- function getTypeLabel(type) {
17
- const { i18n: $__i18n, _: $__ } = $_useLingui();
18
- const labels = {
19
- note: $__i18n._({
20
- id: "KiJn9B",
21
- message: "Note"
22
- }),
23
- article: $__i18n._({
24
- id: "f6e0Ry",
25
- message: "Article"
26
- }),
27
- link: $__i18n._({
28
- id: "yzF66j",
29
- message: "Link"
30
- }),
31
- quote: $__i18n._({
32
- id: "ZhhOwV",
33
- message: "Quote"
34
- }),
35
- image: $__i18n._({
36
- id: "hG89Ed",
37
- message: "Image"
38
- }),
39
- page: $__i18n._({
40
- id: "6WdDG7",
41
- message: "Page"
42
- })
43
- };
44
- return labels[type] ?? type;
45
- }
46
- function getTypeLabelPlural(type) {
47
- const { i18n: $__i18n, _: $__ } = $_useLingui();
48
- const labels = {
49
- note: $__i18n._({
50
- id: "1DBGsz",
51
- message: "Notes"
52
- }),
53
- article: $__i18n._({
54
- id: "Tt5T6+",
55
- message: "Articles"
56
- }),
57
- link: $__i18n._({
58
- id: "Rj01Fz",
59
- message: "Links"
60
- }),
61
- quote: $__i18n._({
62
- id: "eWLklq",
63
- message: "Quotes"
64
- }),
65
- image: $__i18n._({
66
- id: "an5hVd",
67
- message: "Images"
68
- }),
69
- page: $__i18n._({
70
- id: "wRR604",
71
- message: "Pages"
72
- })
73
- };
74
- return labels[type] ?? `${type}s`;
75
- }
76
- function formatYearMonth(yearMonth) {
77
- const [year, month] = yearMonth.split("-");
78
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- yearMonth format YYYY-MM guarantees both year and month exist
79
- const date = new Date(parseInt(year, 10), parseInt(month, 10) - 1);
80
- return date.toLocaleDateString("en-US", {
81
- year: "numeric",
82
- month: "long"
83
- });
84
- }
85
- function ArchiveContent({ displayPosts, hasMore, nextCursor, type, grouped, replyCounts }) {
86
- const { i18n: $__i18n, _: $__ } = $_useLingui();
87
- const title = type ? getTypeLabelPlural(type) : $__i18n._({
88
- id: "B495Gs",
89
- message: "Archive"
90
- });
91
- return /*#__PURE__*/ _jsxs("div", {
92
- children: [
93
- /*#__PURE__*/ _jsxs("header", {
94
- class: "mb-8",
95
- children: [
96
- /*#__PURE__*/ _jsx("h1", {
97
- class: "text-2xl font-semibold",
98
- children: title
99
- }),
100
- /*#__PURE__*/ _jsxs("nav", {
101
- class: "flex flex-wrap gap-2 mt-4",
102
- children: [
103
- /*#__PURE__*/ _jsx("a", {
104
- href: "/archive",
105
- class: `badge ${!type ? "badge-primary" : "badge-outline"}`,
106
- children: $__i18n._({
107
- id: "N40H+G",
108
- message: "All"
109
- })
110
- }),
111
- POST_TYPES.filter((t)=>t !== "page").map((typeKey)=>/*#__PURE__*/ _jsx("a", {
112
- href: `/archive?type=${typeKey}`,
113
- class: `badge ${type === typeKey ? "badge-primary" : "badge-outline"}`,
114
- children: getTypeLabelPlural(typeKey)
115
- }, typeKey))
116
- ]
117
- })
118
- ]
119
- }),
120
- /*#__PURE__*/ _jsx("main", {
121
- children: displayPosts.length === 0 ? /*#__PURE__*/ _jsx("p", {
122
- class: "text-muted-foreground",
123
- children: $__i18n._({
124
- id: "Hzi9AA",
125
- message: "No posts found."
126
- })
127
- }) : Array.from(grouped.entries()).map(([yearMonth, monthPosts])=>/*#__PURE__*/ _jsxs("section", {
128
- class: "mb-8",
129
- children: [
130
- /*#__PURE__*/ _jsx("h2", {
131
- class: "text-lg font-medium mb-4 text-muted-foreground",
132
- children: formatYearMonth(yearMonth)
133
- }),
134
- /*#__PURE__*/ _jsx("div", {
135
- class: "flex flex-col gap-3",
136
- children: monthPosts.map((post)=>{
137
- const replyCount = replyCounts.get(post.id);
138
- return /*#__PURE__*/ _jsxs("article", {
139
- class: "flex items-baseline gap-4",
140
- children: [
141
- /*#__PURE__*/ _jsx("time", {
142
- class: "text-sm text-muted-foreground w-12 shrink-0",
143
- datetime: time.toISOString(post.publishedAt),
144
- children: new Date(post.publishedAt * 1000).getDate()
145
- }),
146
- /*#__PURE__*/ _jsxs("div", {
147
- class: "flex-1 min-w-0",
148
- children: [
149
- /*#__PURE__*/ _jsx("a", {
150
- href: `/p/${sqid.encode(post.id)}`,
151
- class: "hover:underline",
152
- children: post.title || post.content?.slice(0, 80) || `Post #${post.id}`
153
- }),
154
- !type && /*#__PURE__*/ _jsx("span", {
155
- class: "ml-2 badge-outline text-xs",
156
- children: getTypeLabel(post.type)
157
- }),
158
- replyCount && replyCount > 0 && /*#__PURE__*/ _jsxs("span", {
159
- class: "ml-2 text-xs text-muted-foreground",
160
- children: [
161
- "(",
162
- replyCount === 1 ? $__i18n._({
163
- id: "TxE+Mj",
164
- message: "1 reply"
165
- }) : $__i18n._({
166
- id: "90Luob",
167
- message: "{count} replies"
168
- }),
169
- ")"
170
- ]
171
- })
172
- ]
173
- })
174
- ]
175
- }, post.id);
176
- })
177
- })
178
- ]
179
- }, yearMonth))
180
- }),
181
- /*#__PURE__*/ _jsx(Pagination, {
182
- baseUrl: type ? `/archive?type=${type}` : "/archive",
183
- hasMore: hasMore,
184
- nextCursor: nextCursor
185
- })
186
- ]
187
- });
188
- }
189
14
  // Archive page - all posts
190
15
  archiveRoutes.get("/", async (c)=>{
191
16
  const typeParam = c.req.query("type");
@@ -207,9 +32,6 @@ archiveRoutes.get("/", async (c)=>{
207
32
  });
208
33
  const hasMore = posts.length > PAGE_SIZE;
209
34
  const displayPosts = hasMore ? posts.slice(0, PAGE_SIZE) : posts;
210
- // Get reply counts for thread indicators
211
- const postIds = displayPosts.map((p)=>p.id);
212
- const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
213
35
  // Get next cursor
214
36
  const nextCursor = hasMore && displayPosts.length > 0 ? displayPosts[displayPosts.length - 1].id : undefined;
215
37
  // Group posts by year-month
@@ -223,19 +45,20 @@ archiveRoutes.get("/", async (c)=>{
223
45
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Map.set() above guarantees key exists
224
46
  grouped.get(key).push(post);
225
47
  }
226
- return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
48
+ // Transform to View Models
49
+ const mediaCtx = createMediaContext(c);
50
+ const groups = toArchiveGroups(grouped, mediaCtx);
51
+ const components = c.var.config.theme?.components;
52
+ const Page = components?.ArchivePage ?? DefaultArchivePage;
53
+ return renderPublicPage(c, {
227
54
  title: `Archive - ${navData.siteName}`,
228
- c: c,
229
- children: /*#__PURE__*/ _jsx(SiteLayout, {
230
- ...navData,
231
- children: /*#__PURE__*/ _jsx(ArchiveContent, {
232
- displayPosts: displayPosts,
233
- hasMore: hasMore,
234
- nextCursor: nextCursor,
235
- type: type,
236
- grouped: grouped,
237
- replyCounts: replyCounts
238
- })
55
+ navData,
56
+ content: /*#__PURE__*/ _jsx(Page, {
57
+ groups: groups,
58
+ hasMore: hasMore,
59
+ nextCursor: nextCursor,
60
+ type: type,
61
+ theme: components
239
62
  })
240
- }));
63
+ });
241
64
  });
@@ -1,85 +1,31 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
2
  /**
3
3
  * Collection Page Route
4
4
  */ import { Hono } from "hono";
5
- import { useLingui as $_useLingui } from "@jant/core/i18n";
6
- import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
7
- import * as sqid from "../../lib/sqid.js";
8
- import * as time from "../../lib/time.js";
5
+ import { CollectionPage as DefaultCollectionPage } from "../../theme/pages/CollectionPage.js";
9
6
  import { getNavigationData } from "../../lib/navigation.js";
7
+ import { renderPublicPage } from "../../lib/render.js";
8
+ import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
10
9
  export const collectionRoutes = new Hono();
11
- function CollectionContent({ collection, posts }) {
12
- const { i18n: $__i18n, _: $__ } = $_useLingui();
13
- return /*#__PURE__*/ _jsxs("div", {
14
- children: [
15
- /*#__PURE__*/ _jsxs("header", {
16
- class: "mb-8",
17
- children: [
18
- /*#__PURE__*/ _jsx("h1", {
19
- class: "text-2xl font-semibold",
20
- children: collection.title
21
- }),
22
- collection.description && /*#__PURE__*/ _jsx("p", {
23
- class: "text-muted-foreground mt-2",
24
- children: collection.description
25
- })
26
- ]
27
- }),
28
- /*#__PURE__*/ _jsx("main", {
29
- class: "flex flex-col gap-6",
30
- children: posts.length === 0 ? /*#__PURE__*/ _jsx("p", {
31
- class: "text-muted-foreground",
32
- children: $__i18n._({
33
- id: "J4FNfC",
34
- message: "No posts in this collection."
35
- })
36
- }) : posts.map((post)=>/*#__PURE__*/ _jsxs("article", {
37
- class: "h-entry",
38
- children: [
39
- post.title && /*#__PURE__*/ _jsx("h2", {
40
- class: "p-name text-lg font-medium mb-2",
41
- children: /*#__PURE__*/ _jsx("a", {
42
- href: `/p/${sqid.encode(post.id)}`,
43
- class: "u-url hover:underline",
44
- children: post.title
45
- })
46
- }),
47
- /*#__PURE__*/ _jsx("div", {
48
- class: "e-content prose prose-sm",
49
- dangerouslySetInnerHTML: {
50
- __html: post.contentHtml || ""
51
- }
52
- }),
53
- /*#__PURE__*/ _jsx("footer", {
54
- class: "mt-2 text-sm text-muted-foreground",
55
- children: /*#__PURE__*/ _jsx("time", {
56
- class: "dt-published",
57
- datetime: time.toISOString(post.publishedAt),
58
- children: time.formatDate(post.publishedAt)
59
- })
60
- })
61
- ]
62
- }, post.id))
63
- })
64
- ]
65
- });
66
- }
67
10
  collectionRoutes.get("/:path", async (c)=>{
68
11
  const path = c.req.param("path");
69
12
  const collection = await c.var.services.collections.getByPath(path);
70
13
  if (!collection) return c.notFound();
71
14
  const posts = await c.var.services.collections.getPosts(collection.id);
72
15
  const navData = await getNavigationData(c);
73
- return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
16
+ // Transform to View Models
17
+ const mediaCtx = createMediaContext(c);
18
+ const postViews = toPostViewsFromPosts(posts, mediaCtx);
19
+ const components = c.var.config.theme?.components;
20
+ const Page = components?.CollectionPage ?? DefaultCollectionPage;
21
+ return renderPublicPage(c, {
74
22
  title: `${collection.title} - ${navData.siteName}`,
75
23
  description: collection.description ?? undefined,
76
- c: c,
77
- children: /*#__PURE__*/ _jsx(SiteLayout, {
78
- ...navData,
79
- children: /*#__PURE__*/ _jsx(CollectionContent, {
80
- collection: collection,
81
- posts: posts
82
- })
24
+ navData,
25
+ content: /*#__PURE__*/ _jsx(Page, {
26
+ collection: collection,
27
+ posts: postViews,
28
+ theme: components
83
29
  })
84
- }));
30
+ });
85
31
  });
@@ -1,31 +1,16 @@
1
- import { jsx as _jsx, Fragment as _Fragment } from "hono/jsx/jsx-runtime";
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
2
  /**
3
3
  * Home Page Route
4
4
  *
5
5
  * Timeline feed with per-type card components and thread previews.
6
6
  */ import { Hono } from "hono";
7
- import { useLingui as $_useLingui } from "@jant/core/i18n";
8
- import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
9
7
  import { buildMediaMap } from "../../lib/media-helpers.js";
10
- import { resolveTimelineFeed } from "../../lib/theme-components.js";
11
- import { TimelineFeed as DefaultTimelineFeed } from "../../theme/components/timeline/TimelineFeed.js";
12
8
  import { getNavigationData } from "../../lib/navigation.js";
9
+ import { renderPublicPage } from "../../lib/render.js";
10
+ import { HomePage as DefaultHomePage } from "../../theme/pages/HomePage.js";
11
+ import { createMediaContext, toPostView, toPostViews } from "../../lib/view.js";
13
12
  const PAGE_SIZE = 20;
14
13
  export const homeRoutes = new Hono();
15
- function HomeContent({ FeedComponent, feedProps }) {
16
- const { i18n: $__i18n, _: $__ } = $_useLingui();
17
- return /*#__PURE__*/ _jsx(_Fragment, {
18
- children: feedProps.items.length === 0 ? /*#__PURE__*/ _jsx("p", {
19
- class: "text-muted-foreground",
20
- children: $__i18n._({
21
- id: "ODiSoW",
22
- message: "No posts yet."
23
- })
24
- }) : /*#__PURE__*/ _jsx(FeedComponent, {
25
- ...feedProps
26
- })
27
- });
28
- }
29
14
  homeRoutes.get("/", async (c)=>{
30
15
  const navData = await getNavigationData(c);
31
16
  // Fetch one extra to determine if there are more
@@ -45,10 +30,8 @@ homeRoutes.get("/", async (c)=>{
45
30
  // Batch load media attachments
46
31
  const postIds = displayPosts.map((p)=>p.id);
47
32
  const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
48
- const r2PublicUrl = c.env.R2_PUBLIC_URL;
49
- const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
50
- const s3PublicUrl = c.env.S3_PUBLIC_URL;
51
- const mediaMap = buildMediaMap(rawMediaMap, r2PublicUrl, imageTransformUrl, s3PublicUrl);
33
+ const mediaCtx = createMediaContext(c);
34
+ const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
52
35
  // Get reply counts to identify thread roots
53
36
  const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
54
37
  const threadRootIds = postIds.filter((id)=>(replyCounts.get(id) ?? 0) > 0);
@@ -61,50 +44,45 @@ homeRoutes.get("/", async (c)=>{
61
44
  previewReplyIds.push(reply.id);
62
45
  }
63
46
  }
64
- const previewMediaMap = previewReplyIds.length > 0 ? buildMediaMap(await c.var.services.media.getByPostIds(previewReplyIds), r2PublicUrl, imageTransformUrl, s3PublicUrl) : new Map();
65
- // Assemble timeline items
47
+ const previewMediaMap = previewReplyIds.length > 0 ? buildMediaMap(await c.var.services.media.getByPostIds(previewReplyIds), mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl) : new Map();
48
+ // Assemble timeline items with View Models
66
49
  const items = displayPosts.map((post)=>{
67
- const postWithMedia = {
50
+ const postView = toPostView({
68
51
  ...post,
69
52
  mediaAttachments: mediaMap.get(post.id) ?? []
70
- };
53
+ }, mediaCtx);
71
54
  const replyCount = replyCounts.get(post.id) ?? 0;
72
55
  const previewReplies = threadPreviews.get(post.id);
73
56
  if (replyCount > 0 && previewReplies) {
74
57
  return {
75
- post: postWithMedia,
58
+ post: postView,
76
59
  threadPreview: {
77
- replies: previewReplies.map((r)=>({
60
+ replies: toPostViews(previewReplies.map((r)=>({
78
61
  ...r,
79
62
  mediaAttachments: previewMediaMap.get(r.id) ?? []
80
- })),
63
+ })), mediaCtx),
81
64
  totalReplyCount: replyCount
82
65
  }
83
66
  };
84
67
  }
85
68
  return {
86
- post: postWithMedia
69
+ post: postView
87
70
  };
88
71
  });
89
72
  // Determine next cursor
90
73
  const lastPost = displayPosts[displayPosts.length - 1];
91
74
  const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
92
- // Resolve theme components
93
- const Feed = resolveTimelineFeed(DefaultTimelineFeed, c.var.config.theme?.components);
94
- const feedProps = {
95
- items,
96
- hasMore,
97
- nextCursor
98
- };
99
- return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
75
+ // Resolve page component
76
+ const components = c.var.config.theme?.components;
77
+ const Page = components?.HomePage ?? DefaultHomePage;
78
+ return renderPublicPage(c, {
100
79
  title: navData.siteName,
101
- c: c,
102
- children: /*#__PURE__*/ _jsx(SiteLayout, {
103
- ...navData,
104
- children: /*#__PURE__*/ _jsx(HomeContent, {
105
- FeedComponent: Feed,
106
- feedProps: feedProps
107
- })
80
+ navData,
81
+ content: /*#__PURE__*/ _jsx(Page, {
82
+ items: items,
83
+ hasMore: hasMore,
84
+ nextCursor: nextCursor,
85
+ theme: components
108
86
  })
109
- }));
87
+ });
110
88
  });
@@ -1,29 +1,14 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
2
  /**
3
3
  * Custom Page Route
4
4
  *
5
5
  * Catch-all route for custom pages accessible via their path field
6
6
  */ import { Hono } from "hono";
7
- import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
7
+ import { SinglePage as DefaultSinglePage } from "../../theme/pages/SinglePage.js";
8
8
  import { getNavigationData } from "../../lib/navigation.js";
9
+ import { renderPublicPage } from "../../lib/render.js";
10
+ import { createMediaContext, toPostViewFromPost } from "../../lib/view.js";
9
11
  export const pageRoutes = new Hono();
10
- function PageContent({ page }) {
11
- return /*#__PURE__*/ _jsxs("article", {
12
- class: "h-entry",
13
- children: [
14
- page.title && /*#__PURE__*/ _jsx("h1", {
15
- class: "p-name text-3xl font-semibold mb-6",
16
- children: page.title
17
- }),
18
- /*#__PURE__*/ _jsx("div", {
19
- class: "e-content prose",
20
- dangerouslySetInnerHTML: {
21
- __html: page.contentHtml || ""
22
- }
23
- })
24
- ]
25
- });
26
- }
27
12
  // Catch-all for custom page paths
28
13
  pageRoutes.get("/:path", async (c)=>{
29
14
  const path = c.req.param("path");
@@ -38,15 +23,18 @@ pageRoutes.get("/:path", async (c)=>{
38
23
  return c.notFound();
39
24
  }
40
25
  const navData = await getNavigationData(c);
41
- return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
26
+ // Transform to View Model
27
+ const mediaCtx = createMediaContext(c);
28
+ const pageView = toPostViewFromPost(page, mediaCtx);
29
+ const components = c.var.config.theme?.components;
30
+ const Page = components?.SinglePage ?? DefaultSinglePage;
31
+ return renderPublicPage(c, {
42
32
  title: `${page.title} - ${navData.siteName}`,
43
33
  description: page.content?.slice(0, 160),
44
- c: c,
45
- children: /*#__PURE__*/ _jsx(SiteLayout, {
46
- ...navData,
47
- children: /*#__PURE__*/ _jsx(PageContent, {
48
- page: page
49
- })
34
+ navData,
35
+ content: /*#__PURE__*/ _jsx(Page, {
36
+ page: pageView,
37
+ theme: components
50
38
  })
51
- }));
39
+ });
52
40
  });
@@ -1,54 +1,14 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
2
  /**
3
3
  * Single Post Page Route
4
4
  */ import { Hono } from "hono";
5
- import { useLingui as $_useLingui } from "@jant/core/i18n";
6
- import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
7
- import { MediaGallery } from "../../theme/components/index.js";
5
+ import { PostPage as DefaultPostPage } from "../../theme/pages/PostPage.js";
8
6
  import * as sqid from "../../lib/sqid.js";
9
- import * as time from "../../lib/time.js";
10
- import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "../../lib/image.js";
11
7
  import { getNavigationData } from "../../lib/navigation.js";
8
+ import { renderPublicPage } from "../../lib/render.js";
9
+ import { buildMediaMap } from "../../lib/media-helpers.js";
10
+ import { createMediaContext, toPostView } from "../../lib/view.js";
12
11
  export const postRoutes = new Hono();
13
- function PostContent({ post, mediaAttachments }) {
14
- const { i18n: $__i18n, _: $__ } = $_useLingui();
15
- return /*#__PURE__*/ _jsxs("article", {
16
- class: "h-entry",
17
- children: [
18
- post.title && /*#__PURE__*/ _jsx("h1", {
19
- class: "p-name text-2xl font-semibold mb-4",
20
- children: post.title
21
- }),
22
- /*#__PURE__*/ _jsx("div", {
23
- class: "e-content prose",
24
- dangerouslySetInnerHTML: {
25
- __html: post.contentHtml || ""
26
- }
27
- }),
28
- mediaAttachments.length > 0 && /*#__PURE__*/ _jsx(MediaGallery, {
29
- attachments: mediaAttachments
30
- }),
31
- /*#__PURE__*/ _jsxs("footer", {
32
- class: "mt-6 pt-4 border-t text-sm text-muted-foreground",
33
- children: [
34
- /*#__PURE__*/ _jsx("time", {
35
- class: "dt-published",
36
- datetime: time.toISOString(post.publishedAt),
37
- children: time.formatDate(post.publishedAt)
38
- }),
39
- /*#__PURE__*/ _jsx("a", {
40
- href: `/p/${sqid.encode(post.id)}`,
41
- class: "u-url ml-4",
42
- children: $__i18n._({
43
- id: "D9Oea+",
44
- message: "Permalink"
45
- })
46
- })
47
- ]
48
- })
49
- ]
50
- });
51
- }
52
12
  postRoutes.get("/:id", async (c)=>{
53
13
  const paramId = c.req.param("id");
54
14
  // Try to decode as sqid first
@@ -67,42 +27,28 @@ postRoutes.get("/:id", async (c)=>{
67
27
  if (post.visibility === "draft") {
68
28
  return c.notFound();
69
29
  }
70
- // Load media attachments
71
- const rawMedia = await c.var.services.media.getByPostId(post.id);
72
- const r2PublicUrl = c.env.R2_PUBLIC_URL;
73
- const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
74
- const s3PublicUrl = c.env.S3_PUBLIC_URL;
75
- const mediaAttachments = rawMedia.map((m)=>{
76
- const publicUrl = getPublicUrlForProvider(m.provider, r2PublicUrl, s3PublicUrl);
77
- return {
78
- id: m.id,
79
- url: getMediaUrl(m.id, m.storageKey, publicUrl),
80
- previewUrl: getImageUrl(getMediaUrl(m.id, m.storageKey, publicUrl), imageTransformUrl, {
81
- width: 400,
82
- quality: 80,
83
- format: "auto",
84
- fit: "cover"
85
- }),
86
- alt: m.alt,
87
- blurhash: m.blurhash,
88
- width: m.width,
89
- height: m.height,
90
- position: m.position,
91
- mimeType: m.mimeType
92
- };
93
- });
30
+ // Batch load media attachments
31
+ const rawMediaMap = await c.var.services.media.getByPostIds([
32
+ post.id
33
+ ]);
34
+ const mediaCtx = createMediaContext(c);
35
+ const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
36
+ // Transform to View Model
37
+ const postView = toPostView({
38
+ ...post,
39
+ mediaAttachments: mediaMap.get(post.id) ?? []
40
+ }, mediaCtx);
94
41
  const navData = await getNavigationData(c);
95
42
  const title = post.title || navData.siteName;
96
- return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
97
- title: title,
43
+ const components = c.var.config.theme?.components;
44
+ const Page = components?.PostPage ?? DefaultPostPage;
45
+ return renderPublicPage(c, {
46
+ title,
98
47
  description: post.content?.slice(0, 160),
99
- c: c,
100
- children: /*#__PURE__*/ _jsx(SiteLayout, {
101
- ...navData,
102
- children: /*#__PURE__*/ _jsx(PostContent, {
103
- post: post,
104
- mediaAttachments: mediaAttachments
105
- })
48
+ navData,
49
+ content: /*#__PURE__*/ _jsx(Page, {
50
+ post: postView,
51
+ theme: components
106
52
  })
107
- }));
53
+ });
108
54
  });