@jant/core 0.6.11 → 0.6.13

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 (139) hide show
  1. package/bin/commands/import-site.js +47 -30
  2. package/bin/commands/migrate-text-attachments.js +1 -3
  3. package/bin/commands/site/export.js +1 -3
  4. package/bin/commands/telegram/register-webhooks.js +9 -3
  5. package/bin/lib/site-snapshot.js +1 -5
  6. package/dist/app-CWJFPjuR.js +6 -0
  7. package/dist/{app-CpmficmQ.js → app-HnzmsaqU.js} +721 -473
  8. package/dist/client/.vite/manifest.json +3 -3
  9. package/dist/client/_assets/{client-Dd9U383b.js → client-3_OaxhTs.js} +1 -1
  10. package/dist/client/_assets/client-DCrcHVJl.css +2 -0
  11. package/dist/client/_assets/{client-auth-DkpSdIDz.js → client-auth-TsYq90xm.js} +625 -561
  12. package/dist/{export-Ba7NJImL.js → export-Dfbq9aot.js} +12 -15
  13. package/dist/{github-sync-Cb4_6_i7.js → github-sync--PSoE-Ne.js} +1 -1
  14. package/dist/{github-sync-BD4w2m8-.js → github-sync-BGTLx8Mf.js} +2 -2
  15. package/dist/index.js +3 -3
  16. package/dist/node.js +4 -4
  17. package/package.json +1 -1
  18. package/src/__tests__/export-service.test.ts +1 -2
  19. package/src/__tests__/helpers/app.ts +3 -0
  20. package/src/__tests__/helpers/export-fixtures.ts +0 -1
  21. package/src/__tests__/import-site-command.test.ts +76 -2
  22. package/src/__tests__/vite-dev-plugins.test.ts +34 -0
  23. package/src/client/__tests__/compose-launch.test.ts +50 -1
  24. package/src/client/__tests__/compose-shortcuts.test.ts +74 -18
  25. package/src/client/__tests__/site-header-nav.test.ts +36 -2
  26. package/src/client/components/__tests__/compose-title-from-heading.test.ts +57 -0
  27. package/src/client/components/__tests__/jant-command-palette.test.ts +63 -0
  28. package/src/client/components/__tests__/jant-compose-dialog.test.ts +33 -0
  29. package/src/client/components/__tests__/jant-compose-editor-rehost-notice.test.ts +20 -1
  30. package/src/client/components/__tests__/jant-compose-editor.test.ts +94 -2
  31. package/src/client/components/__tests__/jant-nav-manager.test.ts +181 -20
  32. package/src/client/components/__tests__/jant-settings-avatar.test.ts +6 -0
  33. package/src/client/components/__tests__/jant-settings-general.test.ts +72 -0
  34. package/src/client/components/compose-title-from-heading.ts +66 -0
  35. package/src/client/components/compose-types.ts +8 -0
  36. package/src/client/components/inline-image-issues.ts +17 -0
  37. package/src/client/components/jant-command-palette.ts +67 -2
  38. package/src/client/components/jant-compose-dialog.ts +10 -2
  39. package/src/client/components/jant-compose-editor.ts +78 -1
  40. package/src/client/components/jant-compose-fullscreen.ts +2 -0
  41. package/src/client/components/jant-nav-manager.ts +382 -152
  42. package/src/client/components/jant-settings-general.ts +56 -0
  43. package/src/client/components/nav-manager-types.ts +14 -10
  44. package/src/client/components/settings-types.ts +30 -0
  45. package/src/client/compose-launch.ts +30 -0
  46. package/src/client/compose-shortcuts.ts +46 -3
  47. package/src/client/site-header-nav.d.ts +1 -0
  48. package/src/client/site-header-nav.js +105 -47
  49. package/src/client/tiptap/__tests__/block-insertion.test.ts +35 -0
  50. package/src/client/tiptap/__tests__/image-input-rules.test.ts +117 -0
  51. package/src/client/tiptap/__tests__/rehost-images.test.ts +66 -13
  52. package/src/client/tiptap/create-editor.ts +7 -0
  53. package/src/client/tiptap/extensions.ts +7 -2
  54. package/src/client/tiptap/image-input-rules.ts +48 -0
  55. package/src/client/tiptap/image-node.ts +189 -7
  56. package/src/client/tiptap/rehost-images.ts +5 -1
  57. package/src/client-auth.ts +0 -1
  58. package/src/i18n/__tests__/fallback.test.ts +24 -0
  59. package/src/i18n/locales/public/en.po +20 -4
  60. package/src/i18n/locales/public/en.ts +1 -1
  61. package/src/i18n/locales/public/zh-Hans.po +22 -6
  62. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  63. package/src/i18n/locales/public/zh-Hant.po +22 -6
  64. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  65. package/src/i18n/locales/settings/en.po +77 -20
  66. package/src/i18n/locales/settings/en.ts +1 -1
  67. package/src/i18n/locales/settings/zh-Hans.po +85 -28
  68. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  69. package/src/i18n/locales/settings/zh-Hant.po +79 -22
  70. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  71. package/src/lib/__tests__/feed.test.ts +96 -10
  72. package/src/lib/__tests__/navigation.test.ts +4 -37
  73. package/src/lib/__tests__/resolve-config.test.ts +6 -1
  74. package/src/lib/__tests__/view.test.ts +6 -30
  75. package/src/lib/feed.ts +63 -3
  76. package/src/lib/github-sync-site-config.ts +0 -2
  77. package/src/lib/hugo-markdown.ts +1 -1
  78. package/src/lib/navigation.ts +5 -22
  79. package/src/lib/post-display.ts +14 -8
  80. package/src/lib/resolve-config.ts +9 -2
  81. package/src/lib/site-header-fragment.tsx +37 -0
  82. package/src/lib/view.ts +2 -7
  83. package/src/node/__tests__/cli-site-snapshot.test.ts +45 -45
  84. package/src/preset.css +60 -18
  85. package/src/routes/api/__tests__/nav-items.test.ts +26 -0
  86. package/src/routes/api/export.ts +0 -2
  87. package/src/routes/api/internal/__tests__/uploads.test.ts +9 -13
  88. package/src/routes/api/internal/sites.ts +5 -5
  89. package/src/routes/api/nav-items.ts +25 -4
  90. package/src/routes/dash/settings.tsx +48 -3
  91. package/src/routes/feed/__tests__/feed.test.ts +42 -0
  92. package/src/routes/feed/__tests__/sitemap.test.ts +2 -13
  93. package/src/routes/feed/feed.ts +1 -0
  94. package/src/routes/feed/sitemap.ts +2 -6
  95. package/src/routes/pages/__tests__/draft-about-edit.test.ts +65 -0
  96. package/src/routes/pages/archive.tsx +1 -0
  97. package/src/routes/pages/collection.tsx +1 -0
  98. package/src/routes/pages/featured.tsx +0 -4
  99. package/src/routes/pages/home.tsx +4 -51
  100. package/src/routes/pages/latest.tsx +2 -51
  101. package/src/routes/pages/page.tsx +44 -12
  102. package/src/services/__tests__/about-page.test.ts +116 -0
  103. package/src/services/__tests__/github-app-installations.test.ts +10 -10
  104. package/src/services/__tests__/navigation.test.ts +231 -1
  105. package/src/services/__tests__/post-timeline.test.ts +175 -2
  106. package/src/services/__tests__/post.test.ts +33 -0
  107. package/src/services/__tests__/settings.test.ts +5 -20
  108. package/src/services/about-page.ts +143 -0
  109. package/src/services/export-theme/assets/client-site.js +10 -10
  110. package/src/services/export-theme/layouts/_default/rss.xml +7 -16
  111. package/src/services/export-theme/layouts/index.html +26 -39
  112. package/src/services/export-theme/layouts/partials/feed-post-content.xml +25 -1
  113. package/src/services/export-theme/styles/main.css +46 -14
  114. package/src/services/export.ts +5 -10
  115. package/src/services/github-app-installations.ts +1 -3
  116. package/src/services/index.ts +27 -17
  117. package/src/services/navigation.ts +186 -1
  118. package/src/services/post.ts +103 -63
  119. package/src/services/settings.ts +0 -10
  120. package/src/services/site-admin.ts +0 -2
  121. package/src/services/upload-session.ts +4 -18
  122. package/src/styles/components.css +20 -0
  123. package/src/styles/tokens.css +28 -10
  124. package/src/styles/ui.css +349 -13
  125. package/src/types/bindings.ts +0 -1
  126. package/src/types/config.ts +3 -7
  127. package/src/types/views.ts +13 -0
  128. package/src/ui/compose/ComposeDialog.tsx +28 -0
  129. package/src/ui/dash/appearance/NavigationContent.tsx +58 -1
  130. package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +19 -0
  131. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  132. package/src/ui/dash/settings/GeneralContent.tsx +44 -0
  133. package/src/ui/dash/settings/SettingsRootContent.tsx +3 -3
  134. package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +8 -0
  135. package/src/ui/feed/__tests__/timeline-cards.test.ts +162 -1
  136. package/src/ui/layouts/SiteLayout.tsx +249 -216
  137. package/dist/app-DqKkZenB.js +0 -6
  138. package/dist/client/_assets/client-BhHHVvSY.css +0 -2
  139. package/src/client/nav-manager-bridge.ts +0 -54
@@ -723,7 +723,6 @@ describe("toNavItemView", () => {
723
723
  const view = toNavItemView(
724
724
  makeNavItem({ url: "https://example.com/about" }),
725
725
  "/about",
726
- "latest",
727
726
  false,
728
727
  "",
729
728
  undefined,
@@ -738,7 +737,6 @@ describe("toNavItemView", () => {
738
737
  const view = toNavItemView(
739
738
  makeNavItem({ url: "https://other.com/about" }),
740
739
  "/about",
741
- "latest",
742
740
  false,
743
741
  "",
744
742
  undefined,
@@ -755,7 +753,6 @@ describe("toNavItemView", () => {
755
753
  const view = toNavItemView(
756
754
  makeNavItem({ url: "https://jant.example/about" }),
757
755
  "/about",
758
- "latest",
759
756
  false,
760
757
  "",
761
758
  undefined,
@@ -770,7 +767,6 @@ describe("toNavItemView", () => {
770
767
  const view = toNavItemView(
771
768
  makeNavItem({ url: "https://example.com/blog/about" }),
772
769
  "/blog/about",
773
- "latest",
774
770
  false,
775
771
  "/blog",
776
772
  undefined,
@@ -799,7 +795,6 @@ describe("toNavItemView", () => {
799
795
  url: "/settings",
800
796
  }),
801
797
  "/signin",
802
- "latest",
803
798
  false,
804
799
  );
805
800
 
@@ -817,7 +812,6 @@ describe("toNavItemView", () => {
817
812
  url: "/settings",
818
813
  }),
819
814
  "/settings",
820
- "latest",
821
815
  true,
822
816
  );
823
817
 
@@ -826,7 +820,7 @@ describe("toNavItemView", () => {
826
820
  expect(view.isActive).toBe(true);
827
821
  });
828
822
 
829
- it("resolves the latest built-in item to / when it is the home default", () => {
823
+ it("resolves the latest built-in item to /", () => {
830
824
  const view = toNavItemView(
831
825
  makeNavItem({
832
826
  type: "system",
@@ -835,30 +829,13 @@ describe("toNavItemView", () => {
835
829
  url: "/latest",
836
830
  }),
837
831
  "/",
838
- "latest",
839
832
  );
840
833
 
841
834
  expect(view.url).toBe("/");
842
835
  expect(view.isActive).toBe(true);
843
836
  });
844
837
 
845
- it("keeps the latest built-in item on /latest when featured owns the homepage", () => {
846
- const view = toNavItemView(
847
- makeNavItem({
848
- type: "system",
849
- systemKey: "latest",
850
- label: "Latest",
851
- url: "/latest",
852
- }),
853
- "/latest",
854
- "featured",
855
- );
856
-
857
- expect(view.url).toBe("/latest");
858
- expect(view.isActive).toBe(true);
859
- });
860
-
861
- it("resolves the featured built-in item to / when it is the home default", () => {
838
+ it("resolves the featured built-in item to /featured", () => {
862
839
  const view = toNavItemView(
863
840
  makeNavItem({
864
841
  type: "system",
@@ -866,11 +843,10 @@ describe("toNavItemView", () => {
866
843
  label: "Featured",
867
844
  url: "/featured",
868
845
  }),
869
- "/",
870
- "featured",
846
+ "/featured",
871
847
  );
872
848
 
873
- expect(view.url).toBe("/");
849
+ expect(view.url).toBe("/featured");
874
850
  expect(view.isActive).toBe(true);
875
851
  });
876
852
 
@@ -903,9 +879,9 @@ describe("toNavItemViews", () => {
903
879
  makeNavItem({ id: UUID_NAV_2, url: "/archive" }),
904
880
  makeNavItem({ id: UUID_NAV_3, url: "https://github.com" }),
905
881
  ];
906
- const views = toNavItemViews(items, "/archive", "featured");
882
+ const views = toNavItemViews(items, "/archive");
907
883
  expect(views).toHaveLength(3);
908
- expect(views[0]?.url).toBe("/latest");
884
+ expect(views[0]?.url).toBe("/");
909
885
  expect(views[0]).toHaveProperty("isActive", false);
910
886
  expect(views[1]).toHaveProperty("isActive", true);
911
887
  expect(views[2]).toHaveProperty("isExternal", true);
package/src/lib/feed.ts CHANGED
@@ -92,6 +92,49 @@ function getAtomTitle(post: PostView): string {
92
92
  return post.title || "";
93
93
  }
94
94
 
95
+ interface SinglePostContentOptions {
96
+ /**
97
+ * Inline posts do not have their own Atom entry title/link, so their visible
98
+ * feed HTML must carry title and link metadata that top-level entries expose
99
+ * through Atom fields.
100
+ */
101
+ inline?: boolean;
102
+ }
103
+
104
+ function renderLinkedText(text: string, href?: string): string {
105
+ const label = escapeXml(text);
106
+ return href ? `<a href="${escapeXml(href)}">${label}</a>` : label;
107
+ }
108
+
109
+ function renderInlinePostHeader(
110
+ post: PostView,
111
+ permalinkUrl?: string,
112
+ ): string[] {
113
+ if (post.format === "quote") return [];
114
+
115
+ const parts: string[] = [];
116
+
117
+ if (post.format === "link") {
118
+ const linkUrl = post.url || "";
119
+ const domain = linkUrl ? extractDisplayDomain(linkUrl) || linkUrl : "";
120
+ if (domain) {
121
+ parts.push(`<p>${renderLinkedText(domain, linkUrl)}</p>`);
122
+ }
123
+ if (post.title) {
124
+ parts.push(
125
+ `<h2>${renderLinkedText(post.title, linkUrl || permalinkUrl)}</h2>`,
126
+ );
127
+ }
128
+ return parts;
129
+ }
130
+
131
+ if (post.title) {
132
+ parts.push(`<h2>${renderLinkedText(post.title, permalinkUrl)}</h2>`);
133
+ }
134
+
135
+ return parts;
136
+ }
137
+
95
138
  /**
96
139
  * Render a star rating as HTML for feed content.
97
140
  */
@@ -242,10 +285,19 @@ function renderMediaForFeed(
242
285
  *
243
286
  * @param post - Post view data
244
287
  * @param permalinkUrl - Absolute permalink URL back to the blog post
288
+ * @param options - Rendering options for top-level versus inline posts
245
289
  */
246
- function buildSinglePostContent(post: PostView, permalinkUrl?: string): string {
290
+ function buildSinglePostContent(
291
+ post: PostView,
292
+ permalinkUrl?: string,
293
+ options: SinglePostContentOptions = {},
294
+ ): string {
247
295
  const parts: string[] = [];
248
296
 
297
+ if (options.inline) {
298
+ parts.push(...renderInlinePostHeader(post, permalinkUrl));
299
+ }
300
+
249
301
  if (post.format === "quote" && post.quoteText) {
250
302
  const sourceName = post.title || "";
251
303
  const sourceUrl = post.url || "";
@@ -316,12 +368,20 @@ function buildFeedContent(
316
368
  parts.push(
317
369
  `<p><small><time datetime="${escapeXml(reply.publishedAt)}">${escapeXml(reply.publishedAtFormatted)}</time></small></p>`,
318
370
  );
319
- parts.push(buildSinglePostContent(reply, replyPermalink));
371
+ parts.push(buildSinglePostContent(reply, replyPermalink, { inline: true }));
320
372
  }
321
373
 
322
374
  return parts.join("\n");
323
375
  }
324
376
 
377
+ function getEntryMedia(post: FeedPostView): MediaView[] {
378
+ const media = [...post.media];
379
+ for (const reply of post.threadReplies ?? []) {
380
+ media.push(...reply.media);
381
+ }
382
+ return media;
383
+ }
384
+
325
385
  /**
326
386
  * Default Atom feed renderer.
327
387
  *
@@ -354,7 +414,7 @@ export function defaultFeedRenderer(data: FeedData): string {
354
414
  // One <link rel="enclosure"> per attachment so podcast/offline readers
355
415
  // can fetch them. Atom omits length when size is unknown; mimeType is
356
416
  // always known from the upload pipeline.
357
- const enclosureLinks = post.media
417
+ const enclosureLinks = getEntryMedia(post)
358
418
  .map((m) => {
359
419
  const lengthAttr =
360
420
  m.size != null && m.size > 0 ? ` length="${m.size}"` : "";
@@ -14,7 +14,6 @@
14
14
 
15
15
  import type { SiteConfig } from "../services/export.js";
16
16
  import type { AppVariables } from "../types/app-context.js";
17
- import { getHomeDefaultViewFromNavItems } from "./navigation.js";
18
17
 
19
18
  export async function buildSyncSiteConfig(c: {
20
19
  var: Pick<
@@ -31,7 +30,6 @@ export async function buildSyncSiteConfig(c: {
31
30
  siteDescription: appConfig.siteDescription,
32
31
  siteLanguage: appConfig.siteLanguage,
33
32
  showJantBrandingOnHome: appConfig.showJantBrandingOnHome,
34
- homeDefaultView: getHomeDefaultViewFromNavItems(navItems),
35
33
  mainRssFeed: appConfig.mainRssFeed,
36
34
  siteFooter: appConfig.siteFooter,
37
35
  showHeaderAvatar: appConfig.showHeaderAvatar,
@@ -118,7 +118,7 @@ export interface HugoFrontMatter {
118
118
  collections?: HugoCollectionRef[];
119
119
  media?: JantMedia[];
120
120
 
121
- // Escape hatch for page-type-specific keys (home default view, etc.)
121
+ // Escape hatch for page-specific export metadata.
122
122
  [key: string]: unknown;
123
123
  }
124
124
 
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { Context } from "hono";
8
- import type { Collection, FeedKind, NavItem, NavItemView } from "../types.js";
8
+ import type { Collection, NavItem, NavItemView } from "../types.js";
9
9
  import { toNavItemViews } from "./view.js";
10
10
  import { render as renderMarkdown, toPlainText } from "./markdown.js";
11
11
 
@@ -23,24 +23,11 @@ export interface NavigationData {
23
23
  siteDescriptionHtml?: string;
24
24
  isAuthenticated: boolean;
25
25
  collections: Collection[];
26
- homeDefaultView: FeedKind;
27
26
  siteAvatarUrl?: string;
28
27
  showHeaderAvatar?: boolean;
29
28
  siteFooterHtml?: string;
30
29
  }
31
30
 
32
- export function getHomeDefaultViewFromNavItems(
33
- items: Pick<NavItem, "type" | "systemKey">[],
34
- ): FeedKind {
35
- const homeFeedItem = items.find(
36
- (item) =>
37
- item.type === "system" &&
38
- (item.systemKey === "latest" || item.systemKey === "featured"),
39
- );
40
-
41
- return homeFeedItem?.systemKey === "featured" ? "featured" : "latest";
42
- }
43
-
44
31
  /**
45
32
  * Fetch navigation data for public pages.
46
33
  *
@@ -62,18 +49,16 @@ export function getHomeDefaultViewFromNavItems(
62
49
  */
63
50
  export async function getNavigationData(
64
51
  c: Context,
65
- options?: { preloadedItems?: NavItem[] },
52
+ options?: { preloadedItems?: NavItem[]; includeCollections?: boolean },
66
53
  ): Promise<NavigationData> {
67
- // Callers that already fetched nav items (e.g. home route, which needs
68
- // `homeDefaultView` before deciding which timeline to assemble) can pass
69
- // them in to avoid a redundant DB round-trip.
54
+ // Callers that already fetched nav items can pass them in to avoid a
55
+ // redundant DB round-trip.
70
56
  const items =
71
57
  options?.preloadedItems ?? (await c.var.services.navItems.list());
72
58
  const currentPath = c.var.publicPath;
73
59
  const appConfig = c.var.appConfig;
74
60
 
75
61
  const siteName = appConfig.siteName;
76
- const homeDefaultView = getHomeDefaultViewFromNavItems(items);
77
62
  const siteFooter = appConfig.siteFooter;
78
63
 
79
64
  // Only include description if explicitly set (DB or env), not the default
@@ -112,7 +97,6 @@ export async function getNavigationData(
112
97
  const links = toNavItemViews(
113
98
  items,
114
99
  currentPath,
115
- homeDefaultView,
116
100
  isAuthenticated,
117
101
  appConfig.sitePathPrefix,
118
102
  collectionFreshness,
@@ -120,7 +104,7 @@ export async function getNavigationData(
120
104
  );
121
105
 
122
106
  // Only load collections when authenticated (for compose dialog)
123
- if (isAuthenticated) {
107
+ if (isAuthenticated && options?.includeCollections !== false) {
124
108
  collections = await c.var.services.collections.listByRecentActivity();
125
109
  }
126
110
 
@@ -133,7 +117,6 @@ export async function getNavigationData(
133
117
  siteDescriptionHtml,
134
118
  isAuthenticated,
135
119
  collections,
136
- homeDefaultView,
137
120
  siteAvatarUrl,
138
121
  showHeaderAvatar: showHeaderAvatar && !!siteAvatarUrl,
139
122
  siteFooterHtml,
@@ -79,9 +79,17 @@ function resolvePostSocialImage(
79
79
  return undefined;
80
80
  }
81
81
 
82
- function canViewPost(post: Post, isAuthenticated: boolean): boolean {
82
+ interface PostDisplayOptions {
83
+ isAuthenticated?: boolean;
84
+ allowDraft?: boolean;
85
+ }
86
+
87
+ function canViewPost(post: Post, options: PostDisplayOptions = {}): boolean {
88
+ const isAuthenticated = options.isAuthenticated ?? false;
83
89
  if (post.status !== "published") {
84
- return false;
90
+ return Boolean(
91
+ post.status === "draft" && isAuthenticated && options.allowDraft,
92
+ );
85
93
  }
86
94
 
87
95
  if (post.visibility === "private" && !isAuthenticated) {
@@ -102,11 +110,10 @@ function canViewPost(post: Post, isAuthenticated: boolean): boolean {
102
110
  export async function assemblePostCardView(
103
111
  c: Context<Env>,
104
112
  postId: string,
105
- options?: { isAuthenticated?: boolean },
113
+ options?: PostDisplayOptions,
106
114
  ): Promise<PostView | null> {
107
115
  const post = await c.var.services.posts.getById(postId);
108
- const isAuthenticated = options?.isAuthenticated ?? false;
109
- if (!post || !canViewPost(post, isAuthenticated)) {
116
+ if (!post || !canViewPost(post, options)) {
110
117
  return null;
111
118
  }
112
119
 
@@ -149,15 +156,14 @@ export async function assemblePostCardView(
149
156
  export async function assemblePostPageDisplay(
150
157
  c: Context<Env>,
151
158
  postOrId: string | Post,
152
- options?: { isAuthenticated?: boolean },
159
+ options?: PostDisplayOptions,
153
160
  ): Promise<PostPageDisplayData | null> {
154
161
  const post =
155
162
  typeof postOrId === "string"
156
163
  ? await c.var.services.posts.getById(postOrId)
157
164
  : postOrId;
158
- const isAuthenticated = options?.isAuthenticated ?? false;
159
165
 
160
- if (!post || !canViewPost(post, isAuthenticated)) {
166
+ if (!post || !canViewPost(post, options)) {
161
167
  return null;
162
168
  }
163
169
 
@@ -12,6 +12,7 @@
12
12
  import type { Bindings } from "../types/bindings.js";
13
13
  import type { AppConfig } from "../types/config.js";
14
14
  import { CONFIG_FIELDS } from "../types/config.js";
15
+ import type { FeedKind } from "../types/constants.js";
15
16
  import { ASSET_BASE_SEGMENT, getPublicAssetBasePath } from "./asset-path.js";
16
17
  import {
17
18
  getAuthSecret,
@@ -77,6 +78,10 @@ function parseConfigInt(value: string, fallback: number): number {
77
78
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
78
79
  }
79
80
 
81
+ function parseFeedKind(value: string, fallback: FeedKind): FeedKind {
82
+ return value === "latest" || value === "featured" ? value : fallback;
83
+ }
84
+
80
85
  /**
81
86
  * Build a complete AppConfig from environment bindings and DB settings.
82
87
  *
@@ -144,8 +149,10 @@ export function resolveConfig(
144
149
  siteLanguage: resolve("SITE_LANGUAGE", allSettings, env),
145
150
  dashboardLanguage: resolve("DASHBOARD_LANGUAGE", allSettings, env),
146
151
  cjkSerifFont: resolve("CJK_SERIF_FONT", allSettings, env),
147
- homeDefaultView: resolve("HOME_DEFAULT_VIEW", allSettings, env),
148
- mainRssFeed: resolve("MAIN_RSS_FEED", allSettings, env),
152
+ mainRssFeed: parseFeedKind(
153
+ resolve("MAIN_RSS_FEED", allSettings, env),
154
+ "featured",
155
+ ),
149
156
  timeZone: normalizeTimeZone(resolve("TIME_ZONE", allSettings, env)),
150
157
  siteFooter: resolve("SITE_FOOTER", allSettings, env),
151
158
  showJantBrandingOnHome:
@@ -0,0 +1,37 @@
1
+ import type { Context } from "hono";
2
+ import type { Bindings } from "../types.js";
3
+ import type { AppVariables } from "../types/app-context.js";
4
+ import { I18nProvider } from "../i18n/context.js";
5
+ import { getNavigationData } from "./navigation.js";
6
+ import { SiteHeader } from "../ui/layouts/SiteLayout.js";
7
+
8
+ type Env = { Bindings: Bindings; Variables: AppVariables };
9
+
10
+ /**
11
+ * Render the current public site header as a replaceable HTML fragment.
12
+ *
13
+ * @param c - Current Hono request context
14
+ * @returns Server-rendered header, drawer backdrop, and mobile drawer HTML
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const headerHtml = await renderSiteHeaderHtml(c);
19
+ * return c.json({ headerHtml });
20
+ * ```
21
+ */
22
+ export async function renderSiteHeaderHtml(c: Context<Env>): Promise<string> {
23
+ const navData = await getNavigationData(c, { includeCollections: false });
24
+
25
+ return String(
26
+ <I18nProvider c={c}>
27
+ <SiteHeader
28
+ siteName={navData.siteName}
29
+ links={navData.links}
30
+ currentPath={navData.currentPath}
31
+ sitePathPrefix={navData.sitePathPrefix}
32
+ siteAvatarUrl={navData.siteAvatarUrl}
33
+ showHeaderAvatar={navData.showHeaderAvatar}
34
+ />
35
+ </I18nProvider>,
36
+ );
37
+ }
package/src/lib/view.ts CHANGED
@@ -23,7 +23,6 @@ import type {
23
23
  NavItemType,
24
24
  NavItemPlacement,
25
25
  AppConfig,
26
- FeedKind,
27
26
  SystemNavKey,
28
27
  } from "../types.js";
29
28
  import { SYSTEM_NAV_KEYS } from "../types/constants.js";
@@ -474,7 +473,6 @@ export function toPostViewsFromPosts(
474
473
  export function toNavItemView(
475
474
  item: NavItem,
476
475
  currentPath: string,
477
- homeDefaultView: FeedKind = "latest",
478
476
  isAuthenticated = false,
479
477
  sitePathPrefix = "",
480
478
  collectionFreshness?: Map<string, number>,
@@ -491,13 +489,12 @@ export function toNavItemView(
491
489
  url = config.url;
492
490
  }
493
491
 
494
- // Special overrides for conditional system URLs
495
492
  if (item.systemKey === "latest") {
496
- url = homeDefaultView === "latest" ? "/" : "/latest";
493
+ url = "/";
497
494
  }
498
495
 
499
496
  if (item.systemKey === "featured") {
500
- url = homeDefaultView === "featured" ? "/" : "/featured";
497
+ url = "/featured";
501
498
  }
502
499
 
503
500
  if (item.systemKey === "settings") {
@@ -557,7 +554,6 @@ export function toNavItemView(
557
554
  export function toNavItemViews(
558
555
  items: NavItem[],
559
556
  currentPath: string,
560
- homeDefaultView: FeedKind = "latest",
561
557
  isAuthenticated = false,
562
558
  sitePathPrefix = "",
563
559
  collectionFreshness?: Map<string, number>,
@@ -567,7 +563,6 @@ export function toNavItemViews(
567
563
  toNavItemView(
568
564
  item,
569
565
  currentPath,
570
- homeDefaultView,
571
566
  isAuthenticated,
572
567
  sitePathPrefix,
573
568
  collectionFreshness,
@@ -32,32 +32,41 @@ const REMAPPED_MEDIA_KEY = `media/${REMAPPED_TARGET_SITE_ID}/files/${SNAPSHOT_ME
32
32
  const REMAPPED_POSTER_KEY = `media/${REMAPPED_TARGET_SITE_ID}/posters/${SNAPSHOT_MEDIA_ID}.webp`;
33
33
  const REMAPPED_AVATAR_KEY = `media/${REMAPPED_TARGET_SITE_ID}/assets/avatar/${SNAPSHOT_AVATAR_MEDIA_ID}.png`;
34
34
  const REMAPPED_APPLE_TOUCH_KEY = `media/${REMAPPED_TARGET_SITE_ID}/assets/favicon/apple-touch-icon.png`;
35
+ const NODE_CLI_ENV_KEYS = [
36
+ "DATABASE_URL",
37
+ "DATA_DIR",
38
+ "LOCAL_STORAGE_PATH",
39
+ "STORAGE_DRIVER",
40
+ "S3_ENDPOINT",
41
+ "S3_BUCKET",
42
+ "S3_REGION",
43
+ "S3_PUBLIC_URL",
44
+ "S3_ACCESS_KEY_ID",
45
+ "S3_SECRET_ACCESS_KEY",
46
+ "SITE_RESOLUTION_MODE",
47
+ ] as const;
48
+
49
+ function useLocalSnapshotRuntime(databaseUrl: string, storagePath: string) {
50
+ delete process.env.DATA_DIR;
51
+ process.env.DATABASE_URL = databaseUrl;
52
+ process.env.LOCAL_STORAGE_PATH = storagePath;
53
+ process.env.STORAGE_DRIVER = "local";
54
+ }
35
55
 
36
56
  describe("jant site snapshot export/import", () => {
37
57
  const tempDirs: string[] = [];
38
- const originalEnv = {
39
- DATABASE_URL: process.env.DATABASE_URL,
40
- LOCAL_STORAGE_PATH: process.env.LOCAL_STORAGE_PATH,
41
- SITE_RESOLUTION_MODE: process.env.SITE_RESOLUTION_MODE,
42
- };
58
+ const originalEnv = Object.fromEntries(
59
+ NODE_CLI_ENV_KEYS.map((key) => [key, process.env[key]]),
60
+ ) as Record<(typeof NODE_CLI_ENV_KEYS)[number], string | undefined>;
43
61
 
44
62
  afterEach(async () => {
45
- if (originalEnv.DATABASE_URL === undefined) {
46
- delete process.env.DATABASE_URL;
47
- } else {
48
- process.env.DATABASE_URL = originalEnv.DATABASE_URL;
49
- }
50
-
51
- if (originalEnv.LOCAL_STORAGE_PATH === undefined) {
52
- delete process.env.LOCAL_STORAGE_PATH;
53
- } else {
54
- process.env.LOCAL_STORAGE_PATH = originalEnv.LOCAL_STORAGE_PATH;
55
- }
56
-
57
- if (originalEnv.SITE_RESOLUTION_MODE === undefined) {
58
- delete process.env.SITE_RESOLUTION_MODE;
59
- } else {
60
- process.env.SITE_RESOLUTION_MODE = originalEnv.SITE_RESOLUTION_MODE;
63
+ for (const key of NODE_CLI_ENV_KEYS) {
64
+ const value = originalEnv[key];
65
+ if (value === undefined) {
66
+ delete process.env[key];
67
+ } else {
68
+ process.env[key] = value;
69
+ }
61
70
  }
62
71
 
63
72
  await Promise.all(
@@ -219,8 +228,7 @@ describe("jant site snapshot export/import", () => {
219
228
  contentType: "image/png",
220
229
  });
221
230
 
222
- process.env.DATABASE_URL = `file:${sourceDbPath}`;
223
- process.env.LOCAL_STORAGE_PATH = sourceStoragePath;
231
+ useLocalSnapshotRuntime(`file:${sourceDbPath}`, sourceStoragePath);
224
232
 
225
233
  const exportLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
226
234
  const { run: runExport } =
@@ -241,8 +249,7 @@ describe("jant site snapshot export/import", () => {
241
249
  `Exported Node database snapshot to ${snapshotPath}`,
242
250
  );
243
251
 
244
- process.env.DATABASE_URL = `file:${targetDbPath}`;
245
- process.env.LOCAL_STORAGE_PATH = targetStoragePath;
252
+ useLocalSnapshotRuntime(`file:${targetDbPath}`, targetStoragePath);
246
253
 
247
254
  const importLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
248
255
  const { run: runImport } =
@@ -379,8 +386,7 @@ describe("jant site snapshot export/import", () => {
379
386
  sourceSqlite.close();
380
387
  }
381
388
 
382
- process.env.DATABASE_URL = `file:${sourceDbPath}`;
383
- process.env.LOCAL_STORAGE_PATH = sourceStoragePath;
389
+ useLocalSnapshotRuntime(`file:${sourceDbPath}`, sourceStoragePath);
384
390
 
385
391
  vi.spyOn(console, "log").mockImplementation(() => {});
386
392
  const { run: runExport } =
@@ -453,16 +459,14 @@ describe("jant site snapshot export/import", () => {
453
459
  targetSqlite.close();
454
460
  }
455
461
 
456
- process.env.DATABASE_URL = `file:${sourceDbPath}`;
457
- process.env.LOCAL_STORAGE_PATH = sourceStoragePath;
462
+ useLocalSnapshotRuntime(`file:${sourceDbPath}`, sourceStoragePath);
458
463
 
459
464
  vi.spyOn(console, "log").mockImplementation(() => {});
460
465
  const { run: runExport } =
461
466
  await import("../../../bin/commands/site/snapshot/export.js");
462
467
  await runExport(["--output", snapshotPath, "--skip-objects"]);
463
468
 
464
- process.env.DATABASE_URL = `file:${targetDbPath}`;
465
- process.env.LOCAL_STORAGE_PATH = targetStoragePath;
469
+ useLocalSnapshotRuntime(`file:${targetDbPath}`, targetStoragePath);
466
470
 
467
471
  const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
468
472
  const { run: runImport } =
@@ -538,16 +542,14 @@ describe("jant site snapshot export/import", () => {
538
542
  targetSqlite.close();
539
543
  }
540
544
 
541
- process.env.DATABASE_URL = `file:${sourceDbPath}`;
542
- process.env.LOCAL_STORAGE_PATH = sourceStoragePath;
545
+ useLocalSnapshotRuntime(`file:${sourceDbPath}`, sourceStoragePath);
543
546
 
544
547
  vi.spyOn(console, "log").mockImplementation(() => {});
545
548
  const { run: runExport } =
546
549
  await import("../../../bin/commands/site/snapshot/export.js");
547
550
  await runExport(["--output", snapshotPath, "--skip-objects"]);
548
551
 
549
- process.env.DATABASE_URL = `file:${targetDbPath}`;
550
- process.env.LOCAL_STORAGE_PATH = targetStoragePath;
552
+ useLocalSnapshotRuntime(`file:${targetDbPath}`, targetStoragePath);
551
553
 
552
554
  vi.spyOn(console, "warn").mockImplementation(() => {});
553
555
  const { run: runImport } =
@@ -580,8 +582,10 @@ describe("jant site snapshot export/import", () => {
580
582
  tempDirs.push(root);
581
583
 
582
584
  const snapshotPath = join(root, "snapshot");
583
- process.env.DATABASE_URL = `file:${join(root, "jant.sqlite")}`;
584
- process.env.LOCAL_STORAGE_PATH = join(root, "media");
585
+ useLocalSnapshotRuntime(
586
+ `file:${join(root, "jant.sqlite")}`,
587
+ join(root, "media"),
588
+ );
585
589
 
586
590
  await rm(snapshotPath, { recursive: true, force: true });
587
591
  await mkdir(snapshotPath, { recursive: true });
@@ -702,15 +706,13 @@ describe("jant site snapshot export/import", () => {
702
706
  targetSqlite.close();
703
707
  }
704
708
 
705
- process.env.DATABASE_URL = `file:${sourceDbPath}`;
706
- process.env.LOCAL_STORAGE_PATH = sourceStoragePath;
709
+ useLocalSnapshotRuntime(`file:${sourceDbPath}`, sourceStoragePath);
707
710
 
708
711
  const { run: runExport } =
709
712
  await import("../../../bin/commands/site/snapshot/export.js");
710
713
  await runExport(["--output", snapshotPath]);
711
714
 
712
- process.env.DATABASE_URL = `file:${targetDbPath}`;
713
- process.env.LOCAL_STORAGE_PATH = targetStoragePath;
715
+ useLocalSnapshotRuntime(`file:${targetDbPath}`, targetStoragePath);
714
716
 
715
717
  const { run: runImport } =
716
718
  await import("../../../bin/commands/site/snapshot/import.js");
@@ -868,16 +870,14 @@ describe("jant site snapshot export/import", () => {
868
870
  targetSqlite.close();
869
871
  }
870
872
 
871
- process.env.DATABASE_URL = `file:${sourceDbPath}`;
872
- process.env.LOCAL_STORAGE_PATH = sourceStoragePath;
873
+ useLocalSnapshotRuntime(`file:${sourceDbPath}`, sourceStoragePath);
873
874
  delete process.env.SITE_RESOLUTION_MODE;
874
875
 
875
876
  const { run: runExport } =
876
877
  await import("../../../bin/commands/site/snapshot/export.js");
877
878
  await runExport(["--output", snapshotPath]);
878
879
 
879
- process.env.DATABASE_URL = `file:${targetDbPath}`;
880
- process.env.LOCAL_STORAGE_PATH = targetStoragePath;
880
+ useLocalSnapshotRuntime(`file:${targetDbPath}`, targetStoragePath);
881
881
  delete process.env.SITE_RESOLUTION_MODE;
882
882
 
883
883
  const importLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});