@jant/core 0.6.14 → 0.6.15
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/bin/commands/import-site.js +81 -13
- package/bin/commands/migrate.js +55 -0
- package/bin/commands/posts/rebuild-html.js +184 -0
- package/bin/commands/site/snapshot/import.js +14 -10
- package/bin/lib/d1-query.js +23 -5
- package/bin/lib/migration-runner.js +29 -7
- package/bin/lib/node-database.js +32 -0
- package/bin/lib/site-snapshot.js +347 -46
- package/bin/lib/sql-export.js +1 -1
- package/bin/lib/thread-collection-migration.js +326 -0
- package/dist/app-BxCOR3Uc.js +6 -0
- package/dist/{app-2i7-WQHg.js → app-C-oi_t8W.js} +7408 -6343
- package/dist/client/.vite/manifest.json +3 -3
- package/dist/client/_assets/client-B_eGKN5p.css +2 -0
- package/dist/client/_assets/{client-BvID5ucz.js → client-DxY5BFe8.js} +58 -37
- package/dist/client/_assets/client-auth-CakPESv9.js +5533 -0
- package/dist/{export-DWn149b7.js → export-CsFx6F_M.js} +1259 -125
- package/dist/{github-sync-Ckk0sJxK.js → github-sync-Cq1pzzOI.js} +1 -1
- package/dist/{github-sync-C0zyF6XS.js → github-sync-ppWXN3jb.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/node.js +5 -5
- package/package.json +11 -10
- package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
- package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
- package/src/__tests__/export-hugo-build.test.ts +63 -4
- package/src/__tests__/export-import-roundtrip.test.ts +6 -4
- package/src/__tests__/export-service.test.ts +142 -9
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +2 -0
- package/src/__tests__/import-site-command.test.ts +65 -0
- package/src/__tests__/vite-config.test.ts +45 -0
- package/src/app.tsx +16 -3
- package/src/client/__tests__/collection-created-notice.test.ts +36 -0
- package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
- package/src/client/__tests__/collection-page-actions.test.ts +71 -2
- package/src/client/__tests__/compose-bridge.test.ts +64 -0
- package/src/client/__tests__/feed-video-player.test.ts +16 -1
- package/src/client/__tests__/footnote-rail.test.ts +472 -0
- package/src/client/__tests__/hydrate-partial.test.ts +3 -0
- package/src/client/__tests__/toast.test.ts +18 -1
- package/src/client/collection-created-notice.ts +66 -0
- package/src/client/collection-form-bridge.ts +17 -0
- package/src/client/collection-navigation.ts +44 -0
- package/src/client/collection-page-actions.ts +57 -1
- package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +370 -5
- package/src/client/components/__tests__/jant-compose-editor.test.ts +97 -0
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +104 -0
- package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
- package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
- package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
- package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-manager-types.ts +9 -3
- package/src/client/components/compose-types.ts +8 -0
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +2 -1
- package/src/client/components/jant-compose-dialog.ts +85 -21
- package/src/client/components/jant-compose-editor.ts +13 -7
- package/src/client/components/jant-compose-fullscreen.ts +24 -4
- package/src/client/components/jant-config-editor.ts +722 -0
- package/src/client/components/jant-media-lightbox.ts +100 -18
- package/src/client/components/jant-nav-manager.ts +1283 -54
- package/src/client/components/jant-post-form.ts +3 -3
- package/src/client/components/jant-post-menu.ts +86 -74
- package/src/client/components/nav-manager-types.ts +51 -2
- package/src/client/components/post-form-types.ts +1 -1
- package/src/client/compose-bridge.ts +4 -1
- package/src/client/feed-video-player.ts +62 -2
- package/src/client/footnote-rail.ts +478 -0
- package/src/client/hydrate-partial.ts +2 -0
- package/src/client/media-lightbox-events.ts +63 -0
- package/src/client/site-header-fragment.ts +38 -0
- package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
- package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
- package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
- package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
- package/src/client/tiptap/bubble-menu.ts +88 -10
- package/src/client/tiptap/continuous-lists.ts +102 -0
- package/src/client/tiptap/create-editor.ts +3 -0
- package/src/client/tiptap/extensions.ts +14 -5
- package/src/client/tiptap/markdown-clipboard.ts +608 -11
- package/src/client/tiptap/slash-commands.ts +8 -1
- package/src/client/tiptap/structural-keymap.ts +211 -0
- package/src/client/tiptap/tab-indent.ts +8 -22
- package/src/client/tiptap/table-control-labels.ts +30 -0
- package/src/client/tiptap/table-controls.ts +537 -0
- package/src/client/tiptap/table-size-picker.ts +254 -0
- package/src/client/toast.ts +8 -6
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -0
- package/src/db/__tests__/d1-query.test.ts +43 -1
- package/src/db/__tests__/migration-runner.test.ts +63 -0
- package/src/db/__tests__/migrations.test.ts +469 -0
- package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
- package/src/db/migrations/0027_old_blue_blade.sql +39 -0
- package/src/db/migrations/0028_superb_post.sql +2 -0
- package/src/db/migrations/0029_boring_magma.sql +1 -0
- package/src/db/migrations/0030_typical_vivisector.sql +50 -0
- package/src/db/migrations/meta/0027_snapshot.json +2511 -0
- package/src/db/migrations/meta/0028_snapshot.json +2511 -0
- package/src/db/migrations/meta/0029_snapshot.json +2519 -0
- package/src/db/migrations/meta/0030_snapshot.json +2541 -0
- package/src/db/migrations/meta/_journal.json +28 -0
- package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
- package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
- package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
- package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
- package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
- package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
- package/src/db/migrations/pg/meta/_journal.json +28 -0
- package/src/db/pg/schema.ts +28 -20
- package/src/db/rehearsal-fixtures/demo-current.json +39 -4
- package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
- package/src/db/schema.ts +28 -20
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +133 -30
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +128 -25
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +128 -25
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +513 -10
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +515 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +515 -12
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/api-settings.test.ts +299 -0
- package/src/lib/__tests__/constants.test.ts +5 -0
- package/src/lib/__tests__/feed-policy.test.ts +25 -0
- package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
- package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
- package/src/lib/__tests__/markdown.test.ts +8 -6
- package/src/lib/__tests__/navigation.test.ts +59 -2
- package/src/lib/__tests__/post-body-html.test.ts +176 -0
- package/src/lib/__tests__/post-display.test.ts +64 -0
- package/src/lib/__tests__/resolve-config.test.ts +113 -1
- package/src/lib/__tests__/summary.test.ts +82 -0
- package/src/lib/__tests__/timeline.test.ts +109 -70
- package/src/lib/__tests__/timezones.test.ts +31 -3
- package/src/lib/__tests__/tiptap-render.test.ts +268 -9
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
- package/src/lib/__tests__/view.test.ts +53 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
- package/src/lib/api-settings.ts +239 -12
- package/src/lib/collection-sort.ts +7 -5
- package/src/lib/constants.ts +2 -0
- package/src/lib/feed-path.ts +25 -0
- package/src/lib/feed-policy.ts +64 -0
- package/src/lib/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +10 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/markdown-manager.ts +188 -5
- package/src/lib/markdown.ts +10 -3
- package/src/lib/navigation.ts +16 -3
- package/src/lib/post-body-html.ts +131 -0
- package/src/lib/post-display.ts +7 -1
- package/src/lib/render.tsx +9 -2
- package/src/lib/resolve-config.ts +100 -16
- package/src/lib/schemas.ts +133 -1
- package/src/lib/settings-paths.ts +5 -0
- package/src/lib/site-skill.ts +78 -0
- package/src/lib/summary.ts +16 -5
- package/src/lib/timeline.ts +98 -122
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +355 -81
- package/src/lib/view.ts +30 -20
- package/src/lib/worker-response-cache.ts +11 -18
- package/src/middleware/__tests__/public-content-access.test.ts +91 -0
- package/src/middleware/config.ts +2 -2
- package/src/middleware/public-content-access.ts +87 -0
- package/src/node/__tests__/cli-export.test.ts +14 -0
- package/src/node/__tests__/cli-migrate.test.ts +6 -0
- package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
- package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
- package/src/node/index.ts +2 -1
- package/src/preset.css +16 -2
- package/src/routes/__tests__/compose.test.ts +51 -0
- package/src/routes/__tests__/site-skill.test.ts +112 -0
- package/src/routes/api/__tests__/collections.test.ts +91 -19
- package/src/routes/api/__tests__/mcp.test.ts +65 -1
- package/src/routes/api/__tests__/nav-items.test.ts +91 -0
- package/src/routes/api/__tests__/public-access.test.ts +81 -0
- package/src/routes/api/__tests__/settings.test.ts +217 -0
- package/src/routes/api/collections.ts +22 -26
- package/src/routes/api/export.ts +2 -0
- package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
- package/src/routes/api/internal/post-body-html.ts +43 -0
- package/src/routes/api/internal/sites.ts +24 -0
- package/src/routes/api/nav-items.ts +24 -1
- package/src/routes/api/posts.ts +2 -2
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +208 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +41 -24
- package/src/routes/api/search.ts +3 -0
- package/src/routes/api/settings.ts +33 -0
- package/src/routes/auth/__tests__/setup.test.ts +7 -4
- package/src/routes/compose.tsx +2 -1
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +92 -3
- package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
- package/src/routes/pages/__tests__/collections.test.ts +9 -9
- package/src/routes/pages/__tests__/preview.test.ts +174 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +27 -7
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +83 -11
- package/src/routes/site-skill.ts +18 -0
- package/src/services/__tests__/auth.test.ts +7 -5
- package/src/services/__tests__/collection.test.ts +159 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +109 -30
- package/src/services/__tests__/post.test.ts +567 -5
- package/src/services/__tests__/search.test.ts +48 -1
- package/src/services/__tests__/settings.test.ts +8 -0
- package/src/services/__tests__/site-admin.test.ts +121 -1
- package/src/services/auth.ts +2 -2
- package/src/services/bootstrap.ts +3 -2
- package/src/services/collection.ts +226 -167
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +46 -25
- package/src/services/export-theme/layouts/_default/list.html +18 -4
- package/src/services/export-theme/layouts/_default/rss.xml +8 -12
- package/src/services/export-theme/layouts/featured/list.html +5 -4
- package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
- package/src/services/export-theme/styles/main.css +30 -2
- package/src/services/export.ts +91 -80
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/post.ts +874 -271
- package/src/services/search.ts +8 -2
- package/src/services/settings.ts +2 -10
- package/src/services/site-admin.ts +63 -43
- package/src/style-cjk-jp.css +1 -1
- package/src/style-cjk-kr.css +1 -1
- package/src/style-cjk-tc.css +1 -1
- package/src/style-cjk.css +1 -1
- package/src/styles/components.css +540 -0
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +14 -9
- package/src/styles/ui.css +829 -29
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +243 -12
- package/src/types/constants.ts +22 -1
- package/src/types/entities.ts +9 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +8 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +16 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +92 -2
- package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
- package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
- package/src/ui/dash/settings/GeneralContent.tsx +15 -5
- package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
- package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
- package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
- package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
- package/src/ui/feed/LinkCard.tsx +2 -12
- package/src/ui/feed/NoteCard.tsx +10 -20
- package/src/ui/feed/QuoteCard.tsx +2 -12
- package/src/ui/feed/ThreadPreview.tsx +26 -34
- package/src/ui/feed/TimelineFeed.tsx +2 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
- package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
- package/src/ui/feed/thread-preview-state.ts +5 -7
- package/src/ui/font-themes.ts +74 -26
- package/src/ui/layouts/BaseLayout.tsx +22 -8
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
- package/src/ui/pages/CollectionPage.tsx +101 -22
- package/src/ui/pages/CollectionsPage.tsx +3 -1
- package/src/ui/pages/PostPage.tsx +39 -12
- package/src/ui/pages/SearchPage.tsx +6 -14
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +7 -0
- package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
- package/src/ui/shared/CollectionDirectory.tsx +13 -9
- package/src/ui/shared/CollectionsManager.tsx +17 -7
- package/src/ui/shared/DraftPreviewBar.tsx +33 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +3 -1
- package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
- package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
- package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
- package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
- package/src/ui/shared/collection-management-labels.ts +33 -2
- package/src/ui/shared/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +46 -0
- package/dist/app-DZDlmh7C.js +0 -6
- package/dist/client/_assets/client-Cevfpm8H.css +0 -2
- package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
- package/src/client/tiptap/wrapping-input-rules.ts +0 -102
package/src/routes/feed/feed.ts
CHANGED
|
@@ -27,9 +27,13 @@ import type {
|
|
|
27
27
|
} from "../../types.js";
|
|
28
28
|
import type { AppVariables } from "../../types/app-context.js";
|
|
29
29
|
import { defaultFeedRenderer } from "../../lib/feed.js";
|
|
30
|
+
import {
|
|
31
|
+
getFeedEntryUpdatedAt,
|
|
32
|
+
getRssPublishedBefore,
|
|
33
|
+
RSS_FEED_CACHE_CONTROL,
|
|
34
|
+
} from "../../lib/feed-policy.js";
|
|
30
35
|
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
31
36
|
import { getI18n } from "../../i18n/index.js";
|
|
32
|
-
import { toISOString } from "../../lib/time.js";
|
|
33
37
|
import { FORMATS } from "../../types/constants.js";
|
|
34
38
|
|
|
35
39
|
import { createMediaContext, toPostViews } from "../../lib/view.js";
|
|
@@ -118,6 +122,7 @@ function collectReplyIds(threadMap: Map<string, Post[]>): string[] {
|
|
|
118
122
|
async function buildLatestFeedData(
|
|
119
123
|
c: Context<Env>,
|
|
120
124
|
feedLimit: number,
|
|
125
|
+
publishedBefore: number,
|
|
121
126
|
format?: Format,
|
|
122
127
|
): Promise<{ posts: Post[]; postViews: FeedPostView[] }> {
|
|
123
128
|
const posts = await c.var.services.posts.list({
|
|
@@ -127,6 +132,7 @@ async function buildLatestFeedData(
|
|
|
127
132
|
excludePrivate: true,
|
|
128
133
|
format,
|
|
129
134
|
ignorePinnedSort: true,
|
|
135
|
+
publishedBefore,
|
|
130
136
|
limit: feedLimit,
|
|
131
137
|
});
|
|
132
138
|
|
|
@@ -135,7 +141,7 @@ async function buildLatestFeedData(
|
|
|
135
141
|
|
|
136
142
|
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
137
143
|
const [threadMap, mediaMap, aliasesMap] = await Promise.all([
|
|
138
|
-
c.var.services.posts.getPublishedThreads(rootIds),
|
|
144
|
+
c.var.services.posts.getPublishedThreads(rootIds, { publishedBefore }),
|
|
139
145
|
loadMediaMap(c, postIds, mediaCtx),
|
|
140
146
|
c.var.services.paths.getPostAliases(postIds),
|
|
141
147
|
]);
|
|
@@ -162,9 +168,10 @@ async function buildLatestFeedData(
|
|
|
162
168
|
aliasMap,
|
|
163
169
|
).map((postView, index) => {
|
|
164
170
|
const post = posts[index] as (typeof posts)[number];
|
|
171
|
+
const thread = threadMap.get(post.id);
|
|
165
172
|
return {
|
|
166
173
|
...postView,
|
|
167
|
-
feedUpdatedAt:
|
|
174
|
+
feedUpdatedAt: getFeedEntryUpdatedAt(post, thread),
|
|
168
175
|
threadReplies: buildThreadReplies(
|
|
169
176
|
post.id,
|
|
170
177
|
threadMap,
|
|
@@ -187,10 +194,12 @@ async function buildLatestFeedData(
|
|
|
187
194
|
async function buildFeaturedFeedData(
|
|
188
195
|
c: Context<Env>,
|
|
189
196
|
feedLimit: number,
|
|
197
|
+
publishedBefore: number,
|
|
190
198
|
): Promise<{ posts: Post[]; postViews: FeedPostView[] }> {
|
|
191
199
|
const rootIds = await c.var.services.posts.listFeaturedThreadRootIds({
|
|
192
200
|
status: "published",
|
|
193
201
|
excludePrivate: true,
|
|
202
|
+
publishedBefore,
|
|
194
203
|
limit: feedLimit,
|
|
195
204
|
});
|
|
196
205
|
|
|
@@ -198,7 +207,9 @@ async function buildFeaturedFeedData(
|
|
|
198
207
|
return { posts: [], postViews: [] };
|
|
199
208
|
}
|
|
200
209
|
|
|
201
|
-
const threadMap = await c.var.services.posts.getPublishedThreads(rootIds
|
|
210
|
+
const threadMap = await c.var.services.posts.getPublishedThreads(rootIds, {
|
|
211
|
+
publishedBefore,
|
|
212
|
+
});
|
|
202
213
|
|
|
203
214
|
// Extract root posts in the same order as rootIds
|
|
204
215
|
const posts: Post[] = [];
|
|
@@ -241,16 +252,11 @@ async function buildFeaturedFeedData(
|
|
|
241
252
|
aliasMap,
|
|
242
253
|
).map((postView, index) => {
|
|
243
254
|
const post = posts[index] as (typeof posts)[number];
|
|
244
|
-
|
|
245
|
-
// feedUpdatedAt = max(lastActivityAt, featuredAt)
|
|
246
|
-
const lastActivity = toISOString(post.lastActivityAt);
|
|
247
|
-
const featuredIso = post.featuredAt ? toISOString(post.featuredAt) : null;
|
|
248
|
-
const feedUpdatedAt =
|
|
249
|
-
featuredIso && featuredIso > lastActivity ? featuredIso : lastActivity;
|
|
255
|
+
const thread = threadMap.get(post.id);
|
|
250
256
|
|
|
251
257
|
return {
|
|
252
258
|
...postView,
|
|
253
|
-
feedUpdatedAt,
|
|
259
|
+
feedUpdatedAt: getFeedEntryUpdatedAt(post, thread),
|
|
254
260
|
threadReplies: buildThreadReplies(post.id, threadMap, mediaMap, mediaCtx),
|
|
255
261
|
};
|
|
256
262
|
});
|
|
@@ -280,12 +286,15 @@ export async function buildFeedData(
|
|
|
280
286
|
const siteUrl = appConfig.siteUrl;
|
|
281
287
|
const siteLanguage = appConfig.siteLanguage;
|
|
282
288
|
const feedLimit = appConfig.rssFeedLimit;
|
|
289
|
+
const publishedBefore = getRssPublishedBefore(
|
|
290
|
+
appConfig.rssPublishDelaySeconds,
|
|
291
|
+
);
|
|
283
292
|
const kind = opts.kind;
|
|
284
293
|
|
|
285
294
|
const { postViews } =
|
|
286
295
|
kind === "featured"
|
|
287
|
-
? await buildFeaturedFeedData(c, feedLimit)
|
|
288
|
-
: await buildLatestFeedData(c, feedLimit, opts.format);
|
|
296
|
+
? await buildFeaturedFeedData(c, feedLimit, publishedBefore)
|
|
297
|
+
: await buildLatestFeedData(c, feedLimit, publishedBefore, opts.format);
|
|
289
298
|
|
|
290
299
|
return {
|
|
291
300
|
siteName,
|
|
@@ -333,7 +342,7 @@ export function renderFeed(xml: string) {
|
|
|
333
342
|
return new Response(xml, {
|
|
334
343
|
headers: {
|
|
335
344
|
"Content-Type": "application/atom+xml; charset=utf-8",
|
|
336
|
-
"Cache-Control":
|
|
345
|
+
"Cache-Control": RSS_FEED_CACHE_CONTROL,
|
|
337
346
|
},
|
|
338
347
|
});
|
|
339
348
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { createTestApp } from "../../../__tests__/helpers/app.js";
|
|
3
3
|
import { archiveRoutes } from "../archive.js";
|
|
4
4
|
|
|
@@ -21,13 +21,45 @@ async function fetchFeed(
|
|
|
21
21
|
return res.text();
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function setupApp() {
|
|
25
|
-
const { app, services } = createTestApp({
|
|
24
|
+
function setupApp(rssPublishDelaySeconds = 0) {
|
|
25
|
+
const { app, services } = createTestApp({
|
|
26
|
+
authenticated: false,
|
|
27
|
+
rssPublishDelaySeconds,
|
|
28
|
+
});
|
|
26
29
|
app.route("/archive", archiveRoutes);
|
|
27
30
|
return { app, services };
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
vi.restoreAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
30
37
|
describe("archive feed filter params", () => {
|
|
38
|
+
it("applies the RSS publication delay", async () => {
|
|
39
|
+
const currentTime = 2_000_000;
|
|
40
|
+
vi.spyOn(Date, "now").mockReturnValue(currentTime * 1000);
|
|
41
|
+
const { app, services } = setupApp(300);
|
|
42
|
+
await services.posts.create({
|
|
43
|
+
format: "note",
|
|
44
|
+
title: "Eligible Archive Post",
|
|
45
|
+
bodyMarkdown: "Old enough for RSS",
|
|
46
|
+
status: "published",
|
|
47
|
+
publishedAt: currentTime - 300,
|
|
48
|
+
});
|
|
49
|
+
await services.posts.create({
|
|
50
|
+
format: "note",
|
|
51
|
+
title: "Recent Archive Post",
|
|
52
|
+
bodyMarkdown: "Still inside the edit window",
|
|
53
|
+
status: "published",
|
|
54
|
+
publishedAt: currentTime,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const xml = await fetchFeed(app, "");
|
|
58
|
+
|
|
59
|
+
expect(xml).toContain("Eligible Archive Post");
|
|
60
|
+
expect(xml).not.toContain("Recent Archive Post");
|
|
61
|
+
});
|
|
62
|
+
|
|
31
63
|
it("filters by title with the new param and the legacy fallback", async () => {
|
|
32
64
|
const { app, services } = setupApp();
|
|
33
65
|
await services.posts.create({
|
|
@@ -133,3 +165,60 @@ describe("archive page legacy param redirect", () => {
|
|
|
133
165
|
expect(res.headers.get("location")).toBe("/archive?title=any");
|
|
134
166
|
});
|
|
135
167
|
});
|
|
168
|
+
|
|
169
|
+
describe("archive page ordering", () => {
|
|
170
|
+
it("orders threads by their root publication time, not recent replies", async () => {
|
|
171
|
+
const { app, services } = setupApp();
|
|
172
|
+
const olderThread = await services.posts.create({
|
|
173
|
+
format: "note",
|
|
174
|
+
title: "Older active thread",
|
|
175
|
+
bodyMarkdown: "older root",
|
|
176
|
+
publishedAt: Date.UTC(2024, 0, 1) / 1000,
|
|
177
|
+
});
|
|
178
|
+
await services.posts.create({
|
|
179
|
+
format: "note",
|
|
180
|
+
bodyMarkdown: "recent reply",
|
|
181
|
+
replyToId: olderThread.id,
|
|
182
|
+
publishedAt: Date.UTC(2026, 0, 1) / 1000,
|
|
183
|
+
});
|
|
184
|
+
await services.posts.create({
|
|
185
|
+
format: "note",
|
|
186
|
+
title: "Newer root thread",
|
|
187
|
+
bodyMarkdown: "newer root",
|
|
188
|
+
publishedAt: Date.UTC(2025, 0, 1) / 1000,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const res = await app.request("/archive");
|
|
192
|
+
expect(res.status).toBe(200);
|
|
193
|
+
const html = await res.text();
|
|
194
|
+
|
|
195
|
+
expect(html.indexOf("Newer root thread")).toBeLessThan(
|
|
196
|
+
html.indexOf("Older active thread"),
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("keeps newer posts ahead of older pinned posts", async () => {
|
|
201
|
+
const { app, services } = setupApp();
|
|
202
|
+
await services.posts.create({
|
|
203
|
+
format: "note",
|
|
204
|
+
title: "Older pinned post",
|
|
205
|
+
bodyMarkdown: "older",
|
|
206
|
+
pinned: true,
|
|
207
|
+
publishedAt: Date.UTC(2025, 0, 1) / 1000,
|
|
208
|
+
});
|
|
209
|
+
await services.posts.create({
|
|
210
|
+
format: "note",
|
|
211
|
+
title: "Newer unpinned post",
|
|
212
|
+
bodyMarkdown: "newer",
|
|
213
|
+
publishedAt: Date.UTC(2026, 0, 1) / 1000,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const res = await app.request("/archive?view=list");
|
|
217
|
+
expect(res.status).toBe(200);
|
|
218
|
+
const html = await res.text();
|
|
219
|
+
|
|
220
|
+
expect(html.indexOf("Newer unpinned post")).toBeLessThan(
|
|
221
|
+
html.indexOf("Older pinned post"),
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { createTestApp } from "../../../__tests__/helpers/app.js";
|
|
3
3
|
import { collectionRoutes } from "../collection.js";
|
|
4
4
|
import { collectionsPageRoutes } from "../collections.js";
|
|
5
5
|
import { pageRoutes } from "../page.js";
|
|
6
6
|
|
|
7
|
-
function createCollectionRoutingTestApp() {
|
|
8
|
-
const testApp = createTestApp();
|
|
7
|
+
function createCollectionRoutingTestApp(rssPublishDelaySeconds = 0) {
|
|
8
|
+
const testApp = createTestApp({ rssPublishDelaySeconds });
|
|
9
9
|
const { app } = testApp;
|
|
10
10
|
|
|
11
11
|
app.use("*", async (c, next) => {
|
|
@@ -21,6 +21,10 @@ function createCollectionRoutingTestApp() {
|
|
|
21
21
|
return testApp;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
24
28
|
async function createCollectionWithPost(
|
|
25
29
|
services: ReturnType<typeof createTestApp>["services"],
|
|
26
30
|
{
|
|
@@ -42,7 +46,7 @@ async function createCollectionWithPost(
|
|
|
42
46
|
bodyMarkdown: bodyMarkdown ?? postTitle,
|
|
43
47
|
status: "published",
|
|
44
48
|
});
|
|
45
|
-
await services.collections.
|
|
49
|
+
await services.collections.addThread(collection.id, post.id);
|
|
46
50
|
return { collection, post };
|
|
47
51
|
}
|
|
48
52
|
|
|
@@ -50,11 +54,17 @@ describe("Collection Routing", () => {
|
|
|
50
54
|
it("renders a single collection at its root path", async () => {
|
|
51
55
|
const { app, services } = createCollectionRoutingTestApp();
|
|
52
56
|
|
|
53
|
-
await createCollectionWithPost(services, {
|
|
57
|
+
const { post } = await createCollectionWithPost(services, {
|
|
54
58
|
slug: "reading",
|
|
55
59
|
title: "Reading",
|
|
56
60
|
postTitle: "Book log",
|
|
57
61
|
});
|
|
62
|
+
await services.posts.create({
|
|
63
|
+
format: "note",
|
|
64
|
+
bodyMarkdown: "Thread follow-up",
|
|
65
|
+
status: "published",
|
|
66
|
+
replyToId: post.id,
|
|
67
|
+
});
|
|
58
68
|
|
|
59
69
|
const res = await app.request("/reading");
|
|
60
70
|
|
|
@@ -64,6 +74,23 @@ describe("Collection Routing", () => {
|
|
|
64
74
|
const html = await res.text();
|
|
65
75
|
expect(html).toContain("Reading");
|
|
66
76
|
expect(html).toContain("Book log");
|
|
77
|
+
expect(html).toContain("Thread follow-up");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("hides the collection feed button when feeds are disabled", async () => {
|
|
81
|
+
const { app, services } = createCollectionRoutingTestApp();
|
|
82
|
+
await services.settings.set("RSS_FEEDS_ENABLED", "false");
|
|
83
|
+
await createCollectionWithPost(services, {
|
|
84
|
+
slug: "reading",
|
|
85
|
+
title: "Reading",
|
|
86
|
+
postTitle: "Book log",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const response = await app.request("/reading");
|
|
90
|
+
const html = await response.text();
|
|
91
|
+
|
|
92
|
+
expect(response.status).toBe(200);
|
|
93
|
+
expect(html).not.toContain('class="feed-link"');
|
|
67
94
|
});
|
|
68
95
|
|
|
69
96
|
it("redirects namespaced single-collection paths to the root canonical URL", async () => {
|
|
@@ -139,6 +166,37 @@ describe("Collection Routing", () => {
|
|
|
139
166
|
expect(xml).toContain("Film log");
|
|
140
167
|
});
|
|
141
168
|
|
|
169
|
+
it("applies the RSS publication delay to Collection feeds", async () => {
|
|
170
|
+
const currentTime = 2_000_000;
|
|
171
|
+
vi.spyOn(Date, "now").mockReturnValue(currentTime * 1000);
|
|
172
|
+
const { app, services } = createCollectionRoutingTestApp(300);
|
|
173
|
+
const collection = await services.collections.create({
|
|
174
|
+
slug: "reading",
|
|
175
|
+
title: "Reading",
|
|
176
|
+
});
|
|
177
|
+
const eligible = await services.posts.create({
|
|
178
|
+
format: "note",
|
|
179
|
+
title: "Eligible Book Log",
|
|
180
|
+
bodyMarkdown: "Old enough for RSS",
|
|
181
|
+
status: "published",
|
|
182
|
+
publishedAt: currentTime - 300,
|
|
183
|
+
});
|
|
184
|
+
const recent = await services.posts.create({
|
|
185
|
+
format: "note",
|
|
186
|
+
title: "Recent Book Log",
|
|
187
|
+
bodyMarkdown: "Still inside the edit window",
|
|
188
|
+
status: "published",
|
|
189
|
+
publishedAt: currentTime,
|
|
190
|
+
});
|
|
191
|
+
await services.collections.addThread(collection.id, eligible.id);
|
|
192
|
+
await services.collections.addThread(collection.id, recent.id);
|
|
193
|
+
|
|
194
|
+
const xml = await (await app.request("/reading/feed")).text();
|
|
195
|
+
|
|
196
|
+
expect(xml).toContain("Eligible Book Log");
|
|
197
|
+
expect(xml).not.toContain("Recent Book Log");
|
|
198
|
+
});
|
|
199
|
+
|
|
142
200
|
it("redirects canonical collection pages and feeds to collection aliases", async () => {
|
|
143
201
|
const { app, services } = createCollectionRoutingTestApp();
|
|
144
202
|
|
|
@@ -30,7 +30,7 @@ describe("Collections Listing Page - Data Logic", () => {
|
|
|
30
30
|
);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
it("returns collections with
|
|
33
|
+
it("returns collections with Thread counts and recent activity", async () => {
|
|
34
34
|
const recipes = await collectionService.create({
|
|
35
35
|
slug: "recipes",
|
|
36
36
|
title: "Recipes",
|
|
@@ -40,7 +40,7 @@ describe("Collections Listing Page - Data Logic", () => {
|
|
|
40
40
|
title: "Travel",
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
// Add
|
|
43
|
+
// Add Threads to the recipes Collection.
|
|
44
44
|
const p1 = await postService.create({
|
|
45
45
|
format: "note",
|
|
46
46
|
bodyMarkdown: "Recipe 1",
|
|
@@ -49,8 +49,8 @@ describe("Collections Listing Page - Data Logic", () => {
|
|
|
49
49
|
format: "note",
|
|
50
50
|
bodyMarkdown: "Recipe 2",
|
|
51
51
|
});
|
|
52
|
-
await collectionService.
|
|
53
|
-
await collectionService.
|
|
52
|
+
await collectionService.addThread(recipes.id, p1.id);
|
|
53
|
+
await collectionService.addThread(recipes.id, p2.id);
|
|
54
54
|
|
|
55
55
|
const directory = await collectionService.listDirectoryData();
|
|
56
56
|
|
|
@@ -59,9 +59,9 @@ describe("Collections Listing Page - Data Logic", () => {
|
|
|
59
59
|
(c) => c.slug === "recipes",
|
|
60
60
|
);
|
|
61
61
|
const travelResult = directory.collections.find((c) => c.slug === "travel");
|
|
62
|
-
expect(recipesResult?.
|
|
62
|
+
expect(recipesResult?.threadCount).toBe(2);
|
|
63
63
|
expect(recipesResult?.recentActivityAt).toBe(p2.lastActivityAt);
|
|
64
|
-
expect(travelResult?.
|
|
64
|
+
expect(travelResult?.threadCount).toBe(0);
|
|
65
65
|
expect(travelResult?.recentActivityAt).toBeGreaterThan(0);
|
|
66
66
|
});
|
|
67
67
|
|
|
@@ -86,13 +86,13 @@ describe("Collections Listing Page - Data Logic", () => {
|
|
|
86
86
|
bodyMarkdown: "Will remain",
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
await collectionService.
|
|
90
|
-
await collectionService.
|
|
89
|
+
await collectionService.addThread(col.id, post.id);
|
|
90
|
+
await collectionService.addThread(col.id, post2.id);
|
|
91
91
|
|
|
92
92
|
await postService.delete(post.id);
|
|
93
93
|
|
|
94
94
|
const directory = await collectionService.listDirectoryData();
|
|
95
|
-
expect(directory.collections[0]?.
|
|
95
|
+
expect(directory.collections[0]?.threadCount).toBe(1);
|
|
96
96
|
expect(directory.collections[0]?.recentActivityAt).toBe(
|
|
97
97
|
post2.lastActivityAt,
|
|
98
98
|
);
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createTestApp } from "../../../__tests__/helpers/app.js";
|
|
3
|
+
import { pageRoutes } from "../page.js";
|
|
4
|
+
|
|
5
|
+
function createPageTestApp(options: { authenticated?: boolean } = {}) {
|
|
6
|
+
const testApp = createTestApp(options);
|
|
7
|
+
testApp.app.route("/", pageRoutes);
|
|
8
|
+
return testApp;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function createDraft(
|
|
12
|
+
services: ReturnType<typeof createTestApp>["services"],
|
|
13
|
+
title = "A private draft",
|
|
14
|
+
) {
|
|
15
|
+
return services.posts.create({
|
|
16
|
+
format: "note",
|
|
17
|
+
title,
|
|
18
|
+
bodyMarkdown: `${title} body`,
|
|
19
|
+
status: "draft",
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("draft preview route", () => {
|
|
24
|
+
it("redirects anonymous visitors to sign in and preserves the preview path", async () => {
|
|
25
|
+
const { app, services } = createPageTestApp();
|
|
26
|
+
const draft = await createDraft(services);
|
|
27
|
+
|
|
28
|
+
const res = await app.request(`/preview/${draft.slug}`, {
|
|
29
|
+
redirect: "manual",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(res.status).toBe(302);
|
|
33
|
+
expect(res.headers.get("Location")).toBe(
|
|
34
|
+
`/signin?redirect=${encodeURIComponent(`/preview/${draft.slug}`)}`,
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("renders the public post view with explicit private preview signals", async () => {
|
|
39
|
+
const { app, services } = createPageTestApp({ authenticated: true });
|
|
40
|
+
const draft = await createDraft(services);
|
|
41
|
+
|
|
42
|
+
const res = await app.request(`/preview/${draft.slug}`);
|
|
43
|
+
|
|
44
|
+
expect(res.status).toBe(200);
|
|
45
|
+
expect(res.headers.get("Cache-Control")).toBe("private, no-store");
|
|
46
|
+
expect(res.headers.get("X-Robots-Tag")).toBe("noindex, nofollow");
|
|
47
|
+
|
|
48
|
+
const html = await res.text();
|
|
49
|
+
expect(html).toContain(`data-post-id="${draft.id}"`);
|
|
50
|
+
expect(html).toContain("A private draft body");
|
|
51
|
+
expect(html).toContain("data-preview-status");
|
|
52
|
+
expect(html).toContain("Draft preview");
|
|
53
|
+
expect(html).toContain("This post isn’t published.");
|
|
54
|
+
expect(html).toContain('<meta name="robots" content="noindex, nofollow"');
|
|
55
|
+
expect(html).not.toContain('rel="canonical"');
|
|
56
|
+
expect(html).not.toContain("application/ld+json");
|
|
57
|
+
expect(html).not.toContain("article:published_time");
|
|
58
|
+
expect(html).not.toContain('id="compose-dialog"');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("renders every draft in a saved thread", async () => {
|
|
62
|
+
const { app, services } = createPageTestApp({ authenticated: true });
|
|
63
|
+
const root = await createDraft(services, "Thread root");
|
|
64
|
+
const reply = await services.posts.create({
|
|
65
|
+
format: "quote",
|
|
66
|
+
quoteText: "Unpublished reply quote",
|
|
67
|
+
status: "draft",
|
|
68
|
+
replyToId: root.id,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const res = await app.request(`/preview/${root.slug}`);
|
|
72
|
+
|
|
73
|
+
expect(res.status).toBe(200);
|
|
74
|
+
const html = await res.text();
|
|
75
|
+
expect(html).toContain("Thread root body");
|
|
76
|
+
expect(html).toContain("Unpublished reply quote");
|
|
77
|
+
expect(html).toContain(`/preview/${reply.slug}`);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("redirects a published post preview to its public permalink", async () => {
|
|
81
|
+
const { app, services } = createPageTestApp({ authenticated: true });
|
|
82
|
+
const post = await services.posts.create({
|
|
83
|
+
format: "note",
|
|
84
|
+
bodyMarkdown: "Already public",
|
|
85
|
+
status: "published",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const res = await app.request(`/preview/${post.slug}`, {
|
|
89
|
+
redirect: "manual",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(res.status).toBe(302);
|
|
93
|
+
expect(res.headers.get("Location")).toBe(`/${post.slug}`);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("returns 404 for the preview root and unknown draft slugs", async () => {
|
|
97
|
+
const { app } = createPageTestApp({ authenticated: true });
|
|
98
|
+
|
|
99
|
+
expect((await app.request("/preview")).status).toBe(404);
|
|
100
|
+
expect((await app.request("/preview/missing-draft")).status).toBe(404);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("reserved path and redirect precedence", () => {
|
|
105
|
+
it("ignores a legacy redirect that collides with the preview route", async () => {
|
|
106
|
+
const { app, services } = createPageTestApp({ authenticated: true });
|
|
107
|
+
const draft = await createDraft(services);
|
|
108
|
+
await services.paths.create({
|
|
109
|
+
path: `preview/${draft.slug}`,
|
|
110
|
+
kind: "redirect",
|
|
111
|
+
redirectToPath: "elsewhere",
|
|
112
|
+
redirectType: 301,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const res = await app.request(`/preview/${draft.slug}`, {
|
|
116
|
+
redirect: "manual",
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(res.status).toBe(200);
|
|
120
|
+
expect(await res.text()).toContain(`data-post-id="${draft.id}"`);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("makes a legacy canonical preview path inert", async () => {
|
|
124
|
+
const { app, services } = createPageTestApp({ authenticated: true });
|
|
125
|
+
const post = await services.posts.create({
|
|
126
|
+
format: "note",
|
|
127
|
+
bodyMarkdown: "Old preview post",
|
|
128
|
+
status: "published",
|
|
129
|
+
});
|
|
130
|
+
await services.paths.updatePostSlug(post.id, "preview");
|
|
131
|
+
|
|
132
|
+
const res = await app.request("/preview", { redirect: "manual" });
|
|
133
|
+
|
|
134
|
+
expect(res.status).toBe(404);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("keeps ordinary redirects working and ahead of derived collection feeds", async () => {
|
|
138
|
+
const { app, services } = createPageTestApp();
|
|
139
|
+
const collection = await services.collections.create({
|
|
140
|
+
slug: "reading",
|
|
141
|
+
title: "Reading",
|
|
142
|
+
});
|
|
143
|
+
const post = await services.posts.create({
|
|
144
|
+
format: "note",
|
|
145
|
+
bodyMarkdown: "Book log",
|
|
146
|
+
status: "published",
|
|
147
|
+
});
|
|
148
|
+
await services.collections.addThread(collection.id, post.id);
|
|
149
|
+
await services.paths.create({
|
|
150
|
+
path: "reading/feed",
|
|
151
|
+
kind: "redirect",
|
|
152
|
+
redirectToPath: "archive",
|
|
153
|
+
redirectType: 302,
|
|
154
|
+
});
|
|
155
|
+
await services.paths.create({
|
|
156
|
+
path: "old-link",
|
|
157
|
+
kind: "redirect",
|
|
158
|
+
redirectToPath: "reading",
|
|
159
|
+
redirectType: 301,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const feedRes = await app.request("/reading/feed", {
|
|
163
|
+
redirect: "manual",
|
|
164
|
+
});
|
|
165
|
+
expect(feedRes.status).toBe(302);
|
|
166
|
+
expect(feedRes.headers.get("Location")).toBe("/archive");
|
|
167
|
+
|
|
168
|
+
const ordinaryRes = await app.request("/old-link", {
|
|
169
|
+
redirect: "manual",
|
|
170
|
+
});
|
|
171
|
+
expect(ordinaryRes.status).toBe(301);
|
|
172
|
+
expect(ordinaryRes.headers.get("Location")).toBe("/reading");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createTestApp } from "../../../__tests__/helpers/app.js";
|
|
3
|
+
import { searchRoutes } from "../search.js";
|
|
4
|
+
|
|
5
|
+
describe("Search Page Routes", () => {
|
|
6
|
+
it("keeps public HTML search available when anonymous JSON APIs are off", async () => {
|
|
7
|
+
const { app, services } = createTestApp({
|
|
8
|
+
authenticated: false,
|
|
9
|
+
fts: true,
|
|
10
|
+
});
|
|
11
|
+
await services.settings.set("PUBLIC_API_ENABLED", "false");
|
|
12
|
+
app.route("/search", searchRoutes);
|
|
13
|
+
|
|
14
|
+
const response = await app.request("/search?q=marker");
|
|
15
|
+
|
|
16
|
+
expect(response.status).toBe(200);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("hides visible Thread collection tags on a matching child post", async () => {
|
|
20
|
+
const { app, services } = createTestApp({ fts: true });
|
|
21
|
+
app.route("/search", searchRoutes);
|
|
22
|
+
|
|
23
|
+
const collection = await services.collections.create({
|
|
24
|
+
slug: "search-thread",
|
|
25
|
+
title: "Search Thread Collection",
|
|
26
|
+
});
|
|
27
|
+
const root = await services.posts.create({
|
|
28
|
+
format: "note",
|
|
29
|
+
bodyMarkdown: "Thread root",
|
|
30
|
+
});
|
|
31
|
+
await services.posts.create({
|
|
32
|
+
format: "note",
|
|
33
|
+
bodyMarkdown: "Unique child search marker",
|
|
34
|
+
replyToId: root.id,
|
|
35
|
+
});
|
|
36
|
+
await services.collections.addThread(collection.id, root.id);
|
|
37
|
+
|
|
38
|
+
const response = await app.request("/search?q=marker");
|
|
39
|
+
|
|
40
|
+
expect(response.status).toBe(200);
|
|
41
|
+
const html = await response.text();
|
|
42
|
+
expect(html).toContain("Unique child search");
|
|
43
|
+
expect(html).toContain("<mark>marker</mark>");
|
|
44
|
+
expect(html).not.toContain("Search Thread Collection");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("shows Thread collection tags on a matching root post", async () => {
|
|
48
|
+
const { app, services } = createTestApp({ fts: true });
|
|
49
|
+
app.route("/search", searchRoutes);
|
|
50
|
+
|
|
51
|
+
const collection = await services.collections.create({
|
|
52
|
+
slug: "search-root",
|
|
53
|
+
title: "Root Search Collection",
|
|
54
|
+
});
|
|
55
|
+
const root = await services.posts.create({
|
|
56
|
+
format: "note",
|
|
57
|
+
bodyMarkdown: "Unique root search marker",
|
|
58
|
+
});
|
|
59
|
+
await services.collections.addThread(collection.id, root.id);
|
|
60
|
+
|
|
61
|
+
const response = await app.request("/search?q=marker");
|
|
62
|
+
|
|
63
|
+
expect(response.status).toBe(200);
|
|
64
|
+
const html = await response.text();
|
|
65
|
+
expect(html).toContain("Unique root search");
|
|
66
|
+
expect(html).toContain("<mark>marker</mark>");
|
|
67
|
+
expect(html).toContain("Root Search Collection");
|
|
68
|
+
});
|
|
69
|
+
});
|