@jant/core 0.6.11 → 0.6.12
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 +45 -20
- package/dist/app-B5P1b4TF.js +6 -0
- package/dist/{app-CpmficmQ.js → app-D-zAhayc.js} +331 -55
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-BUxR-E8O.css +2 -0
- package/dist/client/_assets/{client-Dd9U383b.js → client-S1cdvulk.js} +1 -1
- package/dist/client/_assets/{client-auth-DkpSdIDz.js → client-auth-YW92ZH7u.js} +116 -55
- package/dist/{export-Ba7NJImL.js → export-Ckwh4BiE.js} +4 -4
- package/dist/{github-sync-BD4w2m8-.js → github-sync-BMmK3JZ0.js} +2 -2
- package/dist/{github-sync-Cb4_6_i7.js → github-sync-Dl9wQMar.js} +1 -1
- package/dist/index.js +3 -3
- package/dist/node.js +4 -4
- package/package.json +1 -1
- package/src/__tests__/import-site-command.test.ts +75 -0
- 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/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-nav-manager.test.ts +77 -1
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +6 -0
- package/src/client/components/__tests__/jant-settings-general.test.ts +71 -0
- package/src/client/components/jant-command-palette.ts +67 -2
- package/src/client/components/jant-compose-dialog.ts +8 -2
- package/src/client/components/jant-nav-manager.ts +142 -2
- package/src/client/components/jant-settings-general.ts +56 -0
- package/src/client/components/nav-manager-types.ts +14 -0
- 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/tiptap/__tests__/rehost-images.test.ts +66 -13
- package/src/client/tiptap/rehost-images.ts +5 -1
- package/src/i18n/__tests__/fallback.test.ts +24 -0
- package/src/i18n/locales/public/zh-Hans.po +2 -2
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +2 -2
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +54 -0
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +64 -10
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +56 -2
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/feed.test.ts +96 -10
- package/src/lib/feed.ts +63 -3
- package/src/lib/post-display.ts +14 -8
- package/src/preset.css +60 -18
- package/src/routes/dash/settings.tsx +41 -2
- package/src/routes/pages/__tests__/draft-about-edit.test.ts +65 -0
- package/src/routes/pages/page.tsx +44 -12
- package/src/services/__tests__/about-page.test.ts +116 -0
- package/src/services/__tests__/navigation.test.ts +190 -1
- package/src/services/__tests__/post-timeline.test.ts +119 -2
- package/src/services/about-page.ts +143 -0
- package/src/services/export-theme/layouts/_default/rss.xml +2 -1
- 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/index.ts +27 -17
- package/src/services/navigation.ts +155 -1
- package/src/services/post.ts +24 -7
- package/src/styles/components.css +20 -0
- package/src/styles/tokens.css +26 -8
- package/src/styles/ui.css +149 -7
- package/src/types/views.ts +13 -0
- package/src/ui/dash/appearance/NavigationContent.tsx +57 -0
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +19 -0
- package/src/ui/dash/settings/GeneralContent.tsx +44 -0
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +8 -0
- package/src/ui/feed/__tests__/timeline-cards.test.ts +162 -1
- package/dist/app-DqKkZenB.js +0 -6
- package/dist/client/_assets/client-BhHHVvSY.css +0 -2
package/src/preset.css
CHANGED
|
@@ -182,9 +182,9 @@ html {
|
|
|
182
182
|
--tw-prose-quote-borders: var(--color-border);
|
|
183
183
|
--tw-prose-captions: var(--color-muted-foreground);
|
|
184
184
|
--tw-prose-kbd: var(--color-foreground);
|
|
185
|
-
--tw-prose-code: var(--
|
|
186
|
-
--tw-prose-pre-code: var(--
|
|
187
|
-
--tw-prose-pre-bg: var(--
|
|
185
|
+
--tw-prose-code: var(--site-code-text);
|
|
186
|
+
--tw-prose-pre-code: var(--site-code-text);
|
|
187
|
+
--tw-prose-pre-bg: var(--site-code-block-bg);
|
|
188
188
|
--tw-prose-th-borders: var(--color-border);
|
|
189
189
|
--tw-prose-td-borders: var(--color-border);
|
|
190
190
|
font-family: var(--font-body);
|
|
@@ -213,14 +213,12 @@ html {
|
|
|
213
213
|
|
|
214
214
|
:where(h2) {
|
|
215
215
|
font-size: var(--type-content-title);
|
|
216
|
-
font-style: italic;
|
|
217
216
|
margin-top: 2.1rem;
|
|
218
217
|
margin-bottom: 1.4rem;
|
|
219
218
|
}
|
|
220
219
|
|
|
221
220
|
:where(h3) {
|
|
222
221
|
font-size: var(--type-content-subtitle);
|
|
223
|
-
font-style: italic;
|
|
224
222
|
margin-top: 2rem;
|
|
225
223
|
margin-bottom: 1.4rem;
|
|
226
224
|
}
|
|
@@ -236,12 +234,12 @@ html {
|
|
|
236
234
|
|
|
237
235
|
:where(ul, ol) {
|
|
238
236
|
margin: 1.25em 0;
|
|
239
|
-
padding-left: 1.
|
|
237
|
+
padding-left: 1.4em;
|
|
240
238
|
}
|
|
241
239
|
|
|
242
240
|
:where(li, dt) {
|
|
243
|
-
margin-top:
|
|
244
|
-
margin-bottom:
|
|
241
|
+
margin-top: 0.65em;
|
|
242
|
+
margin-bottom: 0.65em;
|
|
245
243
|
}
|
|
246
244
|
|
|
247
245
|
/* Normalize li > p so list spacing is controlled by li alone,
|
|
@@ -257,18 +255,35 @@ html {
|
|
|
257
255
|
:where(code, pre code) {
|
|
258
256
|
font-size: var(--type-code);
|
|
259
257
|
line-height: 1.42;
|
|
258
|
+
color: var(--site-code-text);
|
|
260
259
|
overflow-wrap: break-word;
|
|
261
260
|
}
|
|
262
261
|
|
|
263
262
|
:where(:not(pre) > code) {
|
|
264
|
-
padding: 0.
|
|
265
|
-
background-color: var(--site-
|
|
266
|
-
border:
|
|
267
|
-
|
|
263
|
+
padding: 0.08em 0.28em;
|
|
264
|
+
background-color: var(--site-code-bg);
|
|
265
|
+
border-radius: 0.22em;
|
|
266
|
+
box-decoration-break: clone;
|
|
267
|
+
-webkit-box-decoration-break: clone;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
:where(:not(pre) > code)::before,
|
|
271
|
+
:where(:not(pre) > code)::after {
|
|
272
|
+
content: none;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
:where(pre) {
|
|
276
|
+
background-color: var(--site-code-block-bg);
|
|
277
|
+
border: 1px solid var(--site-code-block-border);
|
|
278
|
+
border-radius: 6px;
|
|
268
279
|
}
|
|
269
280
|
|
|
270
281
|
:where(pre code) {
|
|
271
282
|
font-size: var(--type-code-block);
|
|
283
|
+
color: inherit;
|
|
284
|
+
background: transparent;
|
|
285
|
+
border: 0;
|
|
286
|
+
padding: 0;
|
|
272
287
|
}
|
|
273
288
|
|
|
274
289
|
:where(h1, h2, h3) :where(code) {
|
|
@@ -425,6 +440,37 @@ html {
|
|
|
425
440
|
margin-bottom: 0;
|
|
426
441
|
}
|
|
427
442
|
|
|
443
|
+
/*
|
|
444
|
+
* Feed body headings
|
|
445
|
+
*
|
|
446
|
+
* In list contexts, the post title owns the card heading level. Authored
|
|
447
|
+
* headings inside the body should remain visible, but they should read as
|
|
448
|
+
* sections within the card rather than as peer post titles.
|
|
449
|
+
*/
|
|
450
|
+
[data-post]:not([data-page="post"]) [data-post-body].prose {
|
|
451
|
+
:where(h1, h2) {
|
|
452
|
+
font-size: calc(var(--type-content-body) * 1.12);
|
|
453
|
+
font-weight: var(--fw-medium);
|
|
454
|
+
margin-top: 1.45rem;
|
|
455
|
+
margin-bottom: 0.55rem;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
:where(h3, h4) {
|
|
459
|
+
font-size: var(--type-content-body);
|
|
460
|
+
font-weight: var(--fw-medium);
|
|
461
|
+
margin-top: 1.2rem;
|
|
462
|
+
margin-bottom: 0.45rem;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
:where(h5, h6) {
|
|
466
|
+
font-size: var(--type-secondary);
|
|
467
|
+
font-weight: var(--fw-medium);
|
|
468
|
+
color: var(--site-text-secondary);
|
|
469
|
+
margin-top: 1rem;
|
|
470
|
+
margin-bottom: 0.35rem;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
428
474
|
/*
|
|
429
475
|
* Detail-page reading prose
|
|
430
476
|
*
|
|
@@ -455,12 +501,8 @@ html {
|
|
|
455
501
|
);
|
|
456
502
|
--tw-prose-captions: var(--site-reading-caption);
|
|
457
503
|
--tw-prose-kbd: var(--site-reading-heading);
|
|
458
|
-
--tw-prose-code: var(--site-
|
|
459
|
-
--tw-prose-pre-code:
|
|
460
|
-
in srgb,
|
|
461
|
-
var(--site-reading-caption) 86%,
|
|
462
|
-
var(--site-reading-body)
|
|
463
|
-
);
|
|
504
|
+
--tw-prose-code: var(--site-code-text);
|
|
505
|
+
--tw-prose-pre-code: var(--site-code-text);
|
|
464
506
|
--tw-prose-th-borders: color-mix(
|
|
465
507
|
in srgb,
|
|
466
508
|
var(--site-divider) 88%,
|
|
@@ -75,6 +75,7 @@ import {
|
|
|
75
75
|
isSyncPending,
|
|
76
76
|
markSyncPending,
|
|
77
77
|
runBackgroundSync,
|
|
78
|
+
triggerGitHubSyncInline,
|
|
78
79
|
} from "../../lib/github-sync-trigger.js";
|
|
79
80
|
import { buildSyncSiteConfig } from "../../lib/github-sync-site-config.js";
|
|
80
81
|
import {
|
|
@@ -117,6 +118,10 @@ function publicPath(c: Context<Env>, path: string): string {
|
|
|
117
118
|
return toPublicPath(path, c.var.appConfig.sitePathPrefix);
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
function aboutEditPath(c: Context<Env>): string {
|
|
122
|
+
return `${publicPath(c, "/about")}?edit=1`;
|
|
123
|
+
}
|
|
124
|
+
|
|
120
125
|
/**
|
|
121
126
|
* Breadcrumb labels for admin settings pages.
|
|
122
127
|
*
|
|
@@ -302,7 +307,10 @@ settingsRoutes.get("/general", async (c) => {
|
|
|
302
307
|
const dbSiteDescription = allSettings["SITE_DESCRIPTION"] ?? "";
|
|
303
308
|
|
|
304
309
|
const saved = c.req.query("saved") !== undefined;
|
|
305
|
-
const navData = await
|
|
310
|
+
const [navData, aboutPage] = await Promise.all([
|
|
311
|
+
getNavigationData(c),
|
|
312
|
+
c.var.services.aboutPage.getStatus(),
|
|
313
|
+
]);
|
|
306
314
|
const siteUrlForDisplay =
|
|
307
315
|
appConfig.siteUrl || new URL(publicPath(c, "/"), c.req.url).toString();
|
|
308
316
|
|
|
@@ -356,6 +364,9 @@ settingsRoutes.get("/general", async (c) => {
|
|
|
356
364
|
noindex={appConfig.noindex}
|
|
357
365
|
demoMode={appConfig.demoMode}
|
|
358
366
|
timezones={TIMEZONES}
|
|
367
|
+
aboutPage={aboutPage}
|
|
368
|
+
aboutEditUrl={aboutEditPath(c)}
|
|
369
|
+
aboutCreateUrl={publicPath(c, "/settings/general/about-page")}
|
|
359
370
|
/>
|
|
360
371
|
</>
|
|
361
372
|
),
|
|
@@ -534,6 +545,32 @@ settingsRoutes.post("/general/search", async (c) => {
|
|
|
534
545
|
);
|
|
535
546
|
});
|
|
536
547
|
|
|
548
|
+
settingsRoutes.post("/general/about-page", async (c) => {
|
|
549
|
+
try {
|
|
550
|
+
await c.var.services.aboutPage.ensurePage();
|
|
551
|
+
await triggerGitHubSyncInline(c);
|
|
552
|
+
} catch (error) {
|
|
553
|
+
if (error instanceof ValidationError) {
|
|
554
|
+
const wantsJson = c.req.header("accept")?.includes("application/json");
|
|
555
|
+
if (wantsJson) {
|
|
556
|
+
return c.json({ error: error.message, code: error.code }, 400);
|
|
557
|
+
}
|
|
558
|
+
return c.text(error.message, 400);
|
|
559
|
+
}
|
|
560
|
+
throw error;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const wantsJson = c.req.header("accept")?.includes("application/json");
|
|
564
|
+
if (wantsJson) {
|
|
565
|
+
return c.json({
|
|
566
|
+
status: "redirect" as const,
|
|
567
|
+
url: aboutEditPath(c),
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return c.redirect(aboutEditPath(c));
|
|
572
|
+
});
|
|
573
|
+
|
|
537
574
|
// ===========================================================================
|
|
538
575
|
// Avatar
|
|
539
576
|
// ===========================================================================
|
|
@@ -743,9 +780,10 @@ settingsRoutes.post("/avatar/display", async (c) => {
|
|
|
743
780
|
// ===========================================================================
|
|
744
781
|
|
|
745
782
|
settingsRoutes.get("/navigation", async (c) => {
|
|
746
|
-
const [navItems, directoryData] = await Promise.all([
|
|
783
|
+
const [navItems, directoryData, suggestedLinks] = await Promise.all([
|
|
747
784
|
c.var.services.navItems.list(),
|
|
748
785
|
c.var.services.collections.listDirectoryData(),
|
|
786
|
+
c.var.services.navItems.listSuggestedLinks(),
|
|
749
787
|
]);
|
|
750
788
|
const navData = await getNavigationData(c);
|
|
751
789
|
|
|
@@ -762,6 +800,7 @@ settingsRoutes.get("/navigation", async (c) => {
|
|
|
762
800
|
<NavigationContent
|
|
763
801
|
navItems={navItems}
|
|
764
802
|
directoryData={directoryData}
|
|
803
|
+
suggestedLinks={suggestedLinks}
|
|
765
804
|
mainRssFeed={c.var.appConfig.mainRssFeed}
|
|
766
805
|
siteName={navData.siteName}
|
|
767
806
|
sitePathPrefix={c.var.appConfig.sitePathPrefix}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createTestApp } from "../../../__tests__/helpers/app.js";
|
|
3
|
+
import { pageRoutes } from "../page.js";
|
|
4
|
+
|
|
5
|
+
function createPageTestApp(options: { authenticated?: boolean } = {}) {
|
|
6
|
+
const testApp = createTestApp(options);
|
|
7
|
+
const { app } = testApp;
|
|
8
|
+
|
|
9
|
+
app.use("*", async (c, next) => {
|
|
10
|
+
c.set("publicPath", c.req.path);
|
|
11
|
+
c.set("publicRequestUrl", c.req.url);
|
|
12
|
+
await next();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
app.route("/", pageRoutes);
|
|
16
|
+
|
|
17
|
+
return testApp;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function createDraftAbout(
|
|
21
|
+
services: ReturnType<typeof createTestApp>["services"],
|
|
22
|
+
) {
|
|
23
|
+
return services.posts.create({
|
|
24
|
+
format: "note",
|
|
25
|
+
title: "About",
|
|
26
|
+
slug: "about",
|
|
27
|
+
bodyMarkdown: "Draft About body",
|
|
28
|
+
status: "draft",
|
|
29
|
+
visibility: "latest_hidden",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("draft About edit route", () => {
|
|
34
|
+
it("keeps draft About hidden from anonymous edit requests", async () => {
|
|
35
|
+
const { app, services } = createPageTestApp();
|
|
36
|
+
await createDraftAbout(services);
|
|
37
|
+
|
|
38
|
+
const res = await app.request("/about?edit=1");
|
|
39
|
+
|
|
40
|
+
expect(res.status).toBe(404);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("keeps draft About hidden from normal authenticated page requests", async () => {
|
|
44
|
+
const { app, services } = createPageTestApp({ authenticated: true });
|
|
45
|
+
await createDraftAbout(services);
|
|
46
|
+
|
|
47
|
+
const res = await app.request("/about");
|
|
48
|
+
|
|
49
|
+
expect(res.status).toBe(404);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("renders draft About for authenticated edit requests", async () => {
|
|
53
|
+
const { app, services } = createPageTestApp({ authenticated: true });
|
|
54
|
+
const post = await createDraftAbout(services);
|
|
55
|
+
|
|
56
|
+
const res = await app.request("/about?edit=1");
|
|
57
|
+
|
|
58
|
+
expect(res.status).toBe(200);
|
|
59
|
+
const html = await res.text();
|
|
60
|
+
expect(html).toContain(`data-post-id="${post.id}"`);
|
|
61
|
+
expect(html).toContain("data-authenticated");
|
|
62
|
+
expect(html).not.toContain('rel="canonical"');
|
|
63
|
+
expect(html).not.toContain("application/ld+json");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
toPublicHref,
|
|
24
24
|
toPublicPath,
|
|
25
25
|
} from "../../lib/url.js";
|
|
26
|
+
import { ABOUT_PAGE_SLUG } from "../../services/about-page.js";
|
|
26
27
|
import { isTextAttachment } from "../../services/media.js";
|
|
27
28
|
import type { Post } from "../../types.js";
|
|
28
29
|
import { renderArchivePage } from "./archive.js";
|
|
@@ -186,12 +187,30 @@ async function renderPostWithTextPreview(
|
|
|
186
187
|
});
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
|
|
190
|
+
function canRenderDraftAboutEditor(
|
|
191
|
+
c: Context<Env>,
|
|
192
|
+
fullPath: string,
|
|
193
|
+
post: Post,
|
|
194
|
+
): boolean {
|
|
195
|
+
return (
|
|
196
|
+
post.status === "draft" &&
|
|
197
|
+
fullPath === ABOUT_PAGE_SLUG &&
|
|
198
|
+
c.req.query("edit") === "1" &&
|
|
199
|
+
c.var.isAuthenticated
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function renderPost(
|
|
204
|
+
c: Context<Env>,
|
|
205
|
+
post: Post,
|
|
206
|
+
options: { allowDraft?: boolean } = {},
|
|
207
|
+
) {
|
|
190
208
|
// Start navData fetch immediately — it's independent of thread/media queries
|
|
191
209
|
const navDataPromise = getNavigationData(c);
|
|
192
210
|
const display = await assemblePostPageDisplay(c, post, {
|
|
193
211
|
// Private-post access is validated before renderPost() is called.
|
|
194
212
|
isAuthenticated: true,
|
|
213
|
+
allowDraft: options.allowDraft,
|
|
195
214
|
});
|
|
196
215
|
if (!display) {
|
|
197
216
|
return c.notFound();
|
|
@@ -208,15 +227,25 @@ async function renderPost(c: Context<Env>, post: Post) {
|
|
|
208
227
|
return renderPublicPage(c, {
|
|
209
228
|
title: buildPageTitle(meta.title, navData.siteName),
|
|
210
229
|
description: meta.description,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
230
|
+
...(post.status === "published"
|
|
231
|
+
? {
|
|
232
|
+
canonicalHref,
|
|
233
|
+
socialImageUrl: display.socialImage?.url,
|
|
234
|
+
socialImageAlt: display.socialImage?.alt,
|
|
235
|
+
socialImageWidth: display.socialImage?.width,
|
|
236
|
+
socialImageHeight: display.socialImage?.height,
|
|
237
|
+
ogType: "article" as const,
|
|
238
|
+
articlePublishedTime: display.articlePublishedTime,
|
|
239
|
+
articleModifiedTime: display.articleModifiedTime,
|
|
240
|
+
jsonLd: buildPostJsonLd(
|
|
241
|
+
c,
|
|
242
|
+
display,
|
|
243
|
+
meta,
|
|
244
|
+
canonicalHref,
|
|
245
|
+
navData.siteName,
|
|
246
|
+
),
|
|
247
|
+
}
|
|
248
|
+
: {}),
|
|
220
249
|
navData,
|
|
221
250
|
content: (
|
|
222
251
|
<PostPage post={display.postView} threadPosts={display.threadPostViews} />
|
|
@@ -350,7 +379,10 @@ pageRoutes.get("/*", async (c) => {
|
|
|
350
379
|
|
|
351
380
|
if (resolved.postId) {
|
|
352
381
|
const post = await c.var.services.posts.getById(resolved.postId);
|
|
353
|
-
if (!post
|
|
382
|
+
if (!post) return c.notFound();
|
|
383
|
+
|
|
384
|
+
const allowDraft = canRenderDraftAboutEditor(c, fullPath, post);
|
|
385
|
+
if (post.status === "draft" && !allowDraft) return c.notFound();
|
|
354
386
|
|
|
355
387
|
if (post.visibility === "private") {
|
|
356
388
|
const navData = await getNavigationData(c);
|
|
@@ -368,7 +400,7 @@ pageRoutes.get("/*", async (c) => {
|
|
|
368
400
|
}
|
|
369
401
|
}
|
|
370
402
|
|
|
371
|
-
return renderPost(c, post);
|
|
403
|
+
return renderPost(c, post, { allowDraft });
|
|
372
404
|
}
|
|
373
405
|
|
|
374
406
|
if (resolved.collectionId) {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createTestDatabase,
|
|
4
|
+
DEFAULT_TEST_SITE_ID,
|
|
5
|
+
} from "../../__tests__/helpers/db.js";
|
|
6
|
+
import type { Database } from "../../db/index.js";
|
|
7
|
+
import { createAboutPageService } from "../about-page.js";
|
|
8
|
+
import { createCollectionService } from "../collection.js";
|
|
9
|
+
import { createPathService } from "../path.js";
|
|
10
|
+
import { createPostService } from "../post.js";
|
|
11
|
+
|
|
12
|
+
const ABOUT_BODY = JSON.stringify({
|
|
13
|
+
type: "doc",
|
|
14
|
+
content: [
|
|
15
|
+
{
|
|
16
|
+
type: "paragraph",
|
|
17
|
+
content: [{ type: "text", text: "About this site." }],
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("AboutPageService", () => {
|
|
23
|
+
let aboutPage: ReturnType<typeof createAboutPageService>;
|
|
24
|
+
let posts: ReturnType<typeof createPostService>;
|
|
25
|
+
let collections: ReturnType<typeof createCollectionService>;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
const testDb = createTestDatabase();
|
|
29
|
+
const db = testDb.db as unknown as Database;
|
|
30
|
+
const paths = createPathService(db, DEFAULT_TEST_SITE_ID);
|
|
31
|
+
posts = createPostService(
|
|
32
|
+
db,
|
|
33
|
+
{ slugIdLength: 5 },
|
|
34
|
+
DEFAULT_TEST_SITE_ID,
|
|
35
|
+
paths,
|
|
36
|
+
);
|
|
37
|
+
collections = createCollectionService(db, DEFAULT_TEST_SITE_ID, paths);
|
|
38
|
+
aboutPage = createAboutPageService({
|
|
39
|
+
paths,
|
|
40
|
+
posts,
|
|
41
|
+
collections,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("reports a missing About page when /about is unused", async () => {
|
|
46
|
+
await expect(aboutPage.getStatus()).resolves.toMatchObject({
|
|
47
|
+
state: "missing",
|
|
48
|
+
path: "/about",
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("recognizes a post at /about as the About page", async () => {
|
|
53
|
+
const post = await posts.create({
|
|
54
|
+
format: "note",
|
|
55
|
+
title: "About",
|
|
56
|
+
slug: "about",
|
|
57
|
+
visibility: "latest_hidden",
|
|
58
|
+
body: ABOUT_BODY,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await expect(aboutPage.getStatus()).resolves.toMatchObject({
|
|
62
|
+
state: "ready",
|
|
63
|
+
post: {
|
|
64
|
+
id: post.id,
|
|
65
|
+
title: "About",
|
|
66
|
+
visibility: "latest_hidden",
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("creates a hidden About page when /about is unused", async () => {
|
|
72
|
+
const post = await aboutPage.ensurePage();
|
|
73
|
+
expect(post).toMatchObject({
|
|
74
|
+
title: "About",
|
|
75
|
+
status: "published",
|
|
76
|
+
visibility: "latest_hidden",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await expect(aboutPage.getStatus()).resolves.toMatchObject({
|
|
80
|
+
state: "ready",
|
|
81
|
+
post: {
|
|
82
|
+
id: post.id,
|
|
83
|
+
title: "About",
|
|
84
|
+
status: "published",
|
|
85
|
+
visibility: "latest_hidden",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("reports a conflict when a collection owns /about", async () => {
|
|
91
|
+
const collection = await collections.create({
|
|
92
|
+
title: "About",
|
|
93
|
+
slug: "about",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await expect(aboutPage.getStatus()).resolves.toMatchObject({
|
|
97
|
+
state: "conflict",
|
|
98
|
+
conflict: {
|
|
99
|
+
targetType: "collection",
|
|
100
|
+
id: collection.id,
|
|
101
|
+
title: "About",
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("rejects About creation when another item owns /about", async () => {
|
|
107
|
+
await collections.create({
|
|
108
|
+
title: "About",
|
|
109
|
+
slug: "about",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await expect(aboutPage.ensurePage()).rejects.toThrow(
|
|
113
|
+
"/about is already used.",
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
});
|