@jant/core 0.3.27 → 0.3.29

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 (314) hide show
  1. package/bin/reset-password.js +22 -0
  2. package/dist/client/client.css +1 -0
  3. package/dist/client/client.js +31561 -0
  4. package/dist/index.js +15209 -15
  5. package/package.json +25 -15
  6. package/src/__tests__/helpers/app.ts +19 -3
  7. package/src/__tests__/helpers/db.ts +44 -0
  8. package/src/__tests__/helpers/lingui-core-macro-mock.ts +33 -0
  9. package/src/app.tsx +111 -174
  10. package/src/client.ts +13 -0
  11. package/src/db/migrations/0007_post_collections_m2m.sql +94 -0
  12. package/src/db/migrations/0008_add_collection_dividers.sql +8 -0
  13. package/src/db/migrations/0009_drop_collection_show_divider.sql +2 -0
  14. package/src/db/migrations/0010_add_performance_indexes.sql +16 -0
  15. package/src/db/schema.ts +24 -4
  16. package/src/i18n/locales/en.po +810 -385
  17. package/src/i18n/locales/en.ts +1 -1
  18. package/src/i18n/locales/zh-Hans.po +733 -522
  19. package/src/i18n/locales/zh-Hans.ts +1 -1
  20. package/src/i18n/locales/zh-Hant.po +733 -522
  21. package/src/i18n/locales/zh-Hant.ts +1 -1
  22. package/src/i18n/middleware.ts +7 -11
  23. package/src/index.ts +1 -1
  24. package/src/lib/__tests__/icons.test.ts +178 -0
  25. package/src/lib/__tests__/resolve-config.test.ts +184 -0
  26. package/src/lib/__tests__/schemas.test.ts +12 -6
  27. package/src/lib/__tests__/theme.test.ts +62 -0
  28. package/src/lib/__tests__/timezones.test.ts +1 -1
  29. package/src/lib/__tests__/url.test.ts +12 -0
  30. package/src/lib/__tests__/view.test.ts +1 -5
  31. package/src/lib/avatar-upload.ts +18 -10
  32. package/src/lib/collection-form-bridge.ts +52 -0
  33. package/src/lib/collections-reorder.ts +28 -0
  34. package/src/lib/compose-bridge.ts +251 -0
  35. package/src/lib/errors.ts +116 -0
  36. package/src/lib/excerpt.ts +1 -1
  37. package/src/lib/favicon.ts +3 -5
  38. package/src/lib/html.ts +22 -0
  39. package/src/lib/icon-catalog.ts +181 -0
  40. package/src/lib/icons.ts +202 -0
  41. package/src/lib/navigation.ts +18 -33
  42. package/src/lib/pagination.ts +3 -2
  43. package/src/lib/post-form-bridge.ts +136 -0
  44. package/src/lib/render.tsx +11 -4
  45. package/src/lib/resolve-config.ts +157 -0
  46. package/src/lib/schemas.ts +76 -12
  47. package/src/lib/settings-bridge.ts +139 -0
  48. package/src/lib/storage.ts +37 -16
  49. package/src/lib/theme.ts +5 -7
  50. package/src/lib/timeline.ts +4 -8
  51. package/src/lib/toast.ts +134 -0
  52. package/src/lib/upload.ts +71 -0
  53. package/src/lib/url.ts +9 -1
  54. package/src/lib/version.ts +16 -0
  55. package/src/lib/view.ts +9 -10
  56. package/src/middleware/__tests__/auth.test.ts +6 -28
  57. package/src/middleware/__tests__/onboarding.test.ts +1 -1
  58. package/src/middleware/auth.ts +6 -12
  59. package/src/middleware/config.ts +51 -0
  60. package/src/middleware/error-handler.ts +56 -0
  61. package/src/middleware/onboarding.ts +1 -1
  62. package/src/preset.css +6 -0
  63. package/src/routes/__tests__/compose.test.ts +104 -17
  64. package/src/routes/api/__tests__/collections.test.ts +93 -2
  65. package/src/routes/api/__tests__/posts.test.ts +2 -1
  66. package/src/routes/api/__tests__/settings.test.ts +1 -1
  67. package/src/routes/api/collections.ts +64 -68
  68. package/src/routes/api/nav-items.ts +21 -59
  69. package/src/routes/api/pages.ts +18 -46
  70. package/src/routes/api/posts.ts +64 -86
  71. package/src/routes/api/search.ts +6 -4
  72. package/src/routes/api/settings.ts +8 -24
  73. package/src/routes/api/upload.ts +55 -53
  74. package/src/routes/auth/__tests__/setup.test.ts +118 -0
  75. package/src/routes/auth/reset.tsx +17 -66
  76. package/src/routes/auth/setup.tsx +67 -11
  77. package/src/routes/auth/signin.tsx +44 -8
  78. package/src/routes/compose.tsx +194 -0
  79. package/src/routes/dash/__tests__/font-theme.test.ts +110 -0
  80. package/src/routes/dash/__tests__/pages.test.ts +2 -2
  81. package/src/routes/dash/__tests__/settings-avatar.test.ts +23 -12
  82. package/src/routes/dash/appearance.tsx +173 -0
  83. package/src/routes/dash/collections.tsx +80 -14
  84. package/src/routes/dash/index.tsx +12 -14
  85. package/src/routes/dash/media.tsx +46 -49
  86. package/src/routes/dash/pages.tsx +85 -37
  87. package/src/routes/dash/posts.tsx +60 -23
  88. package/src/routes/dash/redirects.tsx +43 -33
  89. package/src/routes/dash/settings.tsx +234 -214
  90. package/src/routes/feed/__tests__/rss.test.ts +7 -3
  91. package/src/routes/feed/rss.ts +11 -16
  92. package/src/routes/feed/sitemap.ts +15 -9
  93. package/src/routes/pages/__tests__/collections.test.ts +9 -8
  94. package/src/routes/pages/archive.tsx +2 -2
  95. package/src/routes/pages/collection.tsx +76 -9
  96. package/src/routes/pages/collections.tsx +3 -1
  97. package/src/routes/pages/featured.tsx +2 -2
  98. package/src/routes/pages/home.tsx +3 -3
  99. package/src/routes/pages/latest.tsx +2 -2
  100. package/src/routes/pages/page.tsx +2 -2
  101. package/src/routes/pages/post.tsx +2 -2
  102. package/src/routes/pages/search.tsx +2 -2
  103. package/src/services/__tests__/collection.test.ts +324 -34
  104. package/src/services/__tests__/media.test.ts +1 -1
  105. package/src/services/__tests__/page.test.ts +116 -1
  106. package/src/services/auth.ts +88 -0
  107. package/src/services/collection.ts +169 -30
  108. package/src/services/index.ts +8 -3
  109. package/src/services/media.ts +39 -12
  110. package/src/services/navigation.ts +17 -5
  111. package/src/services/page.ts +24 -4
  112. package/src/services/post.ts +87 -19
  113. package/src/services/search.ts +0 -1
  114. package/src/services/settings.ts +21 -13
  115. package/src/style.css +3 -0
  116. package/src/styles/components.css +42 -1
  117. package/src/styles/tokens.css +4 -0
  118. package/src/styles/ui.css +902 -73
  119. package/src/types/app-context.ts +25 -0
  120. package/src/types/bindings.ts +1 -0
  121. package/src/types/config.ts +60 -23
  122. package/src/types/entities.ts +12 -2
  123. package/src/types/lingui-react-macro.d.ts +3 -3
  124. package/src/types/operations.ts +2 -4
  125. package/src/types/views.ts +1 -3
  126. package/src/ui/__tests__/font-themes.test.ts +27 -8
  127. package/src/ui/color-themes.ts +1 -1
  128. package/src/ui/components/__tests__/jant-collection-form.test.ts +153 -0
  129. package/src/ui/components/__tests__/jant-compose-dialog.test.ts +512 -0
  130. package/src/ui/components/__tests__/jant-compose-editor.test.ts +272 -0
  131. package/src/ui/components/__tests__/jant-post-form.test.ts +172 -0
  132. package/src/ui/components/__tests__/jant-settings-avatar.test.ts +235 -0
  133. package/src/ui/components/__tests__/jant-settings-general.test.ts +319 -0
  134. package/src/ui/components/collection-types.ts +45 -0
  135. package/src/ui/components/compose-types.ts +75 -0
  136. package/src/ui/components/jant-collection-form.ts +512 -0
  137. package/src/ui/components/jant-compose-dialog.ts +494 -0
  138. package/src/ui/components/jant-compose-editor.ts +799 -0
  139. package/src/ui/components/jant-post-form.ts +290 -0
  140. package/src/ui/components/jant-settings-avatar.ts +231 -0
  141. package/src/ui/components/jant-settings-general.ts +436 -0
  142. package/src/ui/components/post-form-template.ts +260 -0
  143. package/src/ui/components/post-form-types.ts +87 -0
  144. package/src/ui/components/settings-types.ts +62 -0
  145. package/src/ui/compose/ComposeDialog.tsx +141 -385
  146. package/src/ui/compose/ComposePrompt.tsx +3 -3
  147. package/src/ui/dash/PostList.tsx +55 -61
  148. package/src/ui/dash/appearance/AdvancedContent.tsx +80 -0
  149. package/src/ui/dash/appearance/AppearanceNav.tsx +56 -0
  150. package/src/ui/dash/appearance/ColorThemeContent.tsx +129 -0
  151. package/src/ui/dash/appearance/FontThemeContent.tsx +98 -0
  152. package/src/ui/dash/collections/CollectionForm.tsx +130 -117
  153. package/src/ui/dash/collections/CollectionsListContent.tsx +102 -41
  154. package/src/ui/dash/collections/IconPickerGrid.tsx +50 -0
  155. package/src/ui/dash/collections/ViewCollectionContent.tsx +14 -3
  156. package/src/ui/dash/index.ts +1 -1
  157. package/src/ui/dash/posts/PostForm.tsx +248 -0
  158. package/src/ui/dash/settings/AccountContent.tsx +69 -80
  159. package/src/ui/dash/settings/GeneralContent.tsx +159 -478
  160. package/src/ui/dash/settings/SettingsNav.tsx +4 -4
  161. package/src/ui/font-themes.ts +115 -32
  162. package/src/ui/layouts/BaseLayout.tsx +49 -19
  163. package/src/ui/layouts/DashLayout.tsx +14 -9
  164. package/src/ui/layouts/SiteLayout.tsx +38 -23
  165. package/src/ui/pages/CollectionPage.tsx +12 -2
  166. package/src/ui/pages/CollectionsPage.tsx +27 -27
  167. package/src/ui/pages/HomePage.tsx +15 -6
  168. package/src/ui/pages/SearchPage.tsx +1 -2
  169. package/src/ui/shared/CollectionsSidebar.tsx +59 -0
  170. package/src/ui/shared/Pagination.tsx +2 -2
  171. package/dist/app.js +0 -267
  172. package/dist/auth.js +0 -39
  173. package/dist/client.js +0 -13
  174. package/dist/db/index.js +0 -10
  175. package/dist/db/schema.js +0 -224
  176. package/dist/i18n/Trans.js +0 -24
  177. package/dist/i18n/context.js +0 -58
  178. package/dist/i18n/detect.js +0 -26
  179. package/dist/i18n/i18n.js +0 -49
  180. package/dist/i18n/index.js +0 -44
  181. package/dist/i18n/locales/en.js +0 -1
  182. package/dist/i18n/locales/zh-Hans.js +0 -1
  183. package/dist/i18n/locales/zh-Hant.js +0 -1
  184. package/dist/i18n/locales.js +0 -13
  185. package/dist/i18n/middleware.js +0 -30
  186. package/dist/lib/avatar-upload.js +0 -134
  187. package/dist/lib/config.js +0 -143
  188. package/dist/lib/constants.js +0 -50
  189. package/dist/lib/excerpt.js +0 -76
  190. package/dist/lib/favicon.js +0 -102
  191. package/dist/lib/feed.js +0 -123
  192. package/dist/lib/image-processor.js +0 -187
  193. package/dist/lib/image.js +0 -97
  194. package/dist/lib/index.js +0 -7
  195. package/dist/lib/markdown.js +0 -83
  196. package/dist/lib/media-helpers.js +0 -49
  197. package/dist/lib/media-upload.js +0 -104
  198. package/dist/lib/nav-reorder.js +0 -27
  199. package/dist/lib/navigation.js +0 -79
  200. package/dist/lib/pagination.js +0 -44
  201. package/dist/lib/render.js +0 -53
  202. package/dist/lib/schemas.js +0 -174
  203. package/dist/lib/sqid.js +0 -72
  204. package/dist/lib/sse.js +0 -218
  205. package/dist/lib/storage.js +0 -164
  206. package/dist/lib/theme.js +0 -65
  207. package/dist/lib/time.js +0 -159
  208. package/dist/lib/timeline.js +0 -95
  209. package/dist/lib/timezones.js +0 -388
  210. package/dist/lib/url.js +0 -89
  211. package/dist/lib/view.js +0 -217
  212. package/dist/middleware/auth.js +0 -52
  213. package/dist/middleware/onboarding.js +0 -41
  214. package/dist/routes/api/collections.js +0 -124
  215. package/dist/routes/api/nav-items.js +0 -104
  216. package/dist/routes/api/pages.js +0 -91
  217. package/dist/routes/api/posts.js +0 -218
  218. package/dist/routes/api/search.js +0 -48
  219. package/dist/routes/api/settings.js +0 -68
  220. package/dist/routes/api/upload.js +0 -246
  221. package/dist/routes/auth/reset.js +0 -221
  222. package/dist/routes/auth/setup.js +0 -194
  223. package/dist/routes/auth/signin.js +0 -176
  224. package/dist/routes/compose.js +0 -48
  225. package/dist/routes/dash/collections.js +0 -115
  226. package/dist/routes/dash/index.js +0 -118
  227. package/dist/routes/dash/media.js +0 -106
  228. package/dist/routes/dash/pages.js +0 -294
  229. package/dist/routes/dash/posts.js +0 -244
  230. package/dist/routes/dash/redirects.js +0 -257
  231. package/dist/routes/dash/settings.js +0 -379
  232. package/dist/routes/feed/rss.js +0 -62
  233. package/dist/routes/feed/sitemap.js +0 -49
  234. package/dist/routes/pages/archive.js +0 -62
  235. package/dist/routes/pages/collection.js +0 -34
  236. package/dist/routes/pages/collections.js +0 -28
  237. package/dist/routes/pages/featured.js +0 -36
  238. package/dist/routes/pages/home.js +0 -64
  239. package/dist/routes/pages/latest.js +0 -45
  240. package/dist/routes/pages/page.js +0 -68
  241. package/dist/routes/pages/post.js +0 -44
  242. package/dist/routes/pages/search.js +0 -54
  243. package/dist/services/collection.js +0 -109
  244. package/dist/services/index.js +0 -24
  245. package/dist/services/media.js +0 -117
  246. package/dist/services/navigation.js +0 -91
  247. package/dist/services/page.js +0 -84
  248. package/dist/services/post.js +0 -229
  249. package/dist/services/redirect.js +0 -48
  250. package/dist/services/search.js +0 -67
  251. package/dist/services/settings.js +0 -68
  252. package/dist/types/bindings.js +0 -3
  253. package/dist/types/config.js +0 -147
  254. package/dist/types/constants.js +0 -27
  255. package/dist/types/entities.js +0 -3
  256. package/dist/types/lingui-react-macro.d.js +0 -9
  257. package/dist/types/operations.js +0 -3
  258. package/dist/types/props.js +0 -3
  259. package/dist/types/sortablejs.d.js +0 -5
  260. package/dist/types/views.js +0 -5
  261. package/dist/types.js +0 -11
  262. package/dist/ui/color-themes.js +0 -268
  263. package/dist/ui/compose/ComposeDialog.js +0 -467
  264. package/dist/ui/compose/ComposePrompt.js +0 -55
  265. package/dist/ui/dash/ActionButtons.js +0 -46
  266. package/dist/ui/dash/CrudPageHeader.js +0 -22
  267. package/dist/ui/dash/DangerZone.js +0 -36
  268. package/dist/ui/dash/FormatBadge.js +0 -27
  269. package/dist/ui/dash/ListItemRow.js +0 -21
  270. package/dist/ui/dash/PageForm.js +0 -195
  271. package/dist/ui/dash/PostForm.js +0 -395
  272. package/dist/ui/dash/PostList.js +0 -83
  273. package/dist/ui/dash/StatusBadge.js +0 -46
  274. package/dist/ui/dash/collections/CollectionForm.js +0 -152
  275. package/dist/ui/dash/collections/CollectionsListContent.js +0 -68
  276. package/dist/ui/dash/collections/ViewCollectionContent.js +0 -96
  277. package/dist/ui/dash/index.js +0 -10
  278. package/dist/ui/dash/media/MediaListContent.js +0 -166
  279. package/dist/ui/dash/media/ViewMediaContent.js +0 -212
  280. package/dist/ui/dash/pages/LinkFormContent.js +0 -130
  281. package/dist/ui/dash/pages/UnifiedPagesContent.js +0 -193
  282. package/dist/ui/dash/settings/AccountContent.js +0 -209
  283. package/dist/ui/dash/settings/AppearanceContent.js +0 -259
  284. package/dist/ui/dash/settings/GeneralContent.js +0 -536
  285. package/dist/ui/dash/settings/SettingsNav.js +0 -41
  286. package/dist/ui/feed/LinkCard.js +0 -72
  287. package/dist/ui/feed/NoteCard.js +0 -58
  288. package/dist/ui/feed/QuoteCard.js +0 -63
  289. package/dist/ui/feed/ThreadPreview.js +0 -48
  290. package/dist/ui/feed/TimelineFeed.js +0 -41
  291. package/dist/ui/feed/TimelineItem.js +0 -27
  292. package/dist/ui/font-themes.js +0 -36
  293. package/dist/ui/layouts/BaseLayout.js +0 -153
  294. package/dist/ui/layouts/DashLayout.js +0 -141
  295. package/dist/ui/layouts/SiteLayout.js +0 -169
  296. package/dist/ui/pages/ArchivePage.js +0 -143
  297. package/dist/ui/pages/CollectionPage.js +0 -70
  298. package/dist/ui/pages/CollectionsPage.js +0 -76
  299. package/dist/ui/pages/FeaturedPage.js +0 -24
  300. package/dist/ui/pages/HomePage.js +0 -24
  301. package/dist/ui/pages/PostPage.js +0 -55
  302. package/dist/ui/pages/SearchPage.js +0 -122
  303. package/dist/ui/pages/SinglePage.js +0 -23
  304. package/dist/ui/shared/EmptyState.js +0 -27
  305. package/dist/ui/shared/MediaGallery.js +0 -35
  306. package/dist/ui/shared/Pagination.js +0 -195
  307. package/dist/ui/shared/ThreadView.js +0 -108
  308. package/dist/ui/shared/index.js +0 -5
  309. package/dist/vendor/datastar.js +0 -1606
  310. package/src/lib/__tests__/config.test.ts +0 -192
  311. package/src/lib/config.ts +0 -167
  312. package/src/routes/compose.ts +0 -63
  313. package/src/ui/dash/PostForm.tsx +0 -360
  314. package/src/ui/dash/settings/AppearanceContent.tsx +0 -254
@@ -5,10 +5,10 @@
5
5
  import { Hono } from "hono";
6
6
  import type { Context } from "hono";
7
7
  import type { Bindings, FeedData } from "../../types.js";
8
- import type { AppVariables } from "../../app.js";
8
+ import type { AppVariables } from "../../types/app-context.js";
9
9
  import { defaultRssRenderer, defaultAtomRenderer } from "../../lib/feed.js";
10
- import { getSiteLanguage } from "../../lib/config.js";
11
10
  import { buildMediaMap } from "../../lib/media-helpers.js";
11
+
12
12
  import { createMediaContext, toPostViews } from "../../lib/view.js";
13
13
 
14
14
  type Env = { Bindings: Bindings; Variables: AppVariables };
@@ -19,13 +19,12 @@ export const rssRoutes = new Hono<Env>();
19
19
  * Build FeedData from the Hono context.
20
20
  */
21
21
  async function buildFeedData(c: Context<Env>): Promise<FeedData> {
22
- const all = await c.var.services.settings.getAll();
23
- const siteName = all["SITE_NAME"] ?? "Jant";
24
- const siteDescription = all["SITE_DESCRIPTION"] ?? "";
25
- const siteUrl = c.env.SITE_URL;
26
- const siteLanguage = await getSiteLanguage(c);
27
-
28
- const feedLimit = parseInt(c.env.RSS_FEED_LIMIT ?? "50", 10) || 50;
22
+ const { appConfig } = c.var;
23
+ const siteName = appConfig.siteName;
24
+ const siteDescription = appConfig.siteDescription;
25
+ const siteUrl = appConfig.siteUrl;
26
+ const siteLanguage = appConfig.siteLanguage;
27
+ const feedLimit = appConfig.rssFeedLimit;
29
28
 
30
29
  const posts = await c.var.services.posts.list({
31
30
  status: "published",
@@ -36,7 +35,7 @@ async function buildFeedData(c: Context<Env>): Promise<FeedData> {
36
35
  // Batch load media for enclosures
37
36
  const postIds = posts.map((p) => p.id);
38
37
  const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
39
- const mediaCtx = createMediaContext(c);
38
+ const mediaCtx = createMediaContext(appConfig);
40
39
  const mediaMap = buildMediaMap(
41
40
  rawMediaMap,
42
41
  mediaCtx.r2PublicUrl,
@@ -65,9 +64,7 @@ async function buildFeedData(c: Context<Env>): Promise<FeedData> {
65
64
  // RSS 2.0 Feed - main feed at /feed
66
65
  rssRoutes.get("/", async (c) => {
67
66
  const feedData = await buildFeedData(c);
68
-
69
- const renderer = c.var.config.feed?.rss ?? defaultRssRenderer;
70
- const xml = renderer(feedData);
67
+ const xml = defaultRssRenderer(feedData);
71
68
 
72
69
  return new Response(xml, {
73
70
  headers: {
@@ -79,9 +76,7 @@ rssRoutes.get("/", async (c) => {
79
76
  // Atom Feed
80
77
  rssRoutes.get("/atom.xml", async (c) => {
81
78
  const feedData = await buildFeedData(c);
82
-
83
- const renderer = c.var.config.feed?.atom ?? defaultAtomRenderer;
84
- const xml = renderer(feedData);
79
+ const xml = defaultAtomRenderer(feedData);
85
80
 
86
81
  return new Response(xml, {
87
82
  headers: {
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { Hono } from "hono";
6
6
  import type { Bindings } from "../../types.js";
7
- import type { AppVariables } from "../../app.js";
7
+ import type { AppVariables } from "../../types/app-context.js";
8
8
  import { defaultSitemapRenderer } from "../../lib/feed.js";
9
9
  import {
10
10
  createMediaContext,
@@ -18,7 +18,8 @@ export const sitemapRoutes = new Hono<Env>();
18
18
 
19
19
  // XML Sitemap
20
20
  sitemapRoutes.get("/sitemap.xml", async (c) => {
21
- const siteUrl = c.env.SITE_URL;
21
+ const { appConfig } = c.var;
22
+ const siteUrl = appConfig.siteUrl;
22
23
 
23
24
  const posts = await c.var.services.posts.list({
24
25
  status: "published",
@@ -27,16 +28,20 @@ sitemapRoutes.get("/sitemap.xml", async (c) => {
27
28
  });
28
29
 
29
30
  // Fetch published pages
30
- const allPages = await c.var.services.pages.list();
31
- const publishedPages = allPages.filter((p) => p.status === "published");
31
+ const publishedPages = await c.var.services.pages.list({
32
+ status: "published",
33
+ });
32
34
 
33
35
  // Transform to View Models
34
- const mediaCtx = createMediaContext(c);
36
+ const mediaCtx = createMediaContext(appConfig);
35
37
  const postViews = toPostViewsFromPosts(posts, mediaCtx);
36
38
  const pageViews = publishedPages.map(toPageView);
37
39
 
38
- const renderer = c.var.config.feed?.sitemap ?? defaultSitemapRenderer;
39
- const xml = renderer({ siteUrl, posts: postViews, pages: pageViews });
40
+ const xml = defaultSitemapRenderer({
41
+ siteUrl,
42
+ posts: postViews,
43
+ pages: pageViews,
44
+ });
40
45
 
41
46
  return new Response(xml, {
42
47
  headers: {
@@ -47,8 +52,9 @@ sitemapRoutes.get("/sitemap.xml", async (c) => {
47
52
 
48
53
  // robots.txt
49
54
  sitemapRoutes.get("/robots.txt", async (c) => {
50
- const siteUrl = c.env.SITE_URL;
51
- const noindex = (await c.var.services.settings.get("NOINDEX")) === "true";
55
+ const { appConfig } = c.var;
56
+ const siteUrl = appConfig.siteUrl;
57
+ const noindex = appConfig.noindex;
52
58
 
53
59
  const directive = noindex ? "Disallow: /" : "Allow: /";
54
60
  const robots = `User-agent: *
@@ -34,17 +34,17 @@ describe("Collections Listing Page - Data Logic", () => {
34
34
  title: "Travel",
35
35
  });
36
36
 
37
- // Add posts to recipes collection
38
- await postService.create({
37
+ // Add posts to recipes collection via junction table
38
+ const p1 = await postService.create({
39
39
  format: "note",
40
40
  body: "Recipe 1",
41
- collectionId: recipes.id,
42
41
  });
43
- await postService.create({
42
+ const p2 = await postService.create({
44
43
  format: "note",
45
44
  body: "Recipe 2",
46
- collectionId: recipes.id,
47
45
  });
46
+ await collectionService.addPost(recipes.id, p1.id);
47
+ await collectionService.addPost(recipes.id, p2.id);
48
48
 
49
49
  // Simulate route handler logic
50
50
  const [allCollections, postCounts] = await Promise.all([
@@ -78,14 +78,15 @@ describe("Collections Listing Page - Data Logic", () => {
78
78
  const post = await postService.create({
79
79
  format: "note",
80
80
  body: "Will be deleted",
81
- collectionId: col.id,
82
81
  });
83
- await postService.create({
82
+ const post2 = await postService.create({
84
83
  format: "note",
85
84
  body: "Will remain",
86
- collectionId: col.id,
87
85
  });
88
86
 
87
+ await collectionService.addPost(col.id, post.id);
88
+ await collectionService.addPost(col.id, post2.id);
89
+
89
90
  await postService.delete(post.id);
90
91
 
91
92
  const postCounts = await collectionService.getPostCounts();
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { Hono } from "hono";
8
8
  import type { Bindings, Format } from "../../types.js";
9
- import type { AppVariables } from "../../app.js";
9
+ import type { AppVariables } from "../../types/app-context.js";
10
10
  import { FORMATS } from "../../types.js";
11
11
  import { ArchivePage } from "../../ui/pages/ArchivePage.js";
12
12
  import { getNavigationData } from "../../lib/navigation.js";
@@ -66,7 +66,7 @@ archiveRoutes.get("/", async (c) => {
66
66
  }
67
67
 
68
68
  // Transform to View Models
69
- const mediaCtx = createMediaContext(c);
69
+ const mediaCtx = createMediaContext(c.var.appConfig);
70
70
  const groups = toArchiveGroups(grouped, mediaCtx);
71
71
 
72
72
  return renderPublicPage(c, {
@@ -4,11 +4,18 @@
4
4
 
5
5
  import { Hono } from "hono";
6
6
  import type { Bindings } from "../../types.js";
7
- import type { AppVariables } from "../../app.js";
7
+ import type { AppVariables } from "../../types/app-context.js";
8
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
- import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
11
+ import {
12
+ createMediaContext,
13
+ toPostViewsFromPosts,
14
+ toPostViews,
15
+ } from "../../lib/view.js";
16
+ import { defaultRssRenderer } from "../../lib/feed.js";
17
+ import { buildMediaMap } from "../../lib/media-helpers.js";
18
+ import { CollectionsSidebar } from "../../ui/shared/CollectionsSidebar.js";
12
19
 
13
20
  type Env = { Bindings: Bindings; Variables: AppVariables };
14
21
 
@@ -20,23 +27,29 @@ collectionRoutes.get("/:slug", async (c) => {
20
27
  const collection = await c.var.services.collections.getBySlug(slug);
21
28
  if (!collection) return c.notFound();
22
29
 
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
- });
30
+ // Fetch posts and all collections in parallel
31
+ const [posts, allCollections] = await Promise.all([
32
+ c.var.services.posts.list({
33
+ collectionId: collection.id,
34
+ status: "published",
35
+ excludeReplies: true,
36
+ }),
37
+ c.var.services.collections.list(),
38
+ ]);
29
39
 
30
40
  const navData = await getNavigationData(c);
31
41
 
32
42
  // Transform to View Models
33
- const mediaCtx = createMediaContext(c);
43
+ const mediaCtx = createMediaContext(c.var.appConfig);
34
44
  const postViews = toPostViewsFromPosts(posts, mediaCtx);
35
45
 
36
46
  return renderPublicPage(c, {
37
47
  title: `${collection.title} - ${navData.siteName}`,
38
48
  description: collection.description ?? undefined,
39
49
  navData,
50
+ sidebar: (
51
+ <CollectionsSidebar collections={allCollections} activeSlug={slug} />
52
+ ),
40
53
  content: (
41
54
  <CollectionPage
42
55
  collection={collection}
@@ -46,3 +59,57 @@ collectionRoutes.get("/:slug", async (c) => {
46
59
  ),
47
60
  });
48
61
  });
62
+
63
+ // Collection RSS feed
64
+ collectionRoutes.get("/:slug/feed", async (c) => {
65
+ const slug = c.req.param("slug");
66
+
67
+ const collection = await c.var.services.collections.getBySlug(slug);
68
+ if (!collection) return c.notFound();
69
+
70
+ const { appConfig } = c.var;
71
+ const siteName = appConfig.siteName;
72
+ const siteUrl = appConfig.siteUrl;
73
+ const siteLanguage = appConfig.siteLanguage;
74
+ const feedLimit = appConfig.rssFeedLimit;
75
+
76
+ const posts = await c.var.services.posts.list({
77
+ collectionId: collection.id,
78
+ status: "published",
79
+ excludeReplies: true,
80
+ limit: feedLimit,
81
+ });
82
+
83
+ // Batch load media for enclosures
84
+ const postIds = posts.map((p) => p.id);
85
+ const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
86
+ const mediaCtx = createMediaContext(appConfig);
87
+ const mediaMap = buildMediaMap(
88
+ rawMediaMap,
89
+ mediaCtx.r2PublicUrl,
90
+ mediaCtx.imageTransformUrl,
91
+ mediaCtx.s3PublicUrl,
92
+ );
93
+
94
+ const postViews = toPostViews(
95
+ posts.map((p) => ({
96
+ ...p,
97
+ mediaAttachments: mediaMap.get(p.id) ?? [],
98
+ })),
99
+ mediaCtx,
100
+ );
101
+
102
+ const xml = defaultRssRenderer({
103
+ siteName: `${collection.title} - ${siteName}`,
104
+ siteDescription: collection.description ?? "",
105
+ siteUrl,
106
+ siteLanguage,
107
+ posts: postViews,
108
+ });
109
+
110
+ return new Response(xml, {
111
+ headers: {
112
+ "Content-Type": "application/rss+xml; charset=utf-8",
113
+ },
114
+ });
115
+ });
@@ -6,10 +6,11 @@
6
6
 
7
7
  import { Hono } from "hono";
8
8
  import type { Bindings } from "../../types.js";
9
- import type { AppVariables } from "../../app.js";
9
+ import type { AppVariables } from "../../types/app-context.js";
10
10
  import { getNavigationData } from "../../lib/navigation.js";
11
11
  import { renderPublicPage } from "../../lib/render.js";
12
12
  import { CollectionsPage } from "../../ui/pages/CollectionsPage.js";
13
+ import { CollectionsSidebar } from "../../ui/shared/CollectionsSidebar.js";
13
14
 
14
15
  type Env = { Bindings: Bindings; Variables: AppVariables };
15
16
 
@@ -31,6 +32,7 @@ collectionsPageRoutes.get("/", async (c) => {
31
32
  return renderPublicPage(c, {
32
33
  title: `Collections - ${navData.siteName}`,
33
34
  navData,
35
+ sidebar: <CollectionsSidebar collections={allCollections} />,
34
36
  content: <CollectionsPage collections={collections} />,
35
37
  });
36
38
  });
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { Hono } from "hono";
8
8
  import type { Bindings } from "../../types.js";
9
- import type { AppVariables } from "../../app.js";
9
+ import type { AppVariables } from "../../types/app-context.js";
10
10
  import { getNavigationData } from "../../lib/navigation.js";
11
11
  import { renderPublicPage } from "../../lib/render.js";
12
12
  import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
@@ -30,7 +30,7 @@ featuredRoutes.get("/", async (c) => {
30
30
  excludeReplies: true,
31
31
  });
32
32
 
33
- const mediaCtx = createMediaContext(c);
33
+ const mediaCtx = createMediaContext(c.var.appConfig);
34
34
  const postViews = toPostViewsFromPosts(posts, mediaCtx);
35
35
 
36
36
  // Convert to timeline items (simple — no thread previews)
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { Hono } from "hono";
12
12
  import type { Bindings } from "../../types.js";
13
- import type { AppVariables } from "../../app.js";
13
+ import type { AppVariables } from "../../types/app-context.js";
14
14
  import { getNavigationData } from "../../lib/navigation.js";
15
15
  import { renderPublicPage } from "../../lib/render.js";
16
16
  import { assembleTimeline } from "../../lib/timeline.js";
@@ -32,7 +32,7 @@ homeRoutes.get("/", async (c) => {
32
32
  status: "published",
33
33
  excludeReplies: true,
34
34
  });
35
- const mediaCtx = createMediaContext(c);
35
+ const mediaCtx = createMediaContext(c.var.appConfig);
36
36
  const postViews = toPostViewsFromPosts(posts, mediaCtx);
37
37
  const items = postViews.map((post) => ({ post }));
38
38
 
@@ -57,7 +57,7 @@ homeRoutes.get("/", async (c) => {
57
57
  status: "published",
58
58
  excludeReplies: true,
59
59
  });
60
- const mediaCtx = createMediaContext(c);
60
+ const mediaCtx = createMediaContext(c.var.appConfig);
61
61
  const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
62
62
 
63
63
  return renderPublicPage(c, {
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { Hono } from "hono";
11
11
  import type { Bindings } from "../../types.js";
12
- import type { AppVariables } from "../../app.js";
12
+ import type { AppVariables } from "../../types/app-context.js";
13
13
  import { getNavigationData } from "../../lib/navigation.js";
14
14
  import { renderPublicPage } from "../../lib/render.js";
15
15
  import { assembleTimeline } from "../../lib/timeline.js";
@@ -41,7 +41,7 @@ latestRoutes.get("/", async (c) => {
41
41
  status: "published",
42
42
  excludeReplies: true,
43
43
  });
44
- const mediaCtx = createMediaContext(c);
44
+ const mediaCtx = createMediaContext(c.var.appConfig);
45
45
  const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
46
46
 
47
47
  return renderPublicPage(c, {
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { Hono } from "hono";
10
10
  import type { Bindings } from "../../types.js";
11
- import type { AppVariables } from "../../app.js";
11
+ import type { AppVariables } from "../../types/app-context.js";
12
12
  import { SinglePage } from "../../ui/pages/SinglePage.js";
13
13
  import { PostPage } from "../../ui/pages/PostPage.js";
14
14
  import { getNavigationData } from "../../lib/navigation.js";
@@ -58,7 +58,7 @@ pageRoutes.get("/*", async (c) => {
58
58
 
59
59
  // Load media attachments
60
60
  const rawMediaMap = await c.var.services.media.getByPostIds([post.id]);
61
- const mediaCtx = createMediaContext(c);
61
+ const mediaCtx = createMediaContext(c.var.appConfig);
62
62
  const mediaMap = buildMediaMap(
63
63
  rawMediaMap,
64
64
  mediaCtx.r2PublicUrl,
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { Hono } from "hono";
6
6
  import type { Bindings } from "../../types.js";
7
- import type { AppVariables } from "../../app.js";
7
+ import type { AppVariables } from "../../types/app-context.js";
8
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";
@@ -33,7 +33,7 @@ postRoutes.get("/:id", async (c) => {
33
33
 
34
34
  // Batch load media attachments
35
35
  const rawMediaMap = await c.var.services.media.getByPostIds([post.id]);
36
- const mediaCtx = createMediaContext(c);
36
+ const mediaCtx = createMediaContext(c.var.appConfig);
37
37
  const mediaMap = buildMediaMap(
38
38
  rawMediaMap,
39
39
  mediaCtx.r2PublicUrl,
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { Hono } from "hono";
6
6
  import type { Bindings, SearchResult } from "../../types.js";
7
- import type { AppVariables } from "../../app.js";
7
+ import type { AppVariables } from "../../types/app-context.js";
8
8
  import { SearchPage } from "../../ui/pages/SearchPage.js";
9
9
  import { getNavigationData } from "../../lib/navigation.js";
10
10
  import { renderPublicPage } from "../../lib/render.js";
@@ -49,7 +49,7 @@ searchRoutes.get("/", async (c) => {
49
49
  }
50
50
 
51
51
  // Transform to View Models
52
- const mediaCtx = createMediaContext(c);
52
+ const mediaCtx = createMediaContext(c.var.appConfig);
53
53
  const resultViews = toSearchResultViews(results, mediaCtx);
54
54
 
55
55
  return renderPublicPage(c, {