@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
@@ -0,0 +1,802 @@
1
+ /**
2
+ * Export Service
3
+ *
4
+ * Generates a ready-to-use Zola static site as a ZIP archive.
5
+ * Threads are merged into single pages with reply marker comments.
6
+ * Media URLs point to the original site (not exported).
7
+ */
8
+
9
+ import type { PostService } from "./post.js";
10
+ import type { PathService } from "./path.js";
11
+ import type { CollectionService } from "./collection.js";
12
+ import { tiptapJsonToMarkdown } from "../lib/tiptap-to-markdown.js";
13
+ import { toISOString } from "../lib/time.js";
14
+ import type { Post, Collection } from "../types.js";
15
+
16
+ export interface ExportService {
17
+ generateZolaSite(): Promise<Uint8Array>;
18
+ }
19
+
20
+ interface SiteConfig {
21
+ siteName: string;
22
+ siteUrl: string;
23
+ siteDescription: string;
24
+ siteLanguage: string;
25
+ }
26
+
27
+ export function createExportService(
28
+ services: {
29
+ posts: PostService;
30
+ paths: PathService;
31
+ collections: CollectionService;
32
+ },
33
+ siteConfig: SiteConfig,
34
+ ): ExportService {
35
+ return {
36
+ async generateZolaSite() {
37
+ // 1. Query all data
38
+ const [allPosts, allCollections] = await Promise.all([
39
+ services.posts.list({
40
+ excludeReplies: false,
41
+ limit: 10000,
42
+ }),
43
+ services.collections.list(),
44
+ ]);
45
+
46
+ const allPostIds = allPosts.map((p) => p.id);
47
+ const roots = allPosts.filter((p) => p.replyToId === null);
48
+ const replies = allPosts.filter((p) => p.replyToId !== null);
49
+ const rootPostIds = roots.map((p) => p.id);
50
+
51
+ const [collectionsByPost, slugMap, aliasMap, collectionSlugMap] =
52
+ await Promise.all([
53
+ services.collections.getCollectionsByPostIds(allPostIds),
54
+ services.paths.getPostSlugMap(allPostIds),
55
+ services.paths.getPostAliases(rootPostIds),
56
+ services.paths.getCollectionSlugMap(allCollections.map((c) => c.id)),
57
+ ]);
58
+
59
+ // 2. Group replies by threadId
60
+ const repliesByThread = new Map<string, Post[]>();
61
+ for (const reply of replies) {
62
+ const list = repliesByThread.get(reply.threadId) ?? [];
63
+ list.push(reply);
64
+ repliesByThread.set(reply.threadId, list);
65
+ }
66
+ // Sort replies by createdAt within each thread
67
+ for (const list of repliesByThread.values()) {
68
+ list.sort((a, b) => a.createdAt - b.createdAt);
69
+ }
70
+
71
+ // 3. Build ZIP file structure
72
+ const { zipSync } = await import("fflate");
73
+ const files: Record<string, Uint8Array> = {};
74
+
75
+ // Generate post files
76
+ for (const root of roots) {
77
+ const slug = slugMap.get(root.id) ?? root.slug;
78
+ const threadReplies = repliesByThread.get(root.id) ?? [];
79
+ const postCollections = collectionsByPost.get(root.id) ?? [];
80
+ const aliases = aliasMap.get(root.id) ?? [];
81
+
82
+ // Collect reply slugs as aliases
83
+ for (const reply of threadReplies) {
84
+ const replySlug = slugMap.get(reply.id) ?? reply.slug;
85
+ aliases.push(`/${replySlug}`);
86
+ }
87
+
88
+ const markdown = buildPostMarkdown(
89
+ root,
90
+ threadReplies,
91
+ postCollections,
92
+ aliases,
93
+ slugMap,
94
+ collectionSlugMap,
95
+ );
96
+
97
+ files[`content/${slug}/index.md`] = new TextEncoder().encode(markdown);
98
+ }
99
+
100
+ // Generate scaffold
101
+ files["config.toml"] = new TextEncoder().encode(
102
+ buildConfigToml(siteConfig),
103
+ );
104
+ files["content/_index.md"] = new TextEncoder().encode(buildRootSection());
105
+ files["templates/base.html"] = new TextEncoder().encode(TEMPLATE_BASE);
106
+ files["templates/index.html"] = new TextEncoder().encode(TEMPLATE_INDEX);
107
+ files["templates/page.html"] = new TextEncoder().encode(TEMPLATE_PAGE);
108
+ files["templates/section.html"] = new TextEncoder().encode(
109
+ TEMPLATE_SECTION,
110
+ );
111
+ files["templates/taxonomy_list.html"] = new TextEncoder().encode(
112
+ TEMPLATE_TAXONOMY_LIST,
113
+ );
114
+ files["templates/taxonomy_single.html"] = new TextEncoder().encode(
115
+ TEMPLATE_TAXONOMY_SINGLE,
116
+ );
117
+ files["templates/macros.html"] = new TextEncoder().encode(
118
+ TEMPLATE_MACROS,
119
+ );
120
+ files["static/style.css"] = new TextEncoder().encode(STYLE_CSS);
121
+
122
+ return zipSync(files);
123
+ },
124
+ };
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Markdown generation
129
+ // ---------------------------------------------------------------------------
130
+
131
+ /** Escape a string for use inside a TOML double-quoted value */
132
+ function escapeToml(value: string): string {
133
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
134
+ }
135
+
136
+ /** Escape a string for use in YAML (wrap in quotes if needed) */
137
+ function yamlString(value: string): string {
138
+ // If value contains characters that need quoting in YAML
139
+ if (
140
+ /[:#{}[\],&*?|>!%@`"'\n\\]/.test(value) ||
141
+ value.startsWith(" ") ||
142
+ value.endsWith(" ") ||
143
+ value === "" ||
144
+ value === "true" ||
145
+ value === "false" ||
146
+ value === "null"
147
+ ) {
148
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`;
149
+ }
150
+ return value;
151
+ }
152
+
153
+ function buildPostMarkdown(
154
+ root: Post,
155
+ threadReplies: Post[],
156
+ postCollections: Collection[],
157
+ aliases: string[],
158
+ slugMap: Map<string, string>,
159
+ collectionSlugMap: Map<string, string>,
160
+ ): string {
161
+ const parts: string[] = [];
162
+
163
+ // Front matter (YAML)
164
+ parts.push("---");
165
+ if (root.title) {
166
+ parts.push(`title: ${yamlString(root.title)}`);
167
+ }
168
+ const date = root.publishedAt ?? root.createdAt;
169
+ if (date) {
170
+ parts.push(`date: ${toISOString(date)}`);
171
+ }
172
+ if (root.updatedAt && root.updatedAt !== root.publishedAt) {
173
+ parts.push(`updated: ${toISOString(root.updatedAt)}`);
174
+ }
175
+ if (root.status === "draft") {
176
+ parts.push("draft: true");
177
+ }
178
+
179
+ const slug = slugMap.get(root.id) ?? root.slug;
180
+ parts.push(`slug: ${yamlString(slug)}`);
181
+
182
+ if (aliases.length > 0) {
183
+ parts.push("aliases:");
184
+ for (const a of aliases) {
185
+ parts.push(` - ${yamlString(a)}`);
186
+ }
187
+ }
188
+
189
+ // Taxonomies
190
+ if (postCollections.length > 0) {
191
+ parts.push("taxonomies:");
192
+ parts.push(" c:");
193
+ for (const c of postCollections) {
194
+ const colSlug = collectionSlugMap.get(c.id) ?? c.slug;
195
+ parts.push(` - ${yamlString(colSlug)}`);
196
+ }
197
+ }
198
+
199
+ // Extra metadata
200
+ parts.push("extra:");
201
+ parts.push(` format: ${root.format}`);
202
+ if (root.url) {
203
+ parts.push(` link_url: ${yamlString(root.url)}`);
204
+ }
205
+ if (root.quoteText) {
206
+ parts.push(` quote_text: ${yamlString(root.quoteText)}`);
207
+ }
208
+ if (root.rating !== null) {
209
+ parts.push(` rating: ${root.rating}`);
210
+ }
211
+ if (root.pinnedAt !== null) {
212
+ parts.push(" pinned: true");
213
+ }
214
+ if (root.featuredAt !== null) {
215
+ parts.push(" featured: true");
216
+ }
217
+
218
+ parts.push("---");
219
+ parts.push("");
220
+
221
+ // Root body
222
+ if (root.body) {
223
+ parts.push(tiptapJsonToMarkdown(root.body));
224
+ }
225
+
226
+ // Thread replies
227
+ for (const reply of threadReplies) {
228
+ parts.push("");
229
+
230
+ // Reply marker comment
231
+ const replySlug = slugMap.get(reply.id) ?? reply.slug;
232
+ const esc = (s: string) =>
233
+ s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
234
+ let marker = `<!-- jant:reply date="${reply.publishedAt ? toISOString(reply.publishedAt) : ""}" slug="${esc(replySlug)}" format="${reply.format}"`;
235
+
236
+ if (reply.format === "link" && reply.url) {
237
+ marker += ` url="${esc(reply.url)}"`;
238
+ }
239
+ if (reply.format === "quote" && reply.quoteText) {
240
+ marker += ` quote_text="${encodeURIComponent(reply.quoteText)}"`;
241
+ }
242
+ if (reply.rating !== null) {
243
+ marker += ` rating="${reply.rating}"`;
244
+ }
245
+ if (reply.title) {
246
+ marker += ` title="${esc(reply.title)}"`;
247
+ }
248
+ marker += " -->";
249
+
250
+ parts.push(marker);
251
+ parts.push("");
252
+
253
+ if (reply.body) {
254
+ parts.push(tiptapJsonToMarkdown(reply.body));
255
+ }
256
+ }
257
+
258
+ return parts.join("\n");
259
+ }
260
+
261
+ function buildConfigToml(config: SiteConfig): string {
262
+ return `base_url = "${escapeToml(config.siteUrl || "https://example.com")}"
263
+ title = "${escapeToml(config.siteName)}"
264
+ description = "${escapeToml(config.siteDescription)}"
265
+ default_language = "${escapeToml(config.siteLanguage)}"
266
+ generate_feeds = true
267
+ compile_sass = false
268
+
269
+ [[taxonomies]]
270
+ name = "c"
271
+ feed = true
272
+
273
+ [markdown]
274
+ highlight_code = true
275
+ highlight_theme = "css"
276
+
277
+ [extra]
278
+ `;
279
+ }
280
+
281
+ function buildRootSection(): string {
282
+ return `+++
283
+ sort_by = "date"
284
+ paginate_by = 20
285
+ +++
286
+ `;
287
+ }
288
+
289
+ // ---------------------------------------------------------------------------
290
+ // Zola theme templates
291
+ // ---------------------------------------------------------------------------
292
+
293
+ const TEMPLATE_BASE = `<!DOCTYPE html>
294
+ <html lang="{{ config.default_language }}">
295
+ <head>
296
+ <meta charset="utf-8">
297
+ <meta name="viewport" content="width=device-width, initial-scale=1">
298
+ <title>{% block title %}{{ config.title }}{% endblock %}</title>
299
+ <meta name="description" content="{{ config.description }}">
300
+ <link rel="stylesheet" href="{{ get_url(path='style.css') }}">
301
+ <link rel="alternate" type="application/atom+xml" title="{{ config.title }}" href="{{ get_url(path='atom.xml') }}">
302
+ </head>
303
+ <body>
304
+ <header class="site-header">
305
+ <a href="{{ config.base_url }}" class="site-title">{{ config.title }}</a>
306
+ <nav>
307
+ <a href="{{ config.base_url }}/c/">Collections</a>
308
+ <a href="{{ get_url(path='atom.xml') }}">RSS</a>
309
+ </nav>
310
+ </header>
311
+ <main class="site-main">
312
+ {% block content %}{% endblock %}
313
+ </main>
314
+ <footer class="site-footer">
315
+ <p>Powered by <a href="https://github.com/jant-me/jant">Jant</a></p>
316
+ </footer>
317
+ </body>
318
+ </html>
319
+ `;
320
+
321
+ const TEMPLATE_INDEX = `{% extends "base.html" %}
322
+ {% import "macros.html" as macros %}
323
+
324
+ {% block title %}{{ config.title }}{% endblock %}
325
+
326
+ {% block content %}
327
+ <div class="post-list">
328
+ {% for page in paginator.pages %}
329
+ {{ macros::post_card(page=page) }}
330
+ {% endfor %}
331
+ </div>
332
+
333
+ {% if paginator.previous or paginator.next %}
334
+ <nav class="pagination">
335
+ {% if paginator.previous %}<a href="{{ paginator.previous }}">&larr; Newer</a>{% endif %}
336
+ {% if paginator.next %}<a href="{{ paginator.next }}">Older &rarr;</a>{% endif %}
337
+ </nav>
338
+ {% endif %}
339
+ {% endblock %}
340
+ `;
341
+
342
+ const TEMPLATE_PAGE = `{% extends "base.html" %}
343
+ {% import "macros.html" as macros %}
344
+
345
+ {% block title %}{% if page.title %}{{ page.title }} &mdash; {% endif %}{{ config.title }}{% endblock %}
346
+
347
+ {% block content %}
348
+ {{ macros::post_card(page=page, detail=true) }}
349
+ {% endblock %}
350
+ `;
351
+
352
+ const TEMPLATE_SECTION = `{% extends "base.html" %}
353
+ {% import "macros.html" as macros %}
354
+
355
+ {% block title %}{{ section.title }} &mdash; {{ config.title }}{% endblock %}
356
+
357
+ {% block content %}
358
+ <h1>{{ section.title }}</h1>
359
+ {% if section.description %}
360
+ <p class="section-description">{{ section.description }}</p>
361
+ {% endif %}
362
+
363
+ <div class="post-list">
364
+ {% for page in section.pages %}
365
+ {{ macros::post_card(page=page) }}
366
+ {% endfor %}
367
+ </div>
368
+ {% endblock %}
369
+ `;
370
+
371
+ const TEMPLATE_TAXONOMY_LIST = `{% extends "base.html" %}
372
+
373
+ {% block title %}Collections &mdash; {{ config.title }}{% endblock %}
374
+
375
+ {% block content %}
376
+ <h1>Collections</h1>
377
+ <ul class="collection-list">
378
+ {% for term in terms %}
379
+ <li>
380
+ <a href="{{ term.permalink }}">{{ term.name }}</a>
381
+ <span class="count">({{ term.pages | length }})</span>
382
+ </li>
383
+ {% endfor %}
384
+ </ul>
385
+ {% endblock %}
386
+ `;
387
+
388
+ const TEMPLATE_TAXONOMY_SINGLE = `{% extends "base.html" %}
389
+ {% import "macros.html" as macros %}
390
+
391
+ {% block title %}{{ term.name }} &mdash; {{ config.title }}{% endblock %}
392
+
393
+ {% block content %}
394
+ <h1>{{ term.name }}</h1>
395
+ <div class="post-list">
396
+ {% for page in term.pages %}
397
+ {{ macros::post_card(page=page) }}
398
+ {% endfor %}
399
+ </div>
400
+ {% endblock %}
401
+ `;
402
+
403
+ // ---------------------------------------------------------------------------
404
+ // Shared macro — single post card used by all list/detail templates
405
+ // ---------------------------------------------------------------------------
406
+
407
+ const TEMPLATE_MACROS = `{% macro post_card(page, detail=false) %}
408
+ <article class="{% if detail %}post-detail{% else %}post-card{% endif %}{% if page.extra.pinned %} pinned{% endif %}" data-format="{{ page.extra.format | default(value='note') }}">
409
+ {% if page.extra.format == "link" and page.extra.link_url %}
410
+ <div class="post-meta link-domain">
411
+ <a href="{{ page.extra.link_url }}" rel="noopener noreferrer" target="_blank">{{ page.extra.link_url | split(pat="//") | nth(n=1) | split(pat="/") | first }}</a>
412
+ </div>
413
+ {% endif %}
414
+
415
+ {% if page.title %}
416
+ {% if detail %}
417
+ <h1 class="post-title">
418
+ {% if page.extra.format == "link" and page.extra.link_url %}
419
+ <a href="{{ page.extra.link_url }}" rel="noopener noreferrer" target="_blank">{{ page.title }}</a>
420
+ {% else %}
421
+ {{ page.title }}
422
+ {% endif %}
423
+ </h1>
424
+ {% else %}
425
+ <h2 class="post-title">
426
+ {% if page.extra.format == "link" and page.extra.link_url %}
427
+ <a href="{{ page.extra.link_url }}" rel="noopener noreferrer" target="_blank">{{ page.title }}</a>
428
+ {% else %}
429
+ <a href="{{ page.permalink }}">{{ page.title }}</a>
430
+ {% endif %}
431
+ </h2>
432
+ {% endif %}
433
+ {% endif %}
434
+
435
+ {% if page.extra.format == "quote" and page.extra.quote_text %}
436
+ <blockquote class="feed-quote">
437
+ <p>{{ page.extra.quote_text }}</p>
438
+ </blockquote>
439
+ {% endif %}
440
+
441
+ {% if not detail and page.summary %}
442
+ <div class="post-body prose">{{ page.summary | safe }}</div>
443
+ {% elif page.content %}
444
+ <div class="post-body prose">{{ page.content | safe }}</div>
445
+ {% endif %}
446
+
447
+ {% if page.extra.rating %}
448
+ <div class="star-rating">
449
+ {% for i in range(end=page.extra.rating) %}<span class="star filled">&#9733;</span>{% endfor %}{% for i in range(start=page.extra.rating, end=5) %}<span class="star">&#9734;</span>{% endfor %}
450
+ </div>
451
+ {% endif %}
452
+
453
+ <footer class="post-footer">
454
+ <a href="{{ page.permalink }}" class="post-date"><time datetime="{{ page.date }}">{{ page.date | date(format="%b %e, %Y") }}</time></a>
455
+ {% if page.taxonomies.c %}
456
+ <span class="post-collections">
457
+ {% for col in page.taxonomies.c %}
458
+ <a href="{{ get_taxonomy_url(kind='c', name=col) }}" class="collection-tag">{{ col }}</a>
459
+ {% endfor %}
460
+ </span>
461
+ {% endif %}
462
+ </footer>
463
+ </article>
464
+ {% endmacro %}
465
+ `;
466
+
467
+ // ---------------------------------------------------------------------------
468
+ // CSS — Jant "Organic Minimalism" approximation
469
+ // ---------------------------------------------------------------------------
470
+
471
+ const STYLE_CSS = `/* Jant Export Theme — Organic Minimalism */
472
+
473
+ :root {
474
+ --site-width: 500px;
475
+ --font-body: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
476
+ "Helvetica Neue", Helvetica, Arial, sans-serif;
477
+ --font-mono: ui-monospace, Menlo, Monaco, Consolas, "Courier New", monospace;
478
+
479
+ --bg: #fafaf9;
480
+ --fg: #1c1917;
481
+ --muted: #78716c;
482
+ --border: #e7e5e4;
483
+ --accent: #292524;
484
+ --accent-fg: #fff;
485
+ --card-bg: #fff;
486
+ --quote-border: #d6d3d1;
487
+ --star-color: #f59e0b;
488
+ --link-color: #292524;
489
+ }
490
+
491
+ @media (prefers-color-scheme: dark) {
492
+ :root {
493
+ --bg: #1c1917;
494
+ --fg: #e7e5e4;
495
+ --muted: #a8a29e;
496
+ --border: #44403c;
497
+ --accent: #e7e5e4;
498
+ --accent-fg: #1c1917;
499
+ --card-bg: #292524;
500
+ --quote-border: #57534e;
501
+ --star-color: #fbbf24;
502
+ --link-color: #e7e5e4;
503
+ }
504
+ }
505
+
506
+ *,
507
+ *::before,
508
+ *::after {
509
+ box-sizing: border-box;
510
+ margin: 0;
511
+ padding: 0;
512
+ }
513
+
514
+ html {
515
+ font-size: 15px;
516
+ line-height: 1.5;
517
+ }
518
+
519
+ body {
520
+ font-family: var(--font-body);
521
+ color: var(--fg);
522
+ background: var(--bg);
523
+ max-width: var(--site-width);
524
+ margin: 0 auto;
525
+ padding: 2rem 1.5rem;
526
+ }
527
+
528
+ a {
529
+ color: var(--link-color);
530
+ text-decoration-thickness: 1px;
531
+ text-underline-offset: 2px;
532
+ }
533
+
534
+ a:hover {
535
+ text-decoration: none;
536
+ }
537
+
538
+ /* Header */
539
+ .site-header {
540
+ display: flex;
541
+ align-items: center;
542
+ justify-content: space-between;
543
+ padding-bottom: 2rem;
544
+ border-bottom: 1px solid var(--border);
545
+ margin-bottom: 2rem;
546
+ }
547
+
548
+ .site-title {
549
+ font-weight: 700;
550
+ font-size: 1.125rem;
551
+ text-decoration: none;
552
+ color: var(--fg);
553
+ }
554
+
555
+ .site-header nav {
556
+ display: flex;
557
+ gap: 1rem;
558
+ }
559
+
560
+ .site-header nav a {
561
+ color: var(--muted);
562
+ text-decoration: none;
563
+ font-size: 0.875rem;
564
+ }
565
+
566
+ .site-header nav a:hover {
567
+ color: var(--fg);
568
+ }
569
+
570
+ /* Post list */
571
+ .post-list {
572
+ display: flex;
573
+ flex-direction: column;
574
+ }
575
+
576
+ .post-card {
577
+ padding: 1.25rem 0;
578
+ border-bottom: 1px solid var(--border);
579
+ }
580
+
581
+ .post-card:last-child {
582
+ border-bottom: none;
583
+ }
584
+
585
+ .post-title {
586
+ font-size: 1.0625rem;
587
+ font-weight: 600;
588
+ margin-bottom: 0.25rem;
589
+ line-height: 1.4;
590
+ }
591
+
592
+ .post-title a {
593
+ text-decoration: none;
594
+ color: var(--fg);
595
+ }
596
+
597
+ .post-title a:hover {
598
+ text-decoration: underline;
599
+ }
600
+
601
+ /* Link format domain indicator */
602
+ .link-domain {
603
+ font-size: 0.8125rem;
604
+ color: var(--muted);
605
+ margin-bottom: 0.25rem;
606
+ }
607
+
608
+ .link-domain a {
609
+ color: var(--muted);
610
+ text-decoration: none;
611
+ }
612
+
613
+ /* Quote format */
614
+ .feed-quote {
615
+ border-left: 2px solid var(--quote-border);
616
+ padding-left: 1rem;
617
+ margin: 0.5rem 0;
618
+ font-style: italic;
619
+ color: var(--fg);
620
+ }
621
+
622
+ /* Body / prose */
623
+ .post-body {
624
+ font-size: 0.9375rem;
625
+ line-height: 1.6;
626
+ color: var(--fg);
627
+ }
628
+
629
+ .post-body p {
630
+ margin: 0.75rem 0;
631
+ }
632
+
633
+ .post-body p:first-child {
634
+ margin-top: 0;
635
+ }
636
+
637
+ .post-body img {
638
+ max-width: 100%;
639
+ height: auto;
640
+ border-radius: 0.5rem;
641
+ }
642
+
643
+ .post-body pre {
644
+ background: var(--card-bg);
645
+ border: 1px solid var(--border);
646
+ border-radius: 0.375rem;
647
+ padding: 0.75rem 1rem;
648
+ overflow-x: auto;
649
+ font-family: var(--font-mono);
650
+ font-size: 0.8125rem;
651
+ line-height: 1.5;
652
+ }
653
+
654
+ .post-body code {
655
+ font-family: var(--font-mono);
656
+ font-size: 0.875em;
657
+ background: var(--card-bg);
658
+ padding: 0.125rem 0.375rem;
659
+ border-radius: 0.25rem;
660
+ }
661
+
662
+ .post-body pre code {
663
+ background: none;
664
+ padding: 0;
665
+ }
666
+
667
+ .post-body blockquote {
668
+ border-left: 2px solid var(--quote-border);
669
+ padding-left: 1rem;
670
+ color: var(--muted);
671
+ margin: 0.75rem 0;
672
+ }
673
+
674
+ .post-body h1, .post-body h2, .post-body h3,
675
+ .post-body h4, .post-body h5, .post-body h6 {
676
+ margin: 1.5rem 0 0.5rem;
677
+ line-height: 1.3;
678
+ }
679
+
680
+ .post-body ul, .post-body ol {
681
+ padding-left: 1.5rem;
682
+ margin: 0.75rem 0;
683
+ }
684
+
685
+ .post-body table {
686
+ width: 100%;
687
+ border-collapse: collapse;
688
+ margin: 0.75rem 0;
689
+ font-size: 0.875rem;
690
+ }
691
+
692
+ .post-body th, .post-body td {
693
+ border: 1px solid var(--border);
694
+ padding: 0.375rem 0.75rem;
695
+ text-align: left;
696
+ }
697
+
698
+ .post-body th {
699
+ font-weight: 600;
700
+ background: var(--card-bg);
701
+ }
702
+
703
+ /* Star rating */
704
+ .star-rating {
705
+ margin: 0.25rem 0;
706
+ font-size: 0.875rem;
707
+ }
708
+
709
+ .star-rating .star {
710
+ color: var(--border);
711
+ }
712
+
713
+ .star-rating .star.filled {
714
+ color: var(--star-color);
715
+ }
716
+
717
+ /* Post footer */
718
+ .post-footer {
719
+ display: flex;
720
+ align-items: center;
721
+ gap: 0.75rem;
722
+ margin-top: 0.5rem;
723
+ font-size: 0.8125rem;
724
+ color: var(--muted);
725
+ }
726
+
727
+ .post-date {
728
+ color: var(--muted);
729
+ text-decoration: none;
730
+ }
731
+
732
+ .post-date:hover {
733
+ color: var(--fg);
734
+ }
735
+
736
+ .collection-tag {
737
+ color: var(--muted);
738
+ text-decoration: none;
739
+ font-size: 0.75rem;
740
+ border: 1px solid var(--border);
741
+ padding: 0.0625rem 0.375rem;
742
+ border-radius: 999px;
743
+ }
744
+
745
+ .collection-tag:hover {
746
+ color: var(--fg);
747
+ border-color: var(--fg);
748
+ }
749
+
750
+ /* Detail page */
751
+ .post-detail {
752
+ padding: 1rem 0;
753
+ }
754
+
755
+ .post-detail .post-title {
756
+ font-size: 1.25rem;
757
+ margin-bottom: 0.5rem;
758
+ }
759
+
760
+ .post-detail .post-body {
761
+ margin: 1rem 0;
762
+ }
763
+
764
+ /* Section / Collection */
765
+ .section-description {
766
+ color: var(--muted);
767
+ margin-bottom: 1.5rem;
768
+ }
769
+
770
+ .collection-list {
771
+ list-style: none;
772
+ padding: 0;
773
+ }
774
+
775
+ .collection-list li {
776
+ padding: 0.5rem 0;
777
+ border-bottom: 1px solid var(--border);
778
+ }
779
+
780
+ .collection-list .count {
781
+ color: var(--muted);
782
+ font-size: 0.8125rem;
783
+ }
784
+
785
+ /* Pagination */
786
+ .pagination {
787
+ display: flex;
788
+ justify-content: space-between;
789
+ padding: 2rem 0 1rem;
790
+ font-size: 0.875rem;
791
+ }
792
+
793
+ /* Footer */
794
+ .site-footer {
795
+ margin-top: 3rem;
796
+ padding-top: 1rem;
797
+ border-top: 1px solid var(--border);
798
+ text-align: center;
799
+ font-size: 0.8125rem;
800
+ color: var(--muted);
801
+ }
802
+ `;