@jant/core 0.6.14 → 0.6.16
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 +88 -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-C75Zc8dD.js +6 -0
- package/dist/{app-2i7-WQHg.js → app-C7_4oab9.js} +7774 -6425
- package/dist/client/.vite/manifest.json +8 -8
- package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
- package/dist/client/_assets/{client-BvID5ucz.js → client-Bq0vre8Y.js} +59 -38
- package/dist/client/_assets/client-D3VZyG4L.css +2 -0
- package/dist/client/_assets/client-auth-F2rb9WfF.js +5571 -0
- package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
- package/dist/{export-DWn149b7.js → export-Dl5KCiEs.js} +1348 -128
- package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
- package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
- package/dist/{github-sync-C0zyF6XS.js → github-sync-Bqg62IvV.js} +3 -3
- package/dist/{github-sync-Ckk0sJxK.js → github-sync-HdK2EgvJ.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +7 -7
- package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
- 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__/mise-config.test.ts +22 -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 +316 -2
- package/src/client/__tests__/compose-shortcuts.test.ts +20 -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 +82 -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__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +2268 -206
- package/src/client/components/__tests__/jant-compose-editor.test.ts +147 -59
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +114 -64
- 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-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +55 -3
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +14 -7
- package/src/client/components/jant-compose-dialog.ts +1542 -694
- package/src/client/components/jant-compose-editor.ts +237 -99
- package/src/client/components/jant-compose-fullscreen.ts +27 -9
- 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 +177 -95
- 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 +147 -123
- package/src/client/compose-launch.ts +13 -0
- package/src/client/compose-shortcuts.ts +5 -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/post-refresh.ts +135 -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__/link-input-rules.test.ts +121 -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/link-input-rules.ts +4 -4
- 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 +133 -39
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -0
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -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-activity.test.ts +113 -0
- package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -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/0031_many_ego.sql +3 -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/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/_journal.json +35 -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/0029_rare_hairball.sql +3 -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/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/_journal.json +35 -0
- package/src/db/pg/schema.ts +46 -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 +48 -20
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +222 -51
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +216 -45
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +216 -45
- 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__/feed.test.ts +168 -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 +24 -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__/slug.test.ts +18 -0
- 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__/translit.test.ts +87 -0
- package/src/lib/__tests__/url.test.ts +53 -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/feed.ts +113 -11
- package/src/lib/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +15 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +193 -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 +47 -8
- 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 +137 -131
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +359 -83
- package/src/lib/translit.ts +288 -0
- package/src/lib/url.ts +123 -12
- package/src/lib/view.ts +58 -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__/palette.test.ts +44 -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 +14 -10
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +246 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +48 -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 +8 -3
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +384 -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__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/preview.test.ts +176 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +90 -22
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +86 -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 +190 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +249 -39
- package/src/services/__tests__/post.test.ts +844 -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 +224 -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 +94 -85
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/path.ts +4 -1
- package/src/services/post.ts +1142 -342
- package/src/services/search.ts +17 -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 -14
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +76 -10
- package/src/styles/ui.css +2227 -1347
- 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 +14 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +20 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +22 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +109 -19
- 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/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +10 -20
- package/src/ui/feed/PostStatusBadges.tsx +42 -3
- 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/ArchivePage.tsx +131 -24
- 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 +70 -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 +49 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +38 -13
- 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/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +52 -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
|
@@ -0,0 +1,176 @@
|
|
|
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("Edit draft");
|
|
55
|
+
expect(html).toContain(`/preview/${draft.slug}?edit=1`);
|
|
56
|
+
expect(html).toContain('<meta name="robots" content="noindex, nofollow"');
|
|
57
|
+
expect(html).not.toContain('rel="canonical"');
|
|
58
|
+
expect(html).not.toContain("application/ld+json");
|
|
59
|
+
expect(html).not.toContain("article:published_time");
|
|
60
|
+
expect(html).toContain('id="compose-dialog"');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("renders every draft in a saved thread", async () => {
|
|
64
|
+
const { app, services } = createPageTestApp({ authenticated: true });
|
|
65
|
+
const root = await createDraft(services, "Thread root");
|
|
66
|
+
const reply = await services.posts.create({
|
|
67
|
+
format: "quote",
|
|
68
|
+
quoteText: "Unpublished reply quote",
|
|
69
|
+
status: "draft",
|
|
70
|
+
replyToId: root.id,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const res = await app.request(`/preview/${root.slug}`);
|
|
74
|
+
|
|
75
|
+
expect(res.status).toBe(200);
|
|
76
|
+
const html = await res.text();
|
|
77
|
+
expect(html).toContain("Thread root body");
|
|
78
|
+
expect(html).toContain("Unpublished reply quote");
|
|
79
|
+
expect(html).toContain(`/preview/${reply.slug}`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("redirects a published post preview to its public permalink", async () => {
|
|
83
|
+
const { app, services } = createPageTestApp({ authenticated: true });
|
|
84
|
+
const post = await services.posts.create({
|
|
85
|
+
format: "note",
|
|
86
|
+
bodyMarkdown: "Already public",
|
|
87
|
+
status: "published",
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const res = await app.request(`/preview/${post.slug}`, {
|
|
91
|
+
redirect: "manual",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(res.status).toBe(302);
|
|
95
|
+
expect(res.headers.get("Location")).toBe(`/${post.slug}`);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("returns 404 for the preview root and unknown draft slugs", async () => {
|
|
99
|
+
const { app } = createPageTestApp({ authenticated: true });
|
|
100
|
+
|
|
101
|
+
expect((await app.request("/preview")).status).toBe(404);
|
|
102
|
+
expect((await app.request("/preview/missing-draft")).status).toBe(404);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("reserved path and redirect precedence", () => {
|
|
107
|
+
it("ignores a legacy redirect that collides with the preview route", async () => {
|
|
108
|
+
const { app, services } = createPageTestApp({ authenticated: true });
|
|
109
|
+
const draft = await createDraft(services);
|
|
110
|
+
await services.paths.create({
|
|
111
|
+
path: `preview/${draft.slug}`,
|
|
112
|
+
kind: "redirect",
|
|
113
|
+
redirectToPath: "elsewhere",
|
|
114
|
+
redirectType: 301,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const res = await app.request(`/preview/${draft.slug}`, {
|
|
118
|
+
redirect: "manual",
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(res.status).toBe(200);
|
|
122
|
+
expect(await res.text()).toContain(`data-post-id="${draft.id}"`);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("makes a legacy canonical preview path inert", async () => {
|
|
126
|
+
const { app, services } = createPageTestApp({ authenticated: true });
|
|
127
|
+
const post = await services.posts.create({
|
|
128
|
+
format: "note",
|
|
129
|
+
bodyMarkdown: "Old preview post",
|
|
130
|
+
status: "published",
|
|
131
|
+
});
|
|
132
|
+
await services.paths.updatePostSlug(post.id, "preview");
|
|
133
|
+
|
|
134
|
+
const res = await app.request("/preview", { redirect: "manual" });
|
|
135
|
+
|
|
136
|
+
expect(res.status).toBe(404);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("keeps ordinary redirects working and ahead of derived collection feeds", async () => {
|
|
140
|
+
const { app, services } = createPageTestApp();
|
|
141
|
+
const collection = await services.collections.create({
|
|
142
|
+
slug: "reading",
|
|
143
|
+
title: "Reading",
|
|
144
|
+
});
|
|
145
|
+
const post = await services.posts.create({
|
|
146
|
+
format: "note",
|
|
147
|
+
bodyMarkdown: "Book log",
|
|
148
|
+
status: "published",
|
|
149
|
+
});
|
|
150
|
+
await services.collections.addThread(collection.id, post.id);
|
|
151
|
+
await services.paths.create({
|
|
152
|
+
path: "reading/feed",
|
|
153
|
+
kind: "redirect",
|
|
154
|
+
redirectToPath: "archive",
|
|
155
|
+
redirectType: 302,
|
|
156
|
+
});
|
|
157
|
+
await services.paths.create({
|
|
158
|
+
path: "old-link",
|
|
159
|
+
kind: "redirect",
|
|
160
|
+
redirectToPath: "reading",
|
|
161
|
+
redirectType: 301,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const feedRes = await app.request("/reading/feed", {
|
|
165
|
+
redirect: "manual",
|
|
166
|
+
});
|
|
167
|
+
expect(feedRes.status).toBe(302);
|
|
168
|
+
expect(feedRes.headers.get("Location")).toBe("/archive");
|
|
169
|
+
|
|
170
|
+
const ordinaryRes = await app.request("/old-link", {
|
|
171
|
+
redirect: "manual",
|
|
172
|
+
});
|
|
173
|
+
expect(ordinaryRes.status).toBe(301);
|
|
174
|
+
expect(ordinaryRes.headers.get("Location")).toBe("/reading");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -21,16 +21,22 @@ import type {
|
|
|
21
21
|
import type { AppVariables } from "../../types/app-context.js";
|
|
22
22
|
import type {
|
|
23
23
|
ArchiveFilters,
|
|
24
|
+
ArchiveSort,
|
|
24
25
|
ArchiveView,
|
|
25
26
|
ArchiveVisibility,
|
|
26
27
|
} from "../../types/props.js";
|
|
27
28
|
import { FORMATS, MEDIA_KINDS } from "../../types.js";
|
|
28
29
|
import { ArchivePage } from "../../ui/pages/ArchivePage.js";
|
|
29
30
|
import { defaultFeedRenderer } from "../../lib/feed.js";
|
|
31
|
+
import {
|
|
32
|
+
getFeedEntryUpdatedAt,
|
|
33
|
+
getRssPublishedBefore,
|
|
34
|
+
RSS_FEED_CACHE_CONTROL,
|
|
35
|
+
} from "../../lib/feed-policy.js";
|
|
30
36
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
31
37
|
import { buildPageTitle } from "../../lib/page-title.js";
|
|
32
38
|
import { renderPublicPage } from "../../lib/render.js";
|
|
33
|
-
import { formatYearMonth
|
|
39
|
+
import { formatYearMonth } from "../../lib/time.js";
|
|
34
40
|
import { toAbsoluteSiteUrl, toPublicPath } from "../../lib/url.js";
|
|
35
41
|
import {
|
|
36
42
|
createMediaContext,
|
|
@@ -61,6 +67,7 @@ interface ParsedArchiveParams {
|
|
|
61
67
|
visibility?: ArchiveVisibility;
|
|
62
68
|
visibilityAll: boolean;
|
|
63
69
|
view?: ArchiveView;
|
|
70
|
+
sort: ArchiveSort;
|
|
64
71
|
currentPage: number;
|
|
65
72
|
}
|
|
66
73
|
|
|
@@ -139,6 +146,11 @@ function parseArchiveParams(
|
|
|
139
146
|
? viewParam
|
|
140
147
|
: undefined;
|
|
141
148
|
|
|
149
|
+
// sort selects the time axis for ordering, month grouping, and the year
|
|
150
|
+
// filter alike — they must stay on the same column or the month headers
|
|
151
|
+
// stop agreeing with the order inside them.
|
|
152
|
+
const sort: ArchiveSort = q("sort") === "updated" ? "updated" : "published";
|
|
153
|
+
|
|
142
154
|
// Page always comes from the actual request URL (pagination links use ?page=N)
|
|
143
155
|
const pageParam = c.req.query("page");
|
|
144
156
|
const currentPage = Math.max(1, parseInt(pageParam || "1", 10) || 1);
|
|
@@ -154,6 +166,7 @@ function parseArchiveParams(
|
|
|
154
166
|
visibility,
|
|
155
167
|
visibilityAll,
|
|
156
168
|
view,
|
|
169
|
+
sort,
|
|
157
170
|
currentPage,
|
|
158
171
|
};
|
|
159
172
|
}
|
|
@@ -182,12 +195,15 @@ function buildArchivePostFilters(
|
|
|
182
195
|
: (params.visibility ?? undefined)
|
|
183
196
|
: undefined;
|
|
184
197
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
198
|
+
// The year filter follows the active axis, so every month bucket shown
|
|
199
|
+
// under `year=N` really belongs to that year.
|
|
200
|
+
const yearRange = params.validYear
|
|
201
|
+
? {
|
|
202
|
+
after: Date.UTC(params.validYear, 0, 1) / 1000,
|
|
203
|
+
before: Date.UTC(params.validYear + 1, 0, 1) / 1000,
|
|
204
|
+
}
|
|
205
|
+
: undefined;
|
|
206
|
+
const sortsByActivity = params.sort === "updated";
|
|
191
207
|
|
|
192
208
|
return {
|
|
193
209
|
format: params.format,
|
|
@@ -201,18 +217,28 @@ function buildArchivePostFilters(
|
|
|
201
217
|
? { visibility: effectiveVisibility }
|
|
202
218
|
: {}),
|
|
203
219
|
collectionId,
|
|
204
|
-
|
|
205
|
-
|
|
220
|
+
...(yearRange
|
|
221
|
+
? sortsByActivity
|
|
222
|
+
? { axisAfter: yearRange.after, axisBefore: yearRange.before }
|
|
223
|
+
: { publishedAfter: yearRange.after, publishedBefore: yearRange.before }
|
|
224
|
+
: {}),
|
|
206
225
|
mediaKinds: params.mediaKinds,
|
|
207
226
|
hasMedia: params.hasMedia,
|
|
208
227
|
hasTitle: params.hasTitle,
|
|
209
228
|
hasReplies: params.hasReplies,
|
|
229
|
+
// "thread_updated", not "activity": the archive is the canonical all-posts
|
|
230
|
+
// view, and the quiet-reply switch only promises not to move a Thread on
|
|
231
|
+
// Latest. Here the honest answer is when the Thread last changed.
|
|
232
|
+
sortBy: sortsByActivity ? "thread_updated" : "published",
|
|
233
|
+
ignorePinnedSort: true,
|
|
210
234
|
};
|
|
211
235
|
}
|
|
212
236
|
|
|
213
237
|
/**
|
|
214
238
|
* Build a query string from parsed archive params (for feed self-URL and
|
|
215
|
-
* archive page feed link). Omits page
|
|
239
|
+
* archive page feed link). Omits view and page — those shape the rendered
|
|
240
|
+
* page, not the result set — but carries `sort`, so the feed button on an
|
|
241
|
+
* updated-sorted page hands back the matching feed.
|
|
216
242
|
*/
|
|
217
243
|
function buildArchiveFeedQuery(params: ParsedArchiveParams): string {
|
|
218
244
|
const qs = new URLSearchParams();
|
|
@@ -230,6 +256,7 @@ function buildArchiveFeedQuery(params: ParsedArchiveParams): string {
|
|
|
230
256
|
if (params.hasReplies !== undefined) {
|
|
231
257
|
qs.set("replies", params.hasReplies ? "any" : "none");
|
|
232
258
|
}
|
|
259
|
+
if (params.sort === "updated") qs.set("sort", "updated");
|
|
233
260
|
const str = qs.toString();
|
|
234
261
|
return str ? `?${str}` : "";
|
|
235
262
|
}
|
|
@@ -331,6 +358,7 @@ export async function renderArchivePage(
|
|
|
331
358
|
services.posts.getDistinctYears({
|
|
332
359
|
status: "published",
|
|
333
360
|
excludeReplies: true,
|
|
361
|
+
sortBy: filters.sortBy,
|
|
334
362
|
}),
|
|
335
363
|
services.collections.list(),
|
|
336
364
|
]);
|
|
@@ -358,8 +386,12 @@ export async function renderArchivePage(
|
|
|
358
386
|
|
|
359
387
|
const grouped = new Map<string, PostWithMedia[]>();
|
|
360
388
|
for (const post of posts) {
|
|
361
|
-
|
|
362
|
-
const
|
|
389
|
+
// Bucket on the same axis the query sorted by, so groups stay ordered.
|
|
390
|
+
const groupedAt =
|
|
391
|
+
params.sort === "updated"
|
|
392
|
+
? post.threadUpdatedAt
|
|
393
|
+
: (post.publishedAt ?? post.updatedAt);
|
|
394
|
+
const key = formatYearMonth(groupedAt, appConfig.timeZone);
|
|
363
395
|
if (!grouped.has(key)) {
|
|
364
396
|
grouped.set(key, []);
|
|
365
397
|
}
|
|
@@ -431,6 +463,7 @@ export async function renderArchivePage(
|
|
|
431
463
|
hasReplies: params.hasReplies,
|
|
432
464
|
visibility: effectiveVisibility,
|
|
433
465
|
view: params.view,
|
|
466
|
+
sort: params.sort === "updated" ? "updated" : undefined,
|
|
434
467
|
};
|
|
435
468
|
|
|
436
469
|
const feedQuery = buildArchiveFeedQuery(params);
|
|
@@ -441,7 +474,12 @@ export async function renderArchivePage(
|
|
|
441
474
|
}));
|
|
442
475
|
|
|
443
476
|
return renderPublicPage(c, {
|
|
444
|
-
|
|
477
|
+
// Distinguishes a bookmarked or shared ?sort=updated view in the tab bar.
|
|
478
|
+
title: buildPageTitle(
|
|
479
|
+
"Archive",
|
|
480
|
+
params.sort === "updated" ? "Recently updated" : undefined,
|
|
481
|
+
navData.siteName,
|
|
482
|
+
),
|
|
445
483
|
navData,
|
|
446
484
|
content: (
|
|
447
485
|
<ArchivePage
|
|
@@ -456,7 +494,9 @@ export async function renderArchivePage(
|
|
|
456
494
|
isAuthenticated={navData.isAuthenticated}
|
|
457
495
|
sitePathPrefix={navData.sitePathPrefix}
|
|
458
496
|
timeZone={appConfig.timeZone}
|
|
459
|
-
feedHref={
|
|
497
|
+
feedHref={
|
|
498
|
+
appConfig.rssFeedsEnabled ? `/archive/feed${feedQuery}` : undefined
|
|
499
|
+
}
|
|
460
500
|
/>
|
|
461
501
|
),
|
|
462
502
|
});
|
|
@@ -615,10 +655,27 @@ async function buildArchiveFeedData(
|
|
|
615
655
|
const collection = params.collectionSlug
|
|
616
656
|
? await services.collections.getBySlug(params.collectionSlug)
|
|
617
657
|
: undefined;
|
|
658
|
+
const rssPublishedBefore = getRssPublishedBefore(
|
|
659
|
+
appConfig.rssPublishDelaySeconds,
|
|
660
|
+
);
|
|
661
|
+
const yearPublishedBefore = params.validYear
|
|
662
|
+
? Date.UTC(params.validYear + 1, 0, 1) / 1000
|
|
663
|
+
: undefined;
|
|
618
664
|
|
|
619
665
|
// Feed mirrors the unauthenticated archive page: published + non-private,
|
|
620
666
|
// including Hidden-from-Latest. /archive is the canonical "all posts" view,
|
|
621
667
|
// so its feed must match.
|
|
668
|
+
//
|
|
669
|
+
// Ordered by publication by default, like the page it belongs to. Ordering
|
|
670
|
+
// by activity would make the feed's contents shift under a fixed
|
|
671
|
+
// `rssFeedLimit` — a new reply pulls an old Thread back into the window and
|
|
672
|
+
// pushes something else out — and it buys nothing in return, because readers
|
|
673
|
+
// key entries by id and will not re-surface one that merely moved.
|
|
674
|
+
// /latest/feed is the activity feed; this one is the chronological record.
|
|
675
|
+
//
|
|
676
|
+
// `?sort=updated` opts into the same axis the page uses, for a subscriber
|
|
677
|
+
// who wants that trade-off deliberately.
|
|
678
|
+
const sortsByActivity = params.sort === "updated";
|
|
622
679
|
const filters: PostFilters = {
|
|
623
680
|
format: params.format,
|
|
624
681
|
status: "published",
|
|
@@ -630,13 +687,22 @@ async function buildArchiveFeedData(
|
|
|
630
687
|
hasMedia: params.hasMedia,
|
|
631
688
|
hasTitle: params.hasTitle,
|
|
632
689
|
hasReplies: params.hasReplies,
|
|
690
|
+
sortBy: sortsByActivity ? "thread_updated" : "published",
|
|
633
691
|
ignorePinnedSort: true,
|
|
692
|
+
// The year filter follows the active axis; the RSS delay always stays on
|
|
693
|
+
// publication, so a just-published post is never announced early.
|
|
634
694
|
...(params.validYear
|
|
635
|
-
?
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
695
|
+
? sortsByActivity
|
|
696
|
+
? {
|
|
697
|
+
axisAfter: Date.UTC(params.validYear, 0, 1) / 1000,
|
|
698
|
+
axisBefore: yearPublishedBefore,
|
|
699
|
+
}
|
|
700
|
+
: { publishedAfter: Date.UTC(params.validYear, 0, 1) / 1000 }
|
|
639
701
|
: {}),
|
|
702
|
+
publishedBefore:
|
|
703
|
+
yearPublishedBefore === undefined || sortsByActivity
|
|
704
|
+
? rssPublishedBefore
|
|
705
|
+
: Math.min(yearPublishedBefore, rssPublishedBefore),
|
|
640
706
|
limit: appConfig.rssFeedLimit,
|
|
641
707
|
};
|
|
642
708
|
|
|
@@ -648,7 +714,9 @@ async function buildArchiveFeedData(
|
|
|
648
714
|
// Batch load media, aliases, and thread replies
|
|
649
715
|
const postIds = posts.map((p) => p.id);
|
|
650
716
|
const [threadMap, rawMediaMap, aliasesMap] = await Promise.all([
|
|
651
|
-
services.posts.getPublishedThreads(rootIds
|
|
717
|
+
services.posts.getPublishedThreads(rootIds, {
|
|
718
|
+
publishedBefore: rssPublishedBefore,
|
|
719
|
+
}),
|
|
652
720
|
services.media.getByPostIds(postIds),
|
|
653
721
|
services.paths.getPostAliases(postIds),
|
|
654
722
|
]);
|
|
@@ -702,9 +770,9 @@ async function buildArchiveFeedData(
|
|
|
702
770
|
aliasMap,
|
|
703
771
|
).map((postView, index) => {
|
|
704
772
|
const post = posts[index] as (typeof posts)[number];
|
|
773
|
+
const thread = threadMap.get(post.id);
|
|
705
774
|
|
|
706
775
|
// Build thread replies
|
|
707
|
-
const thread = threadMap.get(post.id);
|
|
708
776
|
const replies =
|
|
709
777
|
thread && thread.length > 1
|
|
710
778
|
? toPostViews(
|
|
@@ -720,7 +788,7 @@ async function buildArchiveFeedData(
|
|
|
720
788
|
|
|
721
789
|
return {
|
|
722
790
|
...postView,
|
|
723
|
-
feedUpdatedAt:
|
|
791
|
+
feedUpdatedAt: getFeedEntryUpdatedAt(post, thread),
|
|
724
792
|
threadReplies: replies,
|
|
725
793
|
};
|
|
726
794
|
});
|
|
@@ -748,7 +816,7 @@ archiveRoutes.get("/feed", async (c) => {
|
|
|
748
816
|
return new Response(defaultFeedRenderer(feedData), {
|
|
749
817
|
headers: {
|
|
750
818
|
"Content-Type": "application/atom+xml; charset=utf-8",
|
|
751
|
-
"Cache-Control":
|
|
819
|
+
"Cache-Control": RSS_FEED_CACHE_CONTROL,
|
|
752
820
|
},
|
|
753
821
|
});
|
|
754
822
|
});
|
|
@@ -19,9 +19,13 @@ import {
|
|
|
19
19
|
} from "../../lib/collection-sort.js";
|
|
20
20
|
import { assembleCollectionTimeline } from "../../lib/timeline.js";
|
|
21
21
|
import { defaultFeedRenderer } from "../../lib/feed.js";
|
|
22
|
+
import {
|
|
23
|
+
getFeedEntryUpdatedAt,
|
|
24
|
+
getRssPublishedBefore,
|
|
25
|
+
RSS_FEED_CACHE_CONTROL,
|
|
26
|
+
} from "../../lib/feed-policy.js";
|
|
22
27
|
import { toPlainText as markdownToPlainText } from "../../lib/markdown.js";
|
|
23
28
|
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
24
|
-
import { toISOString } from "../../lib/time.js";
|
|
25
29
|
import { createMediaContext, toPostViews } from "../../lib/view.js";
|
|
26
30
|
import { toAbsoluteSiteUrl, toPublicPath } from "../../lib/url.js";
|
|
27
31
|
import {
|
|
@@ -147,16 +151,17 @@ export async function renderCollectionPage(
|
|
|
147
151
|
);
|
|
148
152
|
const isAggregate = selection.collections.length > 1;
|
|
149
153
|
|
|
150
|
-
const
|
|
151
|
-
|
|
154
|
+
const ratedThreadCount =
|
|
155
|
+
await c.var.services.posts.countCollectionThreadRootsUpToForCollections(
|
|
152
156
|
collectionIds,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
{
|
|
158
|
+
status: "published",
|
|
159
|
+
excludePrivate: !navData.isAuthenticated,
|
|
160
|
+
hasRating: true,
|
|
161
|
+
},
|
|
162
|
+
2,
|
|
163
|
+
);
|
|
164
|
+
const showRatingSort = supportsCollectionRatingSort(ratedThreadCount);
|
|
160
165
|
const requestedDefaultSort = isAggregate
|
|
161
166
|
? "newest"
|
|
162
167
|
: primaryCollection.sortOrder;
|
|
@@ -219,7 +224,17 @@ export async function renderCollectionPage(
|
|
|
219
224
|
defaultSort={defaultSort}
|
|
220
225
|
showRatingSort={showRatingSort}
|
|
221
226
|
isAuthenticated={navData.isAuthenticated}
|
|
227
|
+
isInNavigation={navData.links.some(
|
|
228
|
+
(item) =>
|
|
229
|
+
item.type === "collection" &&
|
|
230
|
+
item.collectionId === primaryCollection.id,
|
|
231
|
+
)}
|
|
222
232
|
sitePathPrefix={navData.sitePathPrefix}
|
|
233
|
+
feedHref={
|
|
234
|
+
c.var.appConfig.rssFeedsEnabled
|
|
235
|
+
? `${canonicalPagePath}/feed`
|
|
236
|
+
: undefined
|
|
237
|
+
}
|
|
223
238
|
/>
|
|
224
239
|
),
|
|
225
240
|
});
|
|
@@ -254,6 +269,9 @@ export async function renderCollectionFeed(
|
|
|
254
269
|
const siteUrl = appConfig.siteUrl;
|
|
255
270
|
const siteLanguage = appConfig.siteLanguage;
|
|
256
271
|
const feedLimit = appConfig.rssFeedLimit;
|
|
272
|
+
const publishedBefore = getRssPublishedBefore(
|
|
273
|
+
appConfig.rssPublishDelaySeconds,
|
|
274
|
+
);
|
|
257
275
|
const primaryCollection = selection.collections[0];
|
|
258
276
|
if (!primaryCollection) return null;
|
|
259
277
|
|
|
@@ -264,6 +282,7 @@ export async function renderCollectionFeed(
|
|
|
264
282
|
status: "published",
|
|
265
283
|
excludePrivate: true,
|
|
266
284
|
ignoreCollectionPinnedSort: true,
|
|
285
|
+
publishedBefore,
|
|
267
286
|
limit: feedLimit,
|
|
268
287
|
},
|
|
269
288
|
);
|
|
@@ -274,7 +293,7 @@ export async function renderCollectionFeed(
|
|
|
274
293
|
|
|
275
294
|
const postIds = posts.map((post) => post.id);
|
|
276
295
|
const [threadMap, rawMediaMap, aliasesMap] = await Promise.all([
|
|
277
|
-
c.var.services.posts.getPublishedThreads(rootIds),
|
|
296
|
+
c.var.services.posts.getPublishedThreads(rootIds, { publishedBefore }),
|
|
278
297
|
c.var.services.media.getByPostIds(postIds),
|
|
279
298
|
c.var.services.paths.getPostAliases(postIds),
|
|
280
299
|
]);
|
|
@@ -345,15 +364,9 @@ export async function renderCollectionFeed(
|
|
|
345
364
|
)
|
|
346
365
|
: undefined;
|
|
347
366
|
|
|
348
|
-
// feedUpdatedAt = max(lastActivityAt, collectedAt)
|
|
349
|
-
const lastActivity = toISOString(post.lastActivityAt);
|
|
350
|
-
const collectedIso = collectedAt ? toISOString(collectedAt) : null;
|
|
351
|
-
const feedUpdatedAt =
|
|
352
|
-
collectedIso && collectedIso > lastActivity ? collectedIso : lastActivity;
|
|
353
|
-
|
|
354
367
|
return {
|
|
355
368
|
...postView,
|
|
356
|
-
feedUpdatedAt,
|
|
369
|
+
feedUpdatedAt: getFeedEntryUpdatedAt(post, thread, [collectedAt]),
|
|
357
370
|
threadReplies: replies,
|
|
358
371
|
};
|
|
359
372
|
});
|
|
@@ -382,7 +395,7 @@ export async function renderCollectionFeed(
|
|
|
382
395
|
return new Response(xml, {
|
|
383
396
|
headers: {
|
|
384
397
|
"Content-Type": "application/atom+xml; charset=utf-8",
|
|
385
|
-
"Cache-Control":
|
|
398
|
+
"Cache-Control": RSS_FEED_CACHE_CONTROL,
|
|
386
399
|
},
|
|
387
400
|
});
|
|
388
401
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Collections Listing Page Route
|
|
3
3
|
*
|
|
4
|
-
* Lists all collections with their
|
|
4
|
+
* Lists all collections with their Thread counts.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Hono } from "hono";
|
|
@@ -70,6 +70,11 @@ collectionsPageRoutes.get("/", async (c) => {
|
|
|
70
70
|
<CollectionsPage
|
|
71
71
|
items={directoryData.items}
|
|
72
72
|
isAuthenticated={navData.isAuthenticated ?? false}
|
|
73
|
+
navigationCollectionIds={navData.links.flatMap((item) =>
|
|
74
|
+
item.type === "collection" && item.collectionId
|
|
75
|
+
? [item.collectionId]
|
|
76
|
+
: [],
|
|
77
|
+
)}
|
|
73
78
|
sitePathPrefix={navData.sitePathPrefix}
|
|
74
79
|
siteOrigin={c.var.appConfig.siteOrigin}
|
|
75
80
|
/>
|