@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
@@ -5,15 +5,13 @@
5
5
  */
6
6
 
7
7
  import { Hono } from "hono";
8
- import { useLingui } from "@lingui/react/macro";
9
- import type { Bindings, Post, PostType } from "../../types.js";
8
+ import type { Bindings, PostType } from "../../types.js";
10
9
  import type { AppVariables } from "../../app.js";
11
- import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
12
- import { Pagination } from "../../theme/components/index.js";
13
10
  import { POST_TYPES } from "../../types.js";
14
- import * as sqid from "../../lib/sqid.js";
15
- import * as time from "../../lib/time.js";
11
+ import { ArchivePage as DefaultArchivePage } from "../../theme/pages/ArchivePage.js";
16
12
  import { getNavigationData } from "../../lib/navigation.js";
13
+ import { renderPublicPage } from "../../lib/render.js";
14
+ import { createMediaContext, toArchiveGroups } from "../../lib/view.js";
17
15
 
18
16
  type Env = { Bindings: Bindings; Variables: AppVariables };
19
17
 
@@ -21,191 +19,6 @@ const PAGE_SIZE = 50;
21
19
 
22
20
  export const archiveRoutes = new Hono<Env>();
23
21
 
24
- function getTypeLabel(type: string): string {
25
- const { t } = useLingui();
26
- const labels: Record<string, string> = {
27
- note: t({ message: "Note", comment: "@context: Post type label - note" }),
28
- article: t({
29
- message: "Article",
30
- comment: "@context: Post type label - article",
31
- }),
32
- link: t({ message: "Link", comment: "@context: Post type label - link" }),
33
- quote: t({
34
- message: "Quote",
35
- comment: "@context: Post type label - quote",
36
- }),
37
- image: t({
38
- message: "Image",
39
- comment: "@context: Post type label - image",
40
- }),
41
- page: t({ message: "Page", comment: "@context: Post type label - page" }),
42
- };
43
- return labels[type] ?? type;
44
- }
45
-
46
- function getTypeLabelPlural(type: string): string {
47
- const { t } = useLingui();
48
- const labels: Record<string, string> = {
49
- note: t({
50
- message: "Notes",
51
- comment: "@context: Post type label plural - notes",
52
- }),
53
- article: t({
54
- message: "Articles",
55
- comment: "@context: Post type label plural - articles",
56
- }),
57
- link: t({
58
- message: "Links",
59
- comment: "@context: Post type label plural - links",
60
- }),
61
- quote: t({
62
- message: "Quotes",
63
- comment: "@context: Post type label plural - quotes",
64
- }),
65
- image: t({
66
- message: "Images",
67
- comment: "@context: Post type label plural - images",
68
- }),
69
- page: t({
70
- message: "Pages",
71
- comment: "@context: Post type label plural - pages",
72
- }),
73
- };
74
- return labels[type] ?? `${type}s`;
75
- }
76
-
77
- function formatYearMonth(yearMonth: string): string {
78
- const [year, month] = yearMonth.split("-");
79
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- yearMonth format YYYY-MM guarantees both year and month exist
80
- const date = new Date(parseInt(year!, 10), parseInt(month!, 10) - 1);
81
- return date.toLocaleDateString("en-US", { year: "numeric", month: "long" });
82
- }
83
-
84
- function ArchiveContent({
85
- displayPosts,
86
- hasMore,
87
- nextCursor,
88
- type,
89
- grouped,
90
- replyCounts,
91
- }: {
92
- displayPosts: Post[];
93
- hasMore: boolean;
94
- nextCursor?: number;
95
- type?: string;
96
- grouped: Map<string, Post[]>;
97
- replyCounts: Map<number, number>;
98
- }) {
99
- const { t } = useLingui();
100
- const title = type
101
- ? getTypeLabelPlural(type)
102
- : t({ message: "Archive", comment: "@context: Archive page title" });
103
-
104
- return (
105
- <div>
106
- <header class="mb-8">
107
- <h1 class="text-2xl font-semibold">{title}</h1>
108
-
109
- {/* Type filter */}
110
- <nav class="flex flex-wrap gap-2 mt-4">
111
- <a
112
- href="/archive"
113
- class={`badge ${!type ? "badge-primary" : "badge-outline"}`}
114
- >
115
- {t({
116
- message: "All",
117
- comment: "@context: Archive filter - all types",
118
- })}
119
- </a>
120
- {POST_TYPES.filter((t) => t !== "page").map((typeKey) => (
121
- <a
122
- key={typeKey}
123
- href={`/archive?type=${typeKey}`}
124
- class={`badge ${type === typeKey ? "badge-primary" : "badge-outline"}`}
125
- >
126
- {getTypeLabelPlural(typeKey)}
127
- </a>
128
- ))}
129
- </nav>
130
- </header>
131
-
132
- <main>
133
- {displayPosts.length === 0 ? (
134
- <p class="text-muted-foreground">
135
- {t({
136
- message: "No posts found.",
137
- comment: "@context: Archive empty state",
138
- })}
139
- </p>
140
- ) : (
141
- Array.from(grouped.entries()).map(([yearMonth, monthPosts]) => (
142
- <section key={yearMonth} class="mb-8">
143
- <h2 class="text-lg font-medium mb-4 text-muted-foreground">
144
- {formatYearMonth(yearMonth)}
145
- </h2>
146
- <div class="flex flex-col gap-3">
147
- {monthPosts.map((post) => {
148
- const replyCount = replyCounts.get(post.id);
149
- return (
150
- <article key={post.id} class="flex items-baseline gap-4">
151
- <time
152
- class="text-sm text-muted-foreground w-12 shrink-0"
153
- datetime={time.toISOString(post.publishedAt)}
154
- >
155
- {new Date(post.publishedAt * 1000).getDate()}
156
- </time>
157
- <div class="flex-1 min-w-0">
158
- <a
159
- href={`/p/${sqid.encode(post.id)}`}
160
- class="hover:underline"
161
- >
162
- {post.title ||
163
- post.content?.slice(0, 80) ||
164
- `Post #${post.id}`}
165
- </a>
166
- {!type && (
167
- <span class="ml-2 badge-outline text-xs">
168
- {getTypeLabel(post.type)}
169
- </span>
170
- )}
171
- {replyCount && replyCount > 0 && (
172
- <span class="ml-2 text-xs text-muted-foreground">
173
- (
174
- {replyCount === 1
175
- ? t({
176
- message: "1 reply",
177
- comment:
178
- "@context: Archive post reply indicator - single",
179
- })
180
- : t({
181
- message: "{count} replies",
182
- comment:
183
- "@context: Archive post reply indicator - plural",
184
- values: { count: String(replyCount) },
185
- })}
186
- )
187
- </span>
188
- )}
189
- </div>
190
- </article>
191
- );
192
- })}
193
- </div>
194
- </section>
195
- ))
196
- )}
197
- </main>
198
-
199
- {/* Pagination */}
200
- <Pagination
201
- baseUrl={type ? `/archive?type=${type}` : "/archive"}
202
- hasMore={hasMore}
203
- nextCursor={nextCursor}
204
- />
205
- </div>
206
- );
207
- }
208
-
209
22
  // Archive page - all posts
210
23
  archiveRoutes.get("/", async (c) => {
211
24
  const typeParam = c.req.query("type") as PostType | undefined;
@@ -230,10 +43,6 @@ archiveRoutes.get("/", async (c) => {
230
43
  const hasMore = posts.length > PAGE_SIZE;
231
44
  const displayPosts = hasMore ? posts.slice(0, PAGE_SIZE) : posts;
232
45
 
233
- // Get reply counts for thread indicators
234
- const postIds = displayPosts.map((p) => p.id);
235
- const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
236
-
237
46
  // Get next cursor
238
47
  const nextCursor =
239
48
  hasMore && displayPosts.length > 0
@@ -253,18 +62,24 @@ archiveRoutes.get("/", async (c) => {
253
62
  grouped.get(key)!.push(post);
254
63
  }
255
64
 
256
- return c.html(
257
- <BaseLayout title={`Archive - ${navData.siteName}`} c={c}>
258
- <SiteLayout {...navData}>
259
- <ArchiveContent
260
- displayPosts={displayPosts}
261
- hasMore={hasMore}
262
- nextCursor={nextCursor}
263
- type={type}
264
- grouped={grouped}
265
- replyCounts={replyCounts}
266
- />
267
- </SiteLayout>
268
- </BaseLayout>,
269
- );
65
+ // Transform to View Models
66
+ const mediaCtx = createMediaContext(c);
67
+ const groups = toArchiveGroups(grouped, mediaCtx);
68
+
69
+ const components = c.var.config.theme?.components;
70
+ const Page = components?.ArchivePage ?? DefaultArchivePage;
71
+
72
+ return renderPublicPage(c, {
73
+ title: `Archive - ${navData.siteName}`,
74
+ navData,
75
+ content: (
76
+ <Page
77
+ groups={groups}
78
+ hasMore={hasMore}
79
+ nextCursor={nextCursor}
80
+ type={type}
81
+ theme={components}
82
+ />
83
+ ),
84
+ });
270
85
  });
@@ -3,77 +3,17 @@
3
3
  */
4
4
 
5
5
  import { Hono } from "hono";
6
- import { useLingui } from "@lingui/react/macro";
7
- import type { Bindings, Collection, Post } from "../../types.js";
6
+ import type { Bindings } from "../../types.js";
8
7
  import type { AppVariables } from "../../app.js";
9
- import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
10
- import * as sqid from "../../lib/sqid.js";
11
- import * as time from "../../lib/time.js";
8
+ import { CollectionPage as DefaultCollectionPage } from "../../theme/pages/CollectionPage.js";
12
9
  import { getNavigationData } from "../../lib/navigation.js";
10
+ import { renderPublicPage } from "../../lib/render.js";
11
+ import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
13
12
 
14
13
  type Env = { Bindings: Bindings; Variables: AppVariables };
15
14
 
16
15
  export const collectionRoutes = new Hono<Env>();
17
16
 
18
- function CollectionContent({
19
- collection,
20
- posts,
21
- }: {
22
- collection: Collection;
23
- posts: Post[];
24
- }) {
25
- const { t } = useLingui();
26
-
27
- return (
28
- <div>
29
- <header class="mb-8">
30
- <h1 class="text-2xl font-semibold">{collection.title}</h1>
31
- {collection.description && (
32
- <p class="text-muted-foreground mt-2">{collection.description}</p>
33
- )}
34
- </header>
35
-
36
- <main class="flex flex-col gap-6">
37
- {posts.length === 0 ? (
38
- <p class="text-muted-foreground">
39
- {t({
40
- message: "No posts in this collection.",
41
- comment: "@context: Empty state message",
42
- })}
43
- </p>
44
- ) : (
45
- posts.map((post) => (
46
- <article key={post.id} class="h-entry">
47
- {post.title && (
48
- <h2 class="p-name text-lg font-medium mb-2">
49
- <a
50
- href={`/p/${sqid.encode(post.id)}`}
51
- class="u-url hover:underline"
52
- >
53
- {post.title}
54
- </a>
55
- </h2>
56
- )}
57
- <div
58
- class="e-content prose prose-sm"
59
- dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
60
- />
61
- <footer class="mt-2 text-sm text-muted-foreground">
62
- <time
63
- class="dt-published"
64
- datetime={time.toISOString(post.publishedAt)}
65
- >
66
- {time.formatDate(post.publishedAt)}
67
- </time>
68
- </footer>
69
- </article>
70
- ))
71
- )}
72
- </main>
73
- </div>
74
- );
75
- }
76
-
77
17
  collectionRoutes.get("/:path", async (c) => {
78
18
  const path = c.req.param("path");
79
19
 
@@ -83,15 +23,19 @@ collectionRoutes.get("/:path", async (c) => {
83
23
  const posts = await c.var.services.collections.getPosts(collection.id);
84
24
  const navData = await getNavigationData(c);
85
25
 
86
- return c.html(
87
- <BaseLayout
88
- title={`${collection.title} - ${navData.siteName}`}
89
- description={collection.description ?? undefined}
90
- c={c}
91
- >
92
- <SiteLayout {...navData}>
93
- <CollectionContent collection={collection} posts={posts} />
94
- </SiteLayout>
95
- </BaseLayout>,
96
- );
26
+ // Transform to View Models
27
+ const mediaCtx = createMediaContext(c);
28
+ const postViews = toPostViewsFromPosts(posts, mediaCtx);
29
+
30
+ const components = c.var.config.theme?.components;
31
+ const Page = components?.CollectionPage ?? DefaultCollectionPage;
32
+
33
+ return renderPublicPage(c, {
34
+ title: `${collection.title} - ${navData.siteName}`,
35
+ description: collection.description ?? undefined,
36
+ navData,
37
+ content: (
38
+ <Page collection={collection} posts={postViews} theme={components} />
39
+ ),
40
+ });
97
41
  });
@@ -5,20 +5,13 @@
5
5
  */
6
6
 
7
7
  import { Hono } from "hono";
8
- import { useLingui } from "@lingui/react/macro";
9
- import type { FC } from "hono/jsx";
10
- import type {
11
- Bindings,
12
- PostWithMedia,
13
- TimelineItemData,
14
- TimelineFeedProps,
15
- } from "../../types.js";
8
+ import type { Bindings, TimelineItemView } from "../../types.js";
16
9
  import type { AppVariables } from "../../app.js";
17
- import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
18
10
  import { buildMediaMap } from "../../lib/media-helpers.js";
19
- import { resolveTimelineFeed } from "../../lib/theme-components.js";
20
- import { TimelineFeed as DefaultTimelineFeed } from "../../theme/components/timeline/TimelineFeed.js";
21
11
  import { getNavigationData } from "../../lib/navigation.js";
12
+ import { renderPublicPage } from "../../lib/render.js";
13
+ import { HomePage as DefaultHomePage } from "../../theme/pages/HomePage.js";
14
+ import { createMediaContext, toPostView, toPostViews } from "../../lib/view.js";
22
15
 
23
16
  type Env = { Bindings: Bindings; Variables: AppVariables };
24
17
 
@@ -26,31 +19,6 @@ const PAGE_SIZE = 20;
26
19
 
27
20
  export const homeRoutes = new Hono<Env>();
28
21
 
29
- function HomeContent({
30
- FeedComponent,
31
- feedProps,
32
- }: {
33
- FeedComponent: FC<TimelineFeedProps>;
34
- feedProps: TimelineFeedProps;
35
- }) {
36
- const { t } = useLingui();
37
-
38
- return (
39
- <>
40
- {feedProps.items.length === 0 ? (
41
- <p class="text-muted-foreground">
42
- {t({
43
- message: "No posts yet.",
44
- comment: "@context: Empty state message on home page",
45
- })}
46
- </p>
47
- ) : (
48
- <FeedComponent {...feedProps} />
49
- )}
50
- </>
51
- );
52
- }
53
-
54
22
  homeRoutes.get("/", async (c) => {
55
23
  const navData = await getNavigationData(c);
56
24
 
@@ -68,14 +36,12 @@ homeRoutes.get("/", async (c) => {
68
36
  // Batch load media attachments
69
37
  const postIds = displayPosts.map((p) => p.id);
70
38
  const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
71
- const r2PublicUrl = c.env.R2_PUBLIC_URL;
72
- const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
73
- const s3PublicUrl = c.env.S3_PUBLIC_URL;
39
+ const mediaCtx = createMediaContext(c);
74
40
  const mediaMap = buildMediaMap(
75
41
  rawMediaMap,
76
- r2PublicUrl,
77
- imageTransformUrl,
78
- s3PublicUrl,
42
+ mediaCtx.r2PublicUrl,
43
+ mediaCtx.imageTransformUrl,
44
+ mediaCtx.s3PublicUrl,
79
45
  );
80
46
 
81
47
  // Get reply counts to identify thread roots
@@ -99,59 +65,59 @@ homeRoutes.get("/", async (c) => {
99
65
  previewReplyIds.length > 0
100
66
  ? buildMediaMap(
101
67
  await c.var.services.media.getByPostIds(previewReplyIds),
102
- r2PublicUrl,
103
- imageTransformUrl,
104
- s3PublicUrl,
68
+ mediaCtx.r2PublicUrl,
69
+ mediaCtx.imageTransformUrl,
70
+ mediaCtx.s3PublicUrl,
105
71
  )
106
72
  : new Map();
107
73
 
108
- // Assemble timeline items
109
- const items: TimelineItemData[] = displayPosts.map((post) => {
110
- const postWithMedia: PostWithMedia = {
111
- ...post,
112
- mediaAttachments: mediaMap.get(post.id) ?? [],
113
- };
74
+ // Assemble timeline items with View Models
75
+ const items: TimelineItemView[] = displayPosts.map((post) => {
76
+ const postView = toPostView(
77
+ { ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
78
+ mediaCtx,
79
+ );
114
80
 
115
81
  const replyCount = replyCounts.get(post.id) ?? 0;
116
82
  const previewReplies = threadPreviews.get(post.id);
117
83
 
118
84
  if (replyCount > 0 && previewReplies) {
119
85
  return {
120
- post: postWithMedia,
86
+ post: postView,
121
87
  threadPreview: {
122
- replies: previewReplies.map((r) => ({
123
- ...r,
124
- mediaAttachments: previewMediaMap.get(r.id) ?? [],
125
- })),
88
+ replies: toPostViews(
89
+ previewReplies.map((r) => ({
90
+ ...r,
91
+ mediaAttachments: previewMediaMap.get(r.id) ?? [],
92
+ })),
93
+ mediaCtx,
94
+ ),
126
95
  totalReplyCount: replyCount,
127
96
  },
128
97
  };
129
98
  }
130
99
 
131
- return { post: postWithMedia };
100
+ return { post: postView };
132
101
  });
133
102
 
134
103
  // Determine next cursor
135
104
  const lastPost = displayPosts[displayPosts.length - 1];
136
105
  const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
137
106
 
138
- // Resolve theme components
139
- const Feed = resolveTimelineFeed(
140
- DefaultTimelineFeed,
141
- c.var.config.theme?.components,
142
- );
143
-
144
- const feedProps: TimelineFeedProps = {
145
- items,
146
- hasMore,
147
- nextCursor,
148
- };
149
-
150
- return c.html(
151
- <BaseLayout title={navData.siteName} c={c}>
152
- <SiteLayout {...navData}>
153
- <HomeContent FeedComponent={Feed} feedProps={feedProps} />
154
- </SiteLayout>
155
- </BaseLayout>,
156
- );
107
+ // Resolve page component
108
+ const components = c.var.config.theme?.components;
109
+ const Page = components?.HomePage ?? DefaultHomePage;
110
+
111
+ return renderPublicPage(c, {
112
+ title: navData.siteName,
113
+ navData,
114
+ content: (
115
+ <Page
116
+ items={items}
117
+ hasMore={hasMore}
118
+ nextCursor={nextCursor}
119
+ theme={components}
120
+ />
121
+ ),
122
+ });
157
123
  });
@@ -5,30 +5,17 @@
5
5
  */
6
6
 
7
7
  import { Hono } from "hono";
8
- import type { Bindings, Post } from "../../types.js";
8
+ import type { Bindings } from "../../types.js";
9
9
  import type { AppVariables } from "../../app.js";
10
- import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
10
+ import { SinglePage as DefaultSinglePage } from "../../theme/pages/SinglePage.js";
11
11
  import { getNavigationData } from "../../lib/navigation.js";
12
+ import { renderPublicPage } from "../../lib/render.js";
13
+ import { createMediaContext, toPostViewFromPost } from "../../lib/view.js";
12
14
 
13
15
  type Env = { Bindings: Bindings; Variables: AppVariables };
14
16
 
15
17
  export const pageRoutes = new Hono<Env>();
16
18
 
17
- function PageContent({ page }: { page: Post }) {
18
- return (
19
- <article class="h-entry">
20
- {page.title && (
21
- <h1 class="p-name text-3xl font-semibold mb-6">{page.title}</h1>
22
- )}
23
-
24
- <div
25
- class="e-content prose"
26
- dangerouslySetInnerHTML={{ __html: page.contentHtml || "" }}
27
- />
28
- </article>
29
- );
30
- }
31
-
32
19
  // Catch-all for custom page paths
33
20
  pageRoutes.get("/:path", async (c) => {
34
21
  const path = c.req.param("path");
@@ -48,15 +35,17 @@ pageRoutes.get("/:path", async (c) => {
48
35
 
49
36
  const navData = await getNavigationData(c);
50
37
 
51
- return c.html(
52
- <BaseLayout
53
- title={`${page.title} - ${navData.siteName}`}
54
- description={page.content?.slice(0, 160)}
55
- c={c}
56
- >
57
- <SiteLayout {...navData}>
58
- <PageContent page={page} />
59
- </SiteLayout>
60
- </BaseLayout>,
61
- );
38
+ // Transform to View Model
39
+ const mediaCtx = createMediaContext(c);
40
+ const pageView = toPostViewFromPost(page, mediaCtx);
41
+
42
+ const components = c.var.config.theme?.components;
43
+ const Page = components?.SinglePage ?? DefaultSinglePage;
44
+
45
+ return renderPublicPage(c, {
46
+ title: `${page.title} - ${navData.siteName}`,
47
+ description: page.content?.slice(0, 160),
48
+ navData,
49
+ content: <Page page={pageView} theme={components} />,
50
+ });
62
51
  });