@jant/core 0.3.24 → 0.3.26

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 (277) hide show
  1. package/dist/app.js +101 -571
  2. package/dist/client.js +1 -0
  3. package/dist/db/schema.js +1 -1
  4. package/dist/i18n/locales/en.js +1 -1
  5. package/dist/i18n/locales/zh-Hans.js +1 -1
  6. package/dist/i18n/locales/zh-Hant.js +1 -1
  7. package/dist/index.js +3 -9
  8. package/dist/lib/avatar-upload.js +134 -0
  9. package/dist/lib/config.js +39 -0
  10. package/dist/lib/constants.js +10 -9
  11. package/dist/lib/favicon.js +102 -0
  12. package/dist/lib/image.js +13 -17
  13. package/dist/lib/media-helpers.js +2 -2
  14. package/dist/lib/nav-reorder.js +1 -1
  15. package/dist/lib/navigation.js +48 -3
  16. package/dist/lib/pagination.js +44 -0
  17. package/dist/lib/render.js +16 -11
  18. package/dist/lib/schemas.js +34 -3
  19. package/dist/lib/theme.js +4 -4
  20. package/dist/lib/timeline.js +24 -48
  21. package/dist/lib/timezones.js +388 -0
  22. package/dist/lib/view.js +3 -3
  23. package/dist/routes/api/collections.js +124 -0
  24. package/dist/routes/api/nav-items.js +104 -0
  25. package/dist/routes/api/pages.js +91 -0
  26. package/dist/routes/api/posts.js +3 -3
  27. package/dist/routes/api/search.js +2 -2
  28. package/dist/routes/api/settings.js +68 -0
  29. package/dist/routes/api/upload.js +3 -3
  30. package/dist/routes/auth/reset.js +221 -0
  31. package/dist/routes/auth/setup.js +194 -0
  32. package/dist/routes/auth/signin.js +176 -0
  33. package/dist/routes/compose.js +48 -0
  34. package/dist/routes/dash/collections.js +24 -416
  35. package/dist/routes/dash/index.js +1 -1
  36. package/dist/routes/dash/media.js +13 -393
  37. package/dist/routes/dash/pages.js +112 -86
  38. package/dist/routes/dash/posts.js +3 -5
  39. package/dist/routes/dash/redirects.js +20 -14
  40. package/dist/routes/dash/settings.js +213 -518
  41. package/dist/routes/feed/rss.js +4 -3
  42. package/dist/routes/feed/sitemap.js +5 -3
  43. package/dist/routes/pages/archive.js +3 -6
  44. package/dist/routes/pages/collection.js +3 -6
  45. package/dist/routes/pages/collections.js +28 -0
  46. package/dist/routes/pages/featured.js +36 -0
  47. package/dist/routes/pages/home.js +33 -49
  48. package/dist/routes/pages/latest.js +45 -0
  49. package/dist/routes/pages/page.js +29 -32
  50. package/dist/routes/pages/post.js +3 -6
  51. package/dist/routes/pages/search.js +3 -6
  52. package/dist/services/page.js +5 -1
  53. package/dist/services/post.js +45 -31
  54. package/dist/services/search.js +1 -1
  55. package/dist/types/bindings.js +3 -0
  56. package/dist/types/config.js +147 -0
  57. package/dist/types/constants.js +27 -0
  58. package/dist/types/entities.js +3 -0
  59. package/dist/types/operations.js +3 -0
  60. package/dist/types/props.js +3 -0
  61. package/dist/types/views.js +5 -0
  62. package/dist/types.js +8 -111
  63. package/dist/{theme → ui}/color-themes.js +33 -33
  64. package/dist/ui/compose/ComposeDialog.js +467 -0
  65. package/dist/ui/compose/ComposePrompt.js +55 -0
  66. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
  67. package/dist/{theme/components → ui/dash}/PageForm.js +21 -15
  68. package/dist/{theme/components → ui/dash}/PostForm.js +22 -43
  69. package/dist/{theme/components → ui/dash}/PostList.js +6 -6
  70. package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
  71. package/dist/ui/dash/collections/CollectionForm.js +152 -0
  72. package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
  73. package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
  74. package/dist/{theme/components → ui/dash}/index.js +3 -6
  75. package/dist/ui/dash/media/MediaListContent.js +166 -0
  76. package/dist/ui/dash/media/ViewMediaContent.js +212 -0
  77. package/dist/ui/dash/pages/LinkFormContent.js +130 -0
  78. package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
  79. package/dist/ui/dash/settings/AccountContent.js +209 -0
  80. package/dist/ui/dash/settings/AppearanceContent.js +259 -0
  81. package/dist/ui/dash/settings/GeneralContent.js +536 -0
  82. package/dist/ui/dash/settings/SettingsNav.js +41 -0
  83. package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
  84. package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
  85. package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
  86. package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
  87. package/dist/ui/feed/TimelineFeed.js +41 -0
  88. package/dist/ui/feed/TimelineItem.js +27 -0
  89. package/dist/ui/font-themes.js +36 -0
  90. package/dist/{theme → ui}/layouts/BaseLayout.js +34 -2
  91. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  92. package/dist/ui/layouts/SiteLayout.js +169 -0
  93. package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
  94. package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
  95. package/dist/ui/pages/CollectionsPage.js +76 -0
  96. package/dist/ui/pages/FeaturedPage.js +24 -0
  97. package/dist/ui/pages/HomePage.js +24 -0
  98. package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
  99. package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
  100. package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
  101. package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
  102. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  103. package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
  104. package/dist/ui/shared/index.js +5 -0
  105. package/package.json +1 -9
  106. package/src/__tests__/helpers/db.ts +3 -0
  107. package/src/app.tsx +131 -561
  108. package/src/client.ts +1 -0
  109. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  110. package/src/db/migrations/meta/_journal.json +7 -0
  111. package/src/db/schema.ts +1 -1
  112. package/src/i18n/locales/en.po +477 -261
  113. package/src/i18n/locales/en.ts +1 -1
  114. package/src/i18n/locales/zh-Hans.po +477 -261
  115. package/src/i18n/locales/zh-Hans.ts +1 -1
  116. package/src/i18n/locales/zh-Hant.po +477 -261
  117. package/src/i18n/locales/zh-Hant.ts +1 -1
  118. package/src/index.ts +7 -36
  119. package/src/lib/__tests__/config.test.ts +192 -0
  120. package/src/lib/__tests__/favicon.test.ts +151 -0
  121. package/src/lib/__tests__/image.test.ts +2 -6
  122. package/src/lib/__tests__/schemas.test.ts +60 -19
  123. package/src/lib/__tests__/timeline.test.ts +45 -81
  124. package/src/lib/__tests__/timezones.test.ts +61 -0
  125. package/src/lib/__tests__/view.test.ts +15 -9
  126. package/src/lib/avatar-upload.ts +165 -0
  127. package/src/lib/config.ts +47 -0
  128. package/src/lib/constants.ts +19 -10
  129. package/src/lib/favicon.ts +115 -0
  130. package/src/lib/image.ts +13 -21
  131. package/src/lib/media-helpers.ts +2 -2
  132. package/src/lib/nav-reorder.ts +1 -1
  133. package/src/lib/navigation.ts +73 -4
  134. package/src/lib/pagination.ts +50 -0
  135. package/src/lib/render.tsx +22 -15
  136. package/src/lib/schemas.ts +47 -6
  137. package/src/lib/theme.ts +5 -5
  138. package/src/lib/timeline.ts +28 -57
  139. package/src/lib/timezones.ts +325 -0
  140. package/src/lib/view.ts +3 -3
  141. package/src/preset.css +2 -1
  142. package/src/routes/__tests__/compose.test.ts +199 -0
  143. package/src/routes/api/__tests__/collections.test.ts +249 -0
  144. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  145. package/src/routes/api/__tests__/pages.test.ts +218 -0
  146. package/src/routes/api/__tests__/settings.test.ts +132 -0
  147. package/src/routes/api/collections.ts +143 -0
  148. package/src/routes/api/nav-items.ts +115 -0
  149. package/src/routes/api/pages.ts +101 -0
  150. package/src/routes/api/posts.ts +3 -3
  151. package/src/routes/api/search.ts +2 -2
  152. package/src/routes/api/settings.ts +91 -0
  153. package/src/routes/api/upload.ts +2 -3
  154. package/src/routes/auth/reset.tsx +239 -0
  155. package/src/routes/auth/setup.tsx +189 -0
  156. package/src/routes/auth/signin.tsx +163 -0
  157. package/src/routes/compose.ts +63 -0
  158. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  159. package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
  160. package/src/routes/dash/collections.tsx +18 -367
  161. package/src/routes/dash/index.tsx +1 -1
  162. package/src/routes/dash/media.tsx +13 -415
  163. package/src/routes/dash/pages.tsx +131 -98
  164. package/src/routes/dash/posts.tsx +3 -7
  165. package/src/routes/dash/redirects.tsx +22 -16
  166. package/src/routes/dash/settings.tsx +265 -478
  167. package/src/routes/feed/__tests__/rss.test.ts +141 -0
  168. package/src/routes/feed/rss.ts +5 -3
  169. package/src/routes/feed/sitemap.ts +5 -3
  170. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  171. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  172. package/src/routes/pages/archive.tsx +2 -6
  173. package/src/routes/pages/collection.tsx +2 -6
  174. package/src/routes/pages/collections.tsx +36 -0
  175. package/src/routes/pages/featured.tsx +44 -0
  176. package/src/routes/pages/home.tsx +30 -53
  177. package/src/routes/pages/latest.tsx +59 -0
  178. package/src/routes/pages/page.tsx +28 -30
  179. package/src/routes/pages/post.tsx +2 -5
  180. package/src/routes/pages/search.tsx +2 -6
  181. package/src/services/__tests__/page.test.ts +106 -0
  182. package/src/services/__tests__/post.test.ts +114 -15
  183. package/src/services/page.ts +13 -1
  184. package/src/services/post.ts +58 -40
  185. package/src/services/search.ts +2 -2
  186. package/src/styles/components.css +0 -65
  187. package/src/styles/tokens.css +47 -0
  188. package/src/styles/ui.css +475 -0
  189. package/src/types/bindings.ts +30 -0
  190. package/src/types/config.ts +183 -0
  191. package/src/types/constants.ts +26 -0
  192. package/src/types/entities.ts +109 -0
  193. package/src/types/operations.ts +88 -0
  194. package/src/types/props.ts +115 -0
  195. package/src/types/views.ts +172 -0
  196. package/src/types.ts +8 -774
  197. package/src/ui/__tests__/font-themes.test.ts +34 -0
  198. package/src/{theme → ui}/color-themes.ts +34 -34
  199. package/src/ui/compose/ComposeDialog.tsx +414 -0
  200. package/src/ui/compose/ComposePrompt.tsx +55 -0
  201. package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
  202. package/src/{theme/components → ui/dash}/PageForm.tsx +25 -19
  203. package/src/{theme/components → ui/dash}/PostForm.tsx +26 -45
  204. package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
  205. package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
  206. package/src/ui/dash/collections/CollectionForm.tsx +153 -0
  207. package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
  208. package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
  209. package/src/ui/dash/index.ts +10 -0
  210. package/src/ui/dash/media/MediaListContent.tsx +201 -0
  211. package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
  212. package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
  213. package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
  214. package/src/ui/dash/settings/AccountContent.tsx +176 -0
  215. package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
  216. package/src/ui/dash/settings/GeneralContent.tsx +533 -0
  217. package/src/ui/dash/settings/SettingsNav.tsx +56 -0
  218. package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
  219. package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
  220. package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
  221. package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
  222. package/src/ui/feed/TimelineFeed.tsx +49 -0
  223. package/src/ui/feed/TimelineItem.tsx +45 -0
  224. package/src/ui/font-themes.ts +54 -0
  225. package/src/{theme → ui}/layouts/BaseLayout.tsx +28 -1
  226. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  227. package/src/ui/layouts/SiteLayout.tsx +164 -0
  228. package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
  229. package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
  230. package/src/ui/pages/CollectionsPage.tsx +73 -0
  231. package/src/ui/pages/FeaturedPage.tsx +31 -0
  232. package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
  233. package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
  234. package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
  235. package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
  236. package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
  237. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  238. package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
  239. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  240. package/src/ui/shared/index.ts +12 -0
  241. package/bin/jant.js +0 -185
  242. package/dist/lib/theme-components.js +0 -46
  243. package/dist/routes/dash/navigation.js +0 -289
  244. package/dist/theme/index.js +0 -18
  245. package/dist/theme/layouts/index.js +0 -2
  246. package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
  247. package/dist/themes/threads/index.js +0 -81
  248. package/dist/themes/threads/pages/HomePage.js +0 -25
  249. package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
  250. package/dist/themes/threads/timeline/TimelineItem.js +0 -36
  251. package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
  252. package/dist/themes/threads/timeline/groupByDate.js +0 -22
  253. package/dist/themes/threads/timeline/timelineMore.js +0 -107
  254. package/src/lib/__tests__/theme-components.test.ts +0 -105
  255. package/src/lib/theme-components.ts +0 -65
  256. package/src/routes/dash/navigation.tsx +0 -317
  257. package/src/theme/components/index.ts +0 -23
  258. package/src/theme/index.ts +0 -22
  259. package/src/theme/layouts/index.ts +0 -7
  260. package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
  261. package/src/themes/threads/index.ts +0 -100
  262. package/src/themes/threads/style.css +0 -336
  263. package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
  264. package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
  265. package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
  266. package/src/themes/threads/timeline/groupByDate.ts +0 -30
  267. package/src/themes/threads/timeline/timelineMore.tsx +0 -130
  268. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  269. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  270. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  271. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  272. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  273. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  274. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  275. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  276. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  277. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
@@ -14,10 +14,11 @@ export const rssRoutes = new Hono();
14
14
  const siteDescription = all["SITE_DESCRIPTION"] ?? "";
15
15
  const siteUrl = c.env.SITE_URL;
16
16
  const siteLanguage = await getSiteLanguage(c);
17
+ const feedLimit = parseInt(c.env.RSS_FEED_LIMIT ?? "50", 10) || 50;
17
18
  const posts = await c.var.services.posts.list({
18
19
  status: "published",
19
20
  excludeReplies: true,
20
- limit: 50
21
+ limit: feedLimit
21
22
  });
22
23
  // Batch load media for enclosures
23
24
  const postIds = posts.map((p)=>p.id);
@@ -40,7 +41,7 @@ export const rssRoutes = new Hono();
40
41
  // RSS 2.0 Feed - main feed at /feed
41
42
  rssRoutes.get("/", async (c)=>{
42
43
  const feedData = await buildFeedData(c);
43
- const renderer = c.var.config.theme?.feed?.rss ?? defaultRssRenderer;
44
+ const renderer = c.var.config.feed?.rss ?? defaultRssRenderer;
44
45
  const xml = renderer(feedData);
45
46
  return new Response(xml, {
46
47
  headers: {
@@ -51,7 +52,7 @@ rssRoutes.get("/", async (c)=>{
51
52
  // Atom Feed
52
53
  rssRoutes.get("/atom.xml", async (c)=>{
53
54
  const feedData = await buildFeedData(c);
54
- const renderer = c.var.config.theme?.feed?.atom ?? defaultAtomRenderer;
55
+ const renderer = c.var.config.feed?.atom ?? defaultAtomRenderer;
55
56
  const xml = renderer(feedData);
56
57
  return new Response(xml, {
57
58
  headers: {
@@ -19,7 +19,7 @@ sitemapRoutes.get("/sitemap.xml", async (c)=>{
19
19
  const mediaCtx = createMediaContext(c);
20
20
  const postViews = toPostViewsFromPosts(posts, mediaCtx);
21
21
  const pageViews = publishedPages.map(toPageView);
22
- const renderer = c.var.config.theme?.feed?.sitemap ?? defaultSitemapRenderer;
22
+ const renderer = c.var.config.feed?.sitemap ?? defaultSitemapRenderer;
23
23
  const xml = renderer({
24
24
  siteUrl,
25
25
  posts: postViews,
@@ -32,10 +32,12 @@ sitemapRoutes.get("/sitemap.xml", async (c)=>{
32
32
  });
33
33
  });
34
34
  // robots.txt
35
- sitemapRoutes.get("/robots.txt", (c)=>{
35
+ sitemapRoutes.get("/robots.txt", async (c)=>{
36
36
  const siteUrl = c.env.SITE_URL;
37
+ const noindex = await c.var.services.settings.get("NOINDEX") === "true";
38
+ const directive = noindex ? "Disallow: /" : "Allow: /";
37
39
  const robots = `User-agent: *
38
- Allow: /
40
+ ${directive}
39
41
 
40
42
  Sitemap: ${siteUrl}/sitemap.xml
41
43
  `;
@@ -5,7 +5,7 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
5
5
  * Shows all posts, optionally filtered by format or featured status
6
6
  */ import { Hono } from "hono";
7
7
  import { FORMATS } from "../../types.js";
8
- import { ArchivePage as DefaultArchivePage } from "../../themes/threads/pages/ArchivePage.js";
8
+ import { ArchivePage } from "../../ui/pages/ArchivePage.js";
9
9
  import { getNavigationData } from "../../lib/navigation.js";
10
10
  import { renderPublicPage } from "../../lib/render.js";
11
11
  import { createMediaContext, toArchiveGroups } from "../../lib/view.js";
@@ -48,18 +48,15 @@ archiveRoutes.get("/", async (c)=>{
48
48
  // Transform to View Models
49
49
  const mediaCtx = createMediaContext(c);
50
50
  const groups = toArchiveGroups(grouped, mediaCtx);
51
- const components = c.var.config.theme?.components;
52
- const Page = components?.ArchivePage ?? DefaultArchivePage;
53
51
  return renderPublicPage(c, {
54
52
  title: `Archive - ${navData.siteName}`,
55
53
  navData,
56
- content: /*#__PURE__*/ _jsx(Page, {
54
+ content: /*#__PURE__*/ _jsx(ArchivePage, {
57
55
  groups: groups,
58
56
  hasMore: hasMore,
59
57
  nextCursor: nextCursor,
60
58
  format: format,
61
- featured: featured,
62
- theme: components
59
+ featured: featured
63
60
  })
64
61
  });
65
62
  });
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
2
  /**
3
3
  * Collection Page Route
4
4
  */ import { Hono } from "hono";
5
- import { CollectionPage as DefaultCollectionPage } from "../../themes/threads/pages/CollectionPage.js";
5
+ import { CollectionPage } from "../../ui/pages/CollectionPage.js";
6
6
  import { getNavigationData } from "../../lib/navigation.js";
7
7
  import { renderPublicPage } from "../../lib/render.js";
8
8
  import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
@@ -21,17 +21,14 @@ collectionRoutes.get("/:slug", async (c)=>{
21
21
  // Transform to View Models
22
22
  const mediaCtx = createMediaContext(c);
23
23
  const postViews = toPostViewsFromPosts(posts, mediaCtx);
24
- const components = c.var.config.theme?.components;
25
- const Page = components?.CollectionPage ?? DefaultCollectionPage;
26
24
  return renderPublicPage(c, {
27
25
  title: `${collection.title} - ${navData.siteName}`,
28
26
  description: collection.description ?? undefined,
29
27
  navData,
30
- content: /*#__PURE__*/ _jsx(Page, {
28
+ content: /*#__PURE__*/ _jsx(CollectionPage, {
31
29
  collection: collection,
32
30
  posts: postViews,
33
- hasMore: false,
34
- theme: components
31
+ hasMore: false
35
32
  })
36
33
  });
37
34
  });
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
+ /**
3
+ * Collections Listing Page Route
4
+ *
5
+ * Lists all collections with their post counts.
6
+ */ import { Hono } from "hono";
7
+ import { getNavigationData } from "../../lib/navigation.js";
8
+ import { renderPublicPage } from "../../lib/render.js";
9
+ import { CollectionsPage } from "../../ui/pages/CollectionsPage.js";
10
+ export const collectionsPageRoutes = new Hono();
11
+ collectionsPageRoutes.get("/", async (c)=>{
12
+ const [allCollections, postCounts] = await Promise.all([
13
+ c.var.services.collections.list(),
14
+ c.var.services.collections.getPostCounts()
15
+ ]);
16
+ const collections = allCollections.map((col)=>({
17
+ ...col,
18
+ postCount: postCounts.get(col.id) ?? 0
19
+ }));
20
+ const navData = await getNavigationData(c);
21
+ return renderPublicPage(c, {
22
+ title: `Collections - ${navData.siteName}`,
23
+ navData,
24
+ content: /*#__PURE__*/ _jsx(CollectionsPage, {
25
+ collections: collections
26
+ })
27
+ });
28
+ });
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
+ /**
3
+ * Featured Page Route
4
+ *
5
+ * Shows featured posts as a timeline feed.
6
+ */ import { Hono } from "hono";
7
+ import { getNavigationData } from "../../lib/navigation.js";
8
+ import { renderPublicPage } from "../../lib/render.js";
9
+ import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
10
+ import { FeaturedPage } from "../../ui/pages/FeaturedPage.js";
11
+ export const featuredRoutes = new Hono();
12
+ featuredRoutes.get("/", async (c)=>{
13
+ const navData = await getNavigationData(c);
14
+ // When homepage already shows featured, redirect to avoid duplicate content
15
+ if (navData.homeDefaultView === "featured") {
16
+ return c.redirect("/", 302);
17
+ }
18
+ const posts = await c.var.services.posts.list({
19
+ featured: true,
20
+ status: "published",
21
+ excludeReplies: true
22
+ });
23
+ const mediaCtx = createMediaContext(c);
24
+ const postViews = toPostViewsFromPosts(posts, mediaCtx);
25
+ // Convert to timeline items (simple — no thread previews)
26
+ const items = postViews.map((post)=>({
27
+ post
28
+ }));
29
+ return renderPublicPage(c, {
30
+ title: `Featured - ${navData.siteName}`,
31
+ navData,
32
+ content: /*#__PURE__*/ _jsx(FeaturedPage, {
33
+ items: items
34
+ })
35
+ });
36
+ });
@@ -3,59 +3,46 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
3
3
  * Home Page Route
4
4
  *
5
5
  * Timeline feed with per-type card components and thread previews.
6
- * Handles both full-page rendering and load-more SSE responses.
6
+ * Uses page-based pagination.
7
+ *
8
+ * When HOME_DEFAULT_VIEW is "featured", the homepage shows featured posts
9
+ * instead of latest. The /latest route always shows latest posts explicitly.
7
10
  */ import { Hono } from "hono";
8
11
  import { getNavigationData } from "../../lib/navigation.js";
9
12
  import { renderPublicPage } from "../../lib/render.js";
10
13
  import { assembleTimeline } from "../../lib/timeline.js";
11
- import { sse } from "../../lib/sse.js";
12
14
  import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
13
- import { HomePage as DefaultHomePage } from "../../themes/threads/pages/HomePage.js";
15
+ import { HomePage } from "../../ui/pages/HomePage.js";
16
+ import { FeaturedPage } from "../../ui/pages/FeaturedPage.js";
14
17
  export const homeRoutes = new Hono();
15
18
  homeRoutes.get("/", async (c)=>{
16
- const cursorParam = c.req.query("cursor");
17
- const cursor = cursorParam ? parseInt(cursorParam, 10) : undefined;
18
- const lastDate = c.req.query("lastDate");
19
- const { items, hasMore, nextCursor } = await assembleTimeline(c, {
20
- cursor: cursor && !isNaN(cursor) ? cursor : undefined
21
- });
22
- // SSE load-more response
23
- if (cursor && !isNaN(cursor)) {
24
- if (items.length === 0) {
25
- return sse(c, async (stream)=>{
26
- stream.remove("#load-more-container");
27
- });
28
- }
29
- const themeConfig = c.var.config.theme;
30
- const renderMore = themeConfig?.timelineMore;
31
- if (!renderMore) {
32
- // Should never happen — default theme always provides timelineMore
33
- return sse(c, async (stream)=>{
34
- stream.remove("#load-more-container");
35
- });
36
- }
37
- const patches = renderMore({
38
- items,
39
- lastDate: lastDate ?? undefined,
40
- hasMore,
41
- nextCursor,
42
- theme: themeConfig?.components
19
+ const navData = await getNavigationData(c);
20
+ if (navData.homeDefaultView === "featured") {
21
+ // Show featured posts on homepage
22
+ const posts = await c.var.services.posts.list({
23
+ featured: true,
24
+ status: "published",
25
+ excludeReplies: true
43
26
  });
44
- return sse(c, async (stream)=>{
45
- for (const patch of patches){
46
- if (patch.mode === "remove") {
47
- stream.remove(patch.selector);
48
- } else {
49
- stream.patchElements(patch.content, {
50
- mode: patch.mode,
51
- selector: patch.selector
52
- });
53
- }
54
- }
27
+ const mediaCtx = createMediaContext(c);
28
+ const postViews = toPostViewsFromPosts(posts, mediaCtx);
29
+ const items = postViews.map((post)=>({
30
+ post
31
+ }));
32
+ return renderPublicPage(c, {
33
+ title: navData.siteName,
34
+ navData,
35
+ content: /*#__PURE__*/ _jsx(FeaturedPage, {
36
+ items: items
37
+ })
55
38
  });
56
39
  }
57
- // Full page render
58
- const navData = await getNavigationData(c);
40
+ // Default: show latest posts
41
+ const pageParam = c.req.query("page");
42
+ const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
43
+ const { items, currentPage, totalPages } = await assembleTimeline(c, {
44
+ page
45
+ });
59
46
  // Fetch pinned posts
60
47
  const pinnedPosts = await c.var.services.posts.list({
61
48
  pinned: true,
@@ -64,17 +51,14 @@ homeRoutes.get("/", async (c)=>{
64
51
  });
65
52
  const mediaCtx = createMediaContext(c);
66
53
  const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
67
- const components = c.var.config.theme?.components;
68
- const Page = components?.HomePage ?? DefaultHomePage;
69
54
  return renderPublicPage(c, {
70
55
  title: navData.siteName,
71
56
  navData,
72
- content: /*#__PURE__*/ _jsx(Page, {
57
+ content: /*#__PURE__*/ _jsx(HomePage, {
73
58
  items: items,
74
59
  pinnedItems: pinnedItems,
75
- hasMore: hasMore,
76
- nextCursor: nextCursor,
77
- theme: components
60
+ currentPage: currentPage,
61
+ totalPages: totalPages
78
62
  })
79
63
  });
80
64
  });
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
+ /**
3
+ * Latest Page Route
4
+ *
5
+ * Explicit /latest URL that always shows the latest posts timeline.
6
+ * When HOME_DEFAULT_VIEW is "latest" (default), this redirects to /
7
+ * to avoid duplicate content. When it's "featured", this serves as
8
+ * the explicit latest view.
9
+ */ import { Hono } from "hono";
10
+ import { getNavigationData } from "../../lib/navigation.js";
11
+ import { renderPublicPage } from "../../lib/render.js";
12
+ import { assembleTimeline } from "../../lib/timeline.js";
13
+ import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
14
+ import { HomePage } from "../../ui/pages/HomePage.js";
15
+ export const latestRoutes = new Hono();
16
+ latestRoutes.get("/", async (c)=>{
17
+ const navData = await getNavigationData(c);
18
+ // When homepage already shows latest, redirect to avoid duplicate content
19
+ if (navData.homeDefaultView !== "featured") {
20
+ return c.redirect("/", 302);
21
+ }
22
+ const pageParam = c.req.query("page");
23
+ const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
24
+ const { items, currentPage, totalPages } = await assembleTimeline(c, {
25
+ page
26
+ });
27
+ // Fetch pinned posts
28
+ const pinnedPosts = await c.var.services.posts.list({
29
+ pinned: true,
30
+ status: "published",
31
+ excludeReplies: true
32
+ });
33
+ const mediaCtx = createMediaContext(c);
34
+ const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
35
+ return renderPublicPage(c, {
36
+ title: `Latest - ${navData.siteName}`,
37
+ navData,
38
+ content: /*#__PURE__*/ _jsx(HomePage, {
39
+ items: items,
40
+ pinnedItems: pinnedItems,
41
+ currentPage: currentPage,
42
+ totalPages: totalPages
43
+ })
44
+ });
45
+ });
@@ -2,44 +2,44 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
2
  /**
3
3
  * Custom Page Route
4
4
  *
5
- * Serves pages from the pages table and posts with custom slugs.
5
+ * Serves pages from the pages table and posts with custom paths.
6
6
  * This is a catch-all route mounted at "/" - must be registered last.
7
+ * Supports multi-level paths (e.g. /2024/my-post) for posts.
7
8
  */ import { Hono } from "hono";
8
- import { SinglePage as DefaultSinglePage } from "../../themes/threads/pages/SinglePage.js";
9
- import { PostPage as DefaultPostPage } from "../../themes/threads/pages/PostPage.js";
9
+ import { SinglePage } from "../../ui/pages/SinglePage.js";
10
+ import { PostPage } from "../../ui/pages/PostPage.js";
10
11
  import { getNavigationData } from "../../lib/navigation.js";
11
12
  import { renderPublicPage } from "../../lib/render.js";
12
13
  import { buildMediaMap } from "../../lib/media-helpers.js";
13
14
  import { createMediaContext, toPageView, toPostView } from "../../lib/view.js";
14
15
  export const pageRoutes = new Hono();
15
- // Catch-all for custom page paths and post slugs
16
- pageRoutes.get("/:slug", async (c)=>{
17
- const slug = c.req.param("slug");
18
- // First, try to find a page by slug
19
- const page = await c.var.services.pages.getBySlug(slug);
20
- if (page) {
21
- // Don't show draft pages
22
- if (page.status === "draft") {
23
- return c.notFound();
16
+ // Catch-all for custom page slugs and post paths (including multi-level)
17
+ pageRoutes.get("/*", async (c)=>{
18
+ const fullPath = c.req.path.slice(1); // Remove leading /
19
+ if (!fullPath) return c.notFound();
20
+ const isMultiSegment = fullPath.includes("/");
21
+ // Pages only have single-level slugs; skip page lookup for multi-segment paths
22
+ if (!isMultiSegment) {
23
+ const page = await c.var.services.pages.getBySlug(fullPath);
24
+ if (page) {
25
+ if (page.status === "draft") {
26
+ return c.notFound();
27
+ }
28
+ const navData = await getNavigationData(c);
29
+ const pageView = toPageView(page);
30
+ return renderPublicPage(c, {
31
+ title: `${page.title || fullPath} - ${navData.siteName}`,
32
+ description: page.body?.slice(0, 160),
33
+ navData,
34
+ content: /*#__PURE__*/ _jsx(SinglePage, {
35
+ page: pageView
36
+ })
37
+ });
24
38
  }
25
- const navData = await getNavigationData(c);
26
- const pageView = toPageView(page);
27
- const components = c.var.config.theme?.components;
28
- const Page = components?.SinglePage ?? DefaultSinglePage;
29
- return renderPublicPage(c, {
30
- title: `${page.title || slug} - ${navData.siteName}`,
31
- description: page.body?.slice(0, 160),
32
- navData,
33
- content: /*#__PURE__*/ _jsx(Page, {
34
- page: pageView,
35
- theme: components
36
- })
37
- });
38
39
  }
39
- // Then, try to find a post by slug
40
- const post = await c.var.services.posts.getBySlug(slug);
40
+ // Posts support multi-level paths
41
+ const post = await c.var.services.posts.getByPath(fullPath);
41
42
  if (post) {
42
- // Don't show draft posts
43
43
  if (post.status === "draft") {
44
44
  return c.notFound();
45
45
  }
@@ -55,15 +55,12 @@ pageRoutes.get("/:slug", async (c)=>{
55
55
  }, mediaCtx);
56
56
  const navData = await getNavigationData(c);
57
57
  const title = post.title || navData.siteName;
58
- const components = c.var.config.theme?.components;
59
- const PostPage = components?.PostPage ?? DefaultPostPage;
60
58
  return renderPublicPage(c, {
61
59
  title,
62
60
  description: post.body?.slice(0, 160),
63
61
  navData,
64
62
  content: /*#__PURE__*/ _jsx(PostPage, {
65
- post: postView,
66
- theme: components
63
+ post: postView
67
64
  })
68
65
  });
69
66
  }
@@ -2,7 +2,7 @@ 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 { PostPage as DefaultPostPage } from "../../themes/threads/pages/PostPage.js";
5
+ import { PostPage } from "../../ui/pages/PostPage.js";
6
6
  import * as sqid from "../../lib/sqid.js";
7
7
  import { getNavigationData } from "../../lib/navigation.js";
8
8
  import { renderPublicPage } from "../../lib/render.js";
@@ -33,15 +33,12 @@ postRoutes.get("/:id", async (c)=>{
33
33
  }, mediaCtx);
34
34
  const navData = await getNavigationData(c);
35
35
  const title = post.title || navData.siteName;
36
- const components = c.var.config.theme?.components;
37
- const Page = components?.PostPage ?? DefaultPostPage;
38
36
  return renderPublicPage(c, {
39
37
  title,
40
38
  description: post.body?.slice(0, 160),
41
39
  navData,
42
- content: /*#__PURE__*/ _jsx(Page, {
43
- post: postView,
44
- theme: components
40
+ content: /*#__PURE__*/ _jsx(PostPage, {
41
+ post: postView
45
42
  })
46
43
  });
47
44
  });
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "hono/jsx/jsx-runtime";
2
2
  /**
3
3
  * Search Page Route
4
4
  */ import { Hono } from "hono";
5
- import { SearchPage as DefaultSearchPage } from "../../themes/threads/pages/SearchPage.js";
5
+ import { SearchPage } from "../../ui/pages/SearchPage.js";
6
6
  import { getNavigationData } from "../../lib/navigation.js";
7
7
  import { renderPublicPage } from "../../lib/render.js";
8
8
  import { createMediaContext, toSearchResultViews } from "../../lib/view.js";
@@ -40,18 +40,15 @@ searchRoutes.get("/", async (c)=>{
40
40
  // Transform to View Models
41
41
  const mediaCtx = createMediaContext(c);
42
42
  const resultViews = toSearchResultViews(results, mediaCtx);
43
- const components = c.var.config.theme?.components;
44
- const Page = components?.SearchPage ?? DefaultSearchPage;
45
43
  return renderPublicPage(c, {
46
44
  title: query ? `Search: ${query} - ${navData.siteName}` : `Search - ${navData.siteName}`,
47
45
  navData,
48
- content: /*#__PURE__*/ _jsx(Page, {
46
+ content: /*#__PURE__*/ _jsx(SearchPage, {
49
47
  query: query,
50
48
  results: resultViews,
51
49
  error: error,
52
50
  hasMore: hasMore,
53
- page: page,
54
- theme: components
51
+ page: page
55
52
  })
56
53
  });
57
54
  });
@@ -2,7 +2,7 @@
2
2
  * Page Service
3
3
  *
4
4
  * CRUD operations for standalone pages (about, now, etc.)
5
- */ import { eq, desc } from "drizzle-orm";
5
+ */ import { eq, desc, sql } from "drizzle-orm";
6
6
  import { pages, navItems } from "../db/schema.js";
7
7
  import { now } from "../lib/time.js";
8
8
  import { render as renderMarkdown } from "../lib/markdown.js";
@@ -32,6 +32,10 @@ export function createPageService(db) {
32
32
  const rows = await db.select().from(pages).orderBy(desc(pages.createdAt));
33
33
  return rows.map(toPage);
34
34
  },
35
+ async listNotInNav () {
36
+ const rows = await db.select().from(pages).where(sql`${pages.id} NOT IN (SELECT ${navItems.pageId} FROM ${navItems} WHERE ${navItems.pageId} IS NOT NULL)`).orderBy(desc(pages.createdAt));
37
+ return rows.map(toPage);
38
+ },
35
39
  async create (data) {
36
40
  const timestamp = now();
37
41
  const bodyHtml = data.body ? renderMarkdown(data.body) : null;
@@ -9,6 +9,34 @@ import { posts } from "../db/schema.js";
9
9
  import { now } from "../lib/time.js";
10
10
  import { render as renderMarkdown } from "../lib/markdown.js";
11
11
  export function createPostService(db) {
12
+ /** Build WHERE conditions from filters (shared by list and count) */ function buildFilterConditions(filters) {
13
+ const conditions = [];
14
+ if (filters.status) {
15
+ conditions.push(eq(posts.status, filters.status));
16
+ }
17
+ if (filters.featured !== undefined) {
18
+ conditions.push(eq(posts.featured, filters.featured ? 1 : 0));
19
+ }
20
+ if (filters.pinned !== undefined) {
21
+ conditions.push(eq(posts.pinned, filters.pinned ? 1 : 0));
22
+ }
23
+ if (filters.format) {
24
+ conditions.push(eq(posts.format, filters.format));
25
+ }
26
+ if (filters.collectionId !== undefined) {
27
+ conditions.push(eq(posts.collectionId, filters.collectionId));
28
+ }
29
+ if (filters.threadId) {
30
+ conditions.push(eq(posts.threadId, filters.threadId));
31
+ }
32
+ if (filters.excludeReplies) {
33
+ conditions.push(isNull(posts.threadId));
34
+ }
35
+ if (!filters.includeDeleted) {
36
+ conditions.push(isNull(posts.deletedAt));
37
+ }
38
+ return conditions;
39
+ }
12
40
  function toPost(row) {
13
41
  return {
14
42
  id: row.id,
@@ -16,7 +44,7 @@ export function createPostService(db) {
16
44
  status: row.status,
17
45
  featured: row.featured,
18
46
  pinned: row.pinned,
19
- slug: row.slug,
47
+ path: row.path,
20
48
  title: row.title,
21
49
  url: row.url,
22
50
  body: row.body,
@@ -37,43 +65,29 @@ export function createPostService(db) {
37
65
  const result = await db.select().from(posts).where(and(eq(posts.id, id), isNull(posts.deletedAt))).limit(1);
38
66
  return result[0] ? toPost(result[0]) : null;
39
67
  },
40
- async getBySlug (slug) {
41
- const result = await db.select().from(posts).where(and(eq(posts.slug, slug), isNull(posts.deletedAt))).limit(1);
68
+ async getByPath (path) {
69
+ const result = await db.select().from(posts).where(and(eq(posts.path, path), isNull(posts.deletedAt))).limit(1);
42
70
  return result[0] ? toPost(result[0]) : null;
43
71
  },
44
72
  async list (filters = {}) {
45
- const conditions = [];
46
- if (filters.status) {
47
- conditions.push(eq(posts.status, filters.status));
48
- }
49
- if (filters.featured !== undefined) {
50
- conditions.push(eq(posts.featured, filters.featured ? 1 : 0));
51
- }
52
- if (filters.pinned !== undefined) {
53
- conditions.push(eq(posts.pinned, filters.pinned ? 1 : 0));
54
- }
55
- if (filters.format) {
56
- conditions.push(eq(posts.format, filters.format));
57
- }
58
- if (filters.collectionId !== undefined) {
59
- conditions.push(eq(posts.collectionId, filters.collectionId));
60
- }
61
- if (filters.threadId) {
62
- conditions.push(eq(posts.threadId, filters.threadId));
63
- }
64
- if (filters.excludeReplies) {
65
- conditions.push(isNull(posts.threadId));
66
- }
67
- if (!filters.includeDeleted) {
68
- conditions.push(isNull(posts.deletedAt));
69
- }
73
+ const conditions = buildFilterConditions(filters);
70
74
  if (filters.cursor) {
71
75
  conditions.push(sql`${posts.id} < ${filters.cursor}`);
72
76
  }
73
- const query = db.select().from(posts).where(conditions.length > 0 ? and(...conditions) : undefined).orderBy(desc(posts.publishedAt), desc(posts.id)).limit(filters.limit ?? 100);
77
+ let query = db.select().from(posts).where(conditions.length > 0 ? and(...conditions) : undefined).orderBy(desc(posts.publishedAt), desc(posts.id)).limit(filters.limit ?? 100);
78
+ if (filters.offset !== undefined) {
79
+ query = query.offset(filters.offset);
80
+ }
74
81
  const rows = await query;
75
82
  return rows.map(toPost);
76
83
  },
84
+ async count (filters = {}) {
85
+ const conditions = buildFilterConditions(filters);
86
+ const result = await db.select({
87
+ count: sql`count(*)`.as("count")
88
+ }).from(posts).where(conditions.length > 0 ? and(...conditions) : undefined);
89
+ return result[0]?.count ?? 0;
90
+ },
77
91
  async create (data) {
78
92
  const timestamp = now();
79
93
  const bodyHtml = data.body ? renderMarkdown(data.body) : null;
@@ -98,7 +112,7 @@ export function createPostService(db) {
98
112
  status,
99
113
  featured: featured ? 1 : 0,
100
114
  pinned: data.pinned ? 1 : 0,
101
- slug: data.slug ?? null,
115
+ path: data.path ?? null,
102
116
  title: data.title ?? null,
103
117
  url: data.url ?? null,
104
118
  body: data.body ?? null,
@@ -123,7 +137,7 @@ export function createPostService(db) {
123
137
  updatedAt: timestamp
124
138
  };
125
139
  if (data.format !== undefined) updates.format = data.format;
126
- if (data.slug !== undefined) updates.slug = data.slug;
140
+ if (data.path !== undefined) updates.path = data.path;
127
141
  if (data.title !== undefined) updates.title = data.title;
128
142
  if (data.url !== undefined) updates.url = data.url;
129
143
  if (data.quoteText !== undefined) updates.quoteText = data.quoteText;
@@ -44,7 +44,7 @@
44
44
  status: row.status,
45
45
  featured: row.featured,
46
46
  pinned: row.pinned,
47
- slug: row.slug,
47
+ path: row.path,
48
48
  title: row.title,
49
49
  url: row.url,
50
50
  body: row.body,
@@ -0,0 +1,3 @@
1
+ /**
2
+ * Cloudflare Worker Bindings
3
+ */ export { };