@jant/core 0.3.35 → 0.3.37

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 (307) hide show
  1. package/bin/commands/export.js +1 -1
  2. package/bin/commands/import-site.js +529 -0
  3. package/bin/commands/reset-password.js +3 -2
  4. package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
  5. package/dist/client/assets/module-RjUF93sV.js +716 -0
  6. package/dist/client/assets/native-48B9X9Wg.js +1 -0
  7. package/dist/client/assets/url-FWFqPJPb.js +1 -0
  8. package/dist/client/client.css +1 -1
  9. package/dist/client/client.js +4564 -3013
  10. package/dist/index.js +12885 -8161
  11. package/package.json +23 -6
  12. package/src/__tests__/helpers/app.ts +10 -10
  13. package/src/__tests__/helpers/db.ts +91 -87
  14. package/src/app.tsx +157 -31
  15. package/src/auth.ts +20 -2
  16. package/src/client/archive-nav.js +187 -0
  17. package/src/client/audio-player.ts +478 -0
  18. package/src/client/audio-processor.ts +84 -0
  19. package/src/{lib → client}/avatar-upload.ts +4 -3
  20. package/src/{lib → client}/collection-form-bridge.ts +2 -2
  21. package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
  22. package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
  23. package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
  24. package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
  25. package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
  26. package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
  27. package/src/client/components/collection-sidebar-types.ts +43 -0
  28. package/src/{ui → client}/components/collection-types.ts +3 -4
  29. package/src/client/components/compose-types.ts +174 -0
  30. package/src/client/components/jant-collection-form.ts +667 -0
  31. package/src/client/components/jant-collection-sidebar.ts +805 -0
  32. package/src/client/components/jant-compose-dialog.ts +2161 -0
  33. package/src/client/components/jant-compose-editor.ts +1813 -0
  34. package/src/client/components/jant-compose-fullscreen.ts +283 -0
  35. package/src/client/components/jant-media-lightbox.ts +259 -0
  36. package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
  37. package/src/{ui → client}/components/jant-post-form.ts +141 -12
  38. package/src/client/components/jant-post-menu.ts +1019 -0
  39. package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
  40. package/src/{ui → client}/components/jant-settings-general.ts +38 -4
  41. package/src/client/components/jant-text-preview.ts +232 -0
  42. package/src/{ui → client}/components/nav-manager-types.ts +6 -18
  43. package/src/{ui → client}/components/post-form-template.ts +137 -38
  44. package/src/{ui → client}/components/post-form-types.ts +15 -4
  45. package/src/client/compose-bridge.ts +583 -0
  46. package/src/{lib → client}/image-processor.ts +26 -8
  47. package/src/client/lazy-slugify.ts +51 -0
  48. package/src/client/media-metadata.ts +247 -0
  49. package/src/client/multipart-upload.ts +160 -0
  50. package/src/{lib → client}/nav-manager-bridge.ts +1 -1
  51. package/src/{lib → client}/post-form-bridge.ts +53 -2
  52. package/src/{lib → client}/settings-bridge.ts +3 -15
  53. package/src/client/thread-context.ts +140 -0
  54. package/src/client/tiptap/bubble-menu.ts +205 -0
  55. package/src/client/tiptap/create-editor.ts +86 -0
  56. package/src/client/tiptap/exitable-marks.ts +73 -0
  57. package/src/client/tiptap/extensions.ts +65 -0
  58. package/src/client/tiptap/image-node.ts +482 -0
  59. package/src/client/tiptap/link-toolbar.ts +371 -0
  60. package/src/client/tiptap/more-break.ts +50 -0
  61. package/src/client/tiptap/paste-image.ts +129 -0
  62. package/src/client/tiptap/slash-commands.ts +438 -0
  63. package/src/{lib → client}/toast.ts +101 -3
  64. package/src/client/types/sortablejs.d.ts +44 -0
  65. package/src/client/upload-with-metadata.ts +54 -0
  66. package/src/client/video-processor.ts +207 -0
  67. package/src/client.ts +27 -17
  68. package/src/db/__tests__/migrations.test.ts +118 -0
  69. package/src/db/index.ts +52 -0
  70. package/src/db/migrations/0000_baseline.sql +269 -0
  71. package/src/db/migrations/0001_fts_setup.sql +31 -0
  72. package/src/db/migrations/meta/0000_snapshot.json +703 -119
  73. package/src/db/migrations/meta/0001_snapshot.json +1337 -0
  74. package/src/db/migrations/meta/_journal.json +4 -39
  75. package/src/db/schema.ts +409 -140
  76. package/src/i18n/__tests__/detect.test.ts +115 -0
  77. package/src/i18n/context.tsx +2 -2
  78. package/src/i18n/detect.ts +85 -1
  79. package/src/i18n/i18n.ts +1 -1
  80. package/src/i18n/index.ts +2 -1
  81. package/src/i18n/locales/en.po +783 -1087
  82. package/src/i18n/locales/en.ts +1 -1
  83. package/src/i18n/locales/zh-Hans.po +867 -812
  84. package/src/i18n/locales/zh-Hans.ts +1 -1
  85. package/src/i18n/locales/zh-Hant.po +878 -823
  86. package/src/i18n/locales/zh-Hant.ts +1 -1
  87. package/src/i18n/middleware.ts +6 -0
  88. package/src/index.ts +5 -7
  89. package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
  90. package/src/lib/__tests__/constants.test.ts +0 -1
  91. package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
  92. package/src/lib/__tests__/nanoid.test.ts +26 -0
  93. package/src/lib/__tests__/resolve-config.test.ts +2 -2
  94. package/src/lib/__tests__/schemas.test.ts +186 -65
  95. package/src/lib/__tests__/slug.test.ts +126 -0
  96. package/src/lib/__tests__/sse.test.ts +6 -6
  97. package/src/lib/__tests__/summary.test.ts +264 -0
  98. package/src/lib/__tests__/theme.test.ts +1 -1
  99. package/src/lib/__tests__/timeline.test.ts +33 -30
  100. package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
  101. package/src/lib/__tests__/url.test.ts +2 -2
  102. package/src/lib/__tests__/view.test.ts +140 -65
  103. package/src/lib/blurhash-placeholder.ts +102 -0
  104. package/src/lib/constants.ts +3 -1
  105. package/src/lib/emoji-catalog.ts +963 -0
  106. package/src/lib/errors.ts +11 -8
  107. package/src/lib/feed.ts +77 -31
  108. package/src/lib/html.ts +2 -1
  109. package/src/lib/icon-catalog.ts +5033 -1
  110. package/src/lib/icons.ts +3 -2
  111. package/src/lib/index.ts +0 -1
  112. package/src/lib/markdown-to-tiptap.ts +286 -0
  113. package/src/lib/media-helpers.ts +22 -12
  114. package/src/lib/nanoid.ts +29 -0
  115. package/src/lib/navigation.ts +1 -1
  116. package/src/lib/render.tsx +24 -5
  117. package/src/lib/resolve-config.ts +13 -2
  118. package/src/lib/schemas.ts +226 -58
  119. package/src/lib/search-snippet.ts +34 -0
  120. package/src/lib/slug.ts +96 -0
  121. package/src/lib/sse.ts +6 -6
  122. package/src/lib/storage.ts +115 -7
  123. package/src/lib/summary.ts +158 -0
  124. package/src/lib/theme.ts +11 -8
  125. package/src/lib/timeline.ts +76 -34
  126. package/src/lib/tiptap-render.ts +191 -0
  127. package/src/lib/tiptap-to-markdown.ts +305 -0
  128. package/src/lib/upload.ts +263 -14
  129. package/src/lib/url.ts +37 -22
  130. package/src/lib/view.ts +236 -55
  131. package/src/middleware/__tests__/auth.test.ts +191 -11
  132. package/src/middleware/__tests__/onboarding.test.ts +12 -10
  133. package/src/middleware/auth.ts +63 -9
  134. package/src/middleware/error-handler.ts +3 -3
  135. package/src/middleware/onboarding.ts +1 -1
  136. package/src/middleware/secure-headers.ts +40 -0
  137. package/src/preset.css +83 -2
  138. package/src/routes/__tests__/compose.test.ts +17 -24
  139. package/src/routes/api/__tests__/collections.test.ts +109 -61
  140. package/src/routes/api/__tests__/nav-items.test.ts +46 -29
  141. package/src/routes/api/__tests__/posts.test.ts +132 -68
  142. package/src/routes/api/__tests__/search.test.ts +15 -2
  143. package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
  144. package/src/routes/api/collections.ts +57 -31
  145. package/src/routes/api/custom-urls.ts +80 -0
  146. package/src/routes/api/export.ts +31 -0
  147. package/src/routes/api/nav-items.ts +23 -19
  148. package/src/routes/api/posts.ts +81 -62
  149. package/src/routes/api/search.ts +3 -4
  150. package/src/routes/api/upload-multipart.ts +245 -0
  151. package/src/routes/api/upload.ts +92 -24
  152. package/src/routes/auth/__tests__/setup.test.ts +20 -60
  153. package/src/routes/auth/reset.tsx +5 -4
  154. package/src/routes/auth/setup.tsx +39 -31
  155. package/src/routes/auth/signin.tsx +13 -14
  156. package/src/routes/compose.tsx +27 -63
  157. package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
  158. package/src/routes/dash/custom-urls.tsx +414 -0
  159. package/src/routes/dash/settings.tsx +475 -99
  160. package/src/routes/feed/__tests__/rss.test.ts +22 -23
  161. package/src/routes/feed/rss.ts +6 -2
  162. package/src/routes/feed/sitemap.ts +2 -12
  163. package/src/routes/pages/__tests__/collections.test.ts +5 -6
  164. package/src/routes/pages/__tests__/featured.test.ts +36 -18
  165. package/src/routes/pages/archive.tsx +177 -37
  166. package/src/routes/pages/collection.tsx +43 -14
  167. package/src/routes/pages/collections.tsx +11 -2
  168. package/src/routes/pages/featured.tsx +27 -3
  169. package/src/routes/pages/home.tsx +15 -14
  170. package/src/routes/pages/latest.tsx +1 -11
  171. package/src/routes/pages/new.tsx +39 -0
  172. package/src/routes/pages/page.tsx +95 -49
  173. package/src/routes/pages/search.tsx +1 -1
  174. package/src/services/__tests__/api-token.test.ts +135 -0
  175. package/src/services/__tests__/collection.test.ts +275 -227
  176. package/src/services/__tests__/custom-url.test.ts +213 -0
  177. package/src/services/__tests__/media.test.ts +162 -22
  178. package/src/services/__tests__/navigation.test.ts +109 -68
  179. package/src/services/__tests__/post-timeline.test.ts +205 -32
  180. package/src/services/__tests__/post.test.ts +800 -230
  181. package/src/services/__tests__/search.test.ts +67 -10
  182. package/src/services/__tests__/settings.test.ts +3 -3
  183. package/src/services/api-token.ts +166 -0
  184. package/src/services/auth.ts +17 -2
  185. package/src/services/collection.ts +397 -131
  186. package/src/services/custom-url.ts +188 -0
  187. package/src/services/export.ts +802 -0
  188. package/src/services/index.ts +26 -19
  189. package/src/services/media.ts +100 -22
  190. package/src/services/navigation.ts +158 -47
  191. package/src/services/path.ts +339 -0
  192. package/src/services/post.ts +764 -172
  193. package/src/services/search.ts +161 -74
  194. package/src/services/settings.ts +6 -2
  195. package/src/styles/components.css +293 -62
  196. package/src/styles/tokens.css +93 -5
  197. package/src/styles/ui.css +4349 -766
  198. package/src/types/bindings.ts +8 -0
  199. package/src/types/config.ts +34 -4
  200. package/src/types/constants.ts +17 -2
  201. package/src/types/entities.ts +83 -37
  202. package/src/types/operations.ts +20 -27
  203. package/src/types/props.ts +52 -17
  204. package/src/types/views.ts +48 -24
  205. package/src/ui/color-themes.ts +133 -23
  206. package/src/ui/compose/ComposeDialog.tsx +255 -16
  207. package/src/ui/compose/ComposePrompt.tsx +1 -1
  208. package/src/ui/dash/CrudPageHeader.tsx +1 -1
  209. package/src/ui/dash/ListItemRow.tsx +1 -1
  210. package/src/ui/dash/StatusBadge.tsx +12 -2
  211. package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
  212. package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
  213. package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
  214. package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
  215. package/src/ui/dash/index.ts +0 -3
  216. package/src/ui/dash/settings/AccountContent.tsx +87 -146
  217. package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
  218. package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
  219. package/src/ui/dash/settings/AvatarContent.tsx +78 -0
  220. package/src/ui/dash/settings/GeneralContent.tsx +3 -62
  221. package/src/ui/dash/settings/SessionsContent.tsx +159 -0
  222. package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
  223. package/src/ui/feed/LinkCard.tsx +89 -40
  224. package/src/ui/feed/NoteCard.tsx +39 -25
  225. package/src/ui/feed/PostStatusBadges.tsx +67 -0
  226. package/src/ui/feed/QuoteCard.tsx +38 -23
  227. package/src/ui/feed/ThreadPreview.tsx +90 -26
  228. package/src/ui/feed/TimelineFeed.tsx +3 -2
  229. package/src/ui/feed/TimelineItem.tsx +15 -6
  230. package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
  231. package/src/ui/feed/thread-preview-state.ts +61 -0
  232. package/src/ui/font-themes.ts +2 -2
  233. package/src/ui/layouts/BaseLayout.tsx +2 -2
  234. package/src/ui/layouts/SiteLayout.tsx +116 -103
  235. package/src/ui/pages/ArchivePage.tsx +923 -95
  236. package/src/ui/pages/CollectionPage.tsx +6 -35
  237. package/src/ui/pages/CollectionsPage.tsx +2 -1
  238. package/src/ui/pages/ComposePage.tsx +54 -0
  239. package/src/ui/pages/FeaturedPage.tsx +2 -1
  240. package/src/ui/pages/HomePage.tsx +1 -1
  241. package/src/ui/pages/PostPage.tsx +30 -45
  242. package/src/ui/pages/SearchPage.tsx +182 -38
  243. package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
  244. package/src/ui/shared/CollectionsSidebar.tsx +239 -4
  245. package/src/ui/shared/MediaGallery.tsx +475 -41
  246. package/src/ui/shared/PostFooter.tsx +204 -0
  247. package/src/ui/shared/StarRating.tsx +27 -0
  248. package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
  249. package/src/ui/shared/index.ts +0 -1
  250. package/src/db/migrations/0000_square_wallflower.sql +0 -118
  251. package/src/db/migrations/0001_add_search_fts.sql +0 -34
  252. package/src/db/migrations/0002_add_media_attachments.sql +0 -3
  253. package/src/db/migrations/0003_add_navigation_links.sql +0 -8
  254. package/src/db/migrations/0004_add_storage_provider.sql +0 -3
  255. package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
  256. package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
  257. package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
  258. package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
  259. package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
  260. package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
  261. package/src/db/migrations/0011_add_path_registry.sql +0 -23
  262. package/src/db/migrations/meta/0003_snapshot.json +0 -821
  263. package/src/lib/__tests__/sqid.test.ts +0 -65
  264. package/src/lib/collections-reorder.ts +0 -28
  265. package/src/lib/compose-bridge.ts +0 -280
  266. package/src/lib/media-upload.ts +0 -148
  267. package/src/lib/sqid.ts +0 -79
  268. package/src/routes/api/__tests__/pages.test.ts +0 -218
  269. package/src/routes/api/pages.ts +0 -73
  270. package/src/routes/dash/__tests__/pages.test.ts +0 -226
  271. package/src/routes/dash/appearance.tsx +0 -240
  272. package/src/routes/dash/collections.tsx +0 -211
  273. package/src/routes/dash/index.tsx +0 -103
  274. package/src/routes/dash/media.tsx +0 -132
  275. package/src/routes/dash/pages.tsx +0 -239
  276. package/src/routes/dash/posts.tsx +0 -334
  277. package/src/routes/dash/redirects.tsx +0 -257
  278. package/src/routes/pages/post.tsx +0 -59
  279. package/src/services/__tests__/page.test.ts +0 -298
  280. package/src/services/__tests__/path-registry.test.ts +0 -165
  281. package/src/services/__tests__/redirect.test.ts +0 -159
  282. package/src/services/page.ts +0 -203
  283. package/src/services/path-registry.ts +0 -160
  284. package/src/services/redirect.ts +0 -97
  285. package/src/types/sortablejs.d.ts +0 -29
  286. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
  287. package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
  288. package/src/ui/components/compose-types.ts +0 -75
  289. package/src/ui/components/jant-collection-form.ts +0 -512
  290. package/src/ui/components/jant-compose-dialog.ts +0 -495
  291. package/src/ui/components/jant-compose-editor.ts +0 -814
  292. package/src/ui/dash/PageForm.tsx +0 -185
  293. package/src/ui/dash/PostList.tsx +0 -95
  294. package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
  295. package/src/ui/dash/collections/CollectionForm.tsx +0 -166
  296. package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
  297. package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
  298. package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
  299. package/src/ui/dash/media/MediaListContent.tsx +0 -201
  300. package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
  301. package/src/ui/dash/pages/PagesContent.tsx +0 -74
  302. package/src/ui/dash/posts/PostForm.tsx +0 -248
  303. package/src/ui/dash/settings/SettingsNav.tsx +0 -52
  304. package/src/ui/layouts/DashLayout.tsx +0 -165
  305. package/src/ui/pages/SinglePage.tsx +0 -23
  306. package/src/ui/shared/ThreadView.tsx +0 -136
  307. /package/src/{ui → client}/components/settings-types.ts +0 -0
@@ -4,9 +4,9 @@ import type { Bindings } from "../../../types.js";
4
4
  import type { AppVariables } from "../../../types/app-context.js";
5
5
  import { createTestDatabase } from "../../../__tests__/helpers/db.js";
6
6
  import { createPostService } from "../../../services/post.js";
7
+ import { createPathService } from "../../../services/path.js";
7
8
  import { createSettingsService } from "../../../services/settings.js";
8
9
  import { createMediaService } from "../../../services/media.js";
9
- import { createPathRegistryService } from "../../../services/path-registry.js";
10
10
  import { resolveConfig } from "../../../lib/resolve-config.js";
11
11
  import { rssRoutes } from "../rss.js";
12
12
 
@@ -14,12 +14,11 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
14
14
 
15
15
  function createFeedTestApp(envOverrides: Partial<Bindings> = {}) {
16
16
  const { db } = createTestDatabase();
17
+ const pathService = createPathService(db as never);
17
18
 
18
19
  const services = {
19
- posts: createPostService(
20
- db as never,
21
- createPathRegistryService(db as never),
22
- ),
20
+ paths: pathService,
21
+ posts: createPostService(db as never, { slugIdLength: 5 }, pathService),
23
22
  settings: createSettingsService(db as never),
24
23
  media: createMediaService(db as never),
25
24
  };
@@ -28,7 +27,7 @@ function createFeedTestApp(envOverrides: Partial<Bindings> = {}) {
28
27
 
29
28
  app.use("*", async (c, next) => {
30
29
  const env = {
31
- SITE_URL: "http://localhost:9019",
30
+ SITE_URL: "http://localhost:9020",
32
31
  ...envOverrides,
33
32
  } as Bindings;
34
33
  c.env = env;
@@ -54,13 +53,13 @@ describe("RSS Feed Routes", () => {
54
53
  await services.posts.create({
55
54
  format: "note",
56
55
  title: "Regular Post",
57
- body: "Not featured",
56
+ bodyMarkdown: "Not featured",
58
57
  status: "published",
59
58
  });
60
59
  await services.posts.create({
61
60
  format: "note",
62
61
  title: "Featured Post",
63
- body: "This is featured",
62
+ bodyMarkdown: "This is featured",
64
63
  status: "published",
65
64
  featured: true,
66
65
  });
@@ -79,7 +78,7 @@ describe("RSS Feed Routes", () => {
79
78
  await services.posts.create({
80
79
  format: "note",
81
80
  title: "Regular Post",
82
- body: "Not featured",
81
+ bodyMarkdown: "Not featured",
83
82
  status: "published",
84
83
  });
85
84
 
@@ -107,13 +106,13 @@ describe("RSS Feed Routes", () => {
107
106
  await services.posts.create({
108
107
  format: "note",
109
108
  title: "Regular Post",
110
- body: "Not featured",
109
+ bodyMarkdown: "Not featured",
111
110
  status: "published",
112
111
  });
113
112
  await services.posts.create({
114
113
  format: "note",
115
114
  title: "Featured Post",
116
- body: "This is featured",
115
+ bodyMarkdown: "This is featured",
117
116
  status: "published",
118
117
  featured: true,
119
118
  });
@@ -137,20 +136,20 @@ describe("RSS Feed Routes", () => {
137
136
  await services.posts.create({
138
137
  format: "note",
139
138
  title: "Regular Post",
140
- body: "Not featured",
139
+ bodyMarkdown: "Not featured",
141
140
  status: "published",
142
141
  });
143
142
  await services.posts.create({
144
143
  format: "note",
145
144
  title: "Featured Post",
146
- body: "This is featured",
145
+ bodyMarkdown: "This is featured",
147
146
  status: "published",
148
147
  featured: true,
149
148
  });
150
149
  await services.posts.create({
151
150
  format: "note",
152
151
  title: "Draft Post",
153
- body: "Draft",
152
+ bodyMarkdown: "Draft",
154
153
  status: "draft",
155
154
  });
156
155
 
@@ -169,7 +168,7 @@ describe("RSS Feed Routes", () => {
169
168
  await services.posts.create({
170
169
  format: "note",
171
170
  title: "My Note",
172
- body: "A note",
171
+ bodyMarkdown: "A note",
173
172
  status: "published",
174
173
  });
175
174
  await services.posts.create({
@@ -200,7 +199,7 @@ describe("RSS Feed Routes", () => {
200
199
  await services.posts.create({
201
200
  format: "note",
202
201
  title: "My Note",
203
- body: "A note",
202
+ bodyMarkdown: "A note",
204
203
  status: "published",
205
204
  });
206
205
  await services.posts.create({
@@ -236,13 +235,13 @@ describe("RSS Feed Routes", () => {
236
235
  await services.posts.create({
237
236
  format: "note",
238
237
  title: "Regular Post",
239
- body: "Not featured",
238
+ bodyMarkdown: "Not featured",
240
239
  status: "published",
241
240
  });
242
241
  await services.posts.create({
243
242
  format: "note",
244
243
  title: "Featured Post",
245
- body: "This is featured",
244
+ bodyMarkdown: "This is featured",
246
245
  status: "published",
247
246
  featured: true,
248
247
  });
@@ -264,7 +263,7 @@ describe("RSS Feed Routes", () => {
264
263
  await services.posts.create({
265
264
  format: "note",
266
265
  title: "My Note",
267
- body: "A note",
266
+ bodyMarkdown: "A note",
268
267
  status: "published",
269
268
  });
270
269
  await services.posts.create({
@@ -292,7 +291,7 @@ describe("RSS Feed Routes", () => {
292
291
  await services.posts.create({
293
292
  format: "note",
294
293
  title: `Post ${i}`,
295
- body: `Body ${i}`,
294
+ bodyMarkdown: `Body ${i}`,
296
295
  status: "published",
297
296
  featured: true,
298
297
  });
@@ -318,7 +317,7 @@ describe("RSS Feed Routes", () => {
318
317
  await services.posts.create({
319
318
  format: "note",
320
319
  title: `Post ${i}`,
321
- body: `Body ${i}`,
320
+ bodyMarkdown: `Body ${i}`,
322
321
  status: "published",
323
322
  });
324
323
  }
@@ -346,7 +345,7 @@ describe("RSS Feed Routes", () => {
346
345
  await services.posts.create({
347
346
  format: "note",
348
347
  title: `Post ${i}`,
349
- body: `Body ${i}`,
348
+ bodyMarkdown: `Body ${i}`,
350
349
  status: "published",
351
350
  });
352
351
  }
@@ -369,7 +368,7 @@ describe("RSS Feed Routes", () => {
369
368
  await services.posts.create({
370
369
  format: "note",
371
370
  title: `Post ${i}`,
372
- body: `Body ${i}`,
371
+ bodyMarkdown: `Body ${i}`,
373
372
  status: "published",
374
373
  featured: true,
375
374
  });
@@ -23,6 +23,8 @@ export const rssRoutes = new Hono<Env>();
23
23
 
24
24
  interface FeedOptions {
25
25
  featured?: boolean;
26
+ excludeUnlisted?: boolean;
27
+ excludePrivate?: boolean;
26
28
  format?: Format;
27
29
  }
28
30
 
@@ -48,6 +50,8 @@ async function buildFeedData(
48
50
  status: "published",
49
51
  excludeReplies: true,
50
52
  featured: opts?.featured,
53
+ excludeUnlisted: opts?.excludeUnlisted,
54
+ excludePrivate: opts?.excludePrivate ?? true,
51
55
  format: opts?.format,
52
56
  limit: feedLimit,
53
57
  });
@@ -124,7 +128,7 @@ rssRoutes.get("/atom.xml", async (c) => {
124
128
  // RSS 2.0 — /feed/all
125
129
  rssRoutes.get("/all", async (c) => {
126
130
  const format = parseFormatQuery(c);
127
- const feedData = await buildFeedData(c, { format });
131
+ const feedData = await buildFeedData(c, { excludeUnlisted: true, format });
128
132
  const xml = defaultRssRenderer(feedData);
129
133
 
130
134
  return new Response(xml, {
@@ -137,7 +141,7 @@ rssRoutes.get("/all", async (c) => {
137
141
  // Atom — /feed/all/atom.xml
138
142
  rssRoutes.get("/all/atom.xml", async (c) => {
139
143
  const format = parseFormatQuery(c);
140
- const feedData = await buildFeedData(c, { format });
144
+ const feedData = await buildFeedData(c, { excludeUnlisted: true, format });
141
145
  const xml = defaultAtomRenderer(feedData);
142
146
 
143
147
  return new Response(xml, {
@@ -6,11 +6,7 @@ import { Hono } from "hono";
6
6
  import type { Bindings } from "../../types.js";
7
7
  import type { AppVariables } from "../../types/app-context.js";
8
8
  import { defaultSitemapRenderer } from "../../lib/feed.js";
9
- import {
10
- createMediaContext,
11
- toPostViewsFromPosts,
12
- toPageView,
13
- } from "../../lib/view.js";
9
+ import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
14
10
 
15
11
  type Env = { Bindings: Bindings; Variables: AppVariables };
16
12
 
@@ -24,23 +20,17 @@ sitemapRoutes.get("/sitemap.xml", async (c) => {
24
20
  const posts = await c.var.services.posts.list({
25
21
  status: "published",
26
22
  excludeReplies: true,
23
+ excludePrivate: true,
27
24
  limit: 1000,
28
25
  });
29
26
 
30
- // Fetch published pages
31
- const publishedPages = await c.var.services.pages.list({
32
- status: "published",
33
- });
34
-
35
27
  // Transform to View Models
36
28
  const mediaCtx = createMediaContext(appConfig);
37
29
  const postViews = toPostViewsFromPosts(posts, mediaCtx);
38
- const pageViews = publishedPages.map(toPageView);
39
30
 
40
31
  const xml = defaultSitemapRenderer({
41
32
  siteUrl,
42
33
  posts: postViews,
43
- pages: pageViews,
44
34
  });
45
35
 
46
36
  return new Response(xml, {
@@ -10,7 +10,6 @@ import { describe, it, expect, beforeEach } from "vitest";
10
10
  import { createTestDatabase } from "../../../__tests__/helpers/db.js";
11
11
  import { createCollectionService } from "../../../services/collection.js";
12
12
  import { createPostService } from "../../../services/post.js";
13
- import { createPathRegistryService } from "../../../services/path-registry.js";
14
13
  import type { Database } from "../../../db/index.js";
15
14
 
16
15
  describe("Collections Listing Page - Data Logic", () => {
@@ -22,7 +21,7 @@ describe("Collections Listing Page - Data Logic", () => {
22
21
  const testDb = createTestDatabase();
23
22
  db = testDb.db as unknown as Database;
24
23
  collectionService = createCollectionService(db);
25
- postService = createPostService(db, createPathRegistryService(db));
24
+ postService = createPostService(db, { slugIdLength: 5 });
26
25
  });
27
26
 
28
27
  it("returns collections with post counts", async () => {
@@ -38,11 +37,11 @@ describe("Collections Listing Page - Data Logic", () => {
38
37
  // Add posts to recipes collection via junction table
39
38
  const p1 = await postService.create({
40
39
  format: "note",
41
- body: "Recipe 1",
40
+ bodyMarkdown: "Recipe 1",
42
41
  });
43
42
  const p2 = await postService.create({
44
43
  format: "note",
45
- body: "Recipe 2",
44
+ bodyMarkdown: "Recipe 2",
46
45
  });
47
46
  await collectionService.addPost(recipes.id, p1.id);
48
47
  await collectionService.addPost(recipes.id, p2.id);
@@ -78,11 +77,11 @@ describe("Collections Listing Page - Data Logic", () => {
78
77
 
79
78
  const post = await postService.create({
80
79
  format: "note",
81
- body: "Will be deleted",
80
+ bodyMarkdown: "Will be deleted",
82
81
  });
83
82
  const post2 = await postService.create({
84
83
  format: "note",
85
- body: "Will remain",
84
+ bodyMarkdown: "Will remain",
86
85
  });
87
86
 
88
87
  await collectionService.addPost(col.id, post.id);
@@ -9,7 +9,6 @@
9
9
  import { describe, it, expect, beforeEach } from "vitest";
10
10
  import { createTestDatabase } from "../../../__tests__/helpers/db.js";
11
11
  import { createPostService } from "../../../services/post.js";
12
- import { createPathRegistryService } from "../../../services/path-registry.js";
13
12
  import type { Database } from "../../../db/index.js";
14
13
 
15
14
  describe("Featured Page - Data Logic", () => {
@@ -19,25 +18,24 @@ describe("Featured Page - Data Logic", () => {
19
18
  beforeEach(() => {
20
19
  const testDb = createTestDatabase();
21
20
  db = testDb.db as unknown as Database;
22
- postService = createPostService(db, createPathRegistryService(db));
21
+ postService = createPostService(db, { slugIdLength: 5 });
23
22
  });
24
23
 
25
24
  it("returns only featured published posts", async () => {
26
25
  await postService.create({
27
26
  format: "note",
28
- body: "Featured post",
27
+ bodyMarkdown: "Featured post",
29
28
  featured: true,
30
29
  status: "published",
31
30
  });
32
31
  await postService.create({
33
32
  format: "note",
34
- body: "Normal post",
35
- featured: false,
33
+ bodyMarkdown: "Normal post",
36
34
  status: "published",
37
35
  });
38
36
  await postService.create({
39
37
  format: "note",
40
- body: "Draft featured",
38
+ bodyMarkdown: "Draft featured",
41
39
  featured: true,
42
40
  status: "draft",
43
41
  });
@@ -45,51 +43,71 @@ describe("Featured Page - Data Logic", () => {
45
43
  const posts = await postService.list({
46
44
  featured: true,
47
45
  status: "published",
48
- excludeReplies: true,
49
46
  });
50
47
 
51
48
  expect(posts).toHaveLength(1);
52
- expect(posts[0]?.body).toBe("Featured post");
49
+ expect(posts[0]?.bodyText).toBe("Featured post");
53
50
  });
54
51
 
55
52
  it("returns empty list when no featured posts exist", async () => {
56
53
  await postService.create({
57
54
  format: "note",
58
- body: "Normal post",
55
+ bodyMarkdown: "Normal post",
59
56
  status: "published",
60
57
  });
61
58
 
62
59
  const posts = await postService.list({
63
60
  featured: true,
64
61
  status: "published",
65
- excludeReplies: true,
66
62
  });
67
63
 
68
64
  expect(posts).toHaveLength(0);
69
65
  });
70
66
 
71
- it("excludes replies from featured posts", async () => {
67
+ it("includes featured reply posts", async () => {
72
68
  const root = await postService.create({
73
69
  format: "note",
74
- body: "Featured root",
75
- featured: true,
70
+ bodyMarkdown: "Root post",
76
71
  status: "published",
77
72
  });
78
73
 
79
- // Reply inherits featured from root
80
- await postService.create({
74
+ // Create a reply and feature it independently
75
+ const reply = await postService.create({
81
76
  format: "note",
82
- body: "Reply to featured",
77
+ bodyMarkdown: "Reply to root",
83
78
  replyToId: root.id,
84
79
  });
80
+ await postService.update(reply.id, { featured: true });
85
81
 
86
82
  const posts = await postService.list({
87
83
  featured: true,
88
84
  status: "published",
89
- excludeReplies: true,
90
85
  });
91
86
 
92
87
  expect(posts).toHaveLength(1);
93
- expect(posts[0]?.body).toBe("Featured root");
88
+ expect(posts[0]?.bodyText).toBe("Reply to root");
89
+ });
90
+
91
+ it("featured root and featured reply both appear", async () => {
92
+ const root = await postService.create({
93
+ format: "note",
94
+ bodyMarkdown: "Featured root",
95
+ featured: true,
96
+ status: "published",
97
+ });
98
+
99
+ const reply = await postService.create({
100
+ format: "note",
101
+ bodyMarkdown: "Featured reply",
102
+ replyToId: root.id,
103
+ });
104
+ await postService.update(reply.id, { featured: true });
105
+
106
+ const posts = await postService.list({
107
+ featured: true,
108
+ status: "published",
109
+ });
110
+
111
+ expect(posts).toHaveLength(2);
94
112
  });
95
113
  });
@@ -1,73 +1,211 @@
1
1
  /**
2
2
  * Archive Page Route
3
3
  *
4
- * Shows all posts, optionally filtered by format or featured status
4
+ * Tumblr-style archive grid with rich filtering:
5
+ * year, collection, format, media types, title presence.
6
+ * Page-based pagination with media-enriched post tiles.
5
7
  */
6
8
 
7
9
  import { Hono } from "hono";
8
- import type { Bindings, Format } from "../../types.js";
10
+ import type {
11
+ Bindings,
12
+ Format,
13
+ MediaKind,
14
+ PostWithMedia,
15
+ } from "../../types.js";
9
16
  import type { AppVariables } from "../../types/app-context.js";
10
- import { FORMATS } from "../../types.js";
17
+ import type {
18
+ ArchiveFilters,
19
+ ArchiveView,
20
+ ArchiveVisibility,
21
+ } from "../../types/props.js";
22
+ import { FORMATS, MEDIA_KINDS } from "../../types.js";
11
23
  import { ArchivePage } from "../../ui/pages/ArchivePage.js";
12
24
  import { getNavigationData } from "../../lib/navigation.js";
13
25
  import { renderPublicPage } from "../../lib/render.js";
14
- import { createMediaContext, toArchiveGroups } from "../../lib/view.js";
26
+ import {
27
+ createMediaContext,
28
+ toArchiveGroupsWithMedia,
29
+ } from "../../lib/view.js";
30
+ import { buildMediaMap } from "../../lib/media-helpers.js";
31
+ import type { PostFilters } from "../../services/post.js";
15
32
 
16
33
  type Env = { Bindings: Bindings; Variables: AppVariables };
17
34
 
18
- const PAGE_SIZE = 50;
35
+ const PAGE_SIZE = 60;
19
36
 
20
37
  export const archiveRoutes = new Hono<Env>();
21
38
 
22
- // Archive page - all posts
23
39
  archiveRoutes.get("/", async (c) => {
40
+ const { services, appConfig } = c.var;
41
+
42
+ // --- Parse query params ---------------------------------------------------
43
+
24
44
  const formatParam = c.req.query("format") as Format | undefined;
25
45
  const format =
26
46
  formatParam && FORMATS.includes(formatParam) ? formatParam : undefined;
27
- const featuredParam = c.req.query("featured");
28
- const featured = featuredParam === "true" ? true : undefined;
29
47
 
30
- // Parse cursor
31
- const cursorParam = c.req.query("cursor");
32
- const cursor = cursorParam ? parseInt(cursorParam, 10) : undefined;
48
+ const yearParam = c.req.query("year");
49
+ const year = yearParam ? parseInt(yearParam, 10) : undefined;
50
+ const validYear = year && !isNaN(year) && year > 1970 ? year : undefined;
51
+
52
+ const collectionSlug = c.req.query("collection") || undefined;
53
+
54
+ const mediaParam = c.req.query("media") || undefined;
55
+ const mediaKinds = mediaParam
56
+ ? (mediaParam
57
+ .split(",")
58
+ .filter((m): m is MediaKind =>
59
+ (MEDIA_KINDS as readonly string[]).includes(m),
60
+ ) as MediaKind[])
61
+ : undefined;
62
+
63
+ const hasMediaParam = c.req.query("hasMedia");
64
+ const hasMedia =
65
+ hasMediaParam === "1" ? true : hasMediaParam === "0" ? false : undefined;
66
+
67
+ const hasTitleParam = c.req.query("hasTitle");
68
+ const hasTitle =
69
+ hasTitleParam === "1" ? true : hasTitleParam === "0" ? false : undefined;
70
+
71
+ const VALID_VISIBILITIES = ["public", "unlisted", "private", "featured"];
72
+ const visibilityParam = c.req.query("visibility");
73
+ const visibilityAll = visibilityParam === "all";
74
+ const visibility =
75
+ visibilityParam && VALID_VISIBILITIES.includes(visibilityParam)
76
+ ? (visibilityParam as ArchiveVisibility)
77
+ : undefined;
78
+
79
+ const viewParam = c.req.query("view") as ArchiveView | undefined;
80
+ const view =
81
+ viewParam && (viewParam === "grid" || viewParam === "list")
82
+ ? viewParam
83
+ : undefined;
84
+
85
+ const pageParam = c.req.query("page");
86
+ const currentPage = Math.max(1, parseInt(pageParam || "1", 10) || 1);
87
+
88
+ // --- Resolve collection slug to ID ----------------------------------------
89
+
90
+ const collection = collectionSlug
91
+ ? await services.collections.getBySlug(collectionSlug)
92
+ : undefined;
93
+ const collectionId = collection?.id;
94
+
95
+ // --- Build timestamp range for year filter --------------------------------
96
+
97
+ let publishedAfter: number | undefined;
98
+ let publishedBefore: number | undefined;
99
+ if (validYear) {
100
+ publishedAfter = Date.UTC(validYear, 0, 1) / 1000;
101
+ publishedBefore = Date.UTC(validYear + 1, 0, 1) / 1000;
102
+ }
103
+
104
+ // --- Build filters --------------------------------------------------------
33
105
 
34
106
  const navData = await getNavigationData(c);
35
107
 
36
- // Fetch one extra to check for more
37
- const posts = await c.var.services.posts.list({
108
+ // --- Map visibility filter to service-level filters -------------------------
109
+ // Visibility filter is only meaningful when authenticated — unauthenticated
110
+ // users cannot see unlisted or private posts regardless of the query param.
111
+
112
+ // Default to "public" when authenticated unless explicitly set to "all"
113
+ const effectiveVisibility = navData.isAuthenticated
114
+ ? visibilityAll
115
+ ? undefined
116
+ : (visibility ?? "public")
117
+ : undefined;
118
+
119
+ const filters: PostFilters = {
38
120
  format,
39
121
  status: "published",
40
- featured,
41
122
  excludeReplies: true,
42
- cursor,
43
- limit: PAGE_SIZE + 1,
44
- });
123
+ excludePrivate: !navData.isAuthenticated,
124
+ excludeUnlisted: !navData.isAuthenticated,
125
+ ...(effectiveVisibility === "featured"
126
+ ? { featured: true }
127
+ : effectiveVisibility
128
+ ? { visibility: effectiveVisibility }
129
+ : {}),
130
+ collectionId,
131
+ publishedAfter,
132
+ publishedBefore,
133
+ mediaKinds: mediaKinds && mediaKinds.length > 0 ? mediaKinds : undefined,
134
+ hasMedia,
135
+ hasTitle,
136
+ };
45
137
 
46
- const hasMore = posts.length > PAGE_SIZE;
47
- const displayPosts = hasMore ? posts.slice(0, PAGE_SIZE) : posts;
138
+ // --- Parallel data fetches ------------------------------------------------
48
139
 
49
- // Get next cursor
50
- const nextCursor =
51
- hasMore && displayPosts.length > 0
52
- ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Length check above guarantees element exists
53
- displayPosts[displayPosts.length - 1]!.id
54
- : undefined;
140
+ const [totalCount, posts, availableYears, allCollections] = await Promise.all(
141
+ [
142
+ services.posts.count(filters),
143
+ services.posts.list({
144
+ ...filters,
145
+ limit: PAGE_SIZE,
146
+ offset: (currentPage - 1) * PAGE_SIZE,
147
+ }),
148
+ services.posts.getDistinctYears({
149
+ status: "published",
150
+ excludeReplies: true,
151
+ }),
152
+ services.collections.list(),
153
+ ],
154
+ );
155
+
156
+ const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
55
157
 
56
- // Group posts by year-month
57
- const grouped = new Map<string, typeof displayPosts>();
58
- for (const post of displayPosts) {
59
- const date = new Date(post.publishedAt * 1000);
158
+ // --- Batch-load media for posts -------------------------------------------
159
+
160
+ const postIds = posts.map((p) => p.id);
161
+ const rawMediaMap = await services.media.getByPostIds(postIds);
162
+ const mediaCtx = createMediaContext(appConfig);
163
+ const mediaMap = buildMediaMap(
164
+ rawMediaMap,
165
+ mediaCtx.r2PublicUrl,
166
+ mediaCtx.imageTransformUrl,
167
+ mediaCtx.s3PublicUrl,
168
+ );
169
+
170
+ // --- Group posts by year-month with media ---------------------------------
171
+
172
+ const grouped = new Map<string, PostWithMedia[]>();
173
+ for (const post of posts) {
174
+ const publishedAt = post.publishedAt ?? post.updatedAt;
175
+ const date = new Date(publishedAt * 1000);
60
176
  const key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
61
177
  if (!grouped.has(key)) {
62
178
  grouped.set(key, []);
63
179
  }
64
180
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Map.set() above guarantees key exists
65
- grouped.get(key)!.push(post);
181
+ grouped.get(key)!.push({
182
+ ...post,
183
+ mediaAttachments: mediaMap.get(post.id) ?? [],
184
+ });
66
185
  }
67
186
 
68
- // Transform to View Models
69
- const mediaCtx = createMediaContext(c.var.appConfig);
70
- const groups = toArchiveGroups(grouped, mediaCtx);
187
+ const groups = toArchiveGroupsWithMedia(grouped, mediaCtx);
188
+
189
+ // --- Build active filter state for UI -------------------------------------
190
+
191
+ const archiveFilters: ArchiveFilters = {
192
+ year: validYear,
193
+ collectionSlug,
194
+ collectionTitle: collection?.title,
195
+ collectionIcon: collection?.icon,
196
+ format,
197
+ mediaKinds: mediaKinds && mediaKinds.length > 0 ? mediaKinds : undefined,
198
+ hasMedia,
199
+ hasTitle,
200
+ visibility: effectiveVisibility,
201
+ view,
202
+ };
203
+
204
+ const availableCollectionsList = allCollections.map((col) => ({
205
+ slug: col.slug,
206
+ title: col.title,
207
+ icon: col.icon,
208
+ }));
71
209
 
72
210
  return renderPublicPage(c, {
73
211
  title: `Archive - ${navData.siteName}`,
@@ -75,10 +213,12 @@ archiveRoutes.get("/", async (c) => {
75
213
  content: (
76
214
  <ArchivePage
77
215
  groups={groups}
78
- hasMore={hasMore}
79
- nextCursor={nextCursor}
80
- format={format}
81
- featured={featured}
216
+ currentPage={currentPage}
217
+ totalPages={totalPages}
218
+ filters={archiveFilters}
219
+ availableYears={availableYears}
220
+ availableCollections={availableCollectionsList}
221
+ isAuthenticated={navData.isAuthenticated}
82
222
  />
83
223
  ),
84
224
  });