@jant/core 0.3.23 → 0.3.24
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 +4 -5
- package/dist/db/schema.js +72 -47
- 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 -3
- package/dist/lib/constants.js +1 -4
- package/dist/lib/excerpt.js +76 -0
- package/dist/lib/feed.js +18 -7
- package/dist/lib/navigation.js +4 -5
- package/dist/lib/render.js +1 -1
- package/dist/lib/schemas.js +80 -38
- package/dist/lib/theme-components.js +8 -11
- package/dist/lib/time.js +56 -1
- package/dist/lib/timeline.js +119 -0
- package/dist/lib/view.js +61 -72
- package/dist/routes/api/posts.js +29 -35
- package/dist/routes/api/search.js +5 -6
- package/dist/routes/api/upload.js +13 -13
- package/dist/routes/dash/collections.js +22 -40
- package/dist/routes/dash/index.js +2 -2
- package/dist/routes/dash/navigation.js +25 -24
- package/dist/routes/dash/pages.js +42 -57
- package/dist/routes/dash/posts.js +27 -35
- package/dist/routes/feed/rss.js +2 -4
- package/dist/routes/feed/sitemap.js +10 -7
- package/dist/routes/pages/archive.js +12 -11
- package/dist/routes/pages/collection.js +11 -5
- package/dist/routes/pages/home.js +53 -61
- package/dist/routes/pages/page.js +60 -29
- package/dist/routes/pages/post.js +5 -12
- package/dist/routes/pages/search.js +3 -4
- package/dist/services/collection.js +52 -64
- package/dist/services/index.js +5 -3
- package/dist/services/navigation.js +29 -53
- package/dist/services/page.js +80 -0
- package/dist/services/post.js +68 -69
- package/dist/services/search.js +24 -18
- package/dist/theme/components/MediaGallery.js +19 -91
- package/dist/theme/components/PageForm.js +15 -15
- package/dist/theme/components/PostForm.js +136 -129
- package/dist/theme/components/PostList.js +13 -8
- package/dist/theme/components/ThreadView.js +3 -3
- package/dist/theme/components/TypeBadge.js +3 -14
- package/dist/theme/components/VisibilityBadge.js +33 -23
- package/dist/themes/threads/ThreadsSiteLayout.js +172 -0
- package/dist/themes/threads/index.js +81 -0
- package/dist/themes/{minimal → threads}/pages/ArchivePage.js +32 -47
- package/dist/themes/threads/pages/CollectionPage.js +65 -0
- package/dist/themes/{minimal → threads}/pages/HomePage.js +3 -3
- package/dist/themes/{minimal → threads}/pages/PostPage.js +12 -9
- package/dist/themes/{minimal → threads}/pages/SearchPage.js +13 -14
- package/dist/themes/{minimal → threads}/pages/SinglePage.js +4 -4
- package/dist/themes/threads/timeline/LinkCard.js +68 -0
- package/dist/themes/threads/timeline/NoteCard.js +53 -0
- package/dist/themes/threads/timeline/QuoteCard.js +59 -0
- package/dist/themes/{minimal → threads}/timeline/ThreadPreview.js +17 -13
- package/dist/themes/threads/timeline/TimelineFeed.js +58 -0
- package/dist/themes/{minimal → threads}/timeline/TimelineItem.js +8 -16
- package/dist/themes/threads/timeline/TimelineLoadMore.js +23 -0
- package/dist/themes/threads/timeline/groupByDate.js +22 -0
- package/dist/themes/threads/timeline/timelineMore.js +107 -0
- package/dist/types.js +24 -40
- package/package.json +2 -1
- package/src/__tests__/helpers/app.ts +4 -0
- package/src/__tests__/helpers/db.ts +51 -74
- package/src/app.tsx +4 -6
- package/src/db/migrations/0005_v2_schema_migration.sql +268 -0
- package/src/db/migrations/meta/_journal.json +7 -0
- package/src/db/schema.ts +63 -46
- package/src/i18n/locales/en.po +216 -164
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +216 -164
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +216 -164
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/index.ts +28 -12
- package/src/lib/__tests__/excerpt.test.ts +125 -0
- package/src/lib/__tests__/schemas.test.ts +166 -105
- package/src/lib/__tests__/theme-components.test.ts +4 -25
- package/src/lib/__tests__/time.test.ts +62 -0
- package/src/{routes/api → lib}/__tests__/timeline.test.ts +108 -66
- package/src/lib/__tests__/view.test.ts +199 -51
- package/src/lib/constants.ts +1 -4
- package/src/lib/excerpt.ts +87 -0
- package/src/lib/feed.ts +22 -7
- package/src/lib/navigation.ts +6 -7
- package/src/lib/render.tsx +1 -1
- package/src/lib/schemas.ts +118 -52
- package/src/lib/theme-components.ts +10 -13
- package/src/lib/time.ts +64 -0
- package/src/lib/timeline.ts +170 -0
- package/src/lib/view.ts +80 -82
- package/src/preset.css +45 -0
- package/src/routes/api/__tests__/posts.test.ts +50 -108
- package/src/routes/api/__tests__/search.test.ts +2 -3
- package/src/routes/api/posts.ts +30 -30
- package/src/routes/api/search.ts +4 -4
- package/src/routes/api/upload.ts +16 -6
- package/src/routes/dash/collections.tsx +18 -40
- package/src/routes/dash/index.tsx +2 -2
- package/src/routes/dash/navigation.tsx +27 -26
- package/src/routes/dash/pages.tsx +45 -60
- package/src/routes/dash/posts.tsx +44 -52
- package/src/routes/feed/rss.ts +2 -1
- package/src/routes/feed/sitemap.ts +14 -4
- package/src/routes/pages/archive.tsx +14 -10
- package/src/routes/pages/collection.tsx +17 -6
- package/src/routes/pages/home.tsx +56 -81
- package/src/routes/pages/page.tsx +64 -27
- package/src/routes/pages/post.tsx +5 -14
- package/src/routes/pages/search.tsx +2 -2
- package/src/services/__tests__/collection.test.ts +257 -158
- package/src/services/__tests__/media.test.ts +18 -18
- package/src/services/__tests__/navigation.test.ts +161 -87
- package/src/services/__tests__/post-timeline.test.ts +92 -88
- package/src/services/__tests__/post.test.ts +342 -206
- package/src/services/__tests__/search.test.ts +19 -25
- package/src/services/collection.ts +71 -113
- package/src/services/index.ts +9 -8
- package/src/services/navigation.ts +38 -71
- package/src/services/page.ts +124 -0
- package/src/services/post.ts +93 -103
- package/src/services/search.ts +38 -27
- package/src/theme/components/MediaGallery.tsx +27 -96
- package/src/theme/components/PageForm.tsx +21 -21
- package/src/theme/components/PostForm.tsx +122 -118
- package/src/theme/components/PostList.tsx +58 -49
- package/src/theme/components/ThreadView.tsx +6 -3
- package/src/theme/components/TypeBadge.tsx +9 -17
- package/src/theme/components/VisibilityBadge.tsx +40 -23
- package/src/themes/threads/ThreadsSiteLayout.tsx +194 -0
- package/src/themes/{minimal → threads}/index.ts +30 -13
- package/src/themes/{minimal → threads}/pages/ArchivePage.tsx +53 -53
- package/src/themes/threads/pages/CollectionPage.tsx +61 -0
- package/src/themes/{minimal → threads}/pages/HomePage.tsx +3 -3
- package/src/themes/{minimal → threads}/pages/PostPage.tsx +12 -8
- package/src/themes/{minimal → threads}/pages/SearchPage.tsx +15 -13
- package/src/themes/{minimal → threads}/pages/SinglePage.tsx +4 -4
- package/src/themes/threads/style.css +336 -0
- package/src/themes/threads/timeline/LinkCard.tsx +67 -0
- package/src/themes/threads/timeline/NoteCard.tsx +58 -0
- package/src/themes/threads/timeline/QuoteCard.tsx +63 -0
- package/src/themes/{minimal → threads}/timeline/ThreadPreview.tsx +15 -13
- package/src/themes/threads/timeline/TimelineFeed.tsx +62 -0
- package/src/themes/{minimal → threads}/timeline/TimelineItem.tsx +9 -17
- package/src/themes/threads/timeline/TimelineLoadMore.tsx +35 -0
- package/src/themes/threads/timeline/groupByDate.ts +30 -0
- package/src/themes/threads/timeline/timelineMore.tsx +130 -0
- package/src/types.ts +242 -98
- package/dist/routes/api/timeline.js +0 -120
- package/dist/themes/minimal/MinimalSiteLayout.js +0 -83
- package/dist/themes/minimal/index.js +0 -65
- package/dist/themes/minimal/pages/CollectionPage.js +0 -65
- package/dist/themes/minimal/timeline/ArticleCard.js +0 -36
- package/dist/themes/minimal/timeline/ImageCard.js +0 -67
- package/dist/themes/minimal/timeline/LinkCard.js +0 -47
- package/dist/themes/minimal/timeline/NoteCard.js +0 -34
- package/dist/themes/minimal/timeline/QuoteCard.js +0 -48
- package/dist/themes/minimal/timeline/TimelineFeed.js +0 -48
- package/src/routes/api/timeline.tsx +0 -159
- package/src/themes/minimal/MinimalSiteLayout.tsx +0 -100
- package/src/themes/minimal/pages/CollectionPage.tsx +0 -60
- package/src/themes/minimal/timeline/ArticleCard.tsx +0 -37
- package/src/themes/minimal/timeline/ImageCard.tsx +0 -63
- package/src/themes/minimal/timeline/LinkCard.tsx +0 -48
- package/src/themes/minimal/timeline/NoteCard.tsx +0 -35
- package/src/themes/minimal/timeline/QuoteCard.tsx +0 -49
- package/src/themes/minimal/timeline/TimelineFeed.tsx +0 -57
|
@@ -53,7 +53,7 @@ function NewPostContent({ collections }: { collections: Collection[] }) {
|
|
|
53
53
|
// List posts
|
|
54
54
|
postsRoutes.get("/", async (c) => {
|
|
55
55
|
const posts = await c.var.services.posts.list({
|
|
56
|
-
|
|
56
|
+
excludeReplies: true,
|
|
57
57
|
});
|
|
58
58
|
const siteName = await getSiteName(c);
|
|
59
59
|
|
|
@@ -89,25 +89,32 @@ postsRoutes.get("/new", async (c) => {
|
|
|
89
89
|
// Create post
|
|
90
90
|
postsRoutes.post("/", async (c) => {
|
|
91
91
|
const body = await c.req.json<{
|
|
92
|
-
|
|
92
|
+
format: string;
|
|
93
93
|
title?: string;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
body: string;
|
|
95
|
+
status: string;
|
|
96
|
+
featured?: boolean;
|
|
97
|
+
pinned?: boolean;
|
|
98
|
+
slug?: string;
|
|
99
|
+
url?: string;
|
|
100
|
+
quoteText?: string;
|
|
101
|
+
rating?: number;
|
|
102
|
+
collectionId?: number;
|
|
99
103
|
mediaIds?: string[];
|
|
100
|
-
collectionIds?: number[];
|
|
101
104
|
}>();
|
|
102
105
|
|
|
103
106
|
const post = await c.var.services.posts.create({
|
|
104
|
-
|
|
107
|
+
format: body.format as Post["format"],
|
|
105
108
|
title: body.title || undefined,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
body: body.body,
|
|
110
|
+
status: body.status as Post["status"],
|
|
111
|
+
featured: body.featured,
|
|
112
|
+
pinned: body.pinned,
|
|
113
|
+
slug: body.slug || undefined,
|
|
114
|
+
url: body.url || undefined,
|
|
115
|
+
quoteText: body.quoteText || undefined,
|
|
116
|
+
rating: body.rating || undefined,
|
|
117
|
+
collectionId: body.collectionId || undefined,
|
|
111
118
|
});
|
|
112
119
|
|
|
113
120
|
// Attach media if provided
|
|
@@ -115,14 +122,6 @@ postsRoutes.post("/", async (c) => {
|
|
|
115
122
|
await c.var.services.media.attachToPost(post.id, body.mediaIds);
|
|
116
123
|
}
|
|
117
124
|
|
|
118
|
-
// Sync collection associations
|
|
119
|
-
if (body.collectionIds) {
|
|
120
|
-
await c.var.services.collections.syncPostCollections(
|
|
121
|
-
post.id,
|
|
122
|
-
body.collectionIds,
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
125
|
return dsRedirect(`/dash/posts/${sqid.encode(post.id)}`);
|
|
127
126
|
});
|
|
128
127
|
|
|
@@ -132,6 +131,7 @@ function ViewPostContent({ post }: { post: Post }) {
|
|
|
132
131
|
message: "Post",
|
|
133
132
|
comment: "@context: Default post title",
|
|
134
133
|
});
|
|
134
|
+
const permalink = post.slug ? `/${post.slug}` : `/p/${sqid.encode(post.id)}`;
|
|
135
135
|
|
|
136
136
|
return (
|
|
137
137
|
<>
|
|
@@ -143,7 +143,7 @@ function ViewPostContent({ post }: { post: Post }) {
|
|
|
143
143
|
message: "Edit",
|
|
144
144
|
comment: "@context: Button to edit post",
|
|
145
145
|
})}
|
|
146
|
-
viewHref={
|
|
146
|
+
viewHref={permalink}
|
|
147
147
|
viewLabel={t({
|
|
148
148
|
message: "View",
|
|
149
149
|
comment: "@context: Button to view post",
|
|
@@ -155,7 +155,7 @@ function ViewPostContent({ post }: { post: Post }) {
|
|
|
155
155
|
<section>
|
|
156
156
|
<div
|
|
157
157
|
class="prose"
|
|
158
|
-
dangerouslySetInnerHTML={{ __html: post.
|
|
158
|
+
dangerouslySetInnerHTML={{ __html: post.bodyHtml || "" }}
|
|
159
159
|
/>
|
|
160
160
|
</section>
|
|
161
161
|
</div>
|
|
@@ -170,7 +170,6 @@ function EditPostContent({
|
|
|
170
170
|
imageTransformUrl,
|
|
171
171
|
s3PublicUrl,
|
|
172
172
|
collections,
|
|
173
|
-
postCollectionIds,
|
|
174
173
|
}: {
|
|
175
174
|
post: Post;
|
|
176
175
|
mediaAttachments: Media[];
|
|
@@ -178,7 +177,6 @@ function EditPostContent({
|
|
|
178
177
|
imageTransformUrl?: string;
|
|
179
178
|
s3PublicUrl?: string;
|
|
180
179
|
collections: Collection[];
|
|
181
|
-
postCollectionIds: number[];
|
|
182
180
|
}) {
|
|
183
181
|
const { t } = useLingui();
|
|
184
182
|
return (
|
|
@@ -194,7 +192,6 @@ function EditPostContent({
|
|
|
194
192
|
imageTransformUrl={imageTransformUrl}
|
|
195
193
|
s3PublicUrl={s3PublicUrl}
|
|
196
194
|
collections={collections}
|
|
197
|
-
postCollectionIds={postCollectionIds}
|
|
198
195
|
/>
|
|
199
196
|
</>
|
|
200
197
|
);
|
|
@@ -237,9 +234,6 @@ postsRoutes.get("/:id/edit", async (c) => {
|
|
|
237
234
|
const imageTransformUrl = c.env.IMAGE_TRANSFORM_URL;
|
|
238
235
|
const s3PublicUrl = c.env.S3_PUBLIC_URL;
|
|
239
236
|
const collections = await c.var.services.collections.list();
|
|
240
|
-
const postCollections =
|
|
241
|
-
await c.var.services.collections.getCollectionsForPost(post.id);
|
|
242
|
-
const postCollectionIds = postCollections.map((col) => col.id);
|
|
243
237
|
|
|
244
238
|
return c.html(
|
|
245
239
|
<DashLayout
|
|
@@ -255,7 +249,6 @@ postsRoutes.get("/:id/edit", async (c) => {
|
|
|
255
249
|
imageTransformUrl={imageTransformUrl}
|
|
256
250
|
s3PublicUrl={s3PublicUrl}
|
|
257
251
|
collections={collections}
|
|
258
|
-
postCollectionIds={postCollectionIds}
|
|
259
252
|
/>
|
|
260
253
|
</DashLayout>,
|
|
261
254
|
);
|
|
@@ -267,25 +260,32 @@ postsRoutes.post("/:id", async (c) => {
|
|
|
267
260
|
if (!id) return c.notFound();
|
|
268
261
|
|
|
269
262
|
const body = await c.req.json<{
|
|
270
|
-
|
|
263
|
+
format: string;
|
|
271
264
|
title?: string;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
265
|
+
body?: string;
|
|
266
|
+
status: string;
|
|
267
|
+
featured?: boolean;
|
|
268
|
+
pinned?: boolean;
|
|
269
|
+
slug?: string;
|
|
270
|
+
url?: string;
|
|
271
|
+
quoteText?: string;
|
|
272
|
+
rating?: number;
|
|
273
|
+
collectionId?: number;
|
|
277
274
|
mediaIds?: string[];
|
|
278
|
-
collectionIds?: number[];
|
|
279
275
|
}>();
|
|
280
276
|
|
|
281
277
|
await c.var.services.posts.update(id, {
|
|
282
|
-
|
|
278
|
+
format: body.format as Post["format"],
|
|
283
279
|
title: body.title || null,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
280
|
+
body: body.body || null,
|
|
281
|
+
status: body.status as Post["status"],
|
|
282
|
+
featured: body.featured,
|
|
283
|
+
pinned: body.pinned,
|
|
284
|
+
slug: body.slug || null,
|
|
285
|
+
url: body.url || null,
|
|
286
|
+
quoteText: body.quoteText || null,
|
|
287
|
+
rating: body.rating || null,
|
|
288
|
+
collectionId: body.collectionId || null,
|
|
289
289
|
});
|
|
290
290
|
|
|
291
291
|
// Update media attachments if provided
|
|
@@ -293,14 +293,6 @@ postsRoutes.post("/:id", async (c) => {
|
|
|
293
293
|
await c.var.services.media.attachToPost(id, body.mediaIds);
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
-
// Sync collection associations
|
|
297
|
-
if (body.collectionIds !== undefined) {
|
|
298
|
-
await c.var.services.collections.syncPostCollections(
|
|
299
|
-
id,
|
|
300
|
-
body.collectionIds,
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
296
|
return dsRedirect(`/dash/posts/${sqid.encode(id)}`);
|
|
305
297
|
});
|
|
306
298
|
|
package/src/routes/feed/rss.ts
CHANGED
|
@@ -26,7 +26,8 @@ async function buildFeedData(c: Context<Env>): Promise<FeedData> {
|
|
|
26
26
|
const siteLanguage = await getSiteLanguage(c);
|
|
27
27
|
|
|
28
28
|
const posts = await c.var.services.posts.list({
|
|
29
|
-
|
|
29
|
+
status: "published",
|
|
30
|
+
excludeReplies: true,
|
|
30
31
|
limit: 50,
|
|
31
32
|
});
|
|
32
33
|
|
|
@@ -6,7 +6,11 @@ import { Hono } from "hono";
|
|
|
6
6
|
import type { Bindings } from "../../types.js";
|
|
7
7
|
import type { AppVariables } from "../../app.js";
|
|
8
8
|
import { defaultSitemapRenderer } from "../../lib/feed.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
createMediaContext,
|
|
11
|
+
toPostViewsFromPosts,
|
|
12
|
+
toPageView,
|
|
13
|
+
} from "../../lib/view.js";
|
|
10
14
|
|
|
11
15
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
12
16
|
|
|
@@ -17,16 +21,22 @@ sitemapRoutes.get("/sitemap.xml", async (c) => {
|
|
|
17
21
|
const siteUrl = c.env.SITE_URL;
|
|
18
22
|
|
|
19
23
|
const posts = await c.var.services.posts.list({
|
|
20
|
-
|
|
24
|
+
status: "published",
|
|
25
|
+
excludeReplies: true,
|
|
21
26
|
limit: 1000,
|
|
22
27
|
});
|
|
23
28
|
|
|
24
|
-
//
|
|
29
|
+
// Fetch published pages
|
|
30
|
+
const allPages = await c.var.services.pages.list();
|
|
31
|
+
const publishedPages = allPages.filter((p) => p.status === "published");
|
|
32
|
+
|
|
33
|
+
// Transform to View Models
|
|
25
34
|
const mediaCtx = createMediaContext(c);
|
|
26
35
|
const postViews = toPostViewsFromPosts(posts, mediaCtx);
|
|
36
|
+
const pageViews = publishedPages.map(toPageView);
|
|
27
37
|
|
|
28
38
|
const renderer = c.var.config.theme?.feed?.sitemap ?? defaultSitemapRenderer;
|
|
29
|
-
const xml = renderer({ siteUrl, posts: postViews });
|
|
39
|
+
const xml = renderer({ siteUrl, posts: postViews, pages: pageViews });
|
|
30
40
|
|
|
31
41
|
return new Response(xml, {
|
|
32
42
|
headers: {
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Archive Page Route
|
|
3
3
|
*
|
|
4
|
-
* Shows all posts, optionally filtered by
|
|
4
|
+
* Shows all posts, optionally filtered by format or featured status
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Hono } from "hono";
|
|
8
|
-
import type { Bindings,
|
|
8
|
+
import type { Bindings, Format } from "../../types.js";
|
|
9
9
|
import type { AppVariables } from "../../app.js";
|
|
10
|
-
import {
|
|
11
|
-
import { ArchivePage as DefaultArchivePage } from "../../themes/
|
|
10
|
+
import { FORMATS } from "../../types.js";
|
|
11
|
+
import { ArchivePage as DefaultArchivePage } from "../../themes/threads/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";
|
|
@@ -21,9 +21,11 @@ export const archiveRoutes = new Hono<Env>();
|
|
|
21
21
|
|
|
22
22
|
// Archive page - all posts
|
|
23
23
|
archiveRoutes.get("/", async (c) => {
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
24
|
+
const formatParam = c.req.query("format") as Format | undefined;
|
|
25
|
+
const format =
|
|
26
|
+
formatParam && FORMATS.includes(formatParam) ? formatParam : undefined;
|
|
27
|
+
const featuredParam = c.req.query("featured");
|
|
28
|
+
const featured = featuredParam === "true" ? true : undefined;
|
|
27
29
|
|
|
28
30
|
// Parse cursor
|
|
29
31
|
const cursorParam = c.req.query("cursor");
|
|
@@ -33,8 +35,9 @@ archiveRoutes.get("/", async (c) => {
|
|
|
33
35
|
|
|
34
36
|
// Fetch one extra to check for more
|
|
35
37
|
const posts = await c.var.services.posts.list({
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
format,
|
|
39
|
+
status: "published",
|
|
40
|
+
featured,
|
|
38
41
|
excludeReplies: true,
|
|
39
42
|
cursor,
|
|
40
43
|
limit: PAGE_SIZE + 1,
|
|
@@ -77,7 +80,8 @@ archiveRoutes.get("/", async (c) => {
|
|
|
77
80
|
groups={groups}
|
|
78
81
|
hasMore={hasMore}
|
|
79
82
|
nextCursor={nextCursor}
|
|
80
|
-
|
|
83
|
+
format={format}
|
|
84
|
+
featured={featured}
|
|
81
85
|
theme={components}
|
|
82
86
|
/>
|
|
83
87
|
),
|
|
@@ -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 as DefaultCollectionPage } from "../../themes/
|
|
8
|
+
import { CollectionPage as DefaultCollectionPage } from "../../themes/threads/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";
|
|
@@ -14,13 +14,19 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
|
14
14
|
|
|
15
15
|
export const collectionRoutes = new Hono<Env>();
|
|
16
16
|
|
|
17
|
-
collectionRoutes.get("/:
|
|
18
|
-
const
|
|
17
|
+
collectionRoutes.get("/:slug", async (c) => {
|
|
18
|
+
const slug = c.req.param("slug");
|
|
19
19
|
|
|
20
|
-
const collection = await c.var.services.collections.
|
|
20
|
+
const collection = await c.var.services.collections.getBySlug(slug);
|
|
21
21
|
if (!collection) return c.notFound();
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// Fetch posts in this collection
|
|
24
|
+
const posts = await c.var.services.posts.list({
|
|
25
|
+
collectionId: collection.id,
|
|
26
|
+
status: "published",
|
|
27
|
+
excludeReplies: true,
|
|
28
|
+
});
|
|
29
|
+
|
|
24
30
|
const navData = await getNavigationData(c);
|
|
25
31
|
|
|
26
32
|
// Transform to View Models
|
|
@@ -35,7 +41,12 @@ collectionRoutes.get("/:path", async (c) => {
|
|
|
35
41
|
description: collection.description ?? undefined,
|
|
36
42
|
navData,
|
|
37
43
|
content: (
|
|
38
|
-
<Page
|
|
44
|
+
<Page
|
|
45
|
+
collection={collection}
|
|
46
|
+
posts={postViews}
|
|
47
|
+
hasMore={false}
|
|
48
|
+
theme={components}
|
|
49
|
+
/>
|
|
39
50
|
),
|
|
40
51
|
});
|
|
41
52
|
});
|
|
@@ -2,109 +2,83 @@
|
|
|
2
2
|
* Home Page Route
|
|
3
3
|
*
|
|
4
4
|
* Timeline feed with per-type card components and thread previews.
|
|
5
|
+
* Handles both full-page rendering and load-more SSE responses.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { Hono } from "hono";
|
|
8
|
-
import type { Bindings
|
|
9
|
+
import type { Bindings } from "../../types.js";
|
|
9
10
|
import type { AppVariables } from "../../app.js";
|
|
10
|
-
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
11
11
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
12
12
|
import { renderPublicPage } from "../../lib/render.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { assembleTimeline } from "../../lib/timeline.js";
|
|
14
|
+
import { sse } from "../../lib/sse.js";
|
|
15
|
+
import { createMediaContext, toPostViewsFromPosts } from "../../lib/view.js";
|
|
16
|
+
import { HomePage as DefaultHomePage } from "../../themes/threads/pages/HomePage.js";
|
|
15
17
|
|
|
16
18
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
17
19
|
|
|
18
|
-
const PAGE_SIZE = 20;
|
|
19
|
-
|
|
20
20
|
export const homeRoutes = new Hono<Env>();
|
|
21
21
|
|
|
22
22
|
homeRoutes.get("/", async (c) => {
|
|
23
|
-
const
|
|
23
|
+
const cursorParam = c.req.query("cursor");
|
|
24
|
+
const cursor = cursorParam ? parseInt(cursorParam, 10) : undefined;
|
|
25
|
+
const lastDate = c.req.query("lastDate");
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
visibility: ["featured", "quiet"],
|
|
28
|
-
excludeReplies: true,
|
|
29
|
-
excludeTypes: ["page"],
|
|
30
|
-
limit: PAGE_SIZE + 1,
|
|
27
|
+
const { items, hasMore, nextCursor } = await assembleTimeline(c, {
|
|
28
|
+
cursor: cursor && !isNaN(cursor) ? cursor : undefined,
|
|
31
29
|
});
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const mediaCtx = createMediaContext(c);
|
|
40
|
-
const mediaMap = buildMediaMap(
|
|
41
|
-
rawMediaMap,
|
|
42
|
-
mediaCtx.r2PublicUrl,
|
|
43
|
-
mediaCtx.imageTransformUrl,
|
|
44
|
-
mediaCtx.s3PublicUrl,
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
// Get reply counts to identify thread roots
|
|
48
|
-
const replyCounts = await c.var.services.posts.getReplyCounts(postIds);
|
|
49
|
-
const threadRootIds = postIds.filter((id) => (replyCounts.get(id) ?? 0) > 0);
|
|
50
|
-
|
|
51
|
-
// Batch load thread previews
|
|
52
|
-
const threadPreviews = await c.var.services.posts.getThreadPreviews(
|
|
53
|
-
threadRootIds,
|
|
54
|
-
3,
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
// Batch load media for preview replies
|
|
58
|
-
const previewReplyIds: number[] = [];
|
|
59
|
-
for (const replies of threadPreviews.values()) {
|
|
60
|
-
for (const reply of replies) {
|
|
61
|
-
previewReplyIds.push(reply.id);
|
|
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
|
+
});
|
|
62
37
|
}
|
|
63
|
-
}
|
|
64
|
-
const previewMediaMap =
|
|
65
|
-
previewReplyIds.length > 0
|
|
66
|
-
? buildMediaMap(
|
|
67
|
-
await c.var.services.media.getByPostIds(previewReplyIds),
|
|
68
|
-
mediaCtx.r2PublicUrl,
|
|
69
|
-
mediaCtx.imageTransformUrl,
|
|
70
|
-
mediaCtx.s3PublicUrl,
|
|
71
|
-
)
|
|
72
|
-
: new Map();
|
|
73
38
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
}
|
|
80
47
|
|
|
81
|
-
const
|
|
82
|
-
|
|
48
|
+
const patches = renderMore({
|
|
49
|
+
items,
|
|
50
|
+
lastDate: lastDate ?? undefined,
|
|
51
|
+
hasMore,
|
|
52
|
+
nextCursor,
|
|
53
|
+
theme: themeConfig?.components,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return sse(c, async (stream) => {
|
|
57
|
+
for (const patch of patches) {
|
|
58
|
+
if (patch.mode === "remove") {
|
|
59
|
+
stream.remove(patch.selector);
|
|
60
|
+
} else {
|
|
61
|
+
stream.patchElements(patch.content, {
|
|
62
|
+
mode: patch.mode,
|
|
63
|
+
selector: patch.selector,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
83
69
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
post: postView,
|
|
87
|
-
threadPreview: {
|
|
88
|
-
replies: toPostViews(
|
|
89
|
-
previewReplies.map((r) => ({
|
|
90
|
-
...r,
|
|
91
|
-
mediaAttachments: previewMediaMap.get(r.id) ?? [],
|
|
92
|
-
})),
|
|
93
|
-
mediaCtx,
|
|
94
|
-
),
|
|
95
|
-
totalReplyCount: replyCount,
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
}
|
|
70
|
+
// Full page render
|
|
71
|
+
const navData = await getNavigationData(c);
|
|
99
72
|
|
|
100
|
-
|
|
73
|
+
// Fetch pinned posts
|
|
74
|
+
const pinnedPosts = await c.var.services.posts.list({
|
|
75
|
+
pinned: true,
|
|
76
|
+
status: "published",
|
|
77
|
+
excludeReplies: true,
|
|
101
78
|
});
|
|
79
|
+
const mediaCtx = createMediaContext(c);
|
|
80
|
+
const pinnedItems = toPostViewsFromPosts(pinnedPosts, mediaCtx);
|
|
102
81
|
|
|
103
|
-
// Determine next cursor
|
|
104
|
-
const lastPost = displayPosts[displayPosts.length - 1];
|
|
105
|
-
const nextCursor = hasMore && lastPost ? lastPost.id : undefined;
|
|
106
|
-
|
|
107
|
-
// Resolve page component
|
|
108
82
|
const components = c.var.config.theme?.components;
|
|
109
83
|
const Page = components?.HomePage ?? DefaultHomePage;
|
|
110
84
|
|
|
@@ -114,6 +88,7 @@ homeRoutes.get("/", async (c) => {
|
|
|
114
88
|
content: (
|
|
115
89
|
<Page
|
|
116
90
|
items={items}
|
|
91
|
+
pinnedItems={pinnedItems}
|
|
117
92
|
hasMore={hasMore}
|
|
118
93
|
nextCursor={nextCursor}
|
|
119
94
|
theme={components}
|
|
@@ -1,51 +1,88 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Custom Page Route
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Serves pages from the pages table and posts with custom slugs.
|
|
5
|
+
* This is a catch-all route mounted at "/" - must be registered last.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { Hono } from "hono";
|
|
8
9
|
import type { Bindings } from "../../types.js";
|
|
9
10
|
import type { AppVariables } from "../../app.js";
|
|
10
|
-
import { SinglePage as DefaultSinglePage } from "../../themes/
|
|
11
|
+
import { SinglePage as DefaultSinglePage } from "../../themes/threads/pages/SinglePage.js";
|
|
12
|
+
import { PostPage as DefaultPostPage } from "../../themes/threads/pages/PostPage.js";
|
|
11
13
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
12
14
|
import { renderPublicPage } from "../../lib/render.js";
|
|
13
|
-
import {
|
|
15
|
+
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
16
|
+
import { createMediaContext, toPageView, toPostView } from "../../lib/view.js";
|
|
14
17
|
|
|
15
18
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
16
19
|
|
|
17
20
|
export const pageRoutes = new Hono<Env>();
|
|
18
21
|
|
|
19
|
-
// Catch-all for custom page paths
|
|
20
|
-
pageRoutes.get("/:
|
|
21
|
-
const
|
|
22
|
+
// Catch-all for custom page paths and post slugs
|
|
23
|
+
pageRoutes.get("/:slug", async (c) => {
|
|
24
|
+
const slug = c.req.param("slug");
|
|
22
25
|
|
|
23
|
-
//
|
|
24
|
-
const page = await c.var.services.
|
|
26
|
+
// First, try to find a page by slug
|
|
27
|
+
const page = await c.var.services.pages.getBySlug(slug);
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
if (page) {
|
|
30
|
+
// Don't show draft pages
|
|
31
|
+
if (page.status === "draft") {
|
|
32
|
+
return c.notFound();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const navData = await getNavigationData(c);
|
|
36
|
+
const pageView = toPageView(page);
|
|
30
37
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
const components = c.var.config.theme?.components;
|
|
39
|
+
const Page = components?.SinglePage ?? DefaultSinglePage;
|
|
40
|
+
|
|
41
|
+
return renderPublicPage(c, {
|
|
42
|
+
title: `${page.title || slug} - ${navData.siteName}`,
|
|
43
|
+
description: page.body?.slice(0, 160),
|
|
44
|
+
navData,
|
|
45
|
+
content: <Page page={pageView} theme={components} />,
|
|
46
|
+
});
|
|
34
47
|
}
|
|
35
48
|
|
|
36
|
-
|
|
49
|
+
// Then, try to find a post by slug
|
|
50
|
+
const post = await c.var.services.posts.getBySlug(slug);
|
|
51
|
+
|
|
52
|
+
if (post) {
|
|
53
|
+
// Don't show draft posts
|
|
54
|
+
if (post.status === "draft") {
|
|
55
|
+
return c.notFound();
|
|
56
|
+
}
|
|
37
57
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
58
|
+
// Load media attachments
|
|
59
|
+
const rawMediaMap = await c.var.services.media.getByPostIds([post.id]);
|
|
60
|
+
const mediaCtx = createMediaContext(c);
|
|
61
|
+
const mediaMap = buildMediaMap(
|
|
62
|
+
rawMediaMap,
|
|
63
|
+
mediaCtx.r2PublicUrl,
|
|
64
|
+
mediaCtx.imageTransformUrl,
|
|
65
|
+
mediaCtx.s3PublicUrl,
|
|
66
|
+
);
|
|
41
67
|
|
|
42
|
-
|
|
43
|
-
|
|
68
|
+
const postView = toPostView(
|
|
69
|
+
{ ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
|
|
70
|
+
mediaCtx,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const navData = await getNavigationData(c);
|
|
74
|
+
const title = post.title || navData.siteName;
|
|
75
|
+
|
|
76
|
+
const components = c.var.config.theme?.components;
|
|
77
|
+
const PostPage = components?.PostPage ?? DefaultPostPage;
|
|
78
|
+
|
|
79
|
+
return renderPublicPage(c, {
|
|
80
|
+
title,
|
|
81
|
+
description: post.body?.slice(0, 160),
|
|
82
|
+
navData,
|
|
83
|
+
content: <PostPage post={postView} theme={components} />,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
44
86
|
|
|
45
|
-
return
|
|
46
|
-
title: `${page.title} - ${navData.siteName}`,
|
|
47
|
-
description: page.content?.slice(0, 160),
|
|
48
|
-
navData,
|
|
49
|
-
content: <Page page={pageView} theme={components} />,
|
|
50
|
-
});
|
|
87
|
+
return c.notFound();
|
|
51
88
|
});
|