@jant/core 0.3.21 → 0.3.22
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 +1 -1
- package/dist/index.js +8 -0
- 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 +20 -16
- 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 +1 -1
- package/dist/theme/components/timeline/ArticleCard.js +7 -11
- package/dist/theme/components/timeline/ImageCard.js +10 -13
- package/dist/theme/components/timeline/LinkCard.js +4 -7
- package/dist/theme/components/timeline/NoteCard.js +5 -8
- package/dist/theme/components/timeline/QuoteCard.js +3 -6
- package/dist/theme/components/timeline/ThreadPreview.js +9 -10
- package/dist/theme/components/timeline/TimelineFeed.js +8 -5
- package/dist/theme/components/timeline/TimelineItem.js +22 -2
- package/dist/theme/components/timeline/index.js +1 -1
- package/dist/theme/index.js +6 -3
- package/dist/theme/layouts/SiteLayout.js +10 -39
- package/dist/theme/pages/ArchivePage.js +157 -0
- package/dist/theme/pages/CollectionPage.js +63 -0
- package/dist/theme/pages/HomePage.js +26 -0
- package/dist/theme/pages/PostPage.js +48 -0
- package/dist/theme/pages/SearchPage.js +120 -0
- package/dist/theme/pages/SinglePage.js +23 -0
- package/dist/theme/pages/index.js +11 -0
- package/package.json +2 -1
- package/src/app.tsx +1 -1
- package/src/i18n/locales/en.po +31 -31
- package/src/i18n/locales/zh-Hans.po +31 -31
- package/src/i18n/locales/zh-Hant.po +31 -31
- package/src/index.ts +51 -2
- package/src/lib/__tests__/theme-components.test.ts +33 -14
- package/src/lib/__tests__/view.test.ts +375 -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 +32 -25
- 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/theme/components/MediaGallery.tsx +12 -12
- package/src/theme/components/index.ts +1 -0
- package/src/theme/components/timeline/ArticleCard.tsx +7 -19
- package/src/theme/components/timeline/ImageCard.tsx +10 -20
- package/src/theme/components/timeline/LinkCard.tsx +4 -11
- package/src/theme/components/timeline/NoteCard.tsx +5 -12
- package/src/theme/components/timeline/QuoteCard.tsx +3 -10
- package/src/theme/components/timeline/ThreadPreview.tsx +5 -5
- package/src/theme/components/timeline/TimelineFeed.tsx +7 -3
- package/src/theme/components/timeline/TimelineItem.tsx +43 -4
- package/src/theme/components/timeline/index.ts +1 -1
- package/src/theme/index.ts +7 -3
- package/src/theme/layouts/SiteLayout.tsx +25 -77
- package/src/theme/layouts/index.ts +2 -1
- package/src/theme/pages/ArchivePage.tsx +160 -0
- package/src/theme/pages/CollectionPage.tsx +60 -0
- package/src/theme/pages/HomePage.tsx +42 -0
- package/src/theme/pages/PostPage.tsx +44 -0
- package/src/theme/pages/SearchPage.tsx +128 -0
- package/src/theme/pages/SinglePage.tsx +24 -0
- package/src/theme/pages/index.ts +13 -0
- package/src/types.ts +262 -38
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Archive Page Component
|
|
3
|
+
*
|
|
4
|
+
* Renders posts grouped by year-month with type filter and cursor pagination.
|
|
5
|
+
* Theme authors can replace this entirely via ThemeComponents.ArchivePage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FC } from "hono/jsx";
|
|
9
|
+
import { useLingui } from "@lingui/react/macro";
|
|
10
|
+
import type { ArchivePageProps } from "../../types.js";
|
|
11
|
+
import { POST_TYPES } from "../../types.js";
|
|
12
|
+
import { Pagination as DefaultPagination } from "../components/Pagination.js";
|
|
13
|
+
|
|
14
|
+
function getTypeLabel(type: string): string {
|
|
15
|
+
const { t } = useLingui();
|
|
16
|
+
const labels: Record<string, string> = {
|
|
17
|
+
note: t({ message: "Note", comment: "@context: Post type label - note" }),
|
|
18
|
+
article: t({
|
|
19
|
+
message: "Article",
|
|
20
|
+
comment: "@context: Post type label - article",
|
|
21
|
+
}),
|
|
22
|
+
link: t({ message: "Link", comment: "@context: Post type label - link" }),
|
|
23
|
+
quote: t({
|
|
24
|
+
message: "Quote",
|
|
25
|
+
comment: "@context: Post type label - quote",
|
|
26
|
+
}),
|
|
27
|
+
image: t({
|
|
28
|
+
message: "Image",
|
|
29
|
+
comment: "@context: Post type label - image",
|
|
30
|
+
}),
|
|
31
|
+
page: t({ message: "Page", comment: "@context: Post type label - page" }),
|
|
32
|
+
};
|
|
33
|
+
return labels[type] ?? type;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getTypeLabelPlural(type: string): string {
|
|
37
|
+
const { t } = useLingui();
|
|
38
|
+
const labels: Record<string, string> = {
|
|
39
|
+
note: t({
|
|
40
|
+
message: "Notes",
|
|
41
|
+
comment: "@context: Post type label plural - notes",
|
|
42
|
+
}),
|
|
43
|
+
article: t({
|
|
44
|
+
message: "Articles",
|
|
45
|
+
comment: "@context: Post type label plural - articles",
|
|
46
|
+
}),
|
|
47
|
+
link: t({
|
|
48
|
+
message: "Links",
|
|
49
|
+
comment: "@context: Post type label plural - links",
|
|
50
|
+
}),
|
|
51
|
+
quote: t({
|
|
52
|
+
message: "Quotes",
|
|
53
|
+
comment: "@context: Post type label plural - quotes",
|
|
54
|
+
}),
|
|
55
|
+
image: t({
|
|
56
|
+
message: "Images",
|
|
57
|
+
comment: "@context: Post type label plural - images",
|
|
58
|
+
}),
|
|
59
|
+
page: t({
|
|
60
|
+
message: "Pages",
|
|
61
|
+
comment: "@context: Post type label plural - pages",
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
return labels[type] ?? `${type}s`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const ArchivePage: FC<ArchivePageProps> = ({
|
|
68
|
+
groups,
|
|
69
|
+
hasMore,
|
|
70
|
+
nextCursor,
|
|
71
|
+
type,
|
|
72
|
+
theme,
|
|
73
|
+
}) => {
|
|
74
|
+
const { t } = useLingui();
|
|
75
|
+
const title = type
|
|
76
|
+
? getTypeLabelPlural(type)
|
|
77
|
+
: t({ message: "Archive", comment: "@context: Archive page title" });
|
|
78
|
+
|
|
79
|
+
const PaginationComponent = theme?.Pagination ?? DefaultPagination;
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div>
|
|
83
|
+
<header class="mb-8">
|
|
84
|
+
<h1 class="text-2xl font-semibold">{title}</h1>
|
|
85
|
+
|
|
86
|
+
{/* Type filter */}
|
|
87
|
+
<nav class="flex flex-wrap gap-2 mt-4">
|
|
88
|
+
<a
|
|
89
|
+
href="/archive"
|
|
90
|
+
class={`badge ${!type ? "badge-primary" : "badge-outline"}`}
|
|
91
|
+
>
|
|
92
|
+
{t({
|
|
93
|
+
message: "All",
|
|
94
|
+
comment: "@context: Archive filter - all types",
|
|
95
|
+
})}
|
|
96
|
+
</a>
|
|
97
|
+
{POST_TYPES.filter((t) => t !== "page").map((typeKey) => (
|
|
98
|
+
<a
|
|
99
|
+
key={typeKey}
|
|
100
|
+
href={`/archive?type=${typeKey}`}
|
|
101
|
+
class={`badge ${type === typeKey ? "badge-primary" : "badge-outline"}`}
|
|
102
|
+
>
|
|
103
|
+
{getTypeLabelPlural(typeKey)}
|
|
104
|
+
</a>
|
|
105
|
+
))}
|
|
106
|
+
</nav>
|
|
107
|
+
</header>
|
|
108
|
+
|
|
109
|
+
<main>
|
|
110
|
+
{groups.length === 0 ? (
|
|
111
|
+
<p class="text-muted-foreground">
|
|
112
|
+
{t({
|
|
113
|
+
message: "No posts found.",
|
|
114
|
+
comment: "@context: Archive empty state",
|
|
115
|
+
})}
|
|
116
|
+
</p>
|
|
117
|
+
) : (
|
|
118
|
+
groups.map((group) => (
|
|
119
|
+
<section key={`${group.year}-${group.month}`} class="mb-8">
|
|
120
|
+
<h2 class="text-lg font-medium mb-4 text-muted-foreground">
|
|
121
|
+
{group.label}
|
|
122
|
+
</h2>
|
|
123
|
+
<div class="flex flex-col gap-3">
|
|
124
|
+
{group.posts.map((post) => (
|
|
125
|
+
<article key={post.id} class="flex items-baseline gap-4">
|
|
126
|
+
<time
|
|
127
|
+
class="text-sm text-muted-foreground w-12 shrink-0"
|
|
128
|
+
datetime={post.publishedAt}
|
|
129
|
+
>
|
|
130
|
+
{new Date(post.publishedAt).getUTCDate()}
|
|
131
|
+
</time>
|
|
132
|
+
<div class="flex-1 min-w-0">
|
|
133
|
+
<a href={post.permalink} class="hover:underline">
|
|
134
|
+
{post.title ||
|
|
135
|
+
post.content?.slice(0, 80) ||
|
|
136
|
+
`Post #${post.id}`}
|
|
137
|
+
</a>
|
|
138
|
+
{!type && (
|
|
139
|
+
<span class="ml-2 badge-outline text-xs">
|
|
140
|
+
{getTypeLabel(post.type)}
|
|
141
|
+
</span>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
</article>
|
|
145
|
+
))}
|
|
146
|
+
</div>
|
|
147
|
+
</section>
|
|
148
|
+
))
|
|
149
|
+
)}
|
|
150
|
+
</main>
|
|
151
|
+
|
|
152
|
+
{/* Pagination */}
|
|
153
|
+
<PaginationComponent
|
|
154
|
+
baseUrl={type ? `/archive?type=${type}` : "/archive"}
|
|
155
|
+
hasMore={hasMore}
|
|
156
|
+
nextCursor={nextCursor}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Collection Page Component
|
|
3
|
+
*
|
|
4
|
+
* Renders a collection with its posts.
|
|
5
|
+
* Theme authors can replace this entirely via ThemeComponents.CollectionPage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FC } from "hono/jsx";
|
|
9
|
+
import { useLingui } from "@lingui/react/macro";
|
|
10
|
+
import type { CollectionPageProps } from "../../types.js";
|
|
11
|
+
|
|
12
|
+
export const CollectionPage: FC<CollectionPageProps> = ({
|
|
13
|
+
collection,
|
|
14
|
+
posts,
|
|
15
|
+
}) => {
|
|
16
|
+
const { t } = useLingui();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div>
|
|
20
|
+
<header class="mb-8">
|
|
21
|
+
<h1 class="text-2xl font-semibold">{collection.title}</h1>
|
|
22
|
+
{collection.description && (
|
|
23
|
+
<p class="text-muted-foreground mt-2">{collection.description}</p>
|
|
24
|
+
)}
|
|
25
|
+
</header>
|
|
26
|
+
|
|
27
|
+
<main class="flex flex-col gap-6">
|
|
28
|
+
{posts.length === 0 ? (
|
|
29
|
+
<p class="text-muted-foreground">
|
|
30
|
+
{t({
|
|
31
|
+
message: "No posts in this collection.",
|
|
32
|
+
comment: "@context: Empty state message",
|
|
33
|
+
})}
|
|
34
|
+
</p>
|
|
35
|
+
) : (
|
|
36
|
+
posts.map((post) => (
|
|
37
|
+
<article key={post.id} class="h-entry">
|
|
38
|
+
{post.title && (
|
|
39
|
+
<h2 class="p-name text-lg font-medium mb-2">
|
|
40
|
+
<a href={post.permalink} class="u-url hover:underline">
|
|
41
|
+
{post.title}
|
|
42
|
+
</a>
|
|
43
|
+
</h2>
|
|
44
|
+
)}
|
|
45
|
+
<div
|
|
46
|
+
class="e-content prose prose-sm"
|
|
47
|
+
dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
|
|
48
|
+
/>
|
|
49
|
+
<footer class="mt-2 text-sm text-muted-foreground">
|
|
50
|
+
<time class="dt-published" datetime={post.publishedAt}>
|
|
51
|
+
{post.publishedAtFormatted}
|
|
52
|
+
</time>
|
|
53
|
+
</footer>
|
|
54
|
+
</article>
|
|
55
|
+
))
|
|
56
|
+
)}
|
|
57
|
+
</main>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Home Page Component
|
|
3
|
+
*
|
|
4
|
+
* Renders the timeline feed with thread previews.
|
|
5
|
+
* Theme authors can replace this entirely via ThemeComponents.HomePage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FC } from "hono/jsx";
|
|
9
|
+
import { useLingui } from "@lingui/react/macro";
|
|
10
|
+
import type { HomePageProps } from "../../types.js";
|
|
11
|
+
import { TimelineFeed as DefaultTimelineFeed } from "../components/timeline/TimelineFeed.js";
|
|
12
|
+
|
|
13
|
+
export const HomePage: FC<HomePageProps> = ({
|
|
14
|
+
items,
|
|
15
|
+
hasMore,
|
|
16
|
+
nextCursor,
|
|
17
|
+
theme,
|
|
18
|
+
}) => {
|
|
19
|
+
const { t } = useLingui();
|
|
20
|
+
|
|
21
|
+
const Feed = theme?.TimelineFeed ?? DefaultTimelineFeed;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
{items.length === 0 ? (
|
|
26
|
+
<p class="text-muted-foreground">
|
|
27
|
+
{t({
|
|
28
|
+
message: "No posts yet.",
|
|
29
|
+
comment: "@context: Empty state message on home page",
|
|
30
|
+
})}
|
|
31
|
+
</p>
|
|
32
|
+
) : (
|
|
33
|
+
<Feed
|
|
34
|
+
items={items}
|
|
35
|
+
hasMore={hasMore}
|
|
36
|
+
nextCursor={nextCursor}
|
|
37
|
+
theme={theme}
|
|
38
|
+
/>
|
|
39
|
+
)}
|
|
40
|
+
</>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Post Page Component
|
|
3
|
+
*
|
|
4
|
+
* Renders a single post with media gallery.
|
|
5
|
+
* Theme authors can replace this entirely via ThemeComponents.PostPage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FC } from "hono/jsx";
|
|
9
|
+
import { useLingui } from "@lingui/react/macro";
|
|
10
|
+
import type { PostPageProps } from "../../types.js";
|
|
11
|
+
import { MediaGallery as DefaultMediaGallery } from "../components/MediaGallery.js";
|
|
12
|
+
|
|
13
|
+
export const PostPage: FC<PostPageProps> = ({ post, theme }) => {
|
|
14
|
+
const { t } = useLingui();
|
|
15
|
+
|
|
16
|
+
const Gallery = theme?.MediaGallery ?? DefaultMediaGallery;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<article class="h-entry">
|
|
20
|
+
{post.title && (
|
|
21
|
+
<h1 class="p-name text-2xl font-semibold mb-4">{post.title}</h1>
|
|
22
|
+
)}
|
|
23
|
+
|
|
24
|
+
<div
|
|
25
|
+
class="e-content prose"
|
|
26
|
+
dangerouslySetInnerHTML={{ __html: post.contentHtml || "" }}
|
|
27
|
+
/>
|
|
28
|
+
|
|
29
|
+
{post.media.length > 0 && <Gallery attachments={post.media} />}
|
|
30
|
+
|
|
31
|
+
<footer class="mt-6 pt-4 border-t text-sm text-muted-foreground">
|
|
32
|
+
<time class="dt-published" datetime={post.publishedAt}>
|
|
33
|
+
{post.publishedAtFormatted}
|
|
34
|
+
</time>
|
|
35
|
+
<a href={post.permalink} class="u-url ml-4">
|
|
36
|
+
{t({
|
|
37
|
+
message: "Permalink",
|
|
38
|
+
comment: "@context: Link to permanent URL of post",
|
|
39
|
+
})}
|
|
40
|
+
</a>
|
|
41
|
+
</footer>
|
|
42
|
+
</article>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Search Page Component
|
|
3
|
+
*
|
|
4
|
+
* Renders search form and results with page-based pagination.
|
|
5
|
+
* Theme authors can replace this entirely via ThemeComponents.SearchPage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FC } from "hono/jsx";
|
|
9
|
+
import { useLingui } from "@lingui/react/macro";
|
|
10
|
+
import type { SearchPageProps } from "../../types.js";
|
|
11
|
+
import { PagePagination as DefaultPagePagination } from "../components/Pagination.js";
|
|
12
|
+
|
|
13
|
+
export const SearchPage: FC<SearchPageProps> = ({
|
|
14
|
+
query,
|
|
15
|
+
results,
|
|
16
|
+
error,
|
|
17
|
+
hasMore,
|
|
18
|
+
page,
|
|
19
|
+
theme,
|
|
20
|
+
}) => {
|
|
21
|
+
const { t } = useLingui();
|
|
22
|
+
const searchTitle = t({
|
|
23
|
+
message: "Search",
|
|
24
|
+
comment: "@context: Search page title",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const PaginationComponent = theme?.PagePagination ?? DefaultPagePagination;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div>
|
|
31
|
+
<h1 class="text-2xl font-semibold mb-6">{searchTitle}</h1>
|
|
32
|
+
|
|
33
|
+
{/* Search form */}
|
|
34
|
+
<form method="get" action="/search" class="mb-8">
|
|
35
|
+
<div class="flex gap-2">
|
|
36
|
+
<input
|
|
37
|
+
type="search"
|
|
38
|
+
name="q"
|
|
39
|
+
class="input flex-1"
|
|
40
|
+
placeholder={t({
|
|
41
|
+
message: "Search posts...",
|
|
42
|
+
comment: "@context: Search input placeholder",
|
|
43
|
+
})}
|
|
44
|
+
value={query}
|
|
45
|
+
autofocus
|
|
46
|
+
/>
|
|
47
|
+
<button type="submit" class="btn">
|
|
48
|
+
{t({
|
|
49
|
+
message: "Search",
|
|
50
|
+
comment: "@context: Search submit button",
|
|
51
|
+
})}
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
</form>
|
|
55
|
+
|
|
56
|
+
{/* Error */}
|
|
57
|
+
{error && (
|
|
58
|
+
<div class="alert-destructive mb-6">
|
|
59
|
+
<h2>{error}</h2>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
{/* Results */}
|
|
64
|
+
{query && !error && (
|
|
65
|
+
<div>
|
|
66
|
+
<p class="text-sm text-muted-foreground mb-4">
|
|
67
|
+
{results.length === 0
|
|
68
|
+
? t({
|
|
69
|
+
message: "No results found.",
|
|
70
|
+
comment: "@context: Search empty results",
|
|
71
|
+
})
|
|
72
|
+
: results.length === 1
|
|
73
|
+
? t({
|
|
74
|
+
message: "Found 1 result",
|
|
75
|
+
comment: "@context: Search results count - single",
|
|
76
|
+
})
|
|
77
|
+
: t({
|
|
78
|
+
message: "Found {count} results",
|
|
79
|
+
comment: "@context: Search results count - multiple",
|
|
80
|
+
values: { count: String(results.length) },
|
|
81
|
+
})}
|
|
82
|
+
</p>
|
|
83
|
+
|
|
84
|
+
{results.length > 0 && (
|
|
85
|
+
<>
|
|
86
|
+
<div class="flex flex-col gap-4">
|
|
87
|
+
{results.map((result) => (
|
|
88
|
+
<article
|
|
89
|
+
key={result.post.id}
|
|
90
|
+
class="p-4 rounded-lg border hover:border-primary"
|
|
91
|
+
>
|
|
92
|
+
<a href={result.post.permalink} class="block">
|
|
93
|
+
<h2 class="font-medium hover:underline">
|
|
94
|
+
{result.post.title ||
|
|
95
|
+
result.post.content?.slice(0, 60) ||
|
|
96
|
+
`Post #${result.post.id}`}
|
|
97
|
+
</h2>
|
|
98
|
+
|
|
99
|
+
{result.snippet && (
|
|
100
|
+
<p
|
|
101
|
+
class="text-sm text-muted-foreground mt-2 line-clamp-2"
|
|
102
|
+
dangerouslySetInnerHTML={{ __html: result.snippet }}
|
|
103
|
+
/>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
<footer class="flex items-center gap-2 mt-2 text-xs text-muted-foreground">
|
|
107
|
+
<span class="badge-outline">{result.post.type}</span>
|
|
108
|
+
<time datetime={result.post.publishedAt}>
|
|
109
|
+
{result.post.publishedAtFormatted}
|
|
110
|
+
</time>
|
|
111
|
+
</footer>
|
|
112
|
+
</a>
|
|
113
|
+
</article>
|
|
114
|
+
))}
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<PaginationComponent
|
|
118
|
+
baseUrl={`/search?q=${encodeURIComponent(query)}`}
|
|
119
|
+
currentPage={page}
|
|
120
|
+
hasMore={hasMore}
|
|
121
|
+
/>
|
|
122
|
+
</>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Single Page Component
|
|
3
|
+
*
|
|
4
|
+
* Renders a custom page (type "page").
|
|
5
|
+
* Theme authors can replace this entirely via ThemeComponents.SinglePage.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FC } from "hono/jsx";
|
|
9
|
+
import type { SinglePageProps } from "../../types.js";
|
|
10
|
+
|
|
11
|
+
export const SinglePage: FC<SinglePageProps> = ({ page }) => {
|
|
12
|
+
return (
|
|
13
|
+
<article class="h-entry">
|
|
14
|
+
{page.title && (
|
|
15
|
+
<h1 class="p-name text-3xl font-semibold mb-6">{page.title}</h1>
|
|
16
|
+
)}
|
|
17
|
+
|
|
18
|
+
<div
|
|
19
|
+
class="e-content prose"
|
|
20
|
+
dangerouslySetInnerHTML={{ __html: page.contentHtml || "" }}
|
|
21
|
+
/>
|
|
22
|
+
</article>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Page Components
|
|
3
|
+
*
|
|
4
|
+
* These are the built-in page components that render each public page.
|
|
5
|
+
* Theme authors can import these to wrap/extend them.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { HomePage } from "./HomePage.js";
|
|
9
|
+
export { PostPage } from "./PostPage.js";
|
|
10
|
+
export { SinglePage } from "./SinglePage.js";
|
|
11
|
+
export { ArchivePage } from "./ArchivePage.js";
|
|
12
|
+
export { SearchPage } from "./SearchPage.js";
|
|
13
|
+
export { CollectionPage } from "./CollectionPage.js";
|