@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.
- package/bin/commands/import-site.js +47 -30
- package/bin/commands/migrate-text-attachments.js +1 -3
- package/bin/commands/site/export.js +1 -3
- package/bin/commands/telegram/register-webhooks.js +9 -3
- package/bin/lib/site-snapshot.js +1 -5
- package/dist/app-CWJFPjuR.js +6 -0
- package/dist/{app-CpmficmQ.js → app-HnzmsaqU.js} +721 -473
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/{client-Dd9U383b.js → client-3_OaxhTs.js} +1 -1
- package/dist/client/_assets/client-DCrcHVJl.css +2 -0
- package/dist/client/_assets/{client-auth-DkpSdIDz.js → client-auth-TsYq90xm.js} +625 -561
- package/dist/{export-Ba7NJImL.js → export-Dfbq9aot.js} +12 -15
- package/dist/{github-sync-Cb4_6_i7.js → github-sync--PSoE-Ne.js} +1 -1
- package/dist/{github-sync-BD4w2m8-.js → github-sync-BGTLx8Mf.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/node.js +4 -4
- package/package.json +1 -1
- package/src/__tests__/export-service.test.ts +1 -2
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +0 -1
- package/src/__tests__/import-site-command.test.ts +76 -2
- package/src/__tests__/vite-dev-plugins.test.ts +34 -0
- package/src/client/__tests__/compose-launch.test.ts +50 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +74 -18
- package/src/client/__tests__/site-header-nav.test.ts +36 -2
- package/src/client/components/__tests__/compose-title-from-heading.test.ts +57 -0
- package/src/client/components/__tests__/jant-command-palette.test.ts +63 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +33 -0
- package/src/client/components/__tests__/jant-compose-editor-rehost-notice.test.ts +20 -1
- package/src/client/components/__tests__/jant-compose-editor.test.ts +94 -2
- package/src/client/components/__tests__/jant-nav-manager.test.ts +181 -20
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +6 -0
- package/src/client/components/__tests__/jant-settings-general.test.ts +72 -0
- package/src/client/components/compose-title-from-heading.ts +66 -0
- package/src/client/components/compose-types.ts +8 -0
- package/src/client/components/inline-image-issues.ts +17 -0
- package/src/client/components/jant-command-palette.ts +67 -2
- package/src/client/components/jant-compose-dialog.ts +10 -2
- package/src/client/components/jant-compose-editor.ts +78 -1
- package/src/client/components/jant-compose-fullscreen.ts +2 -0
- package/src/client/components/jant-nav-manager.ts +382 -152
- package/src/client/components/jant-settings-general.ts +56 -0
- package/src/client/components/nav-manager-types.ts +14 -10
- package/src/client/components/settings-types.ts +30 -0
- package/src/client/compose-launch.ts +30 -0
- package/src/client/compose-shortcuts.ts +46 -3
- package/src/client/site-header-nav.d.ts +1 -0
- package/src/client/site-header-nav.js +105 -47
- package/src/client/tiptap/__tests__/block-insertion.test.ts +35 -0
- package/src/client/tiptap/__tests__/image-input-rules.test.ts +117 -0
- package/src/client/tiptap/__tests__/rehost-images.test.ts +66 -13
- package/src/client/tiptap/create-editor.ts +7 -0
- package/src/client/tiptap/extensions.ts +7 -2
- package/src/client/tiptap/image-input-rules.ts +48 -0
- package/src/client/tiptap/image-node.ts +189 -7
- package/src/client/tiptap/rehost-images.ts +5 -1
- package/src/client-auth.ts +0 -1
- package/src/i18n/__tests__/fallback.test.ts +24 -0
- package/src/i18n/locales/public/en.po +20 -4
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +22 -6
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +22 -6
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +77 -20
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +85 -28
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +79 -22
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/feed.test.ts +96 -10
- package/src/lib/__tests__/navigation.test.ts +4 -37
- package/src/lib/__tests__/resolve-config.test.ts +6 -1
- package/src/lib/__tests__/view.test.ts +6 -30
- package/src/lib/feed.ts +63 -3
- package/src/lib/github-sync-site-config.ts +0 -2
- package/src/lib/hugo-markdown.ts +1 -1
- package/src/lib/navigation.ts +5 -22
- package/src/lib/post-display.ts +14 -8
- package/src/lib/resolve-config.ts +9 -2
- package/src/lib/site-header-fragment.tsx +37 -0
- package/src/lib/view.ts +2 -7
- package/src/node/__tests__/cli-site-snapshot.test.ts +45 -45
- package/src/preset.css +60 -18
- package/src/routes/api/__tests__/nav-items.test.ts +26 -0
- package/src/routes/api/export.ts +0 -2
- package/src/routes/api/internal/__tests__/uploads.test.ts +9 -13
- package/src/routes/api/internal/sites.ts +5 -5
- package/src/routes/api/nav-items.ts +25 -4
- package/src/routes/dash/settings.tsx +48 -3
- package/src/routes/feed/__tests__/feed.test.ts +42 -0
- package/src/routes/feed/__tests__/sitemap.test.ts +2 -13
- package/src/routes/feed/feed.ts +1 -0
- package/src/routes/feed/sitemap.ts +2 -6
- package/src/routes/pages/__tests__/draft-about-edit.test.ts +65 -0
- package/src/routes/pages/archive.tsx +1 -0
- package/src/routes/pages/collection.tsx +1 -0
- package/src/routes/pages/featured.tsx +0 -4
- package/src/routes/pages/home.tsx +4 -51
- package/src/routes/pages/latest.tsx +2 -51
- package/src/routes/pages/page.tsx +44 -12
- package/src/services/__tests__/about-page.test.ts +116 -0
- package/src/services/__tests__/github-app-installations.test.ts +10 -10
- package/src/services/__tests__/navigation.test.ts +231 -1
- package/src/services/__tests__/post-timeline.test.ts +175 -2
- package/src/services/__tests__/post.test.ts +33 -0
- package/src/services/__tests__/settings.test.ts +5 -20
- package/src/services/about-page.ts +143 -0
- package/src/services/export-theme/assets/client-site.js +10 -10
- package/src/services/export-theme/layouts/_default/rss.xml +7 -16
- package/src/services/export-theme/layouts/index.html +26 -39
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +25 -1
- package/src/services/export-theme/styles/main.css +46 -14
- package/src/services/export.ts +5 -10
- package/src/services/github-app-installations.ts +1 -3
- package/src/services/index.ts +27 -17
- package/src/services/navigation.ts +186 -1
- package/src/services/post.ts +103 -63
- package/src/services/settings.ts +0 -10
- package/src/services/site-admin.ts +0 -2
- package/src/services/upload-session.ts +4 -18
- package/src/styles/components.css +20 -0
- package/src/styles/tokens.css +28 -10
- package/src/styles/ui.css +349 -13
- package/src/types/bindings.ts +0 -1
- package/src/types/config.ts +3 -7
- package/src/types/views.ts +13 -0
- package/src/ui/compose/ComposeDialog.tsx +28 -0
- package/src/ui/dash/appearance/NavigationContent.tsx +58 -1
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +19 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/GeneralContent.tsx +44 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +3 -3
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +8 -0
- package/src/ui/feed/__tests__/timeline-cards.test.ts +162 -1
- package/src/ui/layouts/SiteLayout.tsx +249 -216
- package/dist/app-DqKkZenB.js +0 -6
- package/dist/client/_assets/client-BhHHVvSY.css +0 -2
- 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 /
|
|
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("
|
|
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"
|
|
882
|
+
const views = toNavItemViews(items, "/archive");
|
|
907
883
|
expect(views).toHaveLength(3);
|
|
908
|
-
expect(views[0]?.url).toBe("/
|
|
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(
|
|
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
|
|
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,
|
package/src/lib/hugo-markdown.ts
CHANGED
|
@@ -118,7 +118,7 @@ export interface HugoFrontMatter {
|
|
|
118
118
|
collections?: HugoCollectionRef[];
|
|
119
119
|
media?: JantMedia[];
|
|
120
120
|
|
|
121
|
-
// Escape hatch for page-
|
|
121
|
+
// Escape hatch for page-specific export metadata.
|
|
122
122
|
[key: string]: unknown;
|
|
123
123
|
}
|
|
124
124
|
|
package/src/lib/navigation.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { Context } from "hono";
|
|
8
|
-
import type { Collection,
|
|
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
|
|
68
|
-
//
|
|
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,
|
package/src/lib/post-display.ts
CHANGED
|
@@ -79,9 +79,17 @@ function resolvePostSocialImage(
|
|
|
79
79
|
return undefined;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
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
|
|
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?:
|
|
113
|
+
options?: PostDisplayOptions,
|
|
106
114
|
): Promise<PostView | null> {
|
|
107
115
|
const post = await c.var.services.posts.getById(postId);
|
|
108
|
-
|
|
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?:
|
|
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,
|
|
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
|
-
|
|
148
|
-
|
|
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 =
|
|
493
|
+
url = "/";
|
|
497
494
|
}
|
|
498
495
|
|
|
499
496
|
if (item.systemKey === "featured") {
|
|
500
|
-
url =
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
584
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(() => {});
|