@jant/core 0.3.23 → 0.3.25

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 (248) hide show
  1. package/dist/app.js +50 -26
  2. package/dist/db/schema.js +72 -47
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/index.js +5 -11
  7. package/dist/lib/constants.js +2 -4
  8. package/dist/lib/excerpt.js +76 -0
  9. package/dist/lib/feed.js +18 -7
  10. package/dist/lib/nav-reorder.js +1 -1
  11. package/dist/lib/navigation.js +30 -6
  12. package/dist/lib/pagination.js +44 -0
  13. package/dist/lib/render.js +7 -11
  14. package/dist/lib/schemas.js +80 -38
  15. package/dist/lib/theme.js +4 -4
  16. package/dist/lib/time.js +56 -1
  17. package/dist/lib/timeline.js +95 -0
  18. package/dist/lib/view.js +61 -72
  19. package/dist/routes/api/collections.js +124 -0
  20. package/dist/routes/api/nav-items.js +104 -0
  21. package/dist/routes/api/pages.js +91 -0
  22. package/dist/routes/api/posts.js +27 -33
  23. package/dist/routes/api/search.js +4 -5
  24. package/dist/routes/api/settings.js +68 -0
  25. package/dist/routes/api/upload.js +13 -13
  26. package/dist/routes/compose.js +48 -0
  27. package/dist/routes/dash/collections.js +24 -42
  28. package/dist/routes/dash/index.js +3 -3
  29. package/dist/routes/dash/media.js +2 -2
  30. package/dist/routes/dash/pages.js +440 -106
  31. package/dist/routes/dash/posts.js +27 -37
  32. package/dist/routes/dash/redirects.js +2 -2
  33. package/dist/routes/dash/settings.js +79 -5
  34. package/dist/routes/feed/rss.js +4 -6
  35. package/dist/routes/feed/sitemap.js +11 -8
  36. package/dist/routes/pages/archive.js +13 -15
  37. package/dist/routes/pages/collection.js +12 -9
  38. package/dist/routes/pages/collections.js +28 -0
  39. package/dist/routes/pages/featured.js +32 -0
  40. package/dist/routes/pages/home.js +19 -68
  41. package/dist/routes/pages/page.js +57 -29
  42. package/dist/routes/pages/post.js +7 -17
  43. package/dist/routes/pages/search.js +5 -9
  44. package/dist/services/collection.js +52 -64
  45. package/dist/services/index.js +5 -3
  46. package/dist/services/navigation.js +29 -53
  47. package/dist/services/page.js +84 -0
  48. package/dist/services/post.js +102 -69
  49. package/dist/services/search.js +24 -18
  50. package/dist/types.js +24 -40
  51. package/dist/ui/compose/ComposeDialog.js +452 -0
  52. package/dist/ui/compose/ComposePrompt.js +55 -0
  53. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +3 -15
  54. package/dist/{theme/components → ui/dash}/PageForm.js +15 -15
  55. package/dist/{theme/components → ui/dash}/PostForm.js +117 -137
  56. package/dist/{theme/components → ui/dash}/PostList.js +18 -13
  57. package/dist/ui/dash/StatusBadge.js +46 -0
  58. package/dist/{theme/components → ui/dash}/index.js +3 -6
  59. package/dist/ui/feed/LinkCard.js +72 -0
  60. package/dist/ui/feed/NoteCard.js +58 -0
  61. package/dist/{themes/minimal/timeline → ui/feed}/QuoteCard.js +29 -14
  62. package/dist/{themes/minimal/timeline → ui/feed}/ThreadPreview.js +20 -18
  63. package/dist/ui/feed/TimelineFeed.js +41 -0
  64. package/dist/ui/feed/TimelineItem.js +27 -0
  65. package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
  66. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  67. package/dist/ui/layouts/SiteLayout.js +141 -0
  68. package/dist/{themes/minimal → ui}/pages/ArchivePage.js +37 -50
  69. package/dist/ui/pages/CollectionPage.js +70 -0
  70. package/dist/ui/pages/CollectionsPage.js +76 -0
  71. package/dist/ui/pages/FeaturedPage.js +24 -0
  72. package/dist/ui/pages/HomePage.js +24 -0
  73. package/dist/{themes/minimal → ui}/pages/PostPage.js +20 -12
  74. package/dist/{themes/minimal → ui}/pages/SearchPage.js +19 -18
  75. package/dist/{themes/minimal → ui}/pages/SinglePage.js +5 -4
  76. package/dist/ui/shared/MediaGallery.js +35 -0
  77. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  78. package/dist/{theme/components → ui/shared}/ThreadView.js +3 -3
  79. package/dist/ui/shared/index.js +5 -0
  80. package/package.json +2 -9
  81. package/src/__tests__/helpers/app.ts +4 -0
  82. package/src/__tests__/helpers/db.ts +53 -73
  83. package/src/app.tsx +56 -28
  84. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  85. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  86. package/src/db/migrations/meta/_journal.json +14 -0
  87. package/src/db/schema.ts +63 -46
  88. package/src/i18n/locales/en.po +443 -240
  89. package/src/i18n/locales/en.ts +1 -1
  90. package/src/i18n/locales/zh-Hans.po +443 -240
  91. package/src/i18n/locales/zh-Hans.ts +1 -1
  92. package/src/i18n/locales/zh-Hant.po +443 -240
  93. package/src/i18n/locales/zh-Hant.ts +1 -1
  94. package/src/index.ts +29 -42
  95. package/src/lib/__tests__/excerpt.test.ts +125 -0
  96. package/src/lib/__tests__/schemas.test.ts +201 -99
  97. package/src/lib/__tests__/time.test.ts +62 -0
  98. package/src/{routes/api → lib}/__tests__/timeline.test.ts +81 -75
  99. package/src/lib/__tests__/view.test.ts +204 -50
  100. package/src/lib/constants.ts +2 -4
  101. package/src/lib/excerpt.ts +87 -0
  102. package/src/lib/feed.ts +22 -7
  103. package/src/lib/nav-reorder.ts +1 -1
  104. package/src/lib/navigation.ts +45 -8
  105. package/src/lib/pagination.ts +50 -0
  106. package/src/lib/render.tsx +7 -14
  107. package/src/lib/schemas.ts +119 -51
  108. package/src/lib/theme.ts +5 -5
  109. package/src/lib/time.ts +64 -0
  110. package/src/lib/timeline.ts +141 -0
  111. package/src/lib/view.ts +80 -82
  112. package/src/preset.css +46 -0
  113. package/src/routes/__tests__/compose.test.ts +199 -0
  114. package/src/routes/api/__tests__/collections.test.ts +249 -0
  115. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  116. package/src/routes/api/__tests__/pages.test.ts +218 -0
  117. package/src/routes/api/__tests__/posts.test.ts +50 -108
  118. package/src/routes/api/__tests__/search.test.ts +2 -3
  119. package/src/routes/api/__tests__/settings.test.ts +132 -0
  120. package/src/routes/api/collections.ts +143 -0
  121. package/src/routes/api/nav-items.ts +115 -0
  122. package/src/routes/api/pages.ts +101 -0
  123. package/src/routes/api/posts.ts +28 -28
  124. package/src/routes/api/search.ts +3 -3
  125. package/src/routes/api/settings.ts +91 -0
  126. package/src/routes/api/upload.ts +16 -6
  127. package/src/routes/compose.ts +63 -0
  128. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  129. package/src/routes/dash/collections.tsx +20 -42
  130. package/src/routes/dash/index.tsx +3 -3
  131. package/src/routes/dash/media.tsx +2 -2
  132. package/src/routes/dash/pages.tsx +480 -122
  133. package/src/routes/dash/posts.tsx +42 -54
  134. package/src/routes/dash/redirects.tsx +2 -2
  135. package/src/routes/dash/settings.tsx +83 -5
  136. package/src/routes/feed/rss.ts +4 -3
  137. package/src/routes/feed/sitemap.ts +15 -5
  138. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  139. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  140. package/src/routes/pages/archive.tsx +15 -15
  141. package/src/routes/pages/collection.tsx +16 -9
  142. package/src/routes/pages/collections.tsx +36 -0
  143. package/src/routes/pages/featured.tsx +38 -0
  144. package/src/routes/pages/home.tsx +21 -92
  145. package/src/routes/pages/page.tsx +62 -27
  146. package/src/routes/pages/post.tsx +6 -18
  147. package/src/routes/pages/search.tsx +3 -7
  148. package/src/services/__tests__/collection.test.ts +257 -158
  149. package/src/services/__tests__/media.test.ts +18 -18
  150. package/src/services/__tests__/navigation.test.ts +161 -87
  151. package/src/services/__tests__/page.test.ts +106 -0
  152. package/src/services/__tests__/post-timeline.test.ts +92 -88
  153. package/src/services/__tests__/post.test.ts +432 -197
  154. package/src/services/__tests__/search.test.ts +19 -25
  155. package/src/services/collection.ts +71 -113
  156. package/src/services/index.ts +9 -8
  157. package/src/services/navigation.ts +38 -71
  158. package/src/services/page.ts +136 -0
  159. package/src/services/post.ts +141 -101
  160. package/src/services/search.ts +38 -27
  161. package/src/styles/tokens.css +47 -0
  162. package/src/styles/ui.css +491 -0
  163. package/src/types.ts +212 -198
  164. package/src/ui/compose/ComposeDialog.tsx +395 -0
  165. package/src/ui/compose/ComposePrompt.tsx +55 -0
  166. package/src/ui/dash/FormatBadge.tsx +28 -0
  167. package/src/{theme/components → ui/dash}/PageForm.tsx +21 -21
  168. package/src/{theme/components → ui/dash}/PostForm.tsx +110 -131
  169. package/src/ui/dash/PostList.tsx +101 -0
  170. package/src/ui/dash/StatusBadge.tsx +61 -0
  171. package/src/ui/dash/index.ts +10 -0
  172. package/src/ui/feed/LinkCard.tsx +72 -0
  173. package/src/ui/feed/NoteCard.tsx +63 -0
  174. package/src/ui/feed/QuoteCard.tsx +68 -0
  175. package/src/ui/feed/ThreadPreview.tsx +48 -0
  176. package/src/ui/feed/TimelineFeed.tsx +49 -0
  177. package/src/ui/feed/TimelineItem.tsx +45 -0
  178. package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
  179. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  180. package/src/ui/layouts/SiteLayout.tsx +150 -0
  181. package/src/ui/pages/ArchivePage.tsx +162 -0
  182. package/src/ui/pages/CollectionPage.tsx +70 -0
  183. package/src/ui/pages/CollectionsPage.tsx +73 -0
  184. package/src/ui/pages/FeaturedPage.tsx +31 -0
  185. package/src/ui/pages/HomePage.tsx +37 -0
  186. package/src/ui/pages/PostPage.tsx +56 -0
  187. package/src/{themes/minimal → ui}/pages/SearchPage.tsx +24 -20
  188. package/src/{themes/minimal → ui}/pages/SinglePage.tsx +5 -5
  189. package/src/ui/shared/MediaGallery.tsx +59 -0
  190. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  191. package/src/{theme/components → ui/shared}/ThreadView.tsx +6 -3
  192. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  193. package/src/ui/shared/index.ts +12 -0
  194. package/bin/jant.js +0 -185
  195. package/dist/lib/theme-components.js +0 -49
  196. package/dist/routes/api/timeline.js +0 -120
  197. package/dist/routes/dash/navigation.js +0 -288
  198. package/dist/theme/components/MediaGallery.js +0 -107
  199. package/dist/theme/components/VisibilityBadge.js +0 -37
  200. package/dist/theme/index.js +0 -18
  201. package/dist/theme/layouts/index.js +0 -2
  202. package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
  203. package/dist/themes/minimal/index.js +0 -65
  204. package/dist/themes/minimal/pages/CollectionPage.js +0 -65
  205. package/dist/themes/minimal/pages/HomePage.js +0 -25
  206. package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
  207. package/dist/themes/minimal/timeline/ImageCard.js +0 -67
  208. package/dist/themes/minimal/timeline/LinkCard.js +0 -47
  209. package/dist/themes/minimal/timeline/NoteCard.js +0 -34
  210. package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
  211. package/dist/themes/minimal/timeline/TimelineItem.js +0 -44
  212. package/src/lib/__tests__/theme-components.test.ts +0 -126
  213. package/src/lib/theme-components.ts +0 -68
  214. package/src/routes/api/timeline.tsx +0 -159
  215. package/src/routes/dash/navigation.tsx +0 -316
  216. package/src/theme/components/MediaGallery.tsx +0 -128
  217. package/src/theme/components/PostList.tsx +0 -92
  218. package/src/theme/components/TypeBadge.tsx +0 -37
  219. package/src/theme/components/VisibilityBadge.tsx +0 -45
  220. package/src/theme/components/index.ts +0 -23
  221. package/src/theme/index.ts +0 -22
  222. package/src/theme/layouts/index.ts +0 -7
  223. package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
  224. package/src/themes/minimal/index.ts +0 -83
  225. package/src/themes/minimal/pages/ArchivePage.tsx +0 -157
  226. package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
  227. package/src/themes/minimal/pages/HomePage.tsx +0 -41
  228. package/src/themes/minimal/pages/PostPage.tsx +0 -43
  229. package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
  230. package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
  231. package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
  232. package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
  233. package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
  234. package/src/themes/minimal/timeline/ThreadPreview.tsx +0 -47
  235. package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
  236. package/src/themes/minimal/timeline/TimelineItem.tsx +0 -75
  237. /package/dist/{theme → ui}/color-themes.js +0 -0
  238. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  239. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  240. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  241. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  242. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  243. /package/src/{theme → ui}/color-themes.ts +0 -0
  244. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  245. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  246. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  247. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  248. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * Archive Page Route
3
3
  *
4
- * Shows all posts, optionally filtered by type
4
+ * Shows all posts, optionally filtered by format or featured status
5
5
  */
6
6
 
7
7
  import { Hono } from "hono";
8
- import type { Bindings, PostType } from "../../types.js";
8
+ import type { Bindings, Format } from "../../types.js";
9
9
  import type { AppVariables } from "../../app.js";
10
- import { POST_TYPES } from "../../types.js";
11
- import { ArchivePage as DefaultArchivePage } from "../../themes/minimal/pages/ArchivePage.js";
10
+ import { FORMATS } from "../../types.js";
11
+ import { ArchivePage } from "../../ui/pages/ArchivePage.js";
12
12
  import { getNavigationData } from "../../lib/navigation.js";
13
13
  import { renderPublicPage } from "../../lib/render.js";
14
14
  import { createMediaContext, toArchiveGroups } from "../../lib/view.js";
@@ -21,9 +21,11 @@ export const archiveRoutes = new Hono<Env>();
21
21
 
22
22
  // Archive page - all posts
23
23
  archiveRoutes.get("/", async (c) => {
24
- const typeParam = c.req.query("type") as PostType | undefined;
25
- const type =
26
- typeParam && POST_TYPES.includes(typeParam) ? typeParam : undefined;
24
+ const formatParam = c.req.query("format") as Format | undefined;
25
+ const format =
26
+ formatParam && FORMATS.includes(formatParam) ? formatParam : undefined;
27
+ const featuredParam = c.req.query("featured");
28
+ const featured = featuredParam === "true" ? true : undefined;
27
29
 
28
30
  // Parse cursor
29
31
  const cursorParam = c.req.query("cursor");
@@ -33,8 +35,9 @@ archiveRoutes.get("/", async (c) => {
33
35
 
34
36
  // Fetch one extra to check for more
35
37
  const posts = await c.var.services.posts.list({
36
- type,
37
- visibility: ["featured", "quiet"],
38
+ format,
39
+ status: "published",
40
+ featured,
38
41
  excludeReplies: true,
39
42
  cursor,
40
43
  limit: PAGE_SIZE + 1,
@@ -66,19 +69,16 @@ archiveRoutes.get("/", async (c) => {
66
69
  const mediaCtx = createMediaContext(c);
67
70
  const groups = toArchiveGroups(grouped, mediaCtx);
68
71
 
69
- const components = c.var.config.theme?.components;
70
- const Page = components?.ArchivePage ?? DefaultArchivePage;
71
-
72
72
  return renderPublicPage(c, {
73
73
  title: `Archive - ${navData.siteName}`,
74
74
  navData,
75
75
  content: (
76
- <Page
76
+ <ArchivePage
77
77
  groups={groups}
78
78
  hasMore={hasMore}
79
79
  nextCursor={nextCursor}
80
- type={type}
81
- theme={components}
80
+ format={format}
81
+ featured={featured}
82
82
  />
83
83
  ),
84
84
  });
@@ -5,7 +5,7 @@
5
5
  import { Hono } from "hono";
6
6
  import type { Bindings } from "../../types.js";
7
7
  import type { AppVariables } from "../../app.js";
8
- import { CollectionPage as DefaultCollectionPage } from "../../themes/minimal/pages/CollectionPage.js";
8
+ import { CollectionPage } from "../../ui/pages/CollectionPage.js";
9
9
  import { getNavigationData } from "../../lib/navigation.js";
10
10
  import { renderPublicPage } from "../../lib/render.js";
11
11
  import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
@@ -14,28 +14,35 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
14
14
 
15
15
  export const collectionRoutes = new Hono<Env>();
16
16
 
17
- collectionRoutes.get("/:path", async (c) => {
18
- const path = c.req.param("path");
17
+ collectionRoutes.get("/:slug", async (c) => {
18
+ const slug = c.req.param("slug");
19
19
 
20
- const collection = await c.var.services.collections.getByPath(path);
20
+ const collection = await c.var.services.collections.getBySlug(slug);
21
21
  if (!collection) return c.notFound();
22
22
 
23
- const posts = await c.var.services.collections.getPosts(collection.id);
23
+ // Fetch posts in this collection
24
+ const posts = await c.var.services.posts.list({
25
+ collectionId: collection.id,
26
+ status: "published",
27
+ excludeReplies: true,
28
+ });
29
+
24
30
  const navData = await getNavigationData(c);
25
31
 
26
32
  // Transform to View Models
27
33
  const mediaCtx = createMediaContext(c);
28
34
  const postViews = toPostViewsFromPosts(posts, mediaCtx);
29
35
 
30
- const components = c.var.config.theme?.components;
31
- const Page = components?.CollectionPage ?? DefaultCollectionPage;
32
-
33
36
  return renderPublicPage(c, {
34
37
  title: `${collection.title} - ${navData.siteName}`,
35
38
  description: collection.description ?? undefined,
36
39
  navData,
37
40
  content: (
38
- <Page collection={collection} posts={postViews} theme={components} />
41
+ <CollectionPage
42
+ collection={collection}
43
+ posts={postViews}
44
+ hasMore={false}
45
+ />
39
46
  ),
40
47
  });
41
48
  });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Collections Listing Page Route
3
+ *
4
+ * Lists all collections with their post counts.
5
+ */
6
+
7
+ import { Hono } from "hono";
8
+ import type { Bindings } from "../../types.js";
9
+ import type { AppVariables } from "../../app.js";
10
+ import { getNavigationData } from "../../lib/navigation.js";
11
+ import { renderPublicPage } from "../../lib/render.js";
12
+ import { CollectionsPage } from "../../ui/pages/CollectionsPage.js";
13
+
14
+ type Env = { Bindings: Bindings; Variables: AppVariables };
15
+
16
+ export const collectionsPageRoutes = new Hono<Env>();
17
+
18
+ collectionsPageRoutes.get("/", async (c) => {
19
+ const [allCollections, postCounts] = await Promise.all([
20
+ c.var.services.collections.list(),
21
+ c.var.services.collections.getPostCounts(),
22
+ ]);
23
+
24
+ const collections = allCollections.map((col) => ({
25
+ ...col,
26
+ postCount: postCounts.get(col.id) ?? 0,
27
+ }));
28
+
29
+ const navData = await getNavigationData(c);
30
+
31
+ return renderPublicPage(c, {
32
+ title: `Collections - ${navData.siteName}`,
33
+ navData,
34
+ content: <CollectionsPage collections={collections} />,
35
+ });
36
+ });
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Featured Page Route
3
+ *
4
+ * Shows featured posts as a timeline feed.
5
+ */
6
+
7
+ import { Hono } from "hono";
8
+ import type { Bindings } from "../../types.js";
9
+ import type { AppVariables } from "../../app.js";
10
+ import { getNavigationData } from "../../lib/navigation.js";
11
+ import { renderPublicPage } from "../../lib/render.js";
12
+ import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
13
+ import { FeaturedPage } from "../../ui/pages/FeaturedPage.js";
14
+
15
+ type Env = { Bindings: Bindings; Variables: AppVariables };
16
+
17
+ export const featuredRoutes = new Hono<Env>();
18
+
19
+ featuredRoutes.get("/", async (c) => {
20
+ const posts = await c.var.services.posts.list({
21
+ featured: true,
22
+ status: "published",
23
+ excludeReplies: true,
24
+ });
25
+
26
+ const navData = await getNavigationData(c);
27
+ const mediaCtx = createMediaContext(c);
28
+ const postViews = toPostViewsFromPosts(posts, mediaCtx);
29
+
30
+ // Convert to timeline items (simple — no thread previews)
31
+ const items = postViews.map((post) => ({ post }));
32
+
33
+ return renderPublicPage(c, {
34
+ title: `Featured - ${navData.siteName}`,
35
+ navData,
36
+ content: <FeaturedPage items={items} />,
37
+ });
38
+ });
@@ -2,121 +2,50 @@
2
2
  * Home Page Route
3
3
  *
4
4
  * Timeline feed with per-type card components and thread previews.
5
+ * Uses page-based pagination.
5
6
  */
6
7
 
7
8
  import { Hono } from "hono";
8
- import type { Bindings, TimelineItemView } from "../../types.js";
9
+ import type { Bindings } from "../../types.js";
9
10
  import type { AppVariables } from "../../app.js";
10
- import { buildMediaMap } from "../../lib/media-helpers.js";
11
11
  import { getNavigationData } from "../../lib/navigation.js";
12
12
  import { renderPublicPage } from "../../lib/render.js";
13
- import { HomePage as DefaultHomePage } from "../../themes/minimal/pages/HomePage.js";
14
- import { createMediaContext, toPostView, toPostViews } from "../../lib/view.js";
13
+ import { assembleTimeline } from "../../lib/timeline.js";
14
+ import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
15
+ import { HomePage } from "../../ui/pages/HomePage.js";
15
16
 
16
17
  type Env = { Bindings: Bindings; Variables: AppVariables };
17
18
 
18
- const PAGE_SIZE = 20;
19
-
20
19
  export const homeRoutes = new Hono<Env>();
21
20
 
22
21
  homeRoutes.get("/", async (c) => {
23
- const navData = await getNavigationData(c);
22
+ const pageParam = c.req.query("page");
23
+ const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
24
24
 
25
- // Fetch one extra to determine if there are more
26
- const posts = await c.var.services.posts.list({
27
- visibility: ["featured", "quiet"],
28
- excludeReplies: true,
29
- excludeTypes: ["page"],
30
- limit: PAGE_SIZE + 1,
25
+ const { items, currentPage, totalPages } = await assembleTimeline(c, {
26
+ page,
31
27
  });
32
28
 
33
- const hasMore = posts.length > PAGE_SIZE;
34
- const displayPosts = hasMore ? posts.slice(0, PAGE_SIZE) : posts;
35
-
36
- // Batch load media attachments
37
- const postIds = displayPosts.map((p) => p.id);
38
- const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
39
- const mediaCtx = createMediaContext(c);
40
- const mediaMap = buildMediaMap(
41
- rawMediaMap,
42
- mediaCtx.r2PublicUrl,
43
- mediaCtx.imageTransformUrl,
44
- mediaCtx.s3PublicUrl,
45
- );
46
-
47
- // Get reply counts to identify thread roots
48
- const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
49
- const threadRootIds = postIds.filter((id) => (replyCounts.get(id) ?? 0) > 0);
50
-
51
- // Batch load thread previews
52
- const threadPreviews = await c.var.services.posts.getThreadPreviews(
53
- threadRootIds,
54
- 3,
55
- );
56
-
57
- // Batch load media for preview replies
58
- const previewReplyIds: number[] = [];
59
- for (const replies of threadPreviews.values()) {
60
- for (const reply of replies) {
61
- previewReplyIds.push(reply.id);
62
- }
63
- }
64
- const previewMediaMap =
65
- previewReplyIds.length > 0
66
- ? buildMediaMap(
67
- await c.var.services.media.getByPostIds(previewReplyIds),
68
- mediaCtx.r2PublicUrl,
69
- mediaCtx.imageTransformUrl,
70
- mediaCtx.s3PublicUrl,
71
- )
72
- : new Map();
73
-
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
- );
80
-
81
- const replyCount = replyCounts.get(post.id) ?? 0;
82
- const previewReplies = threadPreviews.get(post.id);
83
-
84
- if (replyCount > 0 && previewReplies) {
85
- return {
86
- post: postView,
87
- threadPreview: {
88
- replies: toPostViews(
89
- previewReplies.map((r) => ({
90
- ...r,
91
- mediaAttachments: previewMediaMap.get(r.id) ?? [],
92
- })),
93
- mediaCtx,
94
- ),
95
- totalReplyCount: replyCount,
96
- },
97
- };
98
- }
29
+ const navData = await getNavigationData(c);
99
30
 
100
- return { post: postView };
31
+ // Fetch pinned posts
32
+ const pinnedPosts = await c.var.services.posts.list({
33
+ pinned: true,
34
+ status: "published",
35
+ excludeReplies: true,
101
36
  });
102
-
103
- // Determine next cursor
104
- const lastPost = displayPosts[displayPosts.length - 1];
105
- const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
106
-
107
- // Resolve page component
108
- const components = c.var.config.theme?.components;
109
- const Page = components?.HomePage ?? DefaultHomePage;
37
+ const mediaCtx = createMediaContext(c);
38
+ const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
110
39
 
111
40
  return renderPublicPage(c, {
112
41
  title: navData.siteName,
113
42
  navData,
114
43
  content: (
115
- <Page
44
+ <HomePage
116
45
  items={items}
117
- hasMore={hasMore}
118
- nextCursor={nextCursor}
119
- theme={components}
46
+ pinnedItems={pinnedItems}
47
+ currentPage={currentPage}
48
+ totalPages={totalPages}
120
49
  />
121
50
  ),
122
51
  });
@@ -1,51 +1,86 @@
1
1
  /**
2
2
  * Custom Page Route
3
3
  *
4
- * Catch-all route for custom pages accessible via their path field
4
+ * Serves pages from the pages table and posts with custom paths.
5
+ * This is a catch-all route mounted at "/" - must be registered last.
6
+ * Supports multi-level paths (e.g. /2024/my-post) for posts.
5
7
  */
6
8
 
7
9
  import { Hono } from "hono";
8
10
  import type { Bindings } from "../../types.js";
9
11
  import type { AppVariables } from "../../app.js";
10
- import { SinglePage as DefaultSinglePage } from "../../themes/minimal/pages/SinglePage.js";
12
+ import { SinglePage } from "../../ui/pages/SinglePage.js";
13
+ import { PostPage } from "../../ui/pages/PostPage.js";
11
14
  import { getNavigationData } from "../../lib/navigation.js";
12
15
  import { renderPublicPage } from "../../lib/render.js";
13
- import { createMediaContext, toPostViewFromPost } from "../../lib/view.js";
16
+ import { buildMediaMap } from "../../lib/media-helpers.js";
17
+ import { createMediaContext, toPageView, toPostView } from "../../lib/view.js";
14
18
 
15
19
  type Env = { Bindings: Bindings; Variables: AppVariables };
16
20
 
17
21
  export const pageRoutes = new Hono<Env>();
18
22
 
19
- // Catch-all for custom page paths
20
- pageRoutes.get("/:path", async (c) => {
21
- const path = c.req.param("path");
23
+ // Catch-all for custom page slugs and post paths (including multi-level)
24
+ pageRoutes.get("/*", async (c) => {
25
+ const fullPath = c.req.path.slice(1); // Remove leading /
26
+ if (!fullPath) return c.notFound();
22
27
 
23
- // Look up page by path
24
- const page = await c.var.services.posts.getByPath(path);
28
+ const isMultiSegment = fullPath.includes("/");
25
29
 
26
- // Not found or not a page
27
- if (!page || page.type !== "page") {
28
- return c.notFound();
29
- }
30
+ // Pages only have single-level slugs; skip page lookup for multi-segment paths
31
+ if (!isMultiSegment) {
32
+ const page = await c.var.services.pages.getBySlug(fullPath);
33
+
34
+ if (page) {
35
+ if (page.status === "draft") {
36
+ return c.notFound();
37
+ }
30
38
 
31
- // Don't show drafts
32
- if (page.visibility === "draft") {
33
- return c.notFound();
39
+ const navData = await getNavigationData(c);
40
+ const pageView = toPageView(page);
41
+
42
+ return renderPublicPage(c, {
43
+ title: `${page.title || fullPath} - ${navData.siteName}`,
44
+ description: page.body?.slice(0, 160),
45
+ navData,
46
+ content: <SinglePage page={pageView} />,
47
+ });
48
+ }
34
49
  }
35
50
 
36
- const navData = await getNavigationData(c);
51
+ // Posts support multi-level paths
52
+ const post = await c.var.services.posts.getByPath(fullPath);
53
+
54
+ if (post) {
55
+ if (post.status === "draft") {
56
+ return c.notFound();
57
+ }
37
58
 
38
- // Transform to View Model
39
- const mediaCtx = createMediaContext(c);
40
- const pageView = toPostViewFromPost(page, mediaCtx);
59
+ // Load media attachments
60
+ const rawMediaMap = await c.var.services.media.getByPostIds([post.id]);
61
+ const mediaCtx = createMediaContext(c);
62
+ const mediaMap = buildMediaMap(
63
+ rawMediaMap,
64
+ mediaCtx.r2PublicUrl,
65
+ mediaCtx.imageTransformUrl,
66
+ mediaCtx.s3PublicUrl,
67
+ );
41
68
 
42
- const components = c.var.config.theme?.components;
43
- const Page = components?.SinglePage ?? DefaultSinglePage;
69
+ const postView = toPostView(
70
+ { ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
71
+ mediaCtx,
72
+ );
73
+
74
+ const navData = await getNavigationData(c);
75
+ const title = post.title || navData.siteName;
76
+
77
+ return renderPublicPage(c, {
78
+ title,
79
+ description: post.body?.slice(0, 160),
80
+ navData,
81
+ content: <PostPage post={postView} />,
82
+ });
83
+ }
44
84
 
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
- });
85
+ return c.notFound();
51
86
  });
@@ -5,7 +5,7 @@
5
5
  import { Hono } from "hono";
6
6
  import type { Bindings } from "../../types.js";
7
7
  import type { AppVariables } from "../../app.js";
8
- import { PostPage as DefaultPostPage } from "../../themes/minimal/pages/PostPage.js";
8
+ import { PostPage } from "../../ui/pages/PostPage.js";
9
9
  import * as sqid from "../../lib/sqid.js";
10
10
  import { getNavigationData } from "../../lib/navigation.js";
11
11
  import { renderPublicPage } from "../../lib/render.js";
@@ -19,24 +19,15 @@ export const postRoutes = new Hono<Env>();
19
19
  postRoutes.get("/:id", async (c) => {
20
20
  const paramId = c.req.param("id");
21
21
 
22
- // Try to decode as sqid first
23
- let id = sqid.decode(paramId);
24
-
25
- // If not a valid sqid, try to find by path
26
- if (!id) {
27
- const post = await c.var.services.posts.getByPath(paramId);
28
- if (post) {
29
- id = post.id;
30
- }
31
- }
32
-
22
+ // Decode sqid to numeric ID
23
+ const id = sqid.decode(paramId);
33
24
  if (!id) return c.notFound();
34
25
 
35
26
  const post = await c.var.services.posts.getById(id);
36
27
  if (!post) return c.notFound();
37
28
 
38
29
  // Don't show drafts on public site
39
- if (post.visibility === "draft") {
30
+ if (post.status === "draft") {
40
31
  return c.notFound();
41
32
  }
42
33
 
@@ -59,13 +50,10 @@ postRoutes.get("/:id", async (c) => {
59
50
  const navData = await getNavigationData(c);
60
51
  const title = post.title || navData.siteName;
61
52
 
62
- const components = c.var.config.theme?.components;
63
- const Page = components?.PostPage ?? DefaultPostPage;
64
-
65
53
  return renderPublicPage(c, {
66
54
  title,
67
- description: post.content?.slice(0, 160),
55
+ description: post.body?.slice(0, 160),
68
56
  navData,
69
- content: <Page post={postView} theme={components} />,
57
+ content: <PostPage post={postView} />,
70
58
  });
71
59
  });
@@ -5,7 +5,7 @@
5
5
  import { Hono } from "hono";
6
6
  import type { Bindings, SearchResult } from "../../types.js";
7
7
  import type { AppVariables } from "../../app.js";
8
- import { SearchPage as DefaultSearchPage } from "../../themes/minimal/pages/SearchPage.js";
8
+ import { SearchPage } from "../../ui/pages/SearchPage.js";
9
9
  import { getNavigationData } from "../../lib/navigation.js";
10
10
  import { renderPublicPage } from "../../lib/render.js";
11
11
  import { createMediaContext, toSearchResultViews } from "../../lib/view.js";
@@ -34,7 +34,7 @@ searchRoutes.get("/", async (c) => {
34
34
  results = await c.var.services.search.search(query, {
35
35
  limit: PAGE_SIZE + 1,
36
36
  offset: (page - 1) * PAGE_SIZE,
37
- visibility: ["featured", "quiet"],
37
+ status: ["published"],
38
38
  });
39
39
 
40
40
  hasMore = results.length > PAGE_SIZE;
@@ -52,22 +52,18 @@ searchRoutes.get("/", async (c) => {
52
52
  const mediaCtx = createMediaContext(c);
53
53
  const resultViews = toSearchResultViews(results, mediaCtx);
54
54
 
55
- const components = c.var.config.theme?.components;
56
- const Page = components?.SearchPage ?? DefaultSearchPage;
57
-
58
55
  return renderPublicPage(c, {
59
56
  title: query
60
57
  ? `Search: ${query} - ${navData.siteName}`
61
58
  : `Search - ${navData.siteName}`,
62
59
  navData,
63
60
  content: (
64
- <Page
61
+ <SearchPage
65
62
  query={query}
66
63
  results={resultViews}
67
64
  error={error}
68
65
  hasMore={hasMore}
69
66
  page={page}
70
- theme={components}
71
67
  />
72
68
  ),
73
69
  });