@jant/core 0.3.24 → 0.3.26
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/dist/app.js +101 -571
- package/dist/client.js +1 -0
- package/dist/db/schema.js +1 -1
- package/dist/i18n/locales/en.js +1 -1
- package/dist/i18n/locales/zh-Hans.js +1 -1
- package/dist/i18n/locales/zh-Hant.js +1 -1
- package/dist/index.js +3 -9
- package/dist/lib/avatar-upload.js +134 -0
- package/dist/lib/config.js +39 -0
- package/dist/lib/constants.js +10 -9
- package/dist/lib/favicon.js +102 -0
- package/dist/lib/image.js +13 -17
- package/dist/lib/media-helpers.js +2 -2
- package/dist/lib/nav-reorder.js +1 -1
- package/dist/lib/navigation.js +48 -3
- package/dist/lib/pagination.js +44 -0
- package/dist/lib/render.js +16 -11
- package/dist/lib/schemas.js +34 -3
- package/dist/lib/theme.js +4 -4
- package/dist/lib/timeline.js +24 -48
- package/dist/lib/timezones.js +388 -0
- package/dist/lib/view.js +3 -3
- package/dist/routes/api/collections.js +124 -0
- package/dist/routes/api/nav-items.js +104 -0
- package/dist/routes/api/pages.js +91 -0
- package/dist/routes/api/posts.js +3 -3
- package/dist/routes/api/search.js +2 -2
- package/dist/routes/api/settings.js +68 -0
- package/dist/routes/api/upload.js +3 -3
- package/dist/routes/auth/reset.js +221 -0
- package/dist/routes/auth/setup.js +194 -0
- package/dist/routes/auth/signin.js +176 -0
- package/dist/routes/compose.js +48 -0
- package/dist/routes/dash/collections.js +24 -416
- package/dist/routes/dash/index.js +1 -1
- package/dist/routes/dash/media.js +13 -393
- package/dist/routes/dash/pages.js +112 -86
- package/dist/routes/dash/posts.js +3 -5
- package/dist/routes/dash/redirects.js +20 -14
- package/dist/routes/dash/settings.js +213 -518
- package/dist/routes/feed/rss.js +4 -3
- package/dist/routes/feed/sitemap.js +5 -3
- package/dist/routes/pages/archive.js +3 -6
- package/dist/routes/pages/collection.js +3 -6
- package/dist/routes/pages/collections.js +28 -0
- package/dist/routes/pages/featured.js +36 -0
- package/dist/routes/pages/home.js +33 -49
- package/dist/routes/pages/latest.js +45 -0
- package/dist/routes/pages/page.js +29 -32
- package/dist/routes/pages/post.js +3 -6
- package/dist/routes/pages/search.js +3 -6
- package/dist/services/page.js +5 -1
- package/dist/services/post.js +45 -31
- package/dist/services/search.js +1 -1
- package/dist/types/bindings.js +3 -0
- package/dist/types/config.js +147 -0
- package/dist/types/constants.js +27 -0
- package/dist/types/entities.js +3 -0
- package/dist/types/operations.js +3 -0
- package/dist/types/props.js +3 -0
- package/dist/types/views.js +5 -0
- package/dist/types.js +8 -111
- package/dist/{theme → ui}/color-themes.js +33 -33
- package/dist/ui/compose/ComposeDialog.js +467 -0
- package/dist/ui/compose/ComposePrompt.js +55 -0
- package/dist/{theme/components/TypeBadge.js → ui/dash/FormatBadge.js} +1 -2
- package/dist/{theme/components → ui/dash}/PageForm.js +21 -15
- package/dist/{theme/components → ui/dash}/PostForm.js +22 -43
- package/dist/{theme/components → ui/dash}/PostList.js +6 -6
- package/dist/{theme/components/VisibilityBadge.js → ui/dash/StatusBadge.js} +1 -2
- package/dist/ui/dash/collections/CollectionForm.js +152 -0
- package/dist/ui/dash/collections/CollectionsListContent.js +68 -0
- package/dist/ui/dash/collections/ViewCollectionContent.js +96 -0
- package/dist/{theme/components → ui/dash}/index.js +3 -6
- package/dist/ui/dash/media/MediaListContent.js +166 -0
- package/dist/ui/dash/media/ViewMediaContent.js +212 -0
- package/dist/ui/dash/pages/LinkFormContent.js +130 -0
- package/dist/ui/dash/pages/UnifiedPagesContent.js +193 -0
- package/dist/ui/dash/settings/AccountContent.js +209 -0
- package/dist/ui/dash/settings/AppearanceContent.js +259 -0
- package/dist/ui/dash/settings/GeneralContent.js +536 -0
- package/dist/ui/dash/settings/SettingsNav.js +41 -0
- package/dist/{themes/threads/timeline → ui/feed}/LinkCard.js +6 -2
- package/dist/{themes/threads/timeline → ui/feed}/NoteCard.js +11 -6
- package/dist/{themes/threads/timeline → ui/feed}/QuoteCard.js +10 -6
- package/dist/{themes/threads/timeline → ui/feed}/ThreadPreview.js +7 -9
- package/dist/ui/feed/TimelineFeed.js +41 -0
- package/dist/ui/feed/TimelineItem.js +27 -0
- package/dist/ui/font-themes.js +36 -0
- package/dist/{theme → ui}/layouts/BaseLayout.js +34 -2
- package/dist/{theme → ui}/layouts/DashLayout.js +0 -8
- package/dist/ui/layouts/SiteLayout.js +169 -0
- package/dist/{themes/threads → ui}/pages/ArchivePage.js +16 -14
- package/dist/{themes/threads → ui}/pages/CollectionPage.js +6 -1
- package/dist/ui/pages/CollectionsPage.js +76 -0
- package/dist/ui/pages/FeaturedPage.js +24 -0
- package/dist/ui/pages/HomePage.js +24 -0
- package/dist/{themes/threads → ui}/pages/PostPage.js +13 -8
- package/dist/{themes/threads → ui}/pages/SearchPage.js +9 -7
- package/dist/{themes/threads → ui}/pages/SinglePage.js +3 -2
- package/dist/{theme/components → ui/shared}/MediaGallery.js +1 -1
- package/dist/{theme/components → ui/shared}/Pagination.js +41 -2
- package/dist/{theme/components → ui/shared}/ThreadView.js +2 -2
- package/dist/ui/shared/index.js +5 -0
- package/package.json +1 -9
- package/src/__tests__/helpers/db.ts +3 -0
- package/src/app.tsx +131 -561
- package/src/client.ts +1 -0
- package/src/db/migrations/0006_rename_slug_to_path.sql +5 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +1 -1
- package/src/i18n/locales/en.po +477 -261
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +477 -261
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +477 -261
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +7 -36
- package/src/lib/__tests__/config.test.ts +192 -0
- package/src/lib/__tests__/favicon.test.ts +151 -0
- package/src/lib/__tests__/image.test.ts +2 -6
- package/src/lib/__tests__/schemas.test.ts +60 -19
- package/src/lib/__tests__/timeline.test.ts +45 -81
- package/src/lib/__tests__/timezones.test.ts +61 -0
- package/src/lib/__tests__/view.test.ts +15 -9
- package/src/lib/avatar-upload.ts +165 -0
- package/src/lib/config.ts +47 -0
- package/src/lib/constants.ts +19 -10
- package/src/lib/favicon.ts +115 -0
- package/src/lib/image.ts +13 -21
- package/src/lib/media-helpers.ts +2 -2
- package/src/lib/nav-reorder.ts +1 -1
- package/src/lib/navigation.ts +73 -4
- package/src/lib/pagination.ts +50 -0
- package/src/lib/render.tsx +22 -15
- package/src/lib/schemas.ts +47 -6
- package/src/lib/theme.ts +5 -5
- package/src/lib/timeline.ts +28 -57
- package/src/lib/timezones.ts +325 -0
- package/src/lib/view.ts +3 -3
- package/src/preset.css +2 -1
- package/src/routes/__tests__/compose.test.ts +199 -0
- package/src/routes/api/__tests__/collections.test.ts +249 -0
- package/src/routes/api/__tests__/nav-items.test.ts +222 -0
- package/src/routes/api/__tests__/pages.test.ts +218 -0
- package/src/routes/api/__tests__/settings.test.ts +132 -0
- package/src/routes/api/collections.ts +143 -0
- package/src/routes/api/nav-items.ts +115 -0
- package/src/routes/api/pages.ts +101 -0
- package/src/routes/api/posts.ts +3 -3
- package/src/routes/api/search.ts +2 -2
- package/src/routes/api/settings.ts +91 -0
- package/src/routes/api/upload.ts +2 -3
- package/src/routes/auth/reset.tsx +239 -0
- package/src/routes/auth/setup.tsx +189 -0
- package/src/routes/auth/signin.tsx +163 -0
- package/src/routes/compose.ts +63 -0
- package/src/routes/dash/__tests__/pages.test.ts +225 -0
- package/src/routes/dash/__tests__/settings-avatar.test.ts +89 -0
- package/src/routes/dash/collections.tsx +18 -367
- package/src/routes/dash/index.tsx +1 -1
- package/src/routes/dash/media.tsx +13 -415
- package/src/routes/dash/pages.tsx +131 -98
- package/src/routes/dash/posts.tsx +3 -7
- package/src/routes/dash/redirects.tsx +22 -16
- package/src/routes/dash/settings.tsx +265 -478
- package/src/routes/feed/__tests__/rss.test.ts +141 -0
- package/src/routes/feed/rss.ts +5 -3
- package/src/routes/feed/sitemap.ts +5 -3
- package/src/routes/pages/__tests__/collections.test.ts +94 -0
- package/src/routes/pages/__tests__/featured.test.ts +94 -0
- package/src/routes/pages/archive.tsx +2 -6
- package/src/routes/pages/collection.tsx +2 -6
- package/src/routes/pages/collections.tsx +36 -0
- package/src/routes/pages/featured.tsx +44 -0
- package/src/routes/pages/home.tsx +30 -53
- package/src/routes/pages/latest.tsx +59 -0
- package/src/routes/pages/page.tsx +28 -30
- package/src/routes/pages/post.tsx +2 -5
- package/src/routes/pages/search.tsx +2 -6
- package/src/services/__tests__/page.test.ts +106 -0
- package/src/services/__tests__/post.test.ts +114 -15
- package/src/services/page.ts +13 -1
- package/src/services/post.ts +58 -40
- package/src/services/search.ts +2 -2
- package/src/styles/components.css +0 -65
- package/src/styles/tokens.css +47 -0
- package/src/styles/ui.css +475 -0
- package/src/types/bindings.ts +30 -0
- package/src/types/config.ts +183 -0
- package/src/types/constants.ts +26 -0
- package/src/types/entities.ts +109 -0
- package/src/types/operations.ts +88 -0
- package/src/types/props.ts +115 -0
- package/src/types/views.ts +172 -0
- package/src/types.ts +8 -774
- package/src/ui/__tests__/font-themes.test.ts +34 -0
- package/src/{theme → ui}/color-themes.ts +34 -34
- package/src/ui/compose/ComposeDialog.tsx +414 -0
- package/src/ui/compose/ComposePrompt.tsx +55 -0
- package/src/{theme/components/TypeBadge.tsx → ui/dash/FormatBadge.tsx} +2 -3
- package/src/{theme/components → ui/dash}/PageForm.tsx +25 -19
- package/src/{theme/components → ui/dash}/PostForm.tsx +26 -45
- package/src/{theme/components → ui/dash}/PostList.tsx +7 -7
- package/src/{theme/components/VisibilityBadge.tsx → ui/dash/StatusBadge.tsx} +2 -3
- package/src/ui/dash/collections/CollectionForm.tsx +153 -0
- package/src/ui/dash/collections/CollectionsListContent.tsx +85 -0
- package/src/ui/dash/collections/ViewCollectionContent.tsx +92 -0
- package/src/ui/dash/index.ts +10 -0
- package/src/ui/dash/media/MediaListContent.tsx +201 -0
- package/src/ui/dash/media/ViewMediaContent.tsx +208 -0
- package/src/ui/dash/pages/LinkFormContent.tsx +119 -0
- package/src/ui/dash/pages/UnifiedPagesContent.tsx +203 -0
- package/src/ui/dash/settings/AccountContent.tsx +176 -0
- package/src/ui/dash/settings/AppearanceContent.tsx +254 -0
- package/src/ui/dash/settings/GeneralContent.tsx +533 -0
- package/src/ui/dash/settings/SettingsNav.tsx +56 -0
- package/src/{themes/threads/timeline → ui/feed}/LinkCard.tsx +9 -4
- package/src/{themes/threads/timeline → ui/feed}/NoteCard.tsx +13 -8
- package/src/{themes/threads/timeline → ui/feed}/QuoteCard.tsx +13 -8
- package/src/{themes/threads/timeline → ui/feed}/ThreadPreview.tsx +7 -8
- package/src/ui/feed/TimelineFeed.tsx +49 -0
- package/src/ui/feed/TimelineItem.tsx +45 -0
- package/src/ui/font-themes.ts +54 -0
- package/src/{theme → ui}/layouts/BaseLayout.tsx +28 -1
- package/src/{theme → ui}/layouts/DashLayout.tsx +0 -10
- package/src/ui/layouts/SiteLayout.tsx +164 -0
- package/src/{themes/threads → ui}/pages/ArchivePage.tsx +22 -17
- package/src/{themes/threads → ui}/pages/CollectionPage.tsx +14 -5
- package/src/ui/pages/CollectionsPage.tsx +73 -0
- package/src/ui/pages/FeaturedPage.tsx +31 -0
- package/src/{themes/threads → ui}/pages/HomePage.tsx +11 -15
- package/src/{themes/threads → ui}/pages/PostPage.tsx +23 -14
- package/src/{themes/threads → ui}/pages/SearchPage.tsx +13 -11
- package/src/{themes/threads → ui}/pages/SinglePage.tsx +4 -4
- package/src/{theme/components → ui/shared}/MediaGallery.tsx +1 -1
- package/src/{theme/components → ui/shared}/Pagination.tsx +67 -4
- package/src/{theme/components → ui/shared}/ThreadView.tsx +2 -2
- package/src/ui/shared/__tests__/pagination.test.ts +46 -0
- package/src/ui/shared/index.ts +12 -0
- package/bin/jant.js +0 -185
- package/dist/lib/theme-components.js +0 -46
- package/dist/routes/dash/navigation.js +0 -289
- package/dist/theme/index.js +0 -18
- package/dist/theme/layouts/index.js +0 -2
- package/dist/themes/threads/ThreadsSiteLayout.js +0 -172
- package/dist/themes/threads/index.js +0 -81
- package/dist/themes/threads/pages/HomePage.js +0 -25
- package/dist/themes/threads/timeline/TimelineFeed.js +0 -58
- package/dist/themes/threads/timeline/TimelineItem.js +0 -36
- package/dist/themes/threads/timeline/TimelineLoadMore.js +0 -23
- package/dist/themes/threads/timeline/groupByDate.js +0 -22
- package/dist/themes/threads/timeline/timelineMore.js +0 -107
- package/src/lib/__tests__/theme-components.test.ts +0 -105
- package/src/lib/theme-components.ts +0 -65
- package/src/routes/dash/navigation.tsx +0 -317
- package/src/theme/components/index.ts +0 -23
- package/src/theme/index.ts +0 -22
- package/src/theme/layouts/index.ts +0 -7
- package/src/themes/threads/ThreadsSiteLayout.tsx +0 -194
- package/src/themes/threads/index.ts +0 -100
- package/src/themes/threads/style.css +0 -336
- package/src/themes/threads/timeline/TimelineFeed.tsx +0 -62
- package/src/themes/threads/timeline/TimelineItem.tsx +0 -67
- package/src/themes/threads/timeline/TimelineLoadMore.tsx +0 -35
- package/src/themes/threads/timeline/groupByDate.ts +0 -30
- package/src/themes/threads/timeline/timelineMore.tsx +0 -130
- /package/dist/{theme/components → ui/dash}/ActionButtons.js +0 -0
- /package/dist/{theme/components → ui/dash}/CrudPageHeader.js +0 -0
- /package/dist/{theme/components → ui/dash}/DangerZone.js +0 -0
- /package/dist/{theme/components → ui/dash}/ListItemRow.js +0 -0
- /package/dist/{theme/components → ui/shared}/EmptyState.js +0 -0
- /package/src/{theme/components → ui/dash}/ActionButtons.tsx +0 -0
- /package/src/{theme/components → ui/dash}/CrudPageHeader.tsx +0 -0
- /package/src/{theme/components → ui/dash}/DangerZone.tsx +0 -0
- /package/src/{theme/components → ui/dash}/ListItemRow.tsx +0 -0
- /package/src/{theme/components → ui/shared}/EmptyState.tsx +0 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import type { Bindings } from "../../../types.js";
|
|
4
|
+
import type { AppVariables } from "../../../app.js";
|
|
5
|
+
import { createTestDatabase } from "../../../__tests__/helpers/db.js";
|
|
6
|
+
import { createPostService } from "../../../services/post.js";
|
|
7
|
+
import { createSettingsService } from "../../../services/settings.js";
|
|
8
|
+
import { createMediaService } from "../../../services/media.js";
|
|
9
|
+
import { rssRoutes } from "../rss.js";
|
|
10
|
+
|
|
11
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
12
|
+
|
|
13
|
+
function createFeedTestApp(envOverrides: Partial<Bindings> = {}) {
|
|
14
|
+
const { db } = createTestDatabase();
|
|
15
|
+
|
|
16
|
+
const services = {
|
|
17
|
+
posts: createPostService(db as never),
|
|
18
|
+
settings: createSettingsService(db as never),
|
|
19
|
+
media: createMediaService(db as never),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const app = new Hono<Env>();
|
|
23
|
+
|
|
24
|
+
app.use("*", async (c, next) => {
|
|
25
|
+
c.env = {
|
|
26
|
+
SITE_URL: "http://localhost:9019",
|
|
27
|
+
...envOverrides,
|
|
28
|
+
} as Bindings;
|
|
29
|
+
|
|
30
|
+
c.set("services", services as AppVariables["services"]);
|
|
31
|
+
c.set("config", {});
|
|
32
|
+
await next();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
app.route("/feed", rssRoutes);
|
|
36
|
+
|
|
37
|
+
return { app, services };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("RSS Feed Routes", () => {
|
|
41
|
+
describe("RSS_FEED_LIMIT env var", () => {
|
|
42
|
+
it("defaults to 50 when RSS_FEED_LIMIT is not set", async () => {
|
|
43
|
+
const { app, services } = createFeedTestApp();
|
|
44
|
+
|
|
45
|
+
// Create 3 posts
|
|
46
|
+
for (let i = 0; i < 3; i++) {
|
|
47
|
+
await services.posts.create({
|
|
48
|
+
format: "note",
|
|
49
|
+
title: `Post ${i}`,
|
|
50
|
+
body: `Body ${i}`,
|
|
51
|
+
status: "published",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const res = await app.request("/feed");
|
|
56
|
+
expect(res.status).toBe(200);
|
|
57
|
+
|
|
58
|
+
const xml = await res.text();
|
|
59
|
+
// All 3 posts should appear (under default limit of 50)
|
|
60
|
+
expect(xml).toContain("Post 0");
|
|
61
|
+
expect(xml).toContain("Post 1");
|
|
62
|
+
expect(xml).toContain("Post 2");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("respects RSS_FEED_LIMIT to limit the number of posts", async () => {
|
|
66
|
+
const { app, services } = createFeedTestApp({
|
|
67
|
+
RSS_FEED_LIMIT: "2",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Create 5 posts
|
|
71
|
+
for (let i = 0; i < 5; i++) {
|
|
72
|
+
await services.posts.create({
|
|
73
|
+
format: "note",
|
|
74
|
+
title: `Post ${i}`,
|
|
75
|
+
body: `Body ${i}`,
|
|
76
|
+
status: "published",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const res = await app.request("/feed");
|
|
81
|
+
expect(res.status).toBe(200);
|
|
82
|
+
|
|
83
|
+
const xml = await res.text();
|
|
84
|
+
// Posts are ordered by publishedAt DESC, so the latest 2 should appear
|
|
85
|
+
// With same timestamp they fall back to id DESC, so Post 4 and Post 3
|
|
86
|
+
expect(xml).toContain("Post 4");
|
|
87
|
+
expect(xml).toContain("Post 3");
|
|
88
|
+
expect(xml).not.toContain("Post 2");
|
|
89
|
+
expect(xml).not.toContain("Post 1");
|
|
90
|
+
expect(xml).not.toContain("Post 0");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("falls back to 50 for invalid RSS_FEED_LIMIT", async () => {
|
|
94
|
+
const { app, services } = createFeedTestApp({
|
|
95
|
+
RSS_FEED_LIMIT: "not-a-number",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Create 2 posts
|
|
99
|
+
for (let i = 0; i < 2; i++) {
|
|
100
|
+
await services.posts.create({
|
|
101
|
+
format: "note",
|
|
102
|
+
title: `Post ${i}`,
|
|
103
|
+
body: `Body ${i}`,
|
|
104
|
+
status: "published",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const res = await app.request("/feed");
|
|
109
|
+
expect(res.status).toBe(200);
|
|
110
|
+
|
|
111
|
+
const xml = await res.text();
|
|
112
|
+
// Both posts should appear (fallback to 50)
|
|
113
|
+
expect(xml).toContain("Post 0");
|
|
114
|
+
expect(xml).toContain("Post 1");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("also applies to atom feed", async () => {
|
|
118
|
+
const { app, services } = createFeedTestApp({
|
|
119
|
+
RSS_FEED_LIMIT: "1",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < 3; i++) {
|
|
123
|
+
await services.posts.create({
|
|
124
|
+
format: "note",
|
|
125
|
+
title: `Post ${i}`,
|
|
126
|
+
body: `Body ${i}`,
|
|
127
|
+
status: "published",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const res = await app.request("/feed/atom.xml");
|
|
132
|
+
expect(res.status).toBe(200);
|
|
133
|
+
|
|
134
|
+
const xml = await res.text();
|
|
135
|
+
// Only the latest post should appear
|
|
136
|
+
expect(xml).toContain("Post 2");
|
|
137
|
+
expect(xml).not.toContain("Post 1");
|
|
138
|
+
expect(xml).not.toContain("Post 0");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
package/src/routes/feed/rss.ts
CHANGED
|
@@ -25,10 +25,12 @@ async function buildFeedData(c: Context<Env>): Promise<FeedData> {
|
|
|
25
25
|
const siteUrl = c.env.SITE_URL;
|
|
26
26
|
const siteLanguage = await getSiteLanguage(c);
|
|
27
27
|
|
|
28
|
+
const feedLimit = parseInt(c.env.RSS_FEED_LIMIT ?? "50", 10) || 50;
|
|
29
|
+
|
|
28
30
|
const posts = await c.var.services.posts.list({
|
|
29
31
|
status: "published",
|
|
30
32
|
excludeReplies: true,
|
|
31
|
-
limit:
|
|
33
|
+
limit: feedLimit,
|
|
32
34
|
});
|
|
33
35
|
|
|
34
36
|
// Batch load media for enclosures
|
|
@@ -64,7 +66,7 @@ async function buildFeedData(c: Context<Env>): Promise<FeedData> {
|
|
|
64
66
|
rssRoutes.get("/", async (c) => {
|
|
65
67
|
const feedData = await buildFeedData(c);
|
|
66
68
|
|
|
67
|
-
const renderer = c.var.config.
|
|
69
|
+
const renderer = c.var.config.feed?.rss ?? defaultRssRenderer;
|
|
68
70
|
const xml = renderer(feedData);
|
|
69
71
|
|
|
70
72
|
return new Response(xml, {
|
|
@@ -78,7 +80,7 @@ rssRoutes.get("/", async (c) => {
|
|
|
78
80
|
rssRoutes.get("/atom.xml", async (c) => {
|
|
79
81
|
const feedData = await buildFeedData(c);
|
|
80
82
|
|
|
81
|
-
const renderer = c.var.config.
|
|
83
|
+
const renderer = c.var.config.feed?.atom ?? defaultAtomRenderer;
|
|
82
84
|
const xml = renderer(feedData);
|
|
83
85
|
|
|
84
86
|
return new Response(xml, {
|
|
@@ -35,7 +35,7 @@ sitemapRoutes.get("/sitemap.xml", async (c) => {
|
|
|
35
35
|
const postViews = toPostViewsFromPosts(posts, mediaCtx);
|
|
36
36
|
const pageViews = publishedPages.map(toPageView);
|
|
37
37
|
|
|
38
|
-
const renderer = c.var.config.
|
|
38
|
+
const renderer = c.var.config.feed?.sitemap ?? defaultSitemapRenderer;
|
|
39
39
|
const xml = renderer({ siteUrl, posts: postViews, pages: pageViews });
|
|
40
40
|
|
|
41
41
|
return new Response(xml, {
|
|
@@ -46,11 +46,13 @@ sitemapRoutes.get("/sitemap.xml", async (c) => {
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
// robots.txt
|
|
49
|
-
sitemapRoutes.get("/robots.txt", (c) => {
|
|
49
|
+
sitemapRoutes.get("/robots.txt", async (c) => {
|
|
50
50
|
const siteUrl = c.env.SITE_URL;
|
|
51
|
+
const noindex = (await c.var.services.settings.get("NOINDEX")) === "true";
|
|
51
52
|
|
|
53
|
+
const directive = noindex ? "Disallow: /" : "Allow: /";
|
|
52
54
|
const robots = `User-agent: *
|
|
53
|
-
|
|
55
|
+
${directive}
|
|
54
56
|
|
|
55
57
|
Sitemap: ${siteUrl}/sitemap.xml
|
|
56
58
|
`;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the collections listing page data logic.
|
|
3
|
+
*
|
|
4
|
+
* Note: Route handler tests that import JSX components with @lingui/react/macro
|
|
5
|
+
* cannot run in vitest (requires SWC plugin). These tests verify the service
|
|
6
|
+
* layer operations that the collections route orchestrates.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
10
|
+
import { createTestDatabase } from "../../../__tests__/helpers/db.js";
|
|
11
|
+
import { createCollectionService } from "../../../services/collection.js";
|
|
12
|
+
import { createPostService } from "../../../services/post.js";
|
|
13
|
+
import type { Database } from "../../../db/index.js";
|
|
14
|
+
|
|
15
|
+
describe("Collections Listing Page - Data Logic", () => {
|
|
16
|
+
let db: Database;
|
|
17
|
+
let collectionService: ReturnType<typeof createCollectionService>;
|
|
18
|
+
let postService: ReturnType<typeof createPostService>;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
const testDb = createTestDatabase();
|
|
22
|
+
db = testDb.db as unknown as Database;
|
|
23
|
+
collectionService = createCollectionService(db);
|
|
24
|
+
postService = createPostService(db);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("returns collections with post counts", async () => {
|
|
28
|
+
const recipes = await collectionService.create({
|
|
29
|
+
slug: "recipes",
|
|
30
|
+
title: "Recipes",
|
|
31
|
+
});
|
|
32
|
+
await collectionService.create({
|
|
33
|
+
slug: "travel",
|
|
34
|
+
title: "Travel",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Add posts to recipes collection
|
|
38
|
+
await postService.create({
|
|
39
|
+
format: "note",
|
|
40
|
+
body: "Recipe 1",
|
|
41
|
+
collectionId: recipes.id,
|
|
42
|
+
});
|
|
43
|
+
await postService.create({
|
|
44
|
+
format: "note",
|
|
45
|
+
body: "Recipe 2",
|
|
46
|
+
collectionId: recipes.id,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Simulate route handler logic
|
|
50
|
+
const [allCollections, postCounts] = await Promise.all([
|
|
51
|
+
collectionService.list(),
|
|
52
|
+
collectionService.getPostCounts(),
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const collections = allCollections.map((col) => ({
|
|
56
|
+
...col,
|
|
57
|
+
postCount: postCounts.get(col.id) ?? 0,
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
expect(collections).toHaveLength(2);
|
|
61
|
+
const recipesResult = collections.find((c) => c.slug === "recipes");
|
|
62
|
+
const travelResult = collections.find((c) => c.slug === "travel");
|
|
63
|
+
expect(recipesResult?.postCount).toBe(2);
|
|
64
|
+
expect(travelResult?.postCount).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("returns empty list when no collections exist", async () => {
|
|
68
|
+
const allCollections = await collectionService.list();
|
|
69
|
+
expect(allCollections).toHaveLength(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("does not count soft-deleted posts", async () => {
|
|
73
|
+
const col = await collectionService.create({
|
|
74
|
+
slug: "test",
|
|
75
|
+
title: "Test",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const post = await postService.create({
|
|
79
|
+
format: "note",
|
|
80
|
+
body: "Will be deleted",
|
|
81
|
+
collectionId: col.id,
|
|
82
|
+
});
|
|
83
|
+
await postService.create({
|
|
84
|
+
format: "note",
|
|
85
|
+
body: "Will remain",
|
|
86
|
+
collectionId: col.id,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await postService.delete(post.id);
|
|
90
|
+
|
|
91
|
+
const postCounts = await collectionService.getPostCounts();
|
|
92
|
+
expect(postCounts.get(col.id)).toBe(1);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the featured page data logic.
|
|
3
|
+
*
|
|
4
|
+
* Note: Route handler tests that import JSX components with @lingui/react/macro
|
|
5
|
+
* cannot run in vitest (requires SWC plugin). These tests verify the service
|
|
6
|
+
* layer operations that the featured route orchestrates.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
10
|
+
import { createTestDatabase } from "../../../__tests__/helpers/db.js";
|
|
11
|
+
import { createPostService } from "../../../services/post.js";
|
|
12
|
+
import type { Database } from "../../../db/index.js";
|
|
13
|
+
|
|
14
|
+
describe("Featured Page - Data Logic", () => {
|
|
15
|
+
let db: Database;
|
|
16
|
+
let postService: ReturnType<typeof createPostService>;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
const testDb = createTestDatabase();
|
|
20
|
+
db = testDb.db as unknown as Database;
|
|
21
|
+
postService = createPostService(db);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns only featured published posts", async () => {
|
|
25
|
+
await postService.create({
|
|
26
|
+
format: "note",
|
|
27
|
+
body: "Featured post",
|
|
28
|
+
featured: true,
|
|
29
|
+
status: "published",
|
|
30
|
+
});
|
|
31
|
+
await postService.create({
|
|
32
|
+
format: "note",
|
|
33
|
+
body: "Normal post",
|
|
34
|
+
featured: false,
|
|
35
|
+
status: "published",
|
|
36
|
+
});
|
|
37
|
+
await postService.create({
|
|
38
|
+
format: "note",
|
|
39
|
+
body: "Draft featured",
|
|
40
|
+
featured: true,
|
|
41
|
+
status: "draft",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const posts = await postService.list({
|
|
45
|
+
featured: true,
|
|
46
|
+
status: "published",
|
|
47
|
+
excludeReplies: true,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(posts).toHaveLength(1);
|
|
51
|
+
expect(posts[0]?.body).toBe("Featured post");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns empty list when no featured posts exist", async () => {
|
|
55
|
+
await postService.create({
|
|
56
|
+
format: "note",
|
|
57
|
+
body: "Normal post",
|
|
58
|
+
status: "published",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const posts = await postService.list({
|
|
62
|
+
featured: true,
|
|
63
|
+
status: "published",
|
|
64
|
+
excludeReplies: true,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(posts).toHaveLength(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("excludes replies from featured posts", async () => {
|
|
71
|
+
const root = await postService.create({
|
|
72
|
+
format: "note",
|
|
73
|
+
body: "Featured root",
|
|
74
|
+
featured: true,
|
|
75
|
+
status: "published",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Reply inherits featured from root
|
|
79
|
+
await postService.create({
|
|
80
|
+
format: "note",
|
|
81
|
+
body: "Reply to featured",
|
|
82
|
+
replyToId: root.id,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const posts = await postService.list({
|
|
86
|
+
featured: true,
|
|
87
|
+
status: "published",
|
|
88
|
+
excludeReplies: true,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(posts).toHaveLength(1);
|
|
92
|
+
expect(posts[0]?.body).toBe("Featured root");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -8,7 +8,7 @@ import { Hono } from "hono";
|
|
|
8
8
|
import type { Bindings, Format } from "../../types.js";
|
|
9
9
|
import type { AppVariables } from "../../app.js";
|
|
10
10
|
import { FORMATS } from "../../types.js";
|
|
11
|
-
import { ArchivePage
|
|
11
|
+
import { ArchivePage } from "../../ui/pages/ArchivePage.js";
|
|
12
12
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
13
13
|
import { renderPublicPage } from "../../lib/render.js";
|
|
14
14
|
import { createMediaContext, toArchiveGroups } from "../../lib/view.js";
|
|
@@ -69,20 +69,16 @@ archiveRoutes.get("/", async (c) => {
|
|
|
69
69
|
const mediaCtx = createMediaContext(c);
|
|
70
70
|
const groups = toArchiveGroups(grouped, mediaCtx);
|
|
71
71
|
|
|
72
|
-
const components = c.var.config.theme?.components;
|
|
73
|
-
const Page = components?.ArchivePage ?? DefaultArchivePage;
|
|
74
|
-
|
|
75
72
|
return renderPublicPage(c, {
|
|
76
73
|
title: `Archive - ${navData.siteName}`,
|
|
77
74
|
navData,
|
|
78
75
|
content: (
|
|
79
|
-
<
|
|
76
|
+
<ArchivePage
|
|
80
77
|
groups={groups}
|
|
81
78
|
hasMore={hasMore}
|
|
82
79
|
nextCursor={nextCursor}
|
|
83
80
|
format={format}
|
|
84
81
|
featured={featured}
|
|
85
|
-
theme={components}
|
|
86
82
|
/>
|
|
87
83
|
),
|
|
88
84
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
6
|
import type { Bindings } from "../../types.js";
|
|
7
7
|
import type { AppVariables } from "../../app.js";
|
|
8
|
-
import { CollectionPage
|
|
8
|
+
import { CollectionPage } from "../../ui/pages/CollectionPage.js";
|
|
9
9
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
10
10
|
import { renderPublicPage } from "../../lib/render.js";
|
|
11
11
|
import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
|
|
@@ -33,19 +33,15 @@ collectionRoutes.get("/:slug", async (c) => {
|
|
|
33
33
|
const mediaCtx = createMediaContext(c);
|
|
34
34
|
const postViews = toPostViewsFromPosts(posts, mediaCtx);
|
|
35
35
|
|
|
36
|
-
const components = c.var.config.theme?.components;
|
|
37
|
-
const Page = components?.CollectionPage ?? DefaultCollectionPage;
|
|
38
|
-
|
|
39
36
|
return renderPublicPage(c, {
|
|
40
37
|
title: `${collection.title} - ${navData.siteName}`,
|
|
41
38
|
description: collection.description ?? undefined,
|
|
42
39
|
navData,
|
|
43
40
|
content: (
|
|
44
|
-
<
|
|
41
|
+
<CollectionPage
|
|
45
42
|
collection={collection}
|
|
46
43
|
posts={postViews}
|
|
47
44
|
hasMore={false}
|
|
48
|
-
theme={components}
|
|
49
45
|
/>
|
|
50
46
|
),
|
|
51
47
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collections Listing Page Route
|
|
3
|
+
*
|
|
4
|
+
* Lists all collections with their post counts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
import type { Bindings } from "../../types.js";
|
|
9
|
+
import type { AppVariables } from "../../app.js";
|
|
10
|
+
import { getNavigationData } from "../../lib/navigation.js";
|
|
11
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
12
|
+
import { CollectionsPage } from "../../ui/pages/CollectionsPage.js";
|
|
13
|
+
|
|
14
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
15
|
+
|
|
16
|
+
export const collectionsPageRoutes = new Hono<Env>();
|
|
17
|
+
|
|
18
|
+
collectionsPageRoutes.get("/", async (c) => {
|
|
19
|
+
const [allCollections, postCounts] = await Promise.all([
|
|
20
|
+
c.var.services.collections.list(),
|
|
21
|
+
c.var.services.collections.getPostCounts(),
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const collections = allCollections.map((col) => ({
|
|
25
|
+
...col,
|
|
26
|
+
postCount: postCounts.get(col.id) ?? 0,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const navData = await getNavigationData(c);
|
|
30
|
+
|
|
31
|
+
return renderPublicPage(c, {
|
|
32
|
+
title: `Collections - ${navData.siteName}`,
|
|
33
|
+
navData,
|
|
34
|
+
content: <CollectionsPage collections={collections} />,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Featured Page Route
|
|
3
|
+
*
|
|
4
|
+
* Shows featured posts as a timeline feed.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
import type { Bindings } from "../../types.js";
|
|
9
|
+
import type { AppVariables } from "../../app.js";
|
|
10
|
+
import { getNavigationData } from "../../lib/navigation.js";
|
|
11
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
12
|
+
import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
|
|
13
|
+
import { FeaturedPage } from "../../ui/pages/FeaturedPage.js";
|
|
14
|
+
|
|
15
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
16
|
+
|
|
17
|
+
export const featuredRoutes = new Hono<Env>();
|
|
18
|
+
|
|
19
|
+
featuredRoutes.get("/", async (c) => {
|
|
20
|
+
const navData = await getNavigationData(c);
|
|
21
|
+
|
|
22
|
+
// When homepage already shows featured, redirect to avoid duplicate content
|
|
23
|
+
if (navData.homeDefaultView === "featured") {
|
|
24
|
+
return c.redirect("/", 302);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const posts = await c.var.services.posts.list({
|
|
28
|
+
featured: true,
|
|
29
|
+
status: "published",
|
|
30
|
+
excludeReplies: true,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const mediaCtx = createMediaContext(c);
|
|
34
|
+
const postViews = toPostViewsFromPosts(posts, mediaCtx);
|
|
35
|
+
|
|
36
|
+
// Convert to timeline items (simple — no thread previews)
|
|
37
|
+
const items = postViews.map((post) => ({ post }));
|
|
38
|
+
|
|
39
|
+
return renderPublicPage(c, {
|
|
40
|
+
title: `Featured - ${navData.siteName}`,
|
|
41
|
+
navData,
|
|
42
|
+
content: <FeaturedPage items={items} />,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
* Home Page Route
|
|
3
3
|
*
|
|
4
4
|
* Timeline feed with per-type card components and thread previews.
|
|
5
|
-
*
|
|
5
|
+
* Uses page-based pagination.
|
|
6
|
+
*
|
|
7
|
+
* When HOME_DEFAULT_VIEW is "featured", the homepage shows featured posts
|
|
8
|
+
* instead of latest. The /latest route always shows latest posts explicitly.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import { Hono } from "hono";
|
|
@@ -11,64 +14,42 @@ import type { AppVariables } from "../../app.js";
|
|
|
11
14
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
12
15
|
import { renderPublicPage } from "../../lib/render.js";
|
|
13
16
|
import { assembleTimeline } from "../../lib/timeline.js";
|
|
14
|
-
import { sse } from "../../lib/sse.js";
|
|
15
17
|
import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
|
|
16
|
-
import { HomePage
|
|
18
|
+
import { HomePage } from "../../ui/pages/HomePage.js";
|
|
19
|
+
import { FeaturedPage } from "../../ui/pages/FeaturedPage.js";
|
|
17
20
|
|
|
18
21
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
19
22
|
|
|
20
23
|
export const homeRoutes = new Hono<Env>();
|
|
21
24
|
|
|
22
25
|
homeRoutes.get("/", async (c) => {
|
|
23
|
-
const
|
|
24
|
-
const cursor = cursorParam ? parseInt(cursorParam, 10) : undefined;
|
|
25
|
-
const lastDate = c.req.query("lastDate");
|
|
26
|
-
|
|
27
|
-
const { items, hasMore, nextCursor } = await assembleTimeline(c, {
|
|
28
|
-
cursor: cursor && !isNaN(cursor) ? cursor : undefined,
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
// SSE load-more response
|
|
32
|
-
if (cursor && !isNaN(cursor)) {
|
|
33
|
-
if (items.length === 0) {
|
|
34
|
-
return sse(c, async (stream) => {
|
|
35
|
-
stream.remove("#load-more-container");
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const themeConfig = c.var.config.theme;
|
|
40
|
-
const renderMore = themeConfig?.timelineMore;
|
|
41
|
-
if (!renderMore) {
|
|
42
|
-
// Should never happen — default theme always provides timelineMore
|
|
43
|
-
return sse(c, async (stream) => {
|
|
44
|
-
stream.remove("#load-more-container");
|
|
45
|
-
});
|
|
46
|
-
}
|
|
26
|
+
const navData = await getNavigationData(c);
|
|
47
27
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
28
|
+
if (navData.homeDefaultView === "featured") {
|
|
29
|
+
// Show featured posts on homepage
|
|
30
|
+
const posts = await c.var.services.posts.list({
|
|
31
|
+
featured: true,
|
|
32
|
+
status: "published",
|
|
33
|
+
excludeReplies: true,
|
|
54
34
|
});
|
|
35
|
+
const mediaCtx = createMediaContext(c);
|
|
36
|
+
const postViews = toPostViewsFromPosts(posts, mediaCtx);
|
|
37
|
+
const items = postViews.map((post) => ({ post }));
|
|
55
38
|
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
} else {
|
|
61
|
-
stream.patchElements(patch.content, {
|
|
62
|
-
mode: patch.mode,
|
|
63
|
-
selector: patch.selector,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
39
|
+
return renderPublicPage(c, {
|
|
40
|
+
title: navData.siteName,
|
|
41
|
+
navData,
|
|
42
|
+
content: <FeaturedPage items={items} />,
|
|
67
43
|
});
|
|
68
44
|
}
|
|
69
45
|
|
|
70
|
-
//
|
|
71
|
-
const
|
|
46
|
+
// Default: show latest posts
|
|
47
|
+
const pageParam = c.req.query("page");
|
|
48
|
+
const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
|
|
49
|
+
|
|
50
|
+
const { items, currentPage, totalPages } = await assembleTimeline(c, {
|
|
51
|
+
page,
|
|
52
|
+
});
|
|
72
53
|
|
|
73
54
|
// Fetch pinned posts
|
|
74
55
|
const pinnedPosts = await c.var.services.posts.list({
|
|
@@ -79,19 +60,15 @@ homeRoutes.get("/", async (c) => {
|
|
|
79
60
|
const mediaCtx = createMediaContext(c);
|
|
80
61
|
const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
|
|
81
62
|
|
|
82
|
-
const components = c.var.config.theme?.components;
|
|
83
|
-
const Page = components?.HomePage ?? DefaultHomePage;
|
|
84
|
-
|
|
85
63
|
return renderPublicPage(c, {
|
|
86
64
|
title: navData.siteName,
|
|
87
65
|
navData,
|
|
88
66
|
content: (
|
|
89
|
-
<
|
|
67
|
+
<HomePage
|
|
90
68
|
items={items}
|
|
91
69
|
pinnedItems={pinnedItems}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
theme={components}
|
|
70
|
+
currentPage={currentPage}
|
|
71
|
+
totalPages={totalPages}
|
|
95
72
|
/>
|
|
96
73
|
),
|
|
97
74
|
});
|