@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
|
@@ -5,20 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Hono } from "hono";
|
|
8
|
-
import {
|
|
9
|
-
import type { FC } from "hono/jsx";
|
|
10
|
-
import type {
|
|
11
|
-
Bindings,
|
|
12
|
-
PostWithMedia,
|
|
13
|
-
TimelineItemData,
|
|
14
|
-
TimelineFeedProps,
|
|
15
|
-
} from "../../types.js";
|
|
8
|
+
import type { Bindings, TimelineItemView } from "../../types.js";
|
|
16
9
|
import type { AppVariables } from "../../app.js";
|
|
17
|
-
import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
|
|
18
10
|
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
19
|
-
import { resolveTimelineFeed } from "../../lib/theme-components.js";
|
|
20
|
-
import { TimelineFeed as DefaultTimelineFeed } from "../../theme/components/timeline/TimelineFeed.js";
|
|
21
11
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
12
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
13
|
+
import { HomePage as DefaultHomePage } from "../../themes/minimal/pages/HomePage.js";
|
|
14
|
+
import { createMediaContext, toPostView, toPostViews } from "../../lib/view.js";
|
|
22
15
|
|
|
23
16
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
24
17
|
|
|
@@ -26,31 +19,6 @@ const PAGE_SIZE = 20;
|
|
|
26
19
|
|
|
27
20
|
export const homeRoutes = new Hono<Env>();
|
|
28
21
|
|
|
29
|
-
function HomeContent({
|
|
30
|
-
FeedComponent,
|
|
31
|
-
feedProps,
|
|
32
|
-
}: {
|
|
33
|
-
FeedComponent: FC<TimelineFeedProps>;
|
|
34
|
-
feedProps: TimelineFeedProps;
|
|
35
|
-
}) {
|
|
36
|
-
const { t } = useLingui();
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<>
|
|
40
|
-
{feedProps.items.length === 0 ? (
|
|
41
|
-
<p class="text-muted-foreground">
|
|
42
|
-
{t({
|
|
43
|
-
message: "No posts yet.",
|
|
44
|
-
comment: "@context: Empty state message on home page",
|
|
45
|
-
})}
|
|
46
|
-
</p>
|
|
47
|
-
) : (
|
|
48
|
-
<FeedComponent {...feedProps} />
|
|
49
|
-
)}
|
|
50
|
-
</>
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
22
|
homeRoutes.get("/", async (c) => {
|
|
55
23
|
const navData = await getNavigationData(c);
|
|
56
24
|
|
|
@@ -68,14 +36,12 @@ homeRoutes.get("/", async (c) => {
|
|
|
68
36
|
// Batch load media attachments
|
|
69
37
|
const postIds = displayPosts.map((p) => p.id);
|
|
70
38
|
const rawMediaMap = await c.var.services.media.getByPostIds(postIds);
|
|
71
|
-
const
|
|
72
|
-
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
73
|
-
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
39
|
+
const mediaCtx = createMediaContext(c);
|
|
74
40
|
const mediaMap = buildMediaMap(
|
|
75
41
|
rawMediaMap,
|
|
76
|
-
r2PublicUrl,
|
|
77
|
-
imageTransformUrl,
|
|
78
|
-
s3PublicUrl,
|
|
42
|
+
mediaCtx.r2PublicUrl,
|
|
43
|
+
mediaCtx.imageTransformUrl,
|
|
44
|
+
mediaCtx.s3PublicUrl,
|
|
79
45
|
);
|
|
80
46
|
|
|
81
47
|
// Get reply counts to identify thread roots
|
|
@@ -99,59 +65,59 @@ homeRoutes.get("/", async (c) => {
|
|
|
99
65
|
previewReplyIds.length > 0
|
|
100
66
|
? buildMediaMap(
|
|
101
67
|
await c.var.services.media.getByPostIds(previewReplyIds),
|
|
102
|
-
r2PublicUrl,
|
|
103
|
-
imageTransformUrl,
|
|
104
|
-
s3PublicUrl,
|
|
68
|
+
mediaCtx.r2PublicUrl,
|
|
69
|
+
mediaCtx.imageTransformUrl,
|
|
70
|
+
mediaCtx.s3PublicUrl,
|
|
105
71
|
)
|
|
106
72
|
: new Map();
|
|
107
73
|
|
|
108
|
-
// Assemble timeline items
|
|
109
|
-
const items:
|
|
110
|
-
const
|
|
111
|
-
...post,
|
|
112
|
-
|
|
113
|
-
|
|
74
|
+
// Assemble timeline items with View Models
|
|
75
|
+
const items: TimelineItemView[] = displayPosts.map((post) => {
|
|
76
|
+
const postView = toPostView(
|
|
77
|
+
{ ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
|
|
78
|
+
mediaCtx,
|
|
79
|
+
);
|
|
114
80
|
|
|
115
81
|
const replyCount = replyCounts.get(post.id) ?? 0;
|
|
116
82
|
const previewReplies = threadPreviews.get(post.id);
|
|
117
83
|
|
|
118
84
|
if (replyCount > 0 && previewReplies) {
|
|
119
85
|
return {
|
|
120
|
-
post:
|
|
86
|
+
post: postView,
|
|
121
87
|
threadPreview: {
|
|
122
|
-
replies:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
88
|
+
replies: toPostViews(
|
|
89
|
+
previewReplies.map((r) => ({
|
|
90
|
+
...r,
|
|
91
|
+
mediaAttachments: previewMediaMap.get(r.id) ?? [],
|
|
92
|
+
})),
|
|
93
|
+
mediaCtx,
|
|
94
|
+
),
|
|
126
95
|
totalReplyCount: replyCount,
|
|
127
96
|
},
|
|
128
97
|
};
|
|
129
98
|
}
|
|
130
99
|
|
|
131
|
-
return { post:
|
|
100
|
+
return { post: postView };
|
|
132
101
|
});
|
|
133
102
|
|
|
134
103
|
// Determine next cursor
|
|
135
104
|
const lastPost = displayPosts[displayPosts.length - 1];
|
|
136
105
|
const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
|
|
137
106
|
|
|
138
|
-
// Resolve
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
</SiteLayout>
|
|
155
|
-
</BaseLayout>,
|
|
156
|
-
);
|
|
107
|
+
// Resolve page component
|
|
108
|
+
const components = c.var.config.theme?.components;
|
|
109
|
+
const Page = components?.HomePage ?? DefaultHomePage;
|
|
110
|
+
|
|
111
|
+
return renderPublicPage(c, {
|
|
112
|
+
title: navData.siteName,
|
|
113
|
+
navData,
|
|
114
|
+
content: (
|
|
115
|
+
<Page
|
|
116
|
+
items={items}
|
|
117
|
+
hasMore={hasMore}
|
|
118
|
+
nextCursor={nextCursor}
|
|
119
|
+
theme={components}
|
|
120
|
+
/>
|
|
121
|
+
),
|
|
122
|
+
});
|
|
157
123
|
});
|
|
@@ -5,30 +5,17 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Hono } from "hono";
|
|
8
|
-
import type { Bindings
|
|
8
|
+
import type { Bindings } from "../../types.js";
|
|
9
9
|
import type { AppVariables } from "../../app.js";
|
|
10
|
-
import {
|
|
10
|
+
import { SinglePage as DefaultSinglePage } from "../../themes/minimal/pages/SinglePage.js";
|
|
11
11
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
12
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
13
|
+
import { createMediaContext, toPostViewFromPost } from "../../lib/view.js";
|
|
12
14
|
|
|
13
15
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
14
16
|
|
|
15
17
|
export const pageRoutes = new Hono<Env>();
|
|
16
18
|
|
|
17
|
-
function PageContent({ page }: { page: Post }) {
|
|
18
|
-
return (
|
|
19
|
-
<article class="h-entry">
|
|
20
|
-
{page.title && (
|
|
21
|
-
<h1 class="p-name text-3xl font-semibold mb-6">{page.title}</h1>
|
|
22
|
-
)}
|
|
23
|
-
|
|
24
|
-
<div
|
|
25
|
-
class="e-content prose"
|
|
26
|
-
dangerouslySetInnerHTML={{ __html: page.contentHtml || "" }}
|
|
27
|
-
/>
|
|
28
|
-
</article>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
19
|
// Catch-all for custom page paths
|
|
33
20
|
pageRoutes.get("/:path", async (c) => {
|
|
34
21
|
const path = c.req.param("path");
|
|
@@ -48,15 +35,17 @@ pageRoutes.get("/:path", async (c) => {
|
|
|
48
35
|
|
|
49
36
|
const navData = await getNavigationData(c);
|
|
50
37
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
38
|
+
// Transform to View Model
|
|
39
|
+
const mediaCtx = createMediaContext(c);
|
|
40
|
+
const pageView = toPostViewFromPost(page, mediaCtx);
|
|
41
|
+
|
|
42
|
+
const components = c.var.config.theme?.components;
|
|
43
|
+
const Page = components?.SinglePage ?? DefaultSinglePage;
|
|
44
|
+
|
|
45
|
+
return renderPublicPage(c, {
|
|
46
|
+
title: `${page.title} - ${navData.siteName}`,
|
|
47
|
+
description: page.content?.slice(0, 160),
|
|
48
|
+
navData,
|
|
49
|
+
content: <Page page={pageView} theme={components} />,
|
|
50
|
+
});
|
|
62
51
|
});
|
|
@@ -3,66 +3,19 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
-
import {
|
|
7
|
-
import type { Bindings, Post, MediaAttachment } from "../../types.js";
|
|
6
|
+
import type { Bindings } from "../../types.js";
|
|
8
7
|
import type { AppVariables } from "../../app.js";
|
|
9
|
-
import {
|
|
10
|
-
import { MediaGallery } from "../../theme/components/index.js";
|
|
8
|
+
import { PostPage as DefaultPostPage } from "../../themes/minimal/pages/PostPage.js";
|
|
11
9
|
import * as sqid from "../../lib/sqid.js";
|
|
12
|
-
import * as time from "../../lib/time.js";
|
|
13
|
-
import {
|
|
14
|
-
getMediaUrl,
|
|
15
|
-
getImageUrl,
|
|
16
|
-
getPublicUrlForProvider,
|
|
17
|
-
} from "../../lib/image.js";
|
|
18
10
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
11
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
12
|
+
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
13
|
+
import { createMediaContext, toPostView } from "../../lib/view.js";
|
|
19
14
|
|
|
20
15
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
21
16
|
|
|
22
17
|
export const postRoutes = new Hono<Env>();
|
|
23
18
|
|
|
24
|
-
function PostContent({
|
|
25
|
-
post,
|
|
26
|
-
mediaAttachments,
|
|
27
|
-
}: {
|
|
28
|
-
post: Post;
|
|
29
|
-
mediaAttachments: MediaAttachment[];
|
|
30
|
-
}) {
|
|
31
|
-
const { t } = useLingui();
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<article class="h-entry">
|
|
35
|
-
{post.title && (
|
|
36
|
-
<h1 class="p-name text-2xl font-semibold mb-4">{post.title}</h1>
|
|
37
|
-
)}
|
|
38
|
-
|
|
39
|
-
<div
|
|
40
|
-
class="e-content prose"
|
|
41
|
-
dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
|
|
42
|
-
/>
|
|
43
|
-
|
|
44
|
-
{mediaAttachments.length > 0 && (
|
|
45
|
-
<MediaGallery attachments={mediaAttachments} />
|
|
46
|
-
)}
|
|
47
|
-
|
|
48
|
-
<footer class="mt-6 pt-4 border-t text-sm text-muted-foreground">
|
|
49
|
-
<time
|
|
50
|
-
class="dt-published"
|
|
51
|
-
datetime={time.toISOString(post.publishedAt)}
|
|
52
|
-
>
|
|
53
|
-
{time.formatDate(post.publishedAt)}
|
|
54
|
-
</time>
|
|
55
|
-
<a href={`/p/${sqid.encode(post.id)}`} class="u-url ml-4">
|
|
56
|
-
{t({
|
|
57
|
-
message: "Permalink",
|
|
58
|
-
comment: "@context: Link to permanent URL of post",
|
|
59
|
-
})}
|
|
60
|
-
</a>
|
|
61
|
-
</footer>
|
|
62
|
-
</article>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
19
|
postRoutes.get("/:id", async (c) => {
|
|
67
20
|
const paramId = c.req.param("id");
|
|
68
21
|
|
|
@@ -87,43 +40,32 @@ postRoutes.get("/:id", async (c) => {
|
|
|
87
40
|
return c.notFound();
|
|
88
41
|
}
|
|
89
42
|
|
|
90
|
-
//
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
43
|
+
// Batch load media attachments
|
|
44
|
+
const rawMediaMap = await c.var.services.media.getByPostIds([post.id]);
|
|
45
|
+
const mediaCtx = createMediaContext(c);
|
|
46
|
+
const mediaMap = buildMediaMap(
|
|
47
|
+
rawMediaMap,
|
|
48
|
+
mediaCtx.r2PublicUrl,
|
|
49
|
+
mediaCtx.imageTransformUrl,
|
|
50
|
+
mediaCtx.s3PublicUrl,
|
|
51
|
+
);
|
|
95
52
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
);
|
|
102
|
-
return {
|
|
103
|
-
id: m.id,
|
|
104
|
-
url: getMediaUrl(m.id, m.storageKey, publicUrl),
|
|
105
|
-
previewUrl: getImageUrl(
|
|
106
|
-
getMediaUrl(m.id, m.storageKey, publicUrl),
|
|
107
|
-
imageTransformUrl,
|
|
108
|
-
{ width: 400, quality: 80, format: "auto", fit: "cover" },
|
|
109
|
-
),
|
|
110
|
-
alt: m.alt,
|
|
111
|
-
blurhash: m.blurhash,
|
|
112
|
-
width: m.width,
|
|
113
|
-
height: m.height,
|
|
114
|
-
position: m.position,
|
|
115
|
-
mimeType: m.mimeType,
|
|
116
|
-
};
|
|
117
|
-
});
|
|
53
|
+
// Transform to View Model
|
|
54
|
+
const postView = toPostView(
|
|
55
|
+
{ ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
|
|
56
|
+
mediaCtx,
|
|
57
|
+
);
|
|
118
58
|
|
|
119
59
|
const navData = await getNavigationData(c);
|
|
120
60
|
const title = post.title || navData.siteName;
|
|
121
61
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
62
|
+
const components = c.var.config.theme?.components;
|
|
63
|
+
const Page = components?.PostPage ?? DefaultPostPage;
|
|
64
|
+
|
|
65
|
+
return renderPublicPage(c, {
|
|
66
|
+
title,
|
|
67
|
+
description: post.content?.slice(0, 160),
|
|
68
|
+
navData,
|
|
69
|
+
content: <Page post={postView} theme={components} />,
|
|
70
|
+
});
|
|
129
71
|
});
|
|
@@ -3,15 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
-
import {
|
|
7
|
-
import type { Bindings } from "../../types.js";
|
|
6
|
+
import type { Bindings, SearchResult } from "../../types.js";
|
|
8
7
|
import type { AppVariables } from "../../app.js";
|
|
9
|
-
import
|
|
10
|
-
import { BaseLayout, SiteLayout } from "../../theme/layouts/index.js";
|
|
11
|
-
import { PagePagination } from "../../theme/components/index.js";
|
|
12
|
-
import * as sqid from "../../lib/sqid.js";
|
|
13
|
-
import * as time from "../../lib/time.js";
|
|
8
|
+
import { SearchPage as DefaultSearchPage } from "../../themes/minimal/pages/SearchPage.js";
|
|
14
9
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
10
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
11
|
+
import { createMediaContext, toSearchResultViews } from "../../lib/view.js";
|
|
15
12
|
|
|
16
13
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
17
14
|
|
|
@@ -19,128 +16,6 @@ const PAGE_SIZE = 10;
|
|
|
19
16
|
|
|
20
17
|
export const searchRoutes = new Hono<Env>();
|
|
21
18
|
|
|
22
|
-
function SearchContent({
|
|
23
|
-
query,
|
|
24
|
-
results,
|
|
25
|
-
error,
|
|
26
|
-
hasMore,
|
|
27
|
-
page,
|
|
28
|
-
}: {
|
|
29
|
-
query: string;
|
|
30
|
-
results: SearchResult[];
|
|
31
|
-
error: string | null;
|
|
32
|
-
hasMore: boolean;
|
|
33
|
-
page: number;
|
|
34
|
-
}) {
|
|
35
|
-
const { t } = useLingui();
|
|
36
|
-
const searchTitle = t({
|
|
37
|
-
message: "Search",
|
|
38
|
-
comment: "@context: Search page title",
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<div>
|
|
43
|
-
<h1 class="text-2xl font-semibold mb-6">{searchTitle}</h1>
|
|
44
|
-
|
|
45
|
-
{/* Search form */}
|
|
46
|
-
<form method="get" action="/search" class="mb-8">
|
|
47
|
-
<div class="flex gap-2">
|
|
48
|
-
<input
|
|
49
|
-
type="search"
|
|
50
|
-
name="q"
|
|
51
|
-
class="input flex-1"
|
|
52
|
-
placeholder={t({
|
|
53
|
-
message: "Search posts...",
|
|
54
|
-
comment: "@context: Search input placeholder",
|
|
55
|
-
})}
|
|
56
|
-
value={query}
|
|
57
|
-
autofocus
|
|
58
|
-
/>
|
|
59
|
-
<button type="submit" class="btn">
|
|
60
|
-
{t({
|
|
61
|
-
message: "Search",
|
|
62
|
-
comment: "@context: Search submit button",
|
|
63
|
-
})}
|
|
64
|
-
</button>
|
|
65
|
-
</div>
|
|
66
|
-
</form>
|
|
67
|
-
|
|
68
|
-
{/* Error */}
|
|
69
|
-
{error && (
|
|
70
|
-
<div class="alert-destructive mb-6">
|
|
71
|
-
<h2>{error}</h2>
|
|
72
|
-
</div>
|
|
73
|
-
)}
|
|
74
|
-
|
|
75
|
-
{/* Results */}
|
|
76
|
-
{query && !error && (
|
|
77
|
-
<div>
|
|
78
|
-
<p class="text-sm text-muted-foreground mb-4">
|
|
79
|
-
{results.length === 0
|
|
80
|
-
? t({
|
|
81
|
-
message: "No results found.",
|
|
82
|
-
comment: "@context: Search empty results",
|
|
83
|
-
})
|
|
84
|
-
: results.length === 1
|
|
85
|
-
? t({
|
|
86
|
-
message: "Found 1 result",
|
|
87
|
-
comment: "@context: Search results count - single",
|
|
88
|
-
})
|
|
89
|
-
: t({
|
|
90
|
-
message: "Found {count} results",
|
|
91
|
-
comment: "@context: Search results count - multiple",
|
|
92
|
-
values: { count: String(results.length) },
|
|
93
|
-
})}
|
|
94
|
-
</p>
|
|
95
|
-
|
|
96
|
-
{results.length > 0 && (
|
|
97
|
-
<>
|
|
98
|
-
<div class="flex flex-col gap-4">
|
|
99
|
-
{results.map((result) => (
|
|
100
|
-
<article
|
|
101
|
-
key={result.post.id}
|
|
102
|
-
class="p-4 rounded-lg border hover:border-primary"
|
|
103
|
-
>
|
|
104
|
-
<a href={`/p/${sqid.encode(result.post.id)}`} class="block">
|
|
105
|
-
<h2 class="font-medium hover:underline">
|
|
106
|
-
{result.post.title ||
|
|
107
|
-
result.post.content?.slice(0, 60) ||
|
|
108
|
-
`Post #${result.post.id}`}
|
|
109
|
-
</h2>
|
|
110
|
-
|
|
111
|
-
{result.snippet && (
|
|
112
|
-
<p
|
|
113
|
-
class="text-sm text-muted-foreground mt-2 line-clamp-2"
|
|
114
|
-
dangerouslySetInnerHTML={{ __html: result.snippet }}
|
|
115
|
-
/>
|
|
116
|
-
)}
|
|
117
|
-
|
|
118
|
-
<footer class="flex items-center gap-2 mt-2 text-xs text-muted-foreground">
|
|
119
|
-
<span class="badge-outline">{result.post.type}</span>
|
|
120
|
-
<time
|
|
121
|
-
datetime={time.toISOString(result.post.publishedAt)}
|
|
122
|
-
>
|
|
123
|
-
{time.formatDate(result.post.publishedAt)}
|
|
124
|
-
</time>
|
|
125
|
-
</footer>
|
|
126
|
-
</a>
|
|
127
|
-
</article>
|
|
128
|
-
))}
|
|
129
|
-
</div>
|
|
130
|
-
|
|
131
|
-
<PagePagination
|
|
132
|
-
baseUrl={`/search?q=${encodeURIComponent(query)}`}
|
|
133
|
-
currentPage={page}
|
|
134
|
-
hasMore={hasMore}
|
|
135
|
-
/>
|
|
136
|
-
</>
|
|
137
|
-
)}
|
|
138
|
-
</div>
|
|
139
|
-
)}
|
|
140
|
-
</div>
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
19
|
searchRoutes.get("/", async (c) => {
|
|
145
20
|
const query = c.req.query("q") || "";
|
|
146
21
|
const pageParam = c.req.query("page");
|
|
@@ -149,8 +24,8 @@ searchRoutes.get("/", async (c) => {
|
|
|
149
24
|
const navData = await getNavigationData(c);
|
|
150
25
|
|
|
151
26
|
// Only search if there's a query
|
|
152
|
-
let results:
|
|
153
|
-
let error: string |
|
|
27
|
+
let results: SearchResult[] = [];
|
|
28
|
+
let error: string | undefined;
|
|
154
29
|
let hasMore = false;
|
|
155
30
|
|
|
156
31
|
if (query.trim()) {
|
|
@@ -173,24 +48,27 @@ searchRoutes.get("/", async (c) => {
|
|
|
173
48
|
}
|
|
174
49
|
}
|
|
175
50
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
51
|
+
// Transform to View Models
|
|
52
|
+
const mediaCtx = createMediaContext(c);
|
|
53
|
+
const resultViews = toSearchResultViews(results, mediaCtx);
|
|
54
|
+
|
|
55
|
+
const components = c.var.config.theme?.components;
|
|
56
|
+
const Page = components?.SearchPage ?? DefaultSearchPage;
|
|
57
|
+
|
|
58
|
+
return renderPublicPage(c, {
|
|
59
|
+
title: query
|
|
60
|
+
? `Search: ${query} - ${navData.siteName}`
|
|
61
|
+
: `Search - ${navData.siteName}`,
|
|
62
|
+
navData,
|
|
63
|
+
content: (
|
|
64
|
+
<Page
|
|
65
|
+
query={query}
|
|
66
|
+
results={resultViews}
|
|
67
|
+
error={error}
|
|
68
|
+
hasMore={hasMore}
|
|
69
|
+
page={page}
|
|
70
|
+
theme={components}
|
|
71
|
+
/>
|
|
72
|
+
),
|
|
73
|
+
});
|
|
196
74
|
});
|
package/src/services/search.ts
CHANGED
|
@@ -4,15 +4,9 @@
|
|
|
4
4
|
* Full-text search using FTS5
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { Post, Visibility } from "../types.js";
|
|
7
|
+
import type { Post, Visibility, SearchResult } from "../types.js";
|
|
8
8
|
|
|
9
|
-
export
|
|
10
|
-
post: Post;
|
|
11
|
-
/** FTS5 rank score (lower is better) */
|
|
12
|
-
rank: number;
|
|
13
|
-
/** Highlighted snippet from content */
|
|
14
|
-
snippet?: string;
|
|
15
|
-
}
|
|
9
|
+
export type { SearchResult };
|
|
16
10
|
|
|
17
11
|
export interface SearchOptions {
|
|
18
12
|
/** Limit number of results */
|
|
@@ -124,60 +124,6 @@
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
/* Timeline cards */
|
|
128
|
-
@layer components {
|
|
129
|
-
.timeline-card {
|
|
130
|
-
@apply rounded-lg border p-4;
|
|
131
|
-
border-color: var(--color-border);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
.timeline-card-link {
|
|
135
|
-
border-left-width: 4px;
|
|
136
|
-
border-left-color: var(--color-primary);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
.timeline-card-quote {
|
|
140
|
-
border-left-width: 4px;
|
|
141
|
-
border-left-color: var(--color-muted-foreground);
|
|
142
|
-
background-color: var(--color-muted);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
.timeline-card-image {
|
|
146
|
-
@apply p-0 overflow-hidden;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
.timeline-card-image-gallery {
|
|
150
|
-
/* Remove default margin from MediaGallery inside image cards */
|
|
151
|
-
> div {
|
|
152
|
-
@apply mt-0;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
.timeline-card-compact {
|
|
157
|
-
@apply p-3 text-sm;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
.timeline-thread-replies {
|
|
161
|
-
@apply relative ml-5;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.timeline-thread-reply {
|
|
165
|
-
@apply relative pl-5 mt-3;
|
|
166
|
-
|
|
167
|
-
&::after {
|
|
168
|
-
content: "";
|
|
169
|
-
@apply absolute left-0 top-0 bottom-0 w-px;
|
|
170
|
-
background-color: var(--color-border);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
&::before {
|
|
174
|
-
content: "";
|
|
175
|
-
@apply absolute left-0 top-4 h-px w-4;
|
|
176
|
-
background-color: var(--color-border);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
127
|
@keyframes toast-in {
|
|
182
128
|
from {
|
|
183
129
|
opacity: 0;
|