@jant/core 0.3.21 → 0.3.23
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 +23 -4
- package/dist/index.js +11 -4
- package/dist/lib/feed.js +112 -0
- package/dist/lib/navigation.js +9 -9
- package/dist/lib/render.js +48 -0
- package/dist/lib/theme-components.js +18 -18
- package/dist/lib/view.js +228 -0
- package/dist/routes/api/timeline.js +22 -18
- package/dist/routes/feed/rss.js +34 -78
- package/dist/routes/feed/sitemap.js +11 -26
- package/dist/routes/pages/archive.js +18 -195
- package/dist/routes/pages/collection.js +16 -70
- package/dist/routes/pages/home.js +25 -47
- package/dist/routes/pages/page.js +15 -27
- package/dist/routes/pages/post.js +25 -79
- package/dist/routes/pages/search.js +20 -130
- package/dist/theme/components/MediaGallery.js +10 -10
- package/dist/theme/components/index.js +0 -2
- package/dist/theme/index.js +10 -13
- package/dist/theme/layouts/index.js +0 -1
- package/dist/themes/minimal/MinimalSiteLayout.js +83 -0
- package/dist/themes/minimal/index.js +65 -0
- package/dist/themes/minimal/pages/ArchivePage.js +156 -0
- package/dist/themes/minimal/pages/CollectionPage.js +65 -0
- package/dist/themes/minimal/pages/HomePage.js +25 -0
- package/dist/themes/minimal/pages/PostPage.js +47 -0
- package/dist/themes/minimal/pages/SearchPage.js +121 -0
- package/dist/themes/minimal/pages/SinglePage.js +22 -0
- package/dist/themes/minimal/timeline/ArticleCard.js +36 -0
- package/dist/themes/minimal/timeline/ImageCard.js +67 -0
- package/dist/themes/minimal/timeline/LinkCard.js +47 -0
- package/dist/themes/minimal/timeline/NoteCard.js +34 -0
- package/dist/{theme/components → themes/minimal}/timeline/QuoteCard.js +9 -12
- package/dist/themes/minimal/timeline/ThreadPreview.js +46 -0
- package/dist/themes/minimal/timeline/TimelineFeed.js +48 -0
- package/dist/themes/minimal/timeline/TimelineItem.js +44 -0
- package/package.json +2 -1
- package/src/app.tsx +27 -4
- package/src/i18n/locales/en.po +53 -53
- package/src/i18n/locales/zh-Hans.po +53 -53
- package/src/i18n/locales/zh-Hant.po +53 -53
- package/src/index.ts +54 -6
- package/src/lib/__tests__/theme-components.test.ts +33 -14
- package/src/lib/__tests__/view.test.ts +377 -0
- package/src/lib/feed.ts +148 -0
- package/src/lib/navigation.ts +11 -11
- package/src/lib/render.tsx +67 -0
- package/src/lib/theme-components.ts +27 -35
- package/src/lib/view.ts +318 -0
- package/src/routes/api/__tests__/timeline.test.ts +3 -3
- package/src/routes/api/timeline.tsx +34 -27
- package/src/routes/feed/rss.ts +47 -94
- package/src/routes/feed/sitemap.ts +8 -30
- package/src/routes/pages/archive.tsx +24 -209
- package/src/routes/pages/collection.tsx +19 -75
- package/src/routes/pages/home.tsx +42 -76
- package/src/routes/pages/page.tsx +17 -28
- package/src/routes/pages/post.tsx +28 -86
- package/src/routes/pages/search.tsx +29 -151
- package/src/services/search.ts +2 -8
- package/src/styles/components.css +0 -54
- package/src/theme/components/MediaGallery.tsx +12 -12
- package/src/theme/components/index.ts +0 -12
- package/src/theme/index.ts +11 -13
- package/src/theme/layouts/index.ts +1 -1
- package/src/themes/minimal/MinimalSiteLayout.tsx +100 -0
- package/src/themes/minimal/index.ts +83 -0
- package/src/themes/minimal/pages/ArchivePage.tsx +157 -0
- package/src/themes/minimal/pages/CollectionPage.tsx +60 -0
- package/src/themes/minimal/pages/HomePage.tsx +41 -0
- package/src/themes/minimal/pages/PostPage.tsx +43 -0
- package/src/themes/minimal/pages/SearchPage.tsx +122 -0
- package/src/themes/minimal/pages/SinglePage.tsx +23 -0
- package/src/themes/minimal/timeline/ArticleCard.tsx +37 -0
- package/src/themes/minimal/timeline/ImageCard.tsx +63 -0
- package/src/themes/minimal/timeline/LinkCard.tsx +48 -0
- package/src/themes/minimal/timeline/NoteCard.tsx +35 -0
- package/src/{theme/components → themes/minimal}/timeline/QuoteCard.tsx +11 -17
- package/src/themes/minimal/timeline/ThreadPreview.tsx +47 -0
- package/src/{theme/components → themes/minimal}/timeline/TimelineFeed.tsx +20 -15
- package/src/themes/minimal/timeline/TimelineItem.tsx +75 -0
- package/src/types.ts +262 -38
- package/dist/theme/components/timeline/ArticleCard.js +0 -50
- package/dist/theme/components/timeline/ImageCard.js +0 -86
- package/dist/theme/components/timeline/LinkCard.js +0 -62
- package/dist/theme/components/timeline/NoteCard.js +0 -37
- package/dist/theme/components/timeline/ThreadPreview.js +0 -52
- package/dist/theme/components/timeline/TimelineFeed.js +0 -43
- package/dist/theme/components/timeline/TimelineItem.js +0 -25
- package/dist/theme/components/timeline/index.js +0 -8
- package/dist/theme/layouts/SiteLayout.js +0 -160
- package/src/theme/components/timeline/ArticleCard.tsx +0 -57
- package/src/theme/components/timeline/ImageCard.tsx +0 -80
- package/src/theme/components/timeline/LinkCard.tsx +0 -66
- package/src/theme/components/timeline/NoteCard.tsx +0 -41
- package/src/theme/components/timeline/ThreadPreview.tsx +0 -49
- package/src/theme/components/timeline/TimelineItem.tsx +0 -39
- package/src/theme/components/timeline/index.ts +0 -8
- package/src/theme/layouts/SiteLayout.tsx +0 -184
package/dist/routes/feed/rss.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* RSS Feed Routes
|
|
3
3
|
*/ import { Hono } from "hono";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
4
|
+
import { defaultRssRenderer, defaultAtomRenderer } from "../../lib/feed.js";
|
|
5
|
+
import { getSiteLanguage } from "../../lib/config.js";
|
|
6
|
+
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
7
|
+
import { createMediaContext, toPostViews } from "../../lib/view.js";
|
|
7
8
|
export const rssRoutes = new Hono();
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Build FeedData from the Hono context.
|
|
11
|
+
*/ async function buildFeedData(c) {
|
|
10
12
|
const all = await c.var.services.settings.getAll();
|
|
11
13
|
const siteName = all["SITE_NAME"] ?? "Jant";
|
|
12
14
|
const siteDescription = all["SITE_DESCRIPTION"] ?? "";
|
|
13
15
|
const siteUrl = c.env.SITE_URL;
|
|
14
|
-
const
|
|
15
|
-
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
16
|
+
const siteLanguage = await getSiteLanguage(c);
|
|
16
17
|
const posts = await c.var.services.posts.list({
|
|
17
18
|
visibility: [
|
|
18
19
|
"featured",
|
|
@@ -22,36 +23,28 @@ rssRoutes.get("/", async (c)=>{
|
|
|
22
23
|
});
|
|
23
24
|
// Batch load media for enclosures
|
|
24
25
|
const postIds = posts.map((p)=>p.id);
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<link>${siteUrl}</link>
|
|
48
|
-
<description>${escapeXml(siteDescription)}</description>
|
|
49
|
-
<language>en</language>
|
|
50
|
-
<atom:link href="${siteUrl}/feed" rel="self" type="application/rss+xml"/>
|
|
51
|
-
${items}
|
|
52
|
-
</channel>
|
|
53
|
-
</rss>`;
|
|
54
|
-
return new Response(rss, {
|
|
26
|
+
const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
27
|
+
const mediaCtx = createMediaContext(c);
|
|
28
|
+
const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
|
|
29
|
+
// Transform to PostView[] with media
|
|
30
|
+
const postViews = toPostViews(posts.map((p)=>({
|
|
31
|
+
...p,
|
|
32
|
+
mediaAttachments: mediaMap.get(p.id) ?? []
|
|
33
|
+
})), mediaCtx);
|
|
34
|
+
return {
|
|
35
|
+
siteName,
|
|
36
|
+
siteDescription,
|
|
37
|
+
siteUrl,
|
|
38
|
+
siteLanguage,
|
|
39
|
+
posts: postViews
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
// RSS 2.0 Feed - main feed at /feed
|
|
43
|
+
rssRoutes.get("/", async (c)=>{
|
|
44
|
+
const feedData = await buildFeedData(c);
|
|
45
|
+
const renderer = c.var.config.theme?.feed?.rss ?? defaultRssRenderer;
|
|
46
|
+
const xml = renderer(feedData);
|
|
47
|
+
return new Response(xml, {
|
|
55
48
|
headers: {
|
|
56
49
|
"Content-Type": "application/rss+xml; charset=utf-8"
|
|
57
50
|
}
|
|
@@ -59,49 +52,12 @@ rssRoutes.get("/", async (c)=>{
|
|
|
59
52
|
});
|
|
60
53
|
// Atom Feed
|
|
61
54
|
rssRoutes.get("/atom.xml", async (c)=>{
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
const posts = await c.var.services.posts.list({
|
|
67
|
-
visibility: [
|
|
68
|
-
"featured",
|
|
69
|
-
"quiet"
|
|
70
|
-
],
|
|
71
|
-
limit: 50
|
|
72
|
-
});
|
|
73
|
-
const entries = posts.map((post)=>{
|
|
74
|
-
const link = `${siteUrl}/p/${sqid.encode(post.id)}`;
|
|
75
|
-
const title = post.title || `Post #${post.id}`;
|
|
76
|
-
const updated = time.toISOString(post.updatedAt);
|
|
77
|
-
const published = time.toISOString(post.publishedAt);
|
|
78
|
-
return `
|
|
79
|
-
<entry>
|
|
80
|
-
<title>${escapeXml(title)}</title>
|
|
81
|
-
<link href="${link}" rel="alternate"/>
|
|
82
|
-
<id>${link}</id>
|
|
83
|
-
<published>${published}</published>
|
|
84
|
-
<updated>${updated}</updated>
|
|
85
|
-
<content type="html"><![CDATA[${post.contentHtml || ""}]]></content>
|
|
86
|
-
</entry>`;
|
|
87
|
-
}).join("");
|
|
88
|
-
const now = time.toISOString(time.now());
|
|
89
|
-
const atom = `<?xml version="1.0" encoding="UTF-8"?>
|
|
90
|
-
<feed xmlns="http://www.w3.org/2005/Atom">
|
|
91
|
-
<title>${escapeXml(siteName)}</title>
|
|
92
|
-
<subtitle>${escapeXml(siteDescription)}</subtitle>
|
|
93
|
-
<link href="${siteUrl}" rel="alternate"/>
|
|
94
|
-
<link href="${siteUrl}/feed/atom.xml" rel="self"/>
|
|
95
|
-
<id>${siteUrl}/</id>
|
|
96
|
-
<updated>${now}</updated>
|
|
97
|
-
${entries}
|
|
98
|
-
</feed>`;
|
|
99
|
-
return new Response(atom, {
|
|
55
|
+
const feedData = await buildFeedData(c);
|
|
56
|
+
const renderer = c.var.config.theme?.feed?.atom ?? defaultAtomRenderer;
|
|
57
|
+
const xml = renderer(feedData);
|
|
58
|
+
return new Response(xml, {
|
|
100
59
|
headers: {
|
|
101
60
|
"Content-Type": "application/atom+xml; charset=utf-8"
|
|
102
61
|
}
|
|
103
62
|
});
|
|
104
63
|
});
|
|
105
|
-
function escapeXml(str) {
|
|
106
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
107
|
-
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sitemap Routes
|
|
3
3
|
*/ import { Hono } from "hono";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import { defaultSitemapRenderer } from "../../lib/feed.js";
|
|
5
|
+
import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
|
|
6
6
|
export const sitemapRoutes = new Hono();
|
|
7
7
|
// XML Sitemap
|
|
8
8
|
sitemapRoutes.get("/sitemap.xml", async (c)=>{
|
|
@@ -14,30 +14,15 @@ sitemapRoutes.get("/sitemap.xml", async (c)=>{
|
|
|
14
14
|
],
|
|
15
15
|
limit: 1000
|
|
16
16
|
});
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
</url>`;
|
|
27
|
-
}).join("");
|
|
28
|
-
// Add homepage
|
|
29
|
-
const homepageUrl = `
|
|
30
|
-
<url>
|
|
31
|
-
<loc>${siteUrl}/</loc>
|
|
32
|
-
<priority>1.0</priority>
|
|
33
|
-
<changefreq>daily</changefreq>
|
|
34
|
-
</url>`;
|
|
35
|
-
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
|
|
36
|
-
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
37
|
-
${homepageUrl}
|
|
38
|
-
${urls}
|
|
39
|
-
</urlset>`;
|
|
40
|
-
return new Response(sitemap, {
|
|
17
|
+
// Transform to PostView[]
|
|
18
|
+
const mediaCtx = createMediaContext(c);
|
|
19
|
+
const postViews = toPostViewsFromPosts(posts, mediaCtx);
|
|
20
|
+
const renderer = c.var.config.theme?.feed?.sitemap ?? defaultSitemapRenderer;
|
|
21
|
+
const xml = renderer({
|
|
22
|
+
siteUrl,
|
|
23
|
+
posts: postViews
|
|
24
|
+
});
|
|
25
|
+
return new Response(xml, {
|
|
41
26
|
headers: {
|
|
42
27
|
"Content-Type": "application/xml; charset=utf-8"
|
|
43
28
|
}
|
|
@@ -1,191 +1,16 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* Archive Page Route
|
|
4
4
|
*
|
|
5
5
|
* Shows all posts, optionally filtered by type
|
|
6
6
|
*/ import { Hono } from "hono";
|
|
7
|
-
import { useLingui as $_useLingui } from "@jant/core/i18n";
|
|
8
|
-
import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
|
|
9
|
-
import { Pagination } from "../../theme/components/index.js";
|
|
10
7
|
import { POST_TYPES } from "../../types.js";
|
|
11
|
-
import
|
|
12
|
-
import * as time from "../../lib/time.js";
|
|
8
|
+
import { ArchivePage as DefaultArchivePage } from "../../themes/minimal/pages/ArchivePage.js";
|
|
13
9
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
10
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
11
|
+
import { createMediaContext, toArchiveGroups } from "../../lib/view.js";
|
|
14
12
|
const PAGE_SIZE = 50;
|
|
15
13
|
export const archiveRoutes = new Hono();
|
|
16
|
-
function getTypeLabel(type) {
|
|
17
|
-
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
18
|
-
const labels = {
|
|
19
|
-
note: $__i18n._({
|
|
20
|
-
id: "KiJn9B",
|
|
21
|
-
message: "Note"
|
|
22
|
-
}),
|
|
23
|
-
article: $__i18n._({
|
|
24
|
-
id: "f6e0Ry",
|
|
25
|
-
message: "Article"
|
|
26
|
-
}),
|
|
27
|
-
link: $__i18n._({
|
|
28
|
-
id: "yzF66j",
|
|
29
|
-
message: "Link"
|
|
30
|
-
}),
|
|
31
|
-
quote: $__i18n._({
|
|
32
|
-
id: "ZhhOwV",
|
|
33
|
-
message: "Quote"
|
|
34
|
-
}),
|
|
35
|
-
image: $__i18n._({
|
|
36
|
-
id: "hG89Ed",
|
|
37
|
-
message: "Image"
|
|
38
|
-
}),
|
|
39
|
-
page: $__i18n._({
|
|
40
|
-
id: "6WdDG7",
|
|
41
|
-
message: "Page"
|
|
42
|
-
})
|
|
43
|
-
};
|
|
44
|
-
return labels[type] ?? type;
|
|
45
|
-
}
|
|
46
|
-
function getTypeLabelPlural(type) {
|
|
47
|
-
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
48
|
-
const labels = {
|
|
49
|
-
note: $__i18n._({
|
|
50
|
-
id: "1DBGsz",
|
|
51
|
-
message: "Notes"
|
|
52
|
-
}),
|
|
53
|
-
article: $__i18n._({
|
|
54
|
-
id: "Tt5T6+",
|
|
55
|
-
message: "Articles"
|
|
56
|
-
}),
|
|
57
|
-
link: $__i18n._({
|
|
58
|
-
id: "Rj01Fz",
|
|
59
|
-
message: "Links"
|
|
60
|
-
}),
|
|
61
|
-
quote: $__i18n._({
|
|
62
|
-
id: "eWLklq",
|
|
63
|
-
message: "Quotes"
|
|
64
|
-
}),
|
|
65
|
-
image: $__i18n._({
|
|
66
|
-
id: "an5hVd",
|
|
67
|
-
message: "Images"
|
|
68
|
-
}),
|
|
69
|
-
page: $__i18n._({
|
|
70
|
-
id: "wRR604",
|
|
71
|
-
message: "Pages"
|
|
72
|
-
})
|
|
73
|
-
};
|
|
74
|
-
return labels[type] ?? `${type}s`;
|
|
75
|
-
}
|
|
76
|
-
function formatYearMonth(yearMonth) {
|
|
77
|
-
const [year, month] = yearMonth.split("-");
|
|
78
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- yearMonth format YYYY-MM guarantees both year and month exist
|
|
79
|
-
const date = new Date(parseInt(year, 10), parseInt(month, 10) - 1);
|
|
80
|
-
return date.toLocaleDateString("en-US", {
|
|
81
|
-
year: "numeric",
|
|
82
|
-
month: "long"
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
function ArchiveContent({ displayPosts, hasMore, nextCursor, type, grouped, replyCounts }) {
|
|
86
|
-
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
87
|
-
const title = type ? getTypeLabelPlural(type) : $__i18n._({
|
|
88
|
-
id: "B495Gs",
|
|
89
|
-
message: "Archive"
|
|
90
|
-
});
|
|
91
|
-
return /*#__PURE__*/ _jsxs("div", {
|
|
92
|
-
children: [
|
|
93
|
-
/*#__PURE__*/ _jsxs("header", {
|
|
94
|
-
class: "mb-8",
|
|
95
|
-
children: [
|
|
96
|
-
/*#__PURE__*/ _jsx("h1", {
|
|
97
|
-
class: "text-2xl font-semibold",
|
|
98
|
-
children: title
|
|
99
|
-
}),
|
|
100
|
-
/*#__PURE__*/ _jsxs("nav", {
|
|
101
|
-
class: "flex flex-wrap gap-2 mt-4",
|
|
102
|
-
children: [
|
|
103
|
-
/*#__PURE__*/ _jsx("a", {
|
|
104
|
-
href: "/archive",
|
|
105
|
-
class: `badge ${!type ? "badge-primary" : "badge-outline"}`,
|
|
106
|
-
children: $__i18n._({
|
|
107
|
-
id: "N40H+G",
|
|
108
|
-
message: "All"
|
|
109
|
-
})
|
|
110
|
-
}),
|
|
111
|
-
POST_TYPES.filter((t)=>t !== "page").map((typeKey)=>/*#__PURE__*/ _jsx("a", {
|
|
112
|
-
href: `/archive?type=${typeKey}`,
|
|
113
|
-
class: `badge ${type === typeKey ? "badge-primary" : "badge-outline"}`,
|
|
114
|
-
children: getTypeLabelPlural(typeKey)
|
|
115
|
-
}, typeKey))
|
|
116
|
-
]
|
|
117
|
-
})
|
|
118
|
-
]
|
|
119
|
-
}),
|
|
120
|
-
/*#__PURE__*/ _jsx("main", {
|
|
121
|
-
children: displayPosts.length === 0 ? /*#__PURE__*/ _jsx("p", {
|
|
122
|
-
class: "text-muted-foreground",
|
|
123
|
-
children: $__i18n._({
|
|
124
|
-
id: "Hzi9AA",
|
|
125
|
-
message: "No posts found."
|
|
126
|
-
})
|
|
127
|
-
}) : Array.from(grouped.entries()).map(([yearMonth, monthPosts])=>/*#__PURE__*/ _jsxs("section", {
|
|
128
|
-
class: "mb-8",
|
|
129
|
-
children: [
|
|
130
|
-
/*#__PURE__*/ _jsx("h2", {
|
|
131
|
-
class: "text-lg font-medium mb-4 text-muted-foreground",
|
|
132
|
-
children: formatYearMonth(yearMonth)
|
|
133
|
-
}),
|
|
134
|
-
/*#__PURE__*/ _jsx("div", {
|
|
135
|
-
class: "flex flex-col gap-3",
|
|
136
|
-
children: monthPosts.map((post)=>{
|
|
137
|
-
const replyCount = replyCounts.get(post.id);
|
|
138
|
-
return /*#__PURE__*/ _jsxs("article", {
|
|
139
|
-
class: "flex items-baseline gap-4",
|
|
140
|
-
children: [
|
|
141
|
-
/*#__PURE__*/ _jsx("time", {
|
|
142
|
-
class: "text-sm text-muted-foreground w-12 shrink-0",
|
|
143
|
-
datetime: time.toISOString(post.publishedAt),
|
|
144
|
-
children: new Date(post.publishedAt * 1000).getDate()
|
|
145
|
-
}),
|
|
146
|
-
/*#__PURE__*/ _jsxs("div", {
|
|
147
|
-
class: "flex-1 min-w-0",
|
|
148
|
-
children: [
|
|
149
|
-
/*#__PURE__*/ _jsx("a", {
|
|
150
|
-
href: `/p/${sqid.encode(post.id)}`,
|
|
151
|
-
class: "hover:underline",
|
|
152
|
-
children: post.title || post.content?.slice(0, 80) || `Post #${post.id}`
|
|
153
|
-
}),
|
|
154
|
-
!type && /*#__PURE__*/ _jsx("span", {
|
|
155
|
-
class: "ml-2 badge-outline text-xs",
|
|
156
|
-
children: getTypeLabel(post.type)
|
|
157
|
-
}),
|
|
158
|
-
replyCount && replyCount > 0 && /*#__PURE__*/ _jsxs("span", {
|
|
159
|
-
class: "ml-2 text-xs text-muted-foreground",
|
|
160
|
-
children: [
|
|
161
|
-
"(",
|
|
162
|
-
replyCount === 1 ? $__i18n._({
|
|
163
|
-
id: "TxE+Mj",
|
|
164
|
-
message: "1 reply"
|
|
165
|
-
}) : $__i18n._({
|
|
166
|
-
id: "90Luob",
|
|
167
|
-
message: "{count} replies"
|
|
168
|
-
}),
|
|
169
|
-
")"
|
|
170
|
-
]
|
|
171
|
-
})
|
|
172
|
-
]
|
|
173
|
-
})
|
|
174
|
-
]
|
|
175
|
-
}, post.id);
|
|
176
|
-
})
|
|
177
|
-
})
|
|
178
|
-
]
|
|
179
|
-
}, yearMonth))
|
|
180
|
-
}),
|
|
181
|
-
/*#__PURE__*/ _jsx(Pagination, {
|
|
182
|
-
baseUrl: type ? `/archive?type=${type}` : "/archive",
|
|
183
|
-
hasMore: hasMore,
|
|
184
|
-
nextCursor: nextCursor
|
|
185
|
-
})
|
|
186
|
-
]
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
14
|
// Archive page - all posts
|
|
190
15
|
archiveRoutes.get("/", async (c)=>{
|
|
191
16
|
const typeParam = c.req.query("type");
|
|
@@ -207,9 +32,6 @@ archiveRoutes.get("/", async (c)=>{
|
|
|
207
32
|
});
|
|
208
33
|
const hasMore = posts.length > PAGE_SIZE;
|
|
209
34
|
const displayPosts = hasMore ? posts.slice(0, PAGE_SIZE) : posts;
|
|
210
|
-
// Get reply counts for thread indicators
|
|
211
|
-
const postIds = displayPosts.map((p)=>p.id);
|
|
212
|
-
const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
|
|
213
35
|
// Get next cursor
|
|
214
36
|
const nextCursor = hasMore && displayPosts.length > 0 ? displayPosts[displayPosts.length - 1].id : undefined;
|
|
215
37
|
// Group posts by year-month
|
|
@@ -223,19 +45,20 @@ archiveRoutes.get("/", async (c)=>{
|
|
|
223
45
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Map.set() above guarantees key exists
|
|
224
46
|
grouped.get(key).push(post);
|
|
225
47
|
}
|
|
226
|
-
|
|
48
|
+
// Transform to View Models
|
|
49
|
+
const mediaCtx = createMediaContext(c);
|
|
50
|
+
const groups = toArchiveGroups(grouped, mediaCtx);
|
|
51
|
+
const components = c.var.config.theme?.components;
|
|
52
|
+
const Page = components?.ArchivePage ?? DefaultArchivePage;
|
|
53
|
+
return renderPublicPage(c, {
|
|
227
54
|
title: `Archive - ${navData.siteName}`,
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
type: type,
|
|
236
|
-
grouped: grouped,
|
|
237
|
-
replyCounts: replyCounts
|
|
238
|
-
})
|
|
55
|
+
navData,
|
|
56
|
+
content: /*#__PURE__*/ _jsx(Page, {
|
|
57
|
+
groups: groups,
|
|
58
|
+
hasMore: hasMore,
|
|
59
|
+
nextCursor: nextCursor,
|
|
60
|
+
type: type,
|
|
61
|
+
theme: components
|
|
239
62
|
})
|
|
240
|
-
})
|
|
63
|
+
});
|
|
241
64
|
});
|
|
@@ -1,85 +1,31 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* Collection Page Route
|
|
4
4
|
*/ import { Hono } from "hono";
|
|
5
|
-
import {
|
|
6
|
-
import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
|
|
7
|
-
import * as sqid from "../../lib/sqid.js";
|
|
8
|
-
import * as time from "../../lib/time.js";
|
|
5
|
+
import { CollectionPage as DefaultCollectionPage } from "../../themes/minimal/pages/CollectionPage.js";
|
|
9
6
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
7
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
8
|
+
import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
|
|
10
9
|
export const collectionRoutes = new Hono();
|
|
11
|
-
function CollectionContent({ collection, posts }) {
|
|
12
|
-
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
13
|
-
return /*#__PURE__*/ _jsxs("div", {
|
|
14
|
-
children: [
|
|
15
|
-
/*#__PURE__*/ _jsxs("header", {
|
|
16
|
-
class: "mb-8",
|
|
17
|
-
children: [
|
|
18
|
-
/*#__PURE__*/ _jsx("h1", {
|
|
19
|
-
class: "text-2xl font-semibold",
|
|
20
|
-
children: collection.title
|
|
21
|
-
}),
|
|
22
|
-
collection.description && /*#__PURE__*/ _jsx("p", {
|
|
23
|
-
class: "text-muted-foreground mt-2",
|
|
24
|
-
children: collection.description
|
|
25
|
-
})
|
|
26
|
-
]
|
|
27
|
-
}),
|
|
28
|
-
/*#__PURE__*/ _jsx("main", {
|
|
29
|
-
class: "flex flex-col gap-6",
|
|
30
|
-
children: posts.length === 0 ? /*#__PURE__*/ _jsx("p", {
|
|
31
|
-
class: "text-muted-foreground",
|
|
32
|
-
children: $__i18n._({
|
|
33
|
-
id: "J4FNfC",
|
|
34
|
-
message: "No posts in this collection."
|
|
35
|
-
})
|
|
36
|
-
}) : posts.map((post)=>/*#__PURE__*/ _jsxs("article", {
|
|
37
|
-
class: "h-entry",
|
|
38
|
-
children: [
|
|
39
|
-
post.title && /*#__PURE__*/ _jsx("h2", {
|
|
40
|
-
class: "p-name text-lg font-medium mb-2",
|
|
41
|
-
children: /*#__PURE__*/ _jsx("a", {
|
|
42
|
-
href: `/p/${sqid.encode(post.id)}`,
|
|
43
|
-
class: "u-url hover:underline",
|
|
44
|
-
children: post.title
|
|
45
|
-
})
|
|
46
|
-
}),
|
|
47
|
-
/*#__PURE__*/ _jsx("div", {
|
|
48
|
-
class: "e-content prose prose-sm",
|
|
49
|
-
dangerouslySetInnerHTML: {
|
|
50
|
-
__html: post.contentHtml || ""
|
|
51
|
-
}
|
|
52
|
-
}),
|
|
53
|
-
/*#__PURE__*/ _jsx("footer", {
|
|
54
|
-
class: "mt-2 text-sm text-muted-foreground",
|
|
55
|
-
children: /*#__PURE__*/ _jsx("time", {
|
|
56
|
-
class: "dt-published",
|
|
57
|
-
datetime: time.toISOString(post.publishedAt),
|
|
58
|
-
children: time.formatDate(post.publishedAt)
|
|
59
|
-
})
|
|
60
|
-
})
|
|
61
|
-
]
|
|
62
|
-
}, post.id))
|
|
63
|
-
})
|
|
64
|
-
]
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
10
|
collectionRoutes.get("/:path", async (c)=>{
|
|
68
11
|
const path = c.req.param("path");
|
|
69
12
|
const collection = await c.var.services.collections.getByPath(path);
|
|
70
13
|
if (!collection) return c.notFound();
|
|
71
14
|
const posts = await c.var.services.collections.getPosts(collection.id);
|
|
72
15
|
const navData = await getNavigationData(c);
|
|
73
|
-
|
|
16
|
+
// Transform to View Models
|
|
17
|
+
const mediaCtx = createMediaContext(c);
|
|
18
|
+
const postViews = toPostViewsFromPosts(posts, mediaCtx);
|
|
19
|
+
const components = c.var.config.theme?.components;
|
|
20
|
+
const Page = components?.CollectionPage ?? DefaultCollectionPage;
|
|
21
|
+
return renderPublicPage(c, {
|
|
74
22
|
title: `${collection.title} - ${navData.siteName}`,
|
|
75
23
|
description: collection.description ?? undefined,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
posts: posts
|
|
82
|
-
})
|
|
24
|
+
navData,
|
|
25
|
+
content: /*#__PURE__*/ _jsx(Page, {
|
|
26
|
+
collection: collection,
|
|
27
|
+
posts: postViews,
|
|
28
|
+
theme: components
|
|
83
29
|
})
|
|
84
|
-
})
|
|
30
|
+
});
|
|
85
31
|
});
|
|
@@ -1,31 +1,16 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "hono/jsx/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* Home Page Route
|
|
4
4
|
*
|
|
5
5
|
* Timeline feed with per-type card components and thread previews.
|
|
6
6
|
*/ import { Hono } from "hono";
|
|
7
|
-
import { useLingui as $_useLingui } from "@jant/core/i18n";
|
|
8
|
-
import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
|
|
9
7
|
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
10
|
-
import { resolveTimelineFeed } from "../../lib/theme-components.js";
|
|
11
|
-
import { TimelineFeed as DefaultTimelineFeed } from "../../theme/components/timeline/TimelineFeed.js";
|
|
12
8
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
9
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
10
|
+
import { HomePage as DefaultHomePage } from "../../themes/minimal/pages/HomePage.js";
|
|
11
|
+
import { createMediaContext, toPostView, toPostViews } from "../../lib/view.js";
|
|
13
12
|
const PAGE_SIZE = 20;
|
|
14
13
|
export const homeRoutes = new Hono();
|
|
15
|
-
function HomeContent({ FeedComponent, feedProps }) {
|
|
16
|
-
const { i18n: $__i18n, _: $__ } = $_useLingui();
|
|
17
|
-
return /*#__PURE__*/ _jsx(_Fragment, {
|
|
18
|
-
children: feedProps.items.length === 0 ? /*#__PURE__*/ _jsx("p", {
|
|
19
|
-
class: "text-muted-foreground",
|
|
20
|
-
children: $__i18n._({
|
|
21
|
-
id: "ODiSoW",
|
|
22
|
-
message: "No posts yet."
|
|
23
|
-
})
|
|
24
|
-
}) : /*#__PURE__*/ _jsx(FeedComponent, {
|
|
25
|
-
...feedProps
|
|
26
|
-
})
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
14
|
homeRoutes.get("/", async (c)=>{
|
|
30
15
|
const navData = await getNavigationData(c);
|
|
31
16
|
// Fetch one extra to determine if there are more
|
|
@@ -45,10 +30,8 @@ homeRoutes.get("/", async (c)=>{
|
|
|
45
30
|
// Batch load media attachments
|
|
46
31
|
const postIds = displayPosts.map((p)=>p.id);
|
|
47
32
|
const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
51
|
-
const mediaMap = buildMediaMap(rawMediaMap, r2PublicUrl, imageTransformUrl, s3PublicUrl);
|
|
33
|
+
const mediaCtx = createMediaContext(c);
|
|
34
|
+
const mediaMap = buildMediaMap(rawMediaMap, mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl);
|
|
52
35
|
// Get reply counts to identify thread roots
|
|
53
36
|
const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
|
|
54
37
|
const threadRootIds = postIds.filter((id)=>(replyCounts.get(id) ?? 0) > 0);
|
|
@@ -61,50 +44,45 @@ homeRoutes.get("/", async (c)=>{
|
|
|
61
44
|
previewReplyIds.push(reply.id);
|
|
62
45
|
}
|
|
63
46
|
}
|
|
64
|
-
const previewMediaMap = previewReplyIds.length > 0 ? buildMediaMap(await c.var.services.media.getByPostIds(previewReplyIds), r2PublicUrl, imageTransformUrl, s3PublicUrl) : new Map();
|
|
65
|
-
// Assemble timeline items
|
|
47
|
+
const previewMediaMap = previewReplyIds.length > 0 ? buildMediaMap(await c.var.services.media.getByPostIds(previewReplyIds), mediaCtx.r2PublicUrl, mediaCtx.imageTransformUrl, mediaCtx.s3PublicUrl) : new Map();
|
|
48
|
+
// Assemble timeline items with View Models
|
|
66
49
|
const items = displayPosts.map((post)=>{
|
|
67
|
-
const
|
|
50
|
+
const postView = toPostView({
|
|
68
51
|
...post,
|
|
69
52
|
mediaAttachments: mediaMap.get(post.id) ?? []
|
|
70
|
-
};
|
|
53
|
+
}, mediaCtx);
|
|
71
54
|
const replyCount = replyCounts.get(post.id) ?? 0;
|
|
72
55
|
const previewReplies = threadPreviews.get(post.id);
|
|
73
56
|
if (replyCount > 0 && previewReplies) {
|
|
74
57
|
return {
|
|
75
|
-
post:
|
|
58
|
+
post: postView,
|
|
76
59
|
threadPreview: {
|
|
77
|
-
replies: previewReplies.map((r)=>({
|
|
60
|
+
replies: toPostViews(previewReplies.map((r)=>({
|
|
78
61
|
...r,
|
|
79
62
|
mediaAttachments: previewMediaMap.get(r.id) ?? []
|
|
80
|
-
})),
|
|
63
|
+
})), mediaCtx),
|
|
81
64
|
totalReplyCount: replyCount
|
|
82
65
|
}
|
|
83
66
|
};
|
|
84
67
|
}
|
|
85
68
|
return {
|
|
86
|
-
post:
|
|
69
|
+
post: postView
|
|
87
70
|
};
|
|
88
71
|
});
|
|
89
72
|
// Determine next cursor
|
|
90
73
|
const lastPost = displayPosts[displayPosts.length - 1];
|
|
91
74
|
const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
|
|
92
|
-
// Resolve
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
hasMore,
|
|
97
|
-
nextCursor
|
|
98
|
-
};
|
|
99
|
-
return c.html(/*#__PURE__*/ _jsx(BaseLayout, {
|
|
75
|
+
// Resolve page component
|
|
76
|
+
const components = c.var.config.theme?.components;
|
|
77
|
+
const Page = components?.HomePage ?? DefaultHomePage;
|
|
78
|
+
return renderPublicPage(c, {
|
|
100
79
|
title: navData.siteName,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
})
|
|
80
|
+
navData,
|
|
81
|
+
content: /*#__PURE__*/ _jsx(Page, {
|
|
82
|
+
items: items,
|
|
83
|
+
hasMore: hasMore,
|
|
84
|
+
nextCursor: nextCursor,
|
|
85
|
+
theme: components
|
|
108
86
|
})
|
|
109
|
-
})
|
|
87
|
+
});
|
|
110
88
|
});
|