@jant/core 0.6.12 → 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 (106) hide show
  1. package/bin/commands/import-site.js +2 -10
  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-D-zAhayc.js → app-HnzmsaqU.js} +416 -444
  8. package/dist/client/.vite/manifest.json +3 -3
  9. package/dist/client/_assets/{client-S1cdvulk.js → client-3_OaxhTs.js} +1 -1
  10. package/dist/client/_assets/{client-BUxR-E8O.css → client-DCrcHVJl.css} +1 -1
  11. package/dist/client/_assets/{client-auth-YW92ZH7u.js → client-auth-TsYq90xm.js} +570 -567
  12. package/dist/{export-Ckwh4BiE.js → export-Dfbq9aot.js} +10 -13
  13. package/dist/{github-sync-Dl9wQMar.js → github-sync--PSoE-Ne.js} +1 -1
  14. package/dist/{github-sync-BMmK3JZ0.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 +1 -2
  22. package/src/client/__tests__/site-header-nav.test.ts +36 -2
  23. package/src/client/components/__tests__/compose-title-from-heading.test.ts +57 -0
  24. package/src/client/components/__tests__/jant-compose-editor.test.ts +94 -2
  25. package/src/client/components/__tests__/jant-nav-manager.test.ts +107 -22
  26. package/src/client/components/__tests__/jant-settings-general.test.ts +1 -0
  27. package/src/client/components/compose-title-from-heading.ts +66 -0
  28. package/src/client/components/compose-types.ts +8 -0
  29. package/src/client/components/inline-image-issues.ts +17 -0
  30. package/src/client/components/jant-compose-dialog.ts +2 -0
  31. package/src/client/components/jant-compose-editor.ts +78 -1
  32. package/src/client/components/jant-compose-fullscreen.ts +2 -0
  33. package/src/client/components/jant-nav-manager.ts +245 -155
  34. package/src/client/components/nav-manager-types.ts +0 -10
  35. package/src/client/site-header-nav.d.ts +1 -0
  36. package/src/client/site-header-nav.js +105 -47
  37. package/src/client/tiptap/__tests__/block-insertion.test.ts +35 -0
  38. package/src/client/tiptap/__tests__/image-input-rules.test.ts +117 -0
  39. package/src/client/tiptap/create-editor.ts +7 -0
  40. package/src/client/tiptap/extensions.ts +7 -2
  41. package/src/client/tiptap/image-input-rules.ts +48 -0
  42. package/src/client/tiptap/image-node.ts +189 -7
  43. package/src/client-auth.ts +0 -1
  44. package/src/i18n/locales/public/en.po +20 -4
  45. package/src/i18n/locales/public/en.ts +1 -1
  46. package/src/i18n/locales/public/zh-Hans.po +20 -4
  47. package/src/i18n/locales/public/zh-Hans.ts +1 -1
  48. package/src/i18n/locales/public/zh-Hant.po +20 -4
  49. package/src/i18n/locales/public/zh-Hant.ts +1 -1
  50. package/src/i18n/locales/settings/en.po +23 -20
  51. package/src/i18n/locales/settings/en.ts +1 -1
  52. package/src/i18n/locales/settings/zh-Hans.po +23 -20
  53. package/src/i18n/locales/settings/zh-Hans.ts +1 -1
  54. package/src/i18n/locales/settings/zh-Hant.po +23 -20
  55. package/src/i18n/locales/settings/zh-Hant.ts +1 -1
  56. package/src/lib/__tests__/navigation.test.ts +4 -37
  57. package/src/lib/__tests__/resolve-config.test.ts +6 -1
  58. package/src/lib/__tests__/view.test.ts +6 -30
  59. package/src/lib/github-sync-site-config.ts +0 -2
  60. package/src/lib/hugo-markdown.ts +1 -1
  61. package/src/lib/navigation.ts +5 -22
  62. package/src/lib/resolve-config.ts +9 -2
  63. package/src/lib/site-header-fragment.tsx +37 -0
  64. package/src/lib/view.ts +2 -7
  65. package/src/node/__tests__/cli-site-snapshot.test.ts +45 -45
  66. package/src/routes/api/__tests__/nav-items.test.ts +26 -0
  67. package/src/routes/api/export.ts +0 -2
  68. package/src/routes/api/internal/__tests__/uploads.test.ts +9 -13
  69. package/src/routes/api/internal/sites.ts +5 -5
  70. package/src/routes/api/nav-items.ts +25 -4
  71. package/src/routes/dash/settings.tsx +8 -2
  72. package/src/routes/feed/__tests__/feed.test.ts +42 -0
  73. package/src/routes/feed/__tests__/sitemap.test.ts +2 -13
  74. package/src/routes/feed/feed.ts +1 -0
  75. package/src/routes/feed/sitemap.ts +2 -6
  76. package/src/routes/pages/archive.tsx +1 -0
  77. package/src/routes/pages/collection.tsx +1 -0
  78. package/src/routes/pages/featured.tsx +0 -4
  79. package/src/routes/pages/home.tsx +4 -51
  80. package/src/routes/pages/latest.tsx +2 -51
  81. package/src/services/__tests__/github-app-installations.test.ts +10 -10
  82. package/src/services/__tests__/navigation.test.ts +41 -0
  83. package/src/services/__tests__/post-timeline.test.ts +56 -0
  84. package/src/services/__tests__/post.test.ts +33 -0
  85. package/src/services/__tests__/settings.test.ts +5 -20
  86. package/src/services/export-theme/assets/client-site.js +10 -10
  87. package/src/services/export-theme/layouts/_default/rss.xml +5 -15
  88. package/src/services/export-theme/layouts/index.html +26 -39
  89. package/src/services/export.ts +5 -10
  90. package/src/services/github-app-installations.ts +1 -3
  91. package/src/services/navigation.ts +44 -13
  92. package/src/services/post.ts +80 -57
  93. package/src/services/settings.ts +0 -10
  94. package/src/services/site-admin.ts +0 -2
  95. package/src/services/upload-session.ts +4 -18
  96. package/src/styles/tokens.css +2 -2
  97. package/src/styles/ui.css +200 -6
  98. package/src/types/bindings.ts +0 -1
  99. package/src/types/config.ts +3 -7
  100. package/src/ui/compose/ComposeDialog.tsx +28 -0
  101. package/src/ui/dash/appearance/NavigationContent.tsx +1 -1
  102. package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
  103. package/src/ui/dash/settings/SettingsRootContent.tsx +3 -3
  104. package/src/ui/layouts/SiteLayout.tsx +249 -216
  105. package/dist/app-B5P1b4TF.js +0 -6
  106. 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);
@@ -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,
@@ -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(() => {});
@@ -77,6 +77,32 @@ describe("Nav Items API Routes", () => {
77
77
  expect(body.label).toBe("GitHub");
78
78
  expect(body.url).toBe("https://github.com");
79
79
  expect(body.type).toBe("link");
80
+ expect(body.headerHtml).toBeUndefined();
81
+ });
82
+
83
+ it("includes a site header fragment for navigation editor requests", async () => {
84
+ const { app } = createTestApp({ authenticated: true });
85
+ app.route("/api/nav-items", navItemsApiRoutes);
86
+
87
+ const res = await app.request("/api/nav-items", {
88
+ method: "POST",
89
+ headers: {
90
+ "Content-Type": "application/json",
91
+ "X-Jant-Site-Header": "include",
92
+ },
93
+ body: JSON.stringify({
94
+ type: "link",
95
+ label: "Docs",
96
+ url: "/docs",
97
+ }),
98
+ });
99
+
100
+ expect(res.status).toBe(201);
101
+ const body = await res.json();
102
+ expect(body.label).toBe("Docs");
103
+ expect(body.headerHtml).toContain('data-site-header-fragment="header"');
104
+ expect(body.headerHtml).toContain('data-site-header-fragment="drawer"');
105
+ expect(body.headerHtml).toContain("Docs");
80
106
  });
81
107
 
82
108
  it("creates a system nav item when authenticated", async () => {
@@ -5,7 +5,6 @@
5
5
  import { Hono } from "hono";
6
6
  import type { Bindings } from "../../types.js";
7
7
  import type { AppVariables } from "../../types/app-context.js";
8
- import { getHomeDefaultViewFromNavItems } from "../../lib/navigation.js";
9
8
  import { requireAuthApi } from "../../middleware/auth.js";
10
9
  import { createExportService } from "../../services/export.js";
11
10
 
@@ -25,7 +24,6 @@ exportApiRoutes.post("/hugo", requireAuthApi(), async (c) => {
25
24
  siteDescription: appConfig.siteDescription,
26
25
  siteLanguage: appConfig.siteLanguage,
27
26
  showJantBrandingOnHome: appConfig.showJantBrandingOnHome,
28
- homeDefaultView: getHomeDefaultViewFromNavItems(navItems),
29
27
  mainRssFeed: appConfig.mainRssFeed,
30
28
  siteFooter: appConfig.siteFooter,
31
29
  showHeaderAvatar: appConfig.showHeaderAvatar,
@@ -136,7 +136,7 @@ describe("Internal upload admin routes", () => {
136
136
  expect(storage.files.has(row.tempStorageKey)).toBe(false);
137
137
  });
138
138
 
139
- it("deletes orphaned media past the grace window and keeps fresh orphans", async () => {
139
+ it("keeps finalized unattached media during upload cleanup", async () => {
140
140
  const storage = createMockStorage();
141
141
  const { app, services, sqlite } = createTestApp({
142
142
  authenticated: false,
@@ -187,28 +187,24 @@ describe("Internal upload admin routes", () => {
187
187
  await expect(res.json()).resolves.toEqual({
188
188
  abortedMultipartUploads: 0,
189
189
  deletedSessions: 0,
190
- deletedOrphanMedia: 1,
191
- // The reaped orphan's object is freshly enqueued (30 days out), so the
192
- // same sweep purges nothing yet.
190
+ deletedOrphanMedia: 0,
193
191
  purgedStorageObjects: 0,
194
192
  });
195
193
 
196
- // Old orphan: DB row gone, original key freed immediately, bytes moved to
197
- // a trash/ key recorded in storage_purge (recoverable within the window).
194
+ // Finalized media may be referenced from post body JSON without being a
195
+ // post attachment, so upload cleanup must not delete it from `postId IS
196
+ // NULL` alone.
198
197
  expect(
199
198
  sqlite.prepare("select id from media where id = ?").get(oldOrphan.id),
200
- ).toBeUndefined();
201
- expect(storage.files.has(oldStorageKey)).toBe(false);
202
- expect([...storage.files.keys()].some((k) => k.startsWith("trash/"))).toBe(
203
- true,
204
- );
199
+ ).toBeDefined();
200
+ expect(storage.files.has(oldStorageKey)).toBe(true);
205
201
  expect(
206
202
  sqlite
207
203
  .prepare("select id from storage_purge where original_key = ?")
208
204
  .get(oldStorageKey),
209
- ).toBeDefined();
205
+ ).toBeUndefined();
210
206
 
211
- // Fresh orphan: untouched.
207
+ // Fresh unattached media is also untouched.
212
208
  expect(
213
209
  sqlite.prepare("select id from media where id = ?").get(freshOrphan.id),
214
210
  ).toBeDefined();
@@ -163,11 +163,11 @@ internalSitesRoutes.get(
163
163
  },
164
164
  );
165
165
 
166
- // Clean up a single managed site's expired upload sessions and orphaned media
167
- // (uploaded during compose but never published). Invoked per-site by the
168
- // hosted control plane's scheduled maintenance. Self-hosted operators use the
169
- // host-scoped `POST /api/internal/uploads/cleanup` route / `jant uploads
170
- // cleanup` CLI instead.
166
+ // Clean up a single managed site's expired upload sessions and due trash
167
+ // objects. Invoked per-site by the hosted control plane's scheduled
168
+ // maintenance. Self-hosted operators use the host-scoped
169
+ // `POST /api/internal/uploads/cleanup` route / `jant uploads cleanup` CLI
170
+ // instead.
171
171
  internalSitesRoutes.post(
172
172
  "/:siteId/uploads/cleanup",
173
173
  requireInternalAdminApi(),