@jant/core 0.3.23 → 0.3.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/dist/app.js +50 -26
  2. package/dist/db/schema.js +72 -47
  3. package/dist/i18n/locales/en.js +1 -1
  4. package/dist/i18n/locales/zh-Hans.js +1 -1
  5. package/dist/i18n/locales/zh-Hant.js +1 -1
  6. package/dist/index.js +5 -11
  7. package/dist/lib/constants.js +2 -4
  8. package/dist/lib/excerpt.js +76 -0
  9. package/dist/lib/feed.js +18 -7
  10. package/dist/lib/nav-reorder.js +1 -1
  11. package/dist/lib/navigation.js +30 -6
  12. package/dist/lib/pagination.js +44 -0
  13. package/dist/lib/render.js +7 -11
  14. package/dist/lib/schemas.js +80 -38
  15. package/dist/lib/theme.js +4 -4
  16. package/dist/lib/time.js +56 -1
  17. package/dist/lib/timeline.js +95 -0
  18. package/dist/lib/view.js +61 -72
  19. package/dist/routes/api/collections.js +124 -0
  20. package/dist/routes/api/nav-items.js +104 -0
  21. package/dist/routes/api/pages.js +91 -0
  22. package/dist/routes/api/posts.js +27 -33
  23. package/dist/routes/api/search.js +4 -5
  24. package/dist/routes/api/settings.js +68 -0
  25. package/dist/routes/api/upload.js +13 -13
  26. package/dist/routes/compose.js +48 -0
  27. package/dist/routes/dash/collections.js +24 -42
  28. package/dist/routes/dash/index.js +3 -3
  29. package/dist/routes/dash/media.js +2 -2
  30. package/dist/routes/dash/pages.js +440 -106
  31. package/dist/routes/dash/posts.js +27 -37
  32. package/dist/routes/dash/redirects.js +2 -2
  33. package/dist/routes/dash/settings.js +79 -5
  34. package/dist/routes/feed/rss.js +4 -6
  35. package/dist/routes/feed/sitemap.js +11 -8
  36. package/dist/routes/pages/archive.js +13 -15
  37. package/dist/routes/pages/collection.js +12 -9
  38. package/dist/routes/pages/collections.js +28 -0
  39. package/dist/routes/pages/featured.js +32 -0
  40. package/dist/routes/pages/home.js +19 -68
  41. package/dist/routes/pages/page.js +57 -29
  42. package/dist/routes/pages/post.js +7 -17
  43. package/dist/routes/pages/search.js +5 -9
  44. package/dist/services/collection.js +52 -64
  45. package/dist/services/index.js +5 -3
  46. package/dist/services/navigation.js +29 -53
  47. package/dist/services/page.js +84 -0
  48. package/dist/services/post.js +102 -69
  49. package/dist/services/search.js +24 -18
  50. package/dist/types.js +24 -40
  51. package/dist/ui/compose/ComposeDialog.js +452 -0
  52. package/dist/ui/compose/ComposePrompt.js +55 -0
  53. package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +3 -15
  54. package/dist/{theme/components → ui/dash}/PageForm.js +15 -15
  55. package/dist/{theme/components → ui/dash}/PostForm.js +117 -137
  56. package/dist/{theme/components → ui/dash}/PostList.js +18 -13
  57. package/dist/ui/dash/StatusBadge.js +46 -0
  58. package/dist/{theme/components → ui/dash}/index.js +3 -6
  59. package/dist/ui/feed/LinkCard.js +72 -0
  60. package/dist/ui/feed/NoteCard.js +58 -0
  61. package/dist/{themes/minimal/timeline → ui/feed}/QuoteCard.js +29 -14
  62. package/dist/{themes/minimal/timeline → ui/feed}/ThreadPreview.js +20 -18
  63. package/dist/ui/feed/TimelineFeed.js +41 -0
  64. package/dist/ui/feed/TimelineItem.js +27 -0
  65. package/dist/{theme → ui}/layouts/BaseLayout.js +10 -0
  66. package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
  67. package/dist/ui/layouts/SiteLayout.js +141 -0
  68. package/dist/{themes/minimal → ui}/pages/ArchivePage.js +37 -50
  69. package/dist/ui/pages/CollectionPage.js +70 -0
  70. package/dist/ui/pages/CollectionsPage.js +76 -0
  71. package/dist/ui/pages/FeaturedPage.js +24 -0
  72. package/dist/ui/pages/HomePage.js +24 -0
  73. package/dist/{themes/minimal → ui}/pages/PostPage.js +20 -12
  74. package/dist/{themes/minimal → ui}/pages/SearchPage.js +19 -18
  75. package/dist/{themes/minimal → ui}/pages/SinglePage.js +5 -4
  76. package/dist/ui/shared/MediaGallery.js +35 -0
  77. package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
  78. package/dist/{theme/components → ui/shared}/ThreadView.js +3 -3
  79. package/dist/ui/shared/index.js +5 -0
  80. package/package.json +2 -9
  81. package/src/__tests__/helpers/app.ts +4 -0
  82. package/src/__tests__/helpers/db.ts +53 -73
  83. package/src/app.tsx +56 -28
  84. package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
  85. package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
  86. package/src/db/migrations/meta/_journal.json +14 -0
  87. package/src/db/schema.ts +63 -46
  88. package/src/i18n/locales/en.po +443 -240
  89. package/src/i18n/locales/en.ts +1 -1
  90. package/src/i18n/locales/zh-Hans.po +443 -240
  91. package/src/i18n/locales/zh-Hans.ts +1 -1
  92. package/src/i18n/locales/zh-Hant.po +443 -240
  93. package/src/i18n/locales/zh-Hant.ts +1 -1
  94. package/src/index.ts +29 -42
  95. package/src/lib/__tests__/excerpt.test.ts +125 -0
  96. package/src/lib/__tests__/schemas.test.ts +201 -99
  97. package/src/lib/__tests__/time.test.ts +62 -0
  98. package/src/{routes/api → lib}/__tests__/timeline.test.ts +81 -75
  99. package/src/lib/__tests__/view.test.ts +204 -50
  100. package/src/lib/constants.ts +2 -4
  101. package/src/lib/excerpt.ts +87 -0
  102. package/src/lib/feed.ts +22 -7
  103. package/src/lib/nav-reorder.ts +1 -1
  104. package/src/lib/navigation.ts +45 -8
  105. package/src/lib/pagination.ts +50 -0
  106. package/src/lib/render.tsx +7 -14
  107. package/src/lib/schemas.ts +119 -51
  108. package/src/lib/theme.ts +5 -5
  109. package/src/lib/time.ts +64 -0
  110. package/src/lib/timeline.ts +141 -0
  111. package/src/lib/view.ts +80 -82
  112. package/src/preset.css +46 -0
  113. package/src/routes/__tests__/compose.test.ts +199 -0
  114. package/src/routes/api/__tests__/collections.test.ts +249 -0
  115. package/src/routes/api/__tests__/nav-items.test.ts +222 -0
  116. package/src/routes/api/__tests__/pages.test.ts +218 -0
  117. package/src/routes/api/__tests__/posts.test.ts +50 -108
  118. package/src/routes/api/__tests__/search.test.ts +2 -3
  119. package/src/routes/api/__tests__/settings.test.ts +132 -0
  120. package/src/routes/api/collections.ts +143 -0
  121. package/src/routes/api/nav-items.ts +115 -0
  122. package/src/routes/api/pages.ts +101 -0
  123. package/src/routes/api/posts.ts +28 -28
  124. package/src/routes/api/search.ts +3 -3
  125. package/src/routes/api/settings.ts +91 -0
  126. package/src/routes/api/upload.ts +16 -6
  127. package/src/routes/compose.ts +63 -0
  128. package/src/routes/dash/__tests__/pages.test.ts +225 -0
  129. package/src/routes/dash/collections.tsx +20 -42
  130. package/src/routes/dash/index.tsx +3 -3
  131. package/src/routes/dash/media.tsx +2 -2
  132. package/src/routes/dash/pages.tsx +480 -122
  133. package/src/routes/dash/posts.tsx +42 -54
  134. package/src/routes/dash/redirects.tsx +2 -2
  135. package/src/routes/dash/settings.tsx +83 -5
  136. package/src/routes/feed/rss.ts +4 -3
  137. package/src/routes/feed/sitemap.ts +15 -5
  138. package/src/routes/pages/__tests__/collections.test.ts +94 -0
  139. package/src/routes/pages/__tests__/featured.test.ts +94 -0
  140. package/src/routes/pages/archive.tsx +15 -15
  141. package/src/routes/pages/collection.tsx +16 -9
  142. package/src/routes/pages/collections.tsx +36 -0
  143. package/src/routes/pages/featured.tsx +38 -0
  144. package/src/routes/pages/home.tsx +21 -92
  145. package/src/routes/pages/page.tsx +62 -27
  146. package/src/routes/pages/post.tsx +6 -18
  147. package/src/routes/pages/search.tsx +3 -7
  148. package/src/services/__tests__/collection.test.ts +257 -158
  149. package/src/services/__tests__/media.test.ts +18 -18
  150. package/src/services/__tests__/navigation.test.ts +161 -87
  151. package/src/services/__tests__/page.test.ts +106 -0
  152. package/src/services/__tests__/post-timeline.test.ts +92 -88
  153. package/src/services/__tests__/post.test.ts +432 -197
  154. package/src/services/__tests__/search.test.ts +19 -25
  155. package/src/services/collection.ts +71 -113
  156. package/src/services/index.ts +9 -8
  157. package/src/services/navigation.ts +38 -71
  158. package/src/services/page.ts +136 -0
  159. package/src/services/post.ts +141 -101
  160. package/src/services/search.ts +38 -27
  161. package/src/styles/tokens.css +47 -0
  162. package/src/styles/ui.css +491 -0
  163. package/src/types.ts +212 -198
  164. package/src/ui/compose/ComposeDialog.tsx +395 -0
  165. package/src/ui/compose/ComposePrompt.tsx +55 -0
  166. package/src/ui/dash/FormatBadge.tsx +28 -0
  167. package/src/{theme/components → ui/dash}/PageForm.tsx +21 -21
  168. package/src/{theme/components → ui/dash}/PostForm.tsx +110 -131
  169. package/src/ui/dash/PostList.tsx +101 -0
  170. package/src/ui/dash/StatusBadge.tsx +61 -0
  171. package/src/ui/dash/index.ts +10 -0
  172. package/src/ui/feed/LinkCard.tsx +72 -0
  173. package/src/ui/feed/NoteCard.tsx +63 -0
  174. package/src/ui/feed/QuoteCard.tsx +68 -0
  175. package/src/ui/feed/ThreadPreview.tsx +48 -0
  176. package/src/ui/feed/TimelineFeed.tsx +49 -0
  177. package/src/ui/feed/TimelineItem.tsx +45 -0
  178. package/src/{theme → ui}/layouts/BaseLayout.tsx +11 -1
  179. package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
  180. package/src/ui/layouts/SiteLayout.tsx +150 -0
  181. package/src/ui/pages/ArchivePage.tsx +162 -0
  182. package/src/ui/pages/CollectionPage.tsx +70 -0
  183. package/src/ui/pages/CollectionsPage.tsx +73 -0
  184. package/src/ui/pages/FeaturedPage.tsx +31 -0
  185. package/src/ui/pages/HomePage.tsx +37 -0
  186. package/src/ui/pages/PostPage.tsx +56 -0
  187. package/src/{themes/minimal → ui}/pages/SearchPage.tsx +24 -20
  188. package/src/{themes/minimal → ui}/pages/SinglePage.tsx +5 -5
  189. package/src/ui/shared/MediaGallery.tsx +59 -0
  190. package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
  191. package/src/{theme/components → ui/shared}/ThreadView.tsx +6 -3
  192. package/src/ui/shared/__tests__/pagination.test.ts +46 -0
  193. package/src/ui/shared/index.ts +12 -0
  194. package/bin/jant.js +0 -185
  195. package/dist/lib/theme-components.js +0 -49
  196. package/dist/routes/api/timeline.js +0 -120
  197. package/dist/routes/dash/navigation.js +0 -288
  198. package/dist/theme/components/MediaGallery.js +0 -107
  199. package/dist/theme/components/VisibilityBadge.js +0 -37
  200. package/dist/theme/index.js +0 -18
  201. package/dist/theme/layouts/index.js +0 -2
  202. package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
  203. package/dist/themes/minimal/index.js +0 -65
  204. package/dist/themes/minimal/pages/CollectionPage.js +0 -65
  205. package/dist/themes/minimal/pages/HomePage.js +0 -25
  206. package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
  207. package/dist/themes/minimal/timeline/ImageCard.js +0 -67
  208. package/dist/themes/minimal/timeline/LinkCard.js +0 -47
  209. package/dist/themes/minimal/timeline/NoteCard.js +0 -34
  210. package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
  211. package/dist/themes/minimal/timeline/TimelineItem.js +0 -44
  212. package/src/lib/__tests__/theme-components.test.ts +0 -126
  213. package/src/lib/theme-components.ts +0 -68
  214. package/src/routes/api/timeline.tsx +0 -159
  215. package/src/routes/dash/navigation.tsx +0 -316
  216. package/src/theme/components/MediaGallery.tsx +0 -128
  217. package/src/theme/components/PostList.tsx +0 -92
  218. package/src/theme/components/TypeBadge.tsx +0 -37
  219. package/src/theme/components/VisibilityBadge.tsx +0 -45
  220. package/src/theme/components/index.ts +0 -23
  221. package/src/theme/index.ts +0 -22
  222. package/src/theme/layouts/index.ts +0 -7
  223. package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
  224. package/src/themes/minimal/index.ts +0 -83
  225. package/src/themes/minimal/pages/ArchivePage.tsx +0 -157
  226. package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
  227. package/src/themes/minimal/pages/HomePage.tsx +0 -41
  228. package/src/themes/minimal/pages/PostPage.tsx +0 -43
  229. package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
  230. package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
  231. package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
  232. package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
  233. package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
  234. package/src/themes/minimal/timeline/ThreadPreview.tsx +0 -47
  235. package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
  236. package/src/themes/minimal/timeline/TimelineItem.tsx +0 -75
  237. /package/dist/{theme → ui}/color-themes.js +0 -0
  238. /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
  239. /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
  240. /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
  241. /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
  242. /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
  243. /package/src/{theme → ui}/color-themes.ts +0 -0
  244. /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
  245. /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
  246. /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
  247. /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
  248. /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
package/src/app.tsx CHANGED
@@ -11,7 +11,6 @@ import { i18nMiddleware } from "./i18n/index.js";
11
11
  import { useLingui } from "@lingui/react/macro";
12
12
  import type { Bindings, JantConfig } from "./types.js";
13
13
  import { SETTINGS_KEYS } from "./lib/constants.js";
14
- import { theme as minimalTheme } from "./themes/minimal/index.js";
15
14
  import { hashPassword } from "better-auth/crypto";
16
15
 
17
16
  // Routes - Pages
@@ -21,6 +20,8 @@ import { pageRoutes } from "./routes/pages/page.js";
21
20
  import { collectionRoutes } from "./routes/pages/collection.js";
22
21
  import { archiveRoutes } from "./routes/pages/archive.js";
23
22
  import { searchRoutes } from "./routes/pages/search.js";
23
+ import { featuredRoutes } from "./routes/pages/featured.js";
24
+ import { collectionsPageRoutes } from "./routes/pages/collections.js";
24
25
 
25
26
  // Routes - Dashboard
26
27
  import { dashIndexRoutes } from "./routes/dash/index.js";
@@ -30,13 +31,17 @@ import { mediaRoutes as dashMediaRoutes } from "./routes/dash/media.js";
30
31
  import { settingsRoutes as dashSettingsRoutes } from "./routes/dash/settings.js";
31
32
  import { redirectsRoutes as dashRedirectsRoutes } from "./routes/dash/redirects.js";
32
33
  import { collectionsRoutes as dashCollectionsRoutes } from "./routes/dash/collections.js";
33
- import { navigationRoutes as dashNavigationRoutes } from "./routes/dash/navigation.js";
34
34
 
35
35
  // Routes - API
36
36
  import { postsApiRoutes } from "./routes/api/posts.js";
37
+ import { pagesApiRoutes } from "./routes/api/pages.js";
38
+ import { navItemsApiRoutes } from "./routes/api/nav-items.js";
39
+ import { collectionsApiRoutes } from "./routes/api/collections.js";
40
+ import { settingsApiRoutes } from "./routes/api/settings.js";
37
41
  import { uploadApiRoutes } from "./routes/api/upload.js";
38
42
  import { searchApiRoutes } from "./routes/api/search.js";
39
- import { timelineApiRoutes } from "./routes/api/timeline.js";
43
+ // Routes - Compose
44
+ import { composeRoutes } from "./routes/compose.js";
40
45
 
41
46
  // Routes - Feed
42
47
  import { rssRoutes } from "./routes/feed/rss.js";
@@ -47,7 +52,7 @@ import { requireAuth } from "./middleware/auth.js";
47
52
  import { requireOnboarding } from "./middleware/onboarding.js";
48
53
 
49
54
  // Layouts for auth pages
50
- import { BaseLayout } from "./theme/layouts/index.js";
55
+ import { BaseLayout } from "./ui/layouts/BaseLayout.js";
51
56
  import { dsRedirect, dsToast } from "./lib/sse.js";
52
57
  import { getAvailableThemes, buildThemeStyle } from "./lib/theme.js";
53
58
  import { createStorageDriver, type StorageDriver } from "./lib/storage.js";
@@ -58,6 +63,8 @@ export interface AppVariables {
58
63
  auth: Auth;
59
64
  config: JantConfig;
60
65
  themeStyle: string;
66
+ customCSS: string;
67
+ isAuthenticated: boolean;
61
68
  storage: StorageDriver | null;
62
69
  }
63
70
 
@@ -78,29 +85,12 @@ export type App = Hono<{ Bindings: Bindings; Variables: AppVariables }>;
78
85
  * import { createApp } from "@jant/core";
79
86
  *
80
87
  * export default createApp({
81
- * theme: { components: { PostPage: MyPostPage } },
88
+ * cssVariables: { "--card-radius": "0" },
82
89
  * });
83
90
  * ```
84
91
  */
85
92
  export function createApp(config: JantConfig = {}): App {
86
- // Merge with default minimal theme
87
- const defaultTheme = minimalTheme();
88
- const resolvedConfig: JantConfig = {
89
- ...config,
90
- theme: {
91
- name: config.theme?.name ?? defaultTheme.name,
92
- components: {
93
- ...defaultTheme.components,
94
- ...config.theme?.components,
95
- },
96
- cssVariables: {
97
- ...defaultTheme.cssVariables,
98
- ...config.theme?.cssVariables,
99
- },
100
- colorThemes: config.theme?.colorThemes ?? defaultTheme.colorThemes,
101
- feed: config.theme?.feed,
102
- },
103
- };
93
+ const resolvedConfig: JantConfig = { ...config };
104
94
 
105
95
  const app = new Hono<{ Bindings: Bindings; Variables: AppVariables }>();
106
96
 
@@ -134,18 +124,37 @@ export function createApp(config: JantConfig = {}): App {
134
124
  // Onboarding gate — redirect to /setup if not yet initialized
135
125
  app.use("*", requireOnboarding());
136
126
 
137
- // Theme middleware - resolve active color theme and build CSS
127
+ // Theme middleware - resolve active color theme, custom CSS, and auth state
138
128
  app.use("*", async (c, next) => {
139
- const themeId = await c.var.services.settings.get(SETTINGS_KEYS.THEME);
129
+ const [themeId, customCSS] = await Promise.all([
130
+ c.var.services.settings.get(SETTINGS_KEYS.THEME),
131
+ c.var.services.settings.get(SETTINGS_KEYS.CUSTOM_CSS),
132
+ ]);
140
133
  const themes = getAvailableThemes(resolvedConfig);
141
134
  const activeTheme = themeId
142
135
  ? themes.find((t) => t.id === themeId)
143
136
  : undefined;
144
137
  const themeStyle = buildThemeStyle(
145
138
  activeTheme,
146
- resolvedConfig.theme?.cssVariables,
139
+ resolvedConfig.cssVariables,
147
140
  );
148
141
  c.set("themeStyle", themeStyle);
142
+ c.set("customCSS", customCSS ?? "");
143
+
144
+ // Check auth state for data-authenticated attribute on <body>
145
+ let isAuthenticated = false;
146
+ if (c.var.auth) {
147
+ try {
148
+ const session = await c.var.auth.api.getSession({
149
+ headers: c.req.raw.headers,
150
+ });
151
+ isAuthenticated = !!session;
152
+ } catch {
153
+ // Not authenticated
154
+ }
155
+ }
156
+ c.set("isAuthenticated", isAuthenticated);
157
+
149
158
  await next();
150
159
  });
151
160
 
@@ -197,7 +206,10 @@ export function createApp(config: JantConfig = {}): App {
197
206
 
198
207
  // API Routes
199
208
  app.route("/api/posts", postsApiRoutes);
200
- app.route("/api/timeline", timelineApiRoutes);
209
+ app.route("/api/pages", pagesApiRoutes);
210
+ app.route("/api/nav-items", navItemsApiRoutes);
211
+ app.route("/api/collections", collectionsApiRoutes);
212
+ app.route("/api/settings", settingsApiRoutes);
201
213
 
202
214
  // Setup page component
203
215
  const SetupContent: FC = () => {
@@ -340,6 +352,18 @@ export function createApp(config: JantConfig = {}): App {
340
352
 
341
353
  await c.var.services.settings.completeOnboarding();
342
354
 
355
+ // Seed default navigation items
356
+ await c.var.services.navItems.create({
357
+ type: "link",
358
+ label: "Featured",
359
+ url: "/featured",
360
+ });
361
+ await c.var.services.navItems.create({
362
+ type: "link",
363
+ label: "Collections",
364
+ url: "/collections",
365
+ });
366
+
343
367
  return dsRedirect("/signin?setup");
344
368
  } catch (err) {
345
369
  // eslint-disable-next-line no-console -- Error logging is intentional
@@ -720,7 +744,6 @@ export function createApp(config: JantConfig = {}): App {
720
744
  app.route("/dash/settings", dashSettingsRoutes);
721
745
  app.route("/dash/redirects", dashRedirectsRoutes);
722
746
  app.route("/dash/collections", dashCollectionsRoutes);
723
- app.route("/dash/navigation", dashNavigationRoutes);
724
747
  // API routes
725
748
  app.route("/api/upload", uploadApiRoutes);
726
749
  app.route("/api/search", searchApiRoutes);
@@ -753,6 +776,9 @@ export function createApp(config: JantConfig = {}): App {
753
776
  return new Response(object.body, { headers });
754
777
  });
755
778
 
779
+ // Compose route (auth enforced in route middleware)
780
+ app.route("/compose", composeRoutes);
781
+
756
782
  // Feed routes
757
783
  app.route("/feed", rssRoutes);
758
784
  app.route("/", sitemapRoutes);
@@ -760,6 +786,8 @@ export function createApp(config: JantConfig = {}): App {
760
786
  // Frontend routes
761
787
  app.route("/search", searchRoutes);
762
788
  app.route("/archive", archiveRoutes);
789
+ app.route("/featured", featuredRoutes);
790
+ app.route("/collections", collectionsPageRoutes);
763
791
  app.route("/c", collectionRoutes);
764
792
  app.route("/p", postRoutes);
765
793
  app.route("/", homeRoutes);
@@ -0,0 +1,268 @@
1
+ -- v2 Schema Migration
2
+ -- Restructures posts, creates pages, updates collections, replaces navigation_links with nav_items
3
+
4
+ -- Disable FK checks for migration (dropping/recreating tables with cross-references)
5
+ PRAGMA foreign_keys = OFF;
6
+ --> statement-breakpoint
7
+
8
+ -- =============================================================================
9
+ -- 1. Create pages table (before modifying posts, migrate type='page' data)
10
+ -- =============================================================================
11
+
12
+ CREATE TABLE `pages` (
13
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
14
+ `slug` text NOT NULL,
15
+ `title` text,
16
+ `body` text,
17
+ `body_html` text,
18
+ `status` text DEFAULT 'published' NOT NULL,
19
+ `created_at` integer NOT NULL,
20
+ `updated_at` integer NOT NULL
21
+ );
22
+ --> statement-breakpoint
23
+ CREATE UNIQUE INDEX `pages_slug_unique` ON `pages` (`slug`);
24
+ --> statement-breakpoint
25
+
26
+ -- Migrate type='page' posts into pages table
27
+ INSERT INTO `pages` (`slug`, `title`, `body`, `body_html`, `status`, `created_at`, `updated_at`)
28
+ SELECT
29
+ CASE
30
+ WHEN `path` IS NOT NULL AND `path` != '' THEN REPLACE(`path`, '/', '')
31
+ ELSE 'page-' || `id`
32
+ END,
33
+ `title`,
34
+ `content`,
35
+ `content_html`,
36
+ CASE WHEN `visibility` = 'draft' THEN 'draft' ELSE 'published' END,
37
+ `created_at`,
38
+ `updated_at`
39
+ FROM `posts`
40
+ WHERE `type` = 'page';
41
+ --> statement-breakpoint
42
+
43
+ -- =============================================================================
44
+ -- 2. Restructure posts table (create new → migrate → drop old → rename)
45
+ -- =============================================================================
46
+
47
+ CREATE TABLE `posts_new` (
48
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
49
+ `format` text DEFAULT 'note' NOT NULL,
50
+ `status` text DEFAULT 'published' NOT NULL,
51
+ `featured` integer DEFAULT 0 NOT NULL,
52
+ `pinned` integer DEFAULT 0 NOT NULL,
53
+ `slug` text,
54
+ `title` text,
55
+ `url` text,
56
+ `body` text,
57
+ `body_html` text,
58
+ `quote_text` text,
59
+ `rating` integer,
60
+ `collection_id` integer,
61
+ `reply_to_id` integer,
62
+ `thread_id` integer,
63
+ `deleted_at` integer,
64
+ `published_at` integer NOT NULL,
65
+ `created_at` integer NOT NULL,
66
+ `updated_at` integer NOT NULL,
67
+ FOREIGN KEY (`collection_id`) REFERENCES `collections`(`id`) ON UPDATE no action ON DELETE set null
68
+ );
69
+ --> statement-breakpoint
70
+
71
+ -- Migrate data from old posts to new posts (excluding type='page')
72
+ INSERT INTO `posts_new` (
73
+ `id`, `format`, `status`, `featured`, `pinned`,
74
+ `slug`, `title`, `url`, `body`, `body_html`, `quote_text`, `rating`,
75
+ `collection_id`, `reply_to_id`, `thread_id`,
76
+ `deleted_at`, `published_at`, `created_at`, `updated_at`
77
+ )
78
+ SELECT
79
+ `id`,
80
+ -- format mapping: article→note, image→note, note→note, link→link, quote→quote
81
+ CASE
82
+ WHEN `type` IN ('article', 'image', 'note') THEN 'note'
83
+ WHEN `type` = 'link' THEN 'link'
84
+ WHEN `type` = 'quote' THEN 'quote'
85
+ ELSE 'note'
86
+ END,
87
+ -- status mapping from visibility
88
+ CASE WHEN `visibility` = 'draft' THEN 'draft' ELSE 'published' END,
89
+ -- featured mapping from visibility
90
+ CASE WHEN `visibility` = 'featured' THEN 1 ELSE 0 END,
91
+ -- pinned: default 0
92
+ 0,
93
+ -- slug: migrate from path (strip leading /)
94
+ CASE
95
+ WHEN `path` IS NOT NULL AND `path` != '' THEN REPLACE(`path`, '/', '')
96
+ ELSE NULL
97
+ END,
98
+ `title`,
99
+ `source_url`,
100
+ `content`,
101
+ `content_html`,
102
+ -- quote_text: for quote type, content was the quote; set to null (manual fix may be needed)
103
+ NULL,
104
+ -- rating: null
105
+ NULL,
106
+ -- collection_id: migrate from post_collections (take first collection for each post)
107
+ (SELECT `collection_id` FROM `post_collections` WHERE `post_collections`.`post_id` = `posts`.`id` LIMIT 1),
108
+ `reply_to_id`,
109
+ `thread_id`,
110
+ `deleted_at`,
111
+ `published_at`,
112
+ `created_at`,
113
+ `updated_at`
114
+ FROM `posts`
115
+ WHERE `type` != 'page';
116
+ --> statement-breakpoint
117
+
118
+ -- Update media references to point at new table (foreign keys reference posts)
119
+ -- media.post_id still works since IDs are preserved
120
+ --> statement-breakpoint
121
+
122
+ -- Drop old posts table and rename new one
123
+ DROP TABLE `posts`;
124
+ --> statement-breakpoint
125
+ ALTER TABLE `posts_new` RENAME TO `posts`;
126
+ --> statement-breakpoint
127
+ CREATE UNIQUE INDEX `posts_slug_unique` ON `posts` (`slug`);
128
+ --> statement-breakpoint
129
+
130
+ -- =============================================================================
131
+ -- 3. Update collections table (add new columns, rename path→slug)
132
+ -- =============================================================================
133
+
134
+ CREATE TABLE `collections_new` (
135
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
136
+ `slug` text NOT NULL,
137
+ `title` text NOT NULL,
138
+ `description` text,
139
+ `icon` text,
140
+ `sort_order` text DEFAULT 'newest' NOT NULL,
141
+ `position` integer DEFAULT 0 NOT NULL,
142
+ `show_divider` integer DEFAULT 0 NOT NULL,
143
+ `created_at` integer NOT NULL,
144
+ `updated_at` integer NOT NULL
145
+ );
146
+ --> statement-breakpoint
147
+ CREATE UNIQUE INDEX `collections_new_slug_unique` ON `collections_new` (`slug`);
148
+ --> statement-breakpoint
149
+
150
+ INSERT INTO `collections_new` (`id`, `slug`, `title`, `description`, `icon`, `sort_order`, `position`, `show_divider`, `created_at`, `updated_at`)
151
+ SELECT
152
+ `id`,
153
+ COALESCE(`path`, 'collection-' || `id`),
154
+ `title`,
155
+ `description`,
156
+ NULL,
157
+ 'newest',
158
+ 0,
159
+ 0,
160
+ `created_at`,
161
+ `updated_at`
162
+ FROM `collections`;
163
+ --> statement-breakpoint
164
+
165
+ DROP TABLE `collections`;
166
+ --> statement-breakpoint
167
+ ALTER TABLE `collections_new` RENAME TO `collections`;
168
+ --> statement-breakpoint
169
+ CREATE UNIQUE INDEX `collections_slug_unique` ON `collections` (`slug`);
170
+ --> statement-breakpoint
171
+
172
+ -- =============================================================================
173
+ -- 4. Replace navigation_links with nav_items
174
+ -- =============================================================================
175
+
176
+ CREATE TABLE `nav_items` (
177
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
178
+ `type` text DEFAULT 'link' NOT NULL,
179
+ `label` text NOT NULL,
180
+ `url` text NOT NULL,
181
+ `page_id` integer,
182
+ `position` integer DEFAULT 0 NOT NULL,
183
+ `created_at` integer NOT NULL,
184
+ `updated_at` integer NOT NULL,
185
+ FOREIGN KEY (`page_id`) REFERENCES `pages`(`id`) ON UPDATE no action ON DELETE cascade
186
+ );
187
+ --> statement-breakpoint
188
+
189
+ -- Migrate existing navigation_links as type='link'
190
+ INSERT INTO `nav_items` (`type`, `label`, `url`, `page_id`, `position`, `created_at`, `updated_at`)
191
+ SELECT 'link', `label`, `url`, NULL, `position`, `created_at`, `updated_at`
192
+ FROM `navigation_links`;
193
+ --> statement-breakpoint
194
+
195
+ DROP TABLE `navigation_links`;
196
+ --> statement-breakpoint
197
+
198
+ -- =============================================================================
199
+ -- 5. Drop post_collections table (replaced by posts.collection_id)
200
+ -- =============================================================================
201
+
202
+ DROP TABLE `post_collections`;
203
+ --> statement-breakpoint
204
+
205
+ -- =============================================================================
206
+ -- 6. Rebuild FTS5 (column rename: content→body, add quote_text)
207
+ -- =============================================================================
208
+
209
+ -- Drop old FTS triggers
210
+ DROP TRIGGER IF EXISTS posts_fts_insert;
211
+ --> statement-breakpoint
212
+ DROP TRIGGER IF EXISTS posts_fts_update;
213
+ --> statement-breakpoint
214
+ DROP TRIGGER IF EXISTS posts_fts_delete;
215
+ --> statement-breakpoint
216
+
217
+ -- Drop old FTS table
218
+ DROP TABLE IF EXISTS posts_fts;
219
+ --> statement-breakpoint
220
+
221
+ -- Create new FTS table with updated columns
222
+ CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
223
+ title,
224
+ body,
225
+ quote_text,
226
+ content=posts,
227
+ content_rowid=id,
228
+ tokenize='trigram'
229
+ );
230
+ --> statement-breakpoint
231
+
232
+ -- Populate FTS with migrated data
233
+ INSERT INTO posts_fts(rowid, title, body, quote_text)
234
+ SELECT id, COALESCE(title, ''), COALESCE(body, ''), COALESCE(quote_text, '')
235
+ FROM posts WHERE deleted_at IS NULL;
236
+ --> statement-breakpoint
237
+
238
+ -- Trigger: sync FTS on INSERT
239
+ CREATE TRIGGER posts_fts_insert AFTER INSERT ON posts
240
+ WHEN NEW.deleted_at IS NULL
241
+ BEGIN
242
+ INSERT INTO posts_fts(rowid, title, body, quote_text)
243
+ VALUES (NEW.id, COALESCE(NEW.title, ''), COALESCE(NEW.body, ''), COALESCE(NEW.quote_text, ''));
244
+ END;
245
+ --> statement-breakpoint
246
+
247
+ -- Trigger: sync FTS on UPDATE
248
+ CREATE TRIGGER posts_fts_update AFTER UPDATE ON posts BEGIN
249
+ DELETE FROM posts_fts WHERE rowid = OLD.id;
250
+ INSERT INTO posts_fts(rowid, title, body, quote_text)
251
+ SELECT NEW.id, COALESCE(NEW.title, ''), COALESCE(NEW.body, ''), COALESCE(NEW.quote_text, '')
252
+ WHERE NEW.deleted_at IS NULL;
253
+ END;
254
+ --> statement-breakpoint
255
+
256
+ -- Trigger: sync FTS on DELETE
257
+ CREATE TRIGGER posts_fts_delete AFTER DELETE ON posts BEGIN
258
+ DELETE FROM posts_fts WHERE rowid = OLD.id;
259
+ END;
260
+ --> statement-breakpoint
261
+
262
+ -- =============================================================================
263
+ -- 7. Re-enable FK checks and verify integrity
264
+ -- =============================================================================
265
+
266
+ PRAGMA foreign_keys = ON;
267
+ --> statement-breakpoint
268
+ PRAGMA foreign_key_check;
@@ -0,0 +1,5 @@
1
+ ALTER TABLE posts RENAME COLUMN slug TO path;
2
+ --> statement-breakpoint
3
+ DROP INDEX IF EXISTS posts_slug_unique;
4
+ --> statement-breakpoint
5
+ CREATE UNIQUE INDEX posts_path_unique ON posts (path);
@@ -36,6 +36,20 @@
36
36
  "when": 1770946168874,
37
37
  "tag": "0004_add_storage_provider",
38
38
  "breakpoints": true
39
+ },
40
+ {
41
+ "idx": 5,
42
+ "version": "6",
43
+ "when": 1771346168875,
44
+ "tag": "0005_v2_schema_migration",
45
+ "breakpoints": true
46
+ },
47
+ {
48
+ "idx": 6,
49
+ "version": "6",
50
+ "when": 1771746168876,
51
+ "tag": "0006_rename_slug_to_path",
52
+ "breakpoints": true
39
53
  }
40
54
  ]
41
55
  }
package/src/db/schema.ts CHANGED
@@ -1,15 +1,10 @@
1
1
  /**
2
2
  * Drizzle Schema
3
3
  *
4
- * Database schema for Jant
4
+ * Database schema for Jant v2
5
5
  */
6
6
 
7
- import {
8
- sqliteTable,
9
- text,
10
- integer,
11
- primaryKey,
12
- } from "drizzle-orm/sqlite-core";
7
+ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
13
8
 
14
9
  // =============================================================================
15
10
  // Posts
@@ -17,21 +12,26 @@ import {
17
12
 
18
13
  export const posts = sqliteTable("posts", {
19
14
  id: integer("id").primaryKey({ autoIncrement: true }),
20
- type: text("type", {
21
- enum: ["note", "article", "link", "quote", "image", "page"],
15
+ format: text("format", {
16
+ enum: ["note", "link", "quote"],
22
17
  }).notNull(),
23
- visibility: text("visibility", {
24
- enum: ["featured", "quiet", "unlisted", "draft"],
18
+ status: text("status", {
19
+ enum: ["draft", "published"],
25
20
  })
26
21
  .notNull()
27
- .default("quiet"),
22
+ .default("published"),
23
+ featured: integer("featured").notNull().default(0),
24
+ pinned: integer("pinned").notNull().default(0),
25
+ path: text("path").unique(),
28
26
  title: text("title"),
29
- path: text("path"),
30
- content: text("content"),
31
- contentHtml: text("content_html"),
32
- sourceUrl: text("source_url"),
33
- sourceName: text("source_name"),
34
- sourceDomain: text("source_domain"),
27
+ url: text("url"),
28
+ body: text("body"),
29
+ bodyHtml: text("body_html"),
30
+ quoteText: text("quote_text"),
31
+ rating: integer("rating"),
32
+ collectionId: integer("collection_id").references(() => collections.id, {
33
+ onDelete: "set null",
34
+ }),
35
35
  replyToId: integer("reply_to_id"),
36
36
  threadId: integer("thread_id"),
37
37
  deletedAt: integer("deleted_at"),
@@ -40,6 +40,25 @@ export const posts = sqliteTable("posts", {
40
40
  updatedAt: integer("updated_at").notNull(),
41
41
  });
42
42
 
43
+ // =============================================================================
44
+ // Pages
45
+ // =============================================================================
46
+
47
+ export const pages = sqliteTable("pages", {
48
+ id: integer("id").primaryKey({ autoIncrement: true }),
49
+ slug: text("slug").notNull().unique(),
50
+ title: text("title"),
51
+ body: text("body"),
52
+ bodyHtml: text("body_html"),
53
+ status: text("status", {
54
+ enum: ["draft", "published"],
55
+ })
56
+ .notNull()
57
+ .default("published"),
58
+ createdAt: integer("created_at").notNull(),
59
+ updatedAt: integer("updated_at").notNull(),
60
+ });
61
+
43
62
  // =============================================================================
44
63
  // Media
45
64
  // =============================================================================
@@ -67,30 +86,41 @@ export const media = sqliteTable("media", {
67
86
 
68
87
  export const collections = sqliteTable("collections", {
69
88
  id: integer("id").primaryKey({ autoIncrement: true }),
70
- path: text("path").unique(),
89
+ slug: text("slug").notNull().unique(),
71
90
  title: text("title").notNull(),
72
91
  description: text("description"),
92
+ icon: text("icon"),
93
+ sortOrder: text("sort_order", {
94
+ enum: ["newest", "oldest", "rating_desc", "rating_asc"],
95
+ })
96
+ .notNull()
97
+ .default("newest"),
98
+ position: integer("position").notNull().default(0),
99
+ showDivider: integer("show_divider").notNull().default(0),
73
100
  createdAt: integer("created_at").notNull(),
74
101
  updatedAt: integer("updated_at").notNull(),
75
102
  });
76
103
 
77
104
  // =============================================================================
78
- // Post-Collections (Many-to-Many)
105
+ // Navigation Items
79
106
  // =============================================================================
80
107
 
81
- export const postCollections = sqliteTable(
82
- "post_collections",
83
- {
84
- postId: integer("post_id")
85
- .notNull()
86
- .references(() => posts.id),
87
- collectionId: integer("collection_id")
88
- .notNull()
89
- .references(() => collections.id),
90
- addedAt: integer("added_at").notNull(),
91
- },
92
- (table) => [primaryKey({ columns: [table.postId, table.collectionId] })],
93
- );
108
+ export const navItems = sqliteTable("nav_items", {
109
+ id: integer("id").primaryKey({ autoIncrement: true }),
110
+ type: text("type", {
111
+ enum: ["page", "link"],
112
+ })
113
+ .notNull()
114
+ .default("link"),
115
+ label: text("label").notNull(),
116
+ url: text("url").notNull(),
117
+ pageId: integer("page_id").references(() => pages.id, {
118
+ onDelete: "cascade",
119
+ }),
120
+ position: integer("position").notNull().default(0),
121
+ createdAt: integer("created_at").notNull(),
122
+ updatedAt: integer("updated_at").notNull(),
123
+ });
94
124
 
95
125
  // =============================================================================
96
126
  // Redirects
@@ -104,19 +134,6 @@ export const redirects = sqliteTable("redirects", {
104
134
  createdAt: integer("created_at").notNull(),
105
135
  });
106
136
 
107
- // =============================================================================
108
- // Navigation Links
109
- // =============================================================================
110
-
111
- export const navigationLinks = sqliteTable("navigation_links", {
112
- id: integer("id").primaryKey({ autoIncrement: true }),
113
- label: text("label").notNull(),
114
- url: text("url").notNull(),
115
- position: integer("position").notNull().default(0),
116
- createdAt: integer("created_at").notNull(),
117
- updatedAt: integer("updated_at").notNull(),
118
- });
119
-
120
137
  // =============================================================================
121
138
  // Settings (Key-Value)
122
139
  // =============================================================================