@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
|
@@ -9,11 +9,50 @@ import { now } from "../../lib/time.js";
|
|
|
9
9
|
|
|
10
10
|
const TEST_COLLECTION_ID = "col_test00000000000000000000001";
|
|
11
11
|
const TEST_COLLECTION_ID_2 = "col_test00000000000000000000002";
|
|
12
|
+
const TEST_POST_ID = "pst_test00000000000000000000001";
|
|
13
|
+
const TEST_POST_ID_2 = "pst_test00000000000000000000002";
|
|
14
|
+
|
|
15
|
+
function insertTestPath(
|
|
16
|
+
sqlite: ReturnType<typeof createTestDatabase>["sqlite"],
|
|
17
|
+
input: {
|
|
18
|
+
id: string;
|
|
19
|
+
path: string;
|
|
20
|
+
kind: "slug" | "alias" | "redirect" | "archive";
|
|
21
|
+
postId?: string | null;
|
|
22
|
+
collectionId?: string | null;
|
|
23
|
+
redirectToPath?: string | null;
|
|
24
|
+
redirectType?: 301 | 302 | null;
|
|
25
|
+
archiveQuery?: string | null;
|
|
26
|
+
},
|
|
27
|
+
) {
|
|
28
|
+
const ts = now();
|
|
29
|
+
sqlite
|
|
30
|
+
.prepare(
|
|
31
|
+
`INSERT INTO path_registry (
|
|
32
|
+
id, site_id, path, kind, post_id, collection_id, redirect_to_path,
|
|
33
|
+
redirect_type, archive_query, created_at, updated_at
|
|
34
|
+
)
|
|
35
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
36
|
+
)
|
|
37
|
+
.run(
|
|
38
|
+
input.id,
|
|
39
|
+
DEFAULT_TEST_SITE_ID,
|
|
40
|
+
input.path,
|
|
41
|
+
input.kind,
|
|
42
|
+
input.postId ?? null,
|
|
43
|
+
input.collectionId ?? null,
|
|
44
|
+
input.redirectToPath ?? null,
|
|
45
|
+
input.redirectType ?? null,
|
|
46
|
+
input.archiveQuery ?? null,
|
|
47
|
+
ts,
|
|
48
|
+
ts,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
12
51
|
|
|
13
52
|
function insertTestCollection(
|
|
14
53
|
sqlite: ReturnType<typeof createTestDatabase>["sqlite"],
|
|
15
54
|
id: string,
|
|
16
|
-
|
|
55
|
+
slug: string,
|
|
17
56
|
title: string,
|
|
18
57
|
) {
|
|
19
58
|
const ts = now();
|
|
@@ -23,6 +62,51 @@ function insertTestCollection(
|
|
|
23
62
|
VALUES (?, ?, ?, 'newest', ?, ?)`,
|
|
24
63
|
)
|
|
25
64
|
.run(id, DEFAULT_TEST_SITE_ID, title, ts, ts);
|
|
65
|
+
insertTestPath(sqlite, {
|
|
66
|
+
id: `pth_col_${id.slice(4)}`,
|
|
67
|
+
path: slug,
|
|
68
|
+
kind: "slug",
|
|
69
|
+
collectionId: id,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function insertTestPost(
|
|
74
|
+
sqlite: ReturnType<typeof createTestDatabase>["sqlite"],
|
|
75
|
+
input: {
|
|
76
|
+
id: string;
|
|
77
|
+
slug: string;
|
|
78
|
+
title: string | null;
|
|
79
|
+
status?: "draft" | "published";
|
|
80
|
+
visibility?: "public" | "latest_hidden" | "private";
|
|
81
|
+
},
|
|
82
|
+
) {
|
|
83
|
+
const ts = now();
|
|
84
|
+
sqlite
|
|
85
|
+
.prepare(
|
|
86
|
+
`INSERT INTO post (
|
|
87
|
+
id, site_id, format, status, visibility, title, thread_id,
|
|
88
|
+
published_at, last_activity_at, created_at, updated_at
|
|
89
|
+
)
|
|
90
|
+
VALUES (?, ?, 'note', ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
91
|
+
)
|
|
92
|
+
.run(
|
|
93
|
+
input.id,
|
|
94
|
+
DEFAULT_TEST_SITE_ID,
|
|
95
|
+
input.status ?? "published",
|
|
96
|
+
input.visibility ?? "latest_hidden",
|
|
97
|
+
input.title,
|
|
98
|
+
input.id,
|
|
99
|
+
ts,
|
|
100
|
+
ts,
|
|
101
|
+
ts,
|
|
102
|
+
ts,
|
|
103
|
+
);
|
|
104
|
+
insertTestPath(sqlite, {
|
|
105
|
+
id: `pth_pst_${input.id.slice(4)}`,
|
|
106
|
+
path: input.slug,
|
|
107
|
+
kind: "slug",
|
|
108
|
+
postId: input.id,
|
|
109
|
+
});
|
|
26
110
|
}
|
|
27
111
|
|
|
28
112
|
describe("NavItemService", () => {
|
|
@@ -277,6 +361,111 @@ describe("NavItemService", () => {
|
|
|
277
361
|
});
|
|
278
362
|
});
|
|
279
363
|
|
|
364
|
+
describe("listSuggestedLinks", () => {
|
|
365
|
+
it("suggests a published /about page as a normal link", async () => {
|
|
366
|
+
insertTestPost(sqlite, {
|
|
367
|
+
id: TEST_POST_ID,
|
|
368
|
+
slug: "about",
|
|
369
|
+
title: "About me",
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const suggestions = await navItemService.listSuggestedLinks();
|
|
373
|
+
|
|
374
|
+
expect(suggestions).toEqual([
|
|
375
|
+
{
|
|
376
|
+
key: "about",
|
|
377
|
+
label: "About me",
|
|
378
|
+
url: "/about",
|
|
379
|
+
targetType: "page",
|
|
380
|
+
navItemType: "link",
|
|
381
|
+
},
|
|
382
|
+
]);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("suggests a canonical /now collection as a collection nav item", async () => {
|
|
386
|
+
insertTestCollection(sqlite, TEST_COLLECTION_ID, "now", "Now");
|
|
387
|
+
|
|
388
|
+
const suggestions = await navItemService.listSuggestedLinks();
|
|
389
|
+
|
|
390
|
+
expect(suggestions).toEqual([
|
|
391
|
+
{
|
|
392
|
+
key: "now",
|
|
393
|
+
label: "Now",
|
|
394
|
+
url: "/now",
|
|
395
|
+
targetType: "collection",
|
|
396
|
+
navItemType: "collection",
|
|
397
|
+
collectionId: TEST_COLLECTION_ID,
|
|
398
|
+
},
|
|
399
|
+
]);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("suggests a /now collection alias as a normal link to preserve the path", async () => {
|
|
403
|
+
insertTestCollection(sqlite, TEST_COLLECTION_ID, "updates", "Updates");
|
|
404
|
+
insertTestPath(sqlite, {
|
|
405
|
+
id: "pth_now_alias",
|
|
406
|
+
path: "now",
|
|
407
|
+
kind: "alias",
|
|
408
|
+
collectionId: TEST_COLLECTION_ID,
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const suggestions = await navItemService.listSuggestedLinks();
|
|
412
|
+
|
|
413
|
+
expect(suggestions).toEqual([
|
|
414
|
+
{
|
|
415
|
+
key: "now",
|
|
416
|
+
label: "Updates",
|
|
417
|
+
url: "/now",
|
|
418
|
+
targetType: "collection",
|
|
419
|
+
navItemType: "link",
|
|
420
|
+
},
|
|
421
|
+
]);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("hides suggestions already represented by path or collection id", async () => {
|
|
425
|
+
insertTestPost(sqlite, {
|
|
426
|
+
id: TEST_POST_ID,
|
|
427
|
+
slug: "about",
|
|
428
|
+
title: "About",
|
|
429
|
+
});
|
|
430
|
+
insertTestCollection(sqlite, TEST_COLLECTION_ID, "now", "Now");
|
|
431
|
+
|
|
432
|
+
await navItemService.create({
|
|
433
|
+
type: "link",
|
|
434
|
+
label: "About",
|
|
435
|
+
url: "/about/",
|
|
436
|
+
});
|
|
437
|
+
await navItemService.create({
|
|
438
|
+
type: "collection",
|
|
439
|
+
collectionId: TEST_COLLECTION_ID,
|
|
440
|
+
label: "Now",
|
|
441
|
+
url: "/now",
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const suggestions = await navItemService.listSuggestedLinks();
|
|
445
|
+
|
|
446
|
+
expect(suggestions).toEqual([]);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("does not suggest draft or private posts", async () => {
|
|
450
|
+
insertTestPost(sqlite, {
|
|
451
|
+
id: TEST_POST_ID,
|
|
452
|
+
slug: "about",
|
|
453
|
+
title: "About",
|
|
454
|
+
status: "draft",
|
|
455
|
+
});
|
|
456
|
+
insertTestPost(sqlite, {
|
|
457
|
+
id: TEST_POST_ID_2,
|
|
458
|
+
slug: "now",
|
|
459
|
+
title: "Now",
|
|
460
|
+
visibility: "private",
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const suggestions = await navItemService.listSuggestedLinks();
|
|
464
|
+
|
|
465
|
+
expect(suggestions).toEqual([]);
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
280
469
|
describe("update", () => {
|
|
281
470
|
it("updates a nav item's label", async () => {
|
|
282
471
|
const created = await navItemService.create({
|
|
@@ -464,8 +464,86 @@ describe("PostService - Timeline features", () => {
|
|
|
464
464
|
});
|
|
465
465
|
});
|
|
466
466
|
|
|
467
|
+
describe("listCollectionThreadRootIds", () => {
|
|
468
|
+
it("bumps a hidden collection thread when a non-quiet reply is published", async () => {
|
|
469
|
+
const collection = await collectionService.create({
|
|
470
|
+
slug: "thinking",
|
|
471
|
+
title: "Thinking",
|
|
472
|
+
});
|
|
473
|
+
const hiddenRoot = await postService.create({
|
|
474
|
+
format: "note",
|
|
475
|
+
bodyMarkdown: "Hidden root",
|
|
476
|
+
visibility: "latest_hidden",
|
|
477
|
+
publishedAt: 1000,
|
|
478
|
+
});
|
|
479
|
+
const newerRoot = await postService.create({
|
|
480
|
+
format: "note",
|
|
481
|
+
bodyMarkdown: "Newer root",
|
|
482
|
+
publishedAt: 2000,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
await collectionService.addPost(collection.id, hiddenRoot.id);
|
|
486
|
+
await collectionService.addPost(collection.id, newerRoot.id);
|
|
487
|
+
|
|
488
|
+
await postService.create({
|
|
489
|
+
format: "note",
|
|
490
|
+
bodyMarkdown: "Normal reply",
|
|
491
|
+
replyToId: hiddenRoot.id,
|
|
492
|
+
publishedAt: 3000,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const rootIds = await postService.listCollectionThreadRootIds(
|
|
496
|
+
collection.id,
|
|
497
|
+
{
|
|
498
|
+
status: "published",
|
|
499
|
+
sortOrder: "newest",
|
|
500
|
+
},
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
expect(rootIds).toEqual([hiddenRoot.id, newerRoot.id]);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("does not bump a collection thread when a reply is quiet", async () => {
|
|
507
|
+
const collection = await collectionService.create({
|
|
508
|
+
slug: "quiet",
|
|
509
|
+
title: "Quiet",
|
|
510
|
+
});
|
|
511
|
+
const olderRoot = await postService.create({
|
|
512
|
+
format: "note",
|
|
513
|
+
bodyMarkdown: "Older root",
|
|
514
|
+
publishedAt: 1000,
|
|
515
|
+
});
|
|
516
|
+
const newerRoot = await postService.create({
|
|
517
|
+
format: "note",
|
|
518
|
+
bodyMarkdown: "Newer root",
|
|
519
|
+
publishedAt: 2000,
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
await collectionService.addPost(collection.id, olderRoot.id);
|
|
523
|
+
await collectionService.addPost(collection.id, newerRoot.id);
|
|
524
|
+
|
|
525
|
+
await postService.create({
|
|
526
|
+
format: "note",
|
|
527
|
+
bodyMarkdown: "Quiet reply",
|
|
528
|
+
replyToId: olderRoot.id,
|
|
529
|
+
publishedAt: 3000,
|
|
530
|
+
quietReply: true,
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const rootIds = await postService.listCollectionThreadRootIds(
|
|
534
|
+
collection.id,
|
|
535
|
+
{
|
|
536
|
+
status: "published",
|
|
537
|
+
sortOrder: "newest",
|
|
538
|
+
},
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
expect(rootIds).toEqual([newerRoot.id, olderRoot.id]);
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
467
545
|
describe("listCollectionFeedEntries", () => {
|
|
468
|
-
it("orders collection feeds by
|
|
546
|
+
it("orders collection feeds by thread activity and returns thread roots", async () => {
|
|
469
547
|
const collection = await collectionService.create({
|
|
470
548
|
slug: "reading",
|
|
471
549
|
title: "Reading",
|
|
@@ -565,11 +643,50 @@ describe("PostService - Timeline features", () => {
|
|
|
565
643
|
);
|
|
566
644
|
|
|
567
645
|
expect(entries).toHaveLength(2);
|
|
568
|
-
// Sorted by publishedAt DESC: secondRoot created last has latest publishedAt
|
|
569
646
|
expect(entries[0]?.post.id).toBe(secondRoot.id);
|
|
570
647
|
expect(entries[0]?.collectedAt).toBe(200);
|
|
571
648
|
expect(entries[1]?.post.id).toBe(sharedRoot.id);
|
|
572
649
|
expect(entries[1]?.collectedAt).toBe(300);
|
|
573
650
|
});
|
|
651
|
+
|
|
652
|
+
it("orders collection feeds by thread activity from non-quiet replies", async () => {
|
|
653
|
+
const collection = await collectionService.create({
|
|
654
|
+
slug: "activity",
|
|
655
|
+
title: "Activity",
|
|
656
|
+
});
|
|
657
|
+
const hiddenRoot = await postService.create({
|
|
658
|
+
format: "note",
|
|
659
|
+
bodyMarkdown: "Hidden root",
|
|
660
|
+
visibility: "latest_hidden",
|
|
661
|
+
publishedAt: 1000,
|
|
662
|
+
});
|
|
663
|
+
const newerRoot = await postService.create({
|
|
664
|
+
format: "note",
|
|
665
|
+
bodyMarkdown: "Newer root",
|
|
666
|
+
publishedAt: 2000,
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
await collectionService.addPost(collection.id, hiddenRoot.id);
|
|
670
|
+
await collectionService.addPost(collection.id, newerRoot.id);
|
|
671
|
+
|
|
672
|
+
await postService.create({
|
|
673
|
+
format: "note",
|
|
674
|
+
bodyMarkdown: "Normal reply",
|
|
675
|
+
replyToId: hiddenRoot.id,
|
|
676
|
+
publishedAt: 3000,
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
const entries = await postService.listCollectionFeedEntries(
|
|
680
|
+
collection.id,
|
|
681
|
+
{
|
|
682
|
+
status: "published",
|
|
683
|
+
},
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
expect(entries.map((entry) => entry.post.id)).toEqual([
|
|
687
|
+
hiddenRoot.id,
|
|
688
|
+
newerRoot.id,
|
|
689
|
+
]);
|
|
690
|
+
});
|
|
574
691
|
});
|
|
575
692
|
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* About Page Service
|
|
3
|
+
*
|
|
4
|
+
* Treats `/about` as the standard site About page. The page itself remains a
|
|
5
|
+
* normal post; this service reads that convention and can create the page when
|
|
6
|
+
* the author follows the settings prompt.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ValidationError } from "../lib/errors.js";
|
|
10
|
+
import type { Visibility } from "../types.js";
|
|
11
|
+
import type { CollectionService } from "./collection.js";
|
|
12
|
+
import type { PathService } from "./path.js";
|
|
13
|
+
import type { PostService } from "./post.js";
|
|
14
|
+
|
|
15
|
+
export const ABOUT_PAGE_PATH = "/about";
|
|
16
|
+
export const ABOUT_PAGE_SLUG = "about";
|
|
17
|
+
|
|
18
|
+
export type AboutPageStatus =
|
|
19
|
+
| {
|
|
20
|
+
state: "missing";
|
|
21
|
+
path: typeof ABOUT_PAGE_PATH;
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
state: "ready";
|
|
25
|
+
path: typeof ABOUT_PAGE_PATH;
|
|
26
|
+
post: {
|
|
27
|
+
id: string;
|
|
28
|
+
title: string | null;
|
|
29
|
+
status: "draft" | "published";
|
|
30
|
+
visibility: Visibility;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
state: "conflict";
|
|
35
|
+
path: typeof ABOUT_PAGE_PATH;
|
|
36
|
+
conflict: {
|
|
37
|
+
targetType: "collection" | "redirect" | "archive" | "post";
|
|
38
|
+
id: string | null;
|
|
39
|
+
title: string | null;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export interface AboutPageService {
|
|
44
|
+
getStatus(): Promise<AboutPageStatus>;
|
|
45
|
+
ensurePage(): Promise<Extract<AboutPageStatus, { state: "ready" }>["post"]>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createAboutPageService(deps: {
|
|
49
|
+
paths: PathService;
|
|
50
|
+
posts: PostService;
|
|
51
|
+
collections: CollectionService;
|
|
52
|
+
}): AboutPageService {
|
|
53
|
+
async function readStatus(): Promise<AboutPageStatus> {
|
|
54
|
+
const resolved = await deps.paths.resolve(ABOUT_PAGE_SLUG);
|
|
55
|
+
const base = {
|
|
56
|
+
path: ABOUT_PAGE_PATH,
|
|
57
|
+
} as const;
|
|
58
|
+
|
|
59
|
+
if (!resolved) {
|
|
60
|
+
return { ...base, state: "missing" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (resolved.targetType === "post" && resolved.postId) {
|
|
64
|
+
const post = await deps.posts.getById(resolved.postId);
|
|
65
|
+
if (!post) {
|
|
66
|
+
return {
|
|
67
|
+
...base,
|
|
68
|
+
state: "conflict",
|
|
69
|
+
conflict: {
|
|
70
|
+
targetType: "post",
|
|
71
|
+
id: resolved.postId,
|
|
72
|
+
title: null,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
...base,
|
|
79
|
+
state: "ready",
|
|
80
|
+
post: {
|
|
81
|
+
id: post.id,
|
|
82
|
+
title: post.title,
|
|
83
|
+
status: post.status,
|
|
84
|
+
visibility: post.visibility,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (resolved.targetType === "collection" && resolved.collectionId) {
|
|
90
|
+
const collection = await deps.collections.getById(resolved.collectionId);
|
|
91
|
+
return {
|
|
92
|
+
...base,
|
|
93
|
+
state: "conflict",
|
|
94
|
+
conflict: {
|
|
95
|
+
targetType: "collection",
|
|
96
|
+
id: resolved.collectionId,
|
|
97
|
+
title: collection?.title ?? null,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
...base,
|
|
104
|
+
state: "conflict",
|
|
105
|
+
conflict: {
|
|
106
|
+
targetType: resolved.targetType,
|
|
107
|
+
id: null,
|
|
108
|
+
title: null,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
getStatus: readStatus,
|
|
115
|
+
|
|
116
|
+
async ensurePage() {
|
|
117
|
+
const status = await readStatus();
|
|
118
|
+
if (status.state === "ready") {
|
|
119
|
+
return status.post;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (status.state === "conflict") {
|
|
123
|
+
throw new ValidationError(
|
|
124
|
+
"/about is already used. Rename that item before creating an About page.",
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const post = await deps.posts.create({
|
|
129
|
+
format: "note",
|
|
130
|
+
title: "About",
|
|
131
|
+
slug: ABOUT_PAGE_SLUG,
|
|
132
|
+
status: "published",
|
|
133
|
+
visibility: "latest_hidden",
|
|
134
|
+
});
|
|
135
|
+
return {
|
|
136
|
+
id: post.id,
|
|
137
|
+
title: post.title,
|
|
138
|
+
status: post.status,
|
|
139
|
+
visibility: post.visibility,
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
body → Hugo-rendered .Content (stripped of <script>/<style>)
|
|
24
24
|
rating → <p>★★★★☆ 4/5</p>
|
|
25
25
|
link → trailing <p><a href=permalink> ★ </a></p>
|
|
26
|
-
replies → <hr/> + <p><small><time>{dt}</time></small></p>
|
|
26
|
+
replies → <hr/> + <p><small><time>{dt}</time></small></p>
|
|
27
|
+
+ inline title/link metadata + content
|
|
27
28
|
|
|
28
29
|
Hugo's default RSS media-type override (`[outputFormats.RSS]`) makes this
|
|
29
30
|
template produce application/atom+xml at /{section}/index.xml.
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
link-format "★" suffix)
|
|
9
9
|
includeReplies — when true, appends each public reply separated by
|
|
10
10
|
<hr/> + <small><time></time></small>
|
|
11
|
+
inline — when true, renders title/link metadata that top-level
|
|
12
|
+
Atom entries expose through their entry fields
|
|
11
13
|
|
|
12
14
|
Output is raw HTML suitable for embedding inside Atom
|
|
13
15
|
<content type="html"><![CDATA[...]]></content>. Since CDATA ends at the
|
|
@@ -18,9 +20,31 @@
|
|
|
18
20
|
{{- $post := .post -}}
|
|
19
21
|
{{- $permalink := .permalink -}}
|
|
20
22
|
{{- $includeReplies := .includeReplies -}}
|
|
23
|
+
{{- $inline := false -}}
|
|
24
|
+
{{- with .inline -}}{{- $inline = . -}}{{- end -}}
|
|
21
25
|
{{- $format := $post.Params.format | default "note" -}}
|
|
22
26
|
{{- $parts := slice -}}
|
|
23
27
|
|
|
28
|
+
{{- /* --- Inline title/link metadata for thread replies --- */ -}}
|
|
29
|
+
{{- if and $inline (ne $format "quote") -}}
|
|
30
|
+
{{- $title := $post.Title | default "" -}}
|
|
31
|
+
{{- $linkUrl := string ($post.Params.link_url | default "") -}}
|
|
32
|
+
{{- if eq $format "link" -}}
|
|
33
|
+
{{- if ne $linkUrl "" -}}
|
|
34
|
+
{{- $domain := $linkUrl -}}
|
|
35
|
+
{{- with urls.Parse $linkUrl -}}{{- if .Host -}}{{- $domain = replaceRE `^(www|m|mobile)\.` "" .Host -}}{{- end -}}{{- end -}}
|
|
36
|
+
{{- $parts = $parts | append (printf `<p><a href=%q>%s</a></p>` $linkUrl (transform.XMLEscape $domain)) -}}
|
|
37
|
+
{{- end -}}
|
|
38
|
+
{{- if ne $title "" -}}
|
|
39
|
+
{{- $titleHref := $permalink -}}
|
|
40
|
+
{{- if ne $linkUrl "" -}}{{- $titleHref = $linkUrl -}}{{- end -}}
|
|
41
|
+
{{- $parts = $parts | append (printf `<h2><a href=%q>%s</a></h2>` $titleHref (transform.XMLEscape $title)) -}}
|
|
42
|
+
{{- end -}}
|
|
43
|
+
{{- else if ne $title "" -}}
|
|
44
|
+
{{- $parts = $parts | append (printf `<h2><a href=%q>%s</a></h2>` $permalink (transform.XMLEscape $title)) -}}
|
|
45
|
+
{{- end -}}
|
|
46
|
+
{{- end -}}
|
|
47
|
+
|
|
24
48
|
{{- /* --- Quote block (format=quote) --- */ -}}
|
|
25
49
|
{{- if eq $format "quote" -}}
|
|
26
50
|
{{- $quoteText := string ($post.Params.quote_text | default "") -}}
|
|
@@ -100,7 +124,7 @@
|
|
|
100
124
|
{{- $replyLabel := .Date.Format "Jan 2, 2006 15:04" -}}
|
|
101
125
|
{{- $parts = $parts | append "<hr/>" -}}
|
|
102
126
|
{{- $parts = $parts | append (printf `<p><small><time datetime=%q>%s</time></small></p>` $replyDatetime (transform.XMLEscape $replyLabel)) -}}
|
|
103
|
-
{{- $parts = $parts | append (partial "feed-post-content.xml" (dict "post" . "permalink" $replyPermalink "includeReplies" false)) -}}
|
|
127
|
+
{{- $parts = $parts | append (partial "feed-post-content.xml" (dict "post" . "permalink" $replyPermalink "includeReplies" false "inline" true)) -}}
|
|
104
128
|
{{- end -}}
|
|
105
129
|
{{- end -}}
|
|
106
130
|
|
|
@@ -84,13 +84,11 @@ h1 {
|
|
|
84
84
|
|
|
85
85
|
h2 {
|
|
86
86
|
font-size: var(--type-content-title);
|
|
87
|
-
font-style: italic;
|
|
88
87
|
margin: 2.1rem 0 1.4rem;
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
h3 {
|
|
92
91
|
font-size: var(--type-content-subtitle);
|
|
93
|
-
font-style: italic;
|
|
94
92
|
margin: 2rem 0 1.4rem;
|
|
95
93
|
}
|
|
96
94
|
|
|
@@ -119,6 +117,12 @@ kbd,
|
|
|
119
117
|
samp {
|
|
120
118
|
font-family: var(--font-mono);
|
|
121
119
|
font-size: var(--type-code);
|
|
120
|
+
line-height: 1.42;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
code {
|
|
124
|
+
color: var(--site-code-text);
|
|
125
|
+
overflow-wrap: break-word;
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
pre {
|
|
@@ -127,23 +131,26 @@ pre {
|
|
|
127
131
|
line-height: 1.5;
|
|
128
132
|
padding: 1rem;
|
|
129
133
|
overflow-x: auto;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
border
|
|
134
|
+
color: var(--site-code-text);
|
|
135
|
+
background-color: var(--site-code-block-bg);
|
|
136
|
+
border: 1px solid var(--site-code-block-border);
|
|
137
|
+
border-radius: 6px;
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
pre code {
|
|
136
141
|
padding: 0;
|
|
137
142
|
background: transparent;
|
|
138
143
|
border: 0;
|
|
144
|
+
color: inherit;
|
|
139
145
|
font-size: inherit;
|
|
140
146
|
}
|
|
141
147
|
|
|
142
148
|
:not(pre) > code {
|
|
143
|
-
padding: 0.
|
|
144
|
-
background-color: var(--site-
|
|
145
|
-
border:
|
|
146
|
-
|
|
149
|
+
padding: 0.08em 0.28em;
|
|
150
|
+
background-color: var(--site-code-bg);
|
|
151
|
+
border-radius: 0.22em;
|
|
152
|
+
box-decoration-break: clone;
|
|
153
|
+
-webkit-box-decoration-break: clone;
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
/* Blockquotes — tinted background card with quote icon (matches main site). */
|
|
@@ -217,13 +224,13 @@ blockquote cite {
|
|
|
217
224
|
|
|
218
225
|
ul,
|
|
219
226
|
ol {
|
|
220
|
-
padding-left: 1.
|
|
227
|
+
padding-left: 1.4em;
|
|
221
228
|
margin: 1.25em 0;
|
|
222
229
|
}
|
|
223
230
|
|
|
224
231
|
li {
|
|
225
|
-
margin-top:
|
|
226
|
-
margin-bottom:
|
|
232
|
+
margin-top: 0.65em;
|
|
233
|
+
margin-bottom: 0.65em;
|
|
227
234
|
}
|
|
228
235
|
|
|
229
236
|
/* Normalize li > p so list spacing is controlled by li alone,
|
|
@@ -850,6 +857,31 @@ hr.feed-divider {
|
|
|
850
857
|
color: var(--site-reading-body);
|
|
851
858
|
}
|
|
852
859
|
|
|
860
|
+
.post-card > .post-card-summary :is(h1, h2),
|
|
861
|
+
.post-card > .post-card-quote-commentary :is(h1, h2) {
|
|
862
|
+
font-size: calc(var(--type-content-body) * 1.12);
|
|
863
|
+
font-weight: var(--fw-medium);
|
|
864
|
+
margin-top: 1.45rem;
|
|
865
|
+
margin-bottom: 0.55rem;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
.post-card > .post-card-summary :is(h3, h4),
|
|
869
|
+
.post-card > .post-card-quote-commentary :is(h3, h4) {
|
|
870
|
+
font-size: var(--type-content-body);
|
|
871
|
+
font-weight: var(--fw-medium);
|
|
872
|
+
margin-top: 1.2rem;
|
|
873
|
+
margin-bottom: 0.45rem;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
.post-card > .post-card-summary :is(h5, h6),
|
|
877
|
+
.post-card > .post-card-quote-commentary :is(h5, h6) {
|
|
878
|
+
font-size: var(--type-secondary);
|
|
879
|
+
font-weight: var(--fw-medium);
|
|
880
|
+
color: var(--site-text-secondary);
|
|
881
|
+
margin-top: 1rem;
|
|
882
|
+
margin-bottom: 0.35rem;
|
|
883
|
+
}
|
|
884
|
+
|
|
853
885
|
/* Link card domain row — shown ABOVE the title (matches main site's
|
|
854
886
|
`.feed-link-domain` pattern). Inline flex with icon + host text. */
|
|
855
887
|
.post-card-link-domain,
|
|
@@ -1009,8 +1041,8 @@ hr.feed-divider {
|
|
|
1009
1041
|
.post-card-quote-content {
|
|
1010
1042
|
font-family: var(--font-serif);
|
|
1011
1043
|
color: var(--site-text-primary);
|
|
1012
|
-
font-size: var(--type-content-
|
|
1013
|
-
line-height: var(--type-
|
|
1044
|
+
font-size: var(--type-content-quote);
|
|
1045
|
+
line-height: var(--type-content-quote-leading);
|
|
1014
1046
|
white-space: pre-line;
|
|
1015
1047
|
text-wrap: pretty;
|
|
1016
1048
|
margin: 0;
|