@jant/core 0.6.16 → 0.7.1
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/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/export.js +3 -1
- package/bin/commands/import-site.js +749 -220
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/setup.js +136 -0
- package/bin/commands/site/export.js +71 -34
- package/bin/commands/site/pull-media.js +2 -6
- package/bin/commands/site/snapshot/export.js +19 -61
- package/bin/commands/site/snapshot/import.js +23 -24
- package/bin/lib/d1-query.js +92 -18
- package/bin/lib/hugo-markdown.js +4 -0
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-pull-media.js +21 -28
- package/bin/lib/site-selection.js +94 -29
- package/bin/lib/site-snapshot.js +338 -3
- package/bin/lib/sql-export.js +68 -5
- package/bin/lib/wrangler-cli.js +37 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/bin/lib/zip-archive.js +187 -0
- package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
- package/dist/client/_assets/client-DAhoqPdr.css +2 -0
- package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
- package/dist/client/_assets/client-knSEyUJO.js +295 -0
- package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
- package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
- package/dist/github-sync-Gw4orAHk.js +8398 -0
- package/dist/index.js +2 -5
- package/dist/node.js +11 -9
- package/package.json +15 -5
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/dev-scripts.test.ts +203 -0
- package/src/__tests__/export-collection-order.test.ts +276 -0
- package/src/__tests__/export-feed-ids.test.ts +184 -0
- package/src/__tests__/export-feed-order.test.ts +294 -0
- package/src/__tests__/export-hugo-build.test.ts +390 -0
- package/src/__tests__/export-import-roundtrip.test.ts +94 -0
- package/src/__tests__/export-service.test.ts +842 -28
- package/src/__tests__/export-smart-collection.test.ts +329 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/helpers/hugo-site.ts +146 -0
- package/src/__tests__/import-site-command.test.ts +487 -1
- package/src/__tests__/mise-config.test.ts +75 -4
- package/src/__tests__/node-dev-tasks.test.ts +264 -0
- package/src/__tests__/site-export-canonical-import.test.ts +149 -0
- package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
- package/src/__tests__/snapshot-settings.test.ts +97 -0
- package/src/__tests__/snapshot-tables.test.ts +219 -0
- package/src/__tests__/sql-export.test.ts +173 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/__tests__/zip-archive.test.ts +106 -0
- package/src/app.tsx +86 -22
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker-types.ts +6 -1
- package/src/client/components/jant-repo-picker.ts +137 -108
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +622 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
- package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/extensions.ts +0 -3
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/tiptap/structural-keymap.ts +158 -47
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/d1-query.test.ts +56 -1
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +555 -73
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +601 -119
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +614 -132
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1526 -15
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/image.test.ts +27 -1
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
- package/src/lib/__tests__/markdown.test.ts +10 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +131 -3
- package/src/lib/__tests__/schemas.test.ts +69 -5
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/timeline.test.ts +87 -0
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +439 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +926 -73
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-repo-name.ts +45 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +62 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/image.ts +17 -4
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/markdown-manager.ts +392 -2
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-body-html.ts +11 -4
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +111 -6
- package/src/lib/schemas.ts +251 -15
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +43 -2
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/tiptap-to-markdown.ts +13 -7
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +159 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +53 -25
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-setup.test.ts +163 -0
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
- package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
- package/src/node/__tests__/runtime.test.ts +75 -0
- package/src/node/index.ts +3 -0
- package/src/node/request-handler.ts +3 -1
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/posts.test.ts +24 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/__tests__/upload.test.ts +34 -0
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +6 -3
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/internal/sites.ts +0 -1
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +161 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +44 -7
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/upload.ts +10 -3
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +9 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +714 -111
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
- package/src/routes/pages/archive.tsx +487 -379
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +43 -18
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +228 -45
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +22 -5
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/__tests__/readiness.test.ts +23 -0
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +67 -1
- package/src/runtime/readiness.ts +15 -0
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/custom-url.test.ts +34 -0
- package/src/services/__tests__/github-app-installations.test.ts +153 -0
- package/src/services/__tests__/github-sync-push.test.ts +46 -0
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/media.test.ts +31 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +236 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post-timeline.test.ts +127 -0
- package/src/services/__tests__/post.test.ts +120 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +408 -39
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +29 -24
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/alias.html +27 -1
- package/src/services/export-theme/layouts/_default/list.html +2 -63
- package/src/services/export-theme/layouts/_default/rss.xml +103 -38
- package/src/services/export-theme/layouts/collections/list.html +3 -3
- package/src/services/export-theme/layouts/featured/list.html +1 -4
- package/src/services/export-theme/layouts/index.html +40 -26
- package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
- package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
- package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
- package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/head.html +1 -1
- package/src/services/export-theme/layouts/partials/header.html +5 -3
- package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
- package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
- package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
- package/src/services/export-theme/layouts/post/list.html +1 -1
- package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
- package/src/services/export-theme/styles/main.css +13 -2
- package/src/services/export-theme/theme.toml +1 -1
- package/src/services/export.ts +880 -72
- package/src/services/github-app-installations.ts +171 -4
- package/src/services/github-sync.ts +69 -15
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +22 -2
- package/src/services/media.ts +22 -7
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +254 -6
- package/src/services/post.ts +1234 -190
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +27 -14
- package/src/services/site.ts +160 -32
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1090 -7216
- package/src/types/app-context.ts +23 -0
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +87 -1
- package/src/types/operations.ts +74 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +277 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/export-Dl5KCiEs.js +0 -5562
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/client/tiptap/exitable-marks.ts +0 -73
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the archive tells crawlers about each of its URLs.
|
|
3
|
+
*
|
|
4
|
+
* The filter space has no ceiling — `media` alone is a comma-joined subset of
|
|
5
|
+
* kinds, multiplied by every other dimension — and the page sits in no cache,
|
|
6
|
+
* so each facet a crawler walks costs a Worker invocation and seven D1
|
|
7
|
+
* queries. None of those URLs is a page the site publishes. The ones that are
|
|
8
|
+
* — the bare path, a stored archive path, and the same page rendered
|
|
9
|
+
* differently — consolidate onto one canonical instead.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
import { createTestApp } from "../../../__tests__/helpers/app.js";
|
|
14
|
+
import { archiveRoutes } from "../archive.js";
|
|
15
|
+
import { pageRoutes } from "../page.js";
|
|
16
|
+
|
|
17
|
+
const SITE_URL = "http://localhost:3000";
|
|
18
|
+
|
|
19
|
+
function setupApp() {
|
|
20
|
+
const { app, services } = createTestApp({ authenticated: false });
|
|
21
|
+
app.route("/archive", archiveRoutes);
|
|
22
|
+
return { app, services };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function setupPageApp() {
|
|
26
|
+
const testApp = createTestApp({ authenticated: false });
|
|
27
|
+
const { app } = testApp;
|
|
28
|
+
app.use("*", async (c, next) => {
|
|
29
|
+
c.set("publicPath", c.req.path);
|
|
30
|
+
c.set("publicRequestUrl", c.req.url);
|
|
31
|
+
await next();
|
|
32
|
+
});
|
|
33
|
+
app.route("/", pageRoutes);
|
|
34
|
+
return testApp;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readCanonical(html: string): string | null {
|
|
38
|
+
return html.match(/<link rel="canonical" href="([^"]+)"/)?.[1] ?? null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readRobotsMeta(html: string): string | null {
|
|
42
|
+
return html.match(/<meta name="robots" content="([^"]+)"/)?.[1] ?? null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The `<link rel="alternate" hreflang>` set only. The language switcher in the
|
|
47
|
+
* nav also carries `hreflang`, on ordinary `<a>` links — that is navigation
|
|
48
|
+
* chrome, not the annotation search engines read.
|
|
49
|
+
*/
|
|
50
|
+
function readAlternates(html: string): string[] {
|
|
51
|
+
return [
|
|
52
|
+
...html.matchAll(
|
|
53
|
+
/<link rel="alternate" hreflang="([^"]+)" href="([^"]+)"/g,
|
|
54
|
+
),
|
|
55
|
+
].map((m) => `${m[1]} ${m[2]}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function seed(services: {
|
|
59
|
+
posts: { create: (input: Record<string, unknown>) => Promise<unknown> };
|
|
60
|
+
}) {
|
|
61
|
+
await services.posts.create({
|
|
62
|
+
format: "note",
|
|
63
|
+
bodyMarkdown: "a note in the archive",
|
|
64
|
+
status: "published",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe("archive indexing", () => {
|
|
69
|
+
it("publishes the bare archive with a self-canonical", async () => {
|
|
70
|
+
const { app, services } = setupApp();
|
|
71
|
+
await seed(services);
|
|
72
|
+
|
|
73
|
+
const res = await app.request("/archive");
|
|
74
|
+
const html = await res.text();
|
|
75
|
+
|
|
76
|
+
expect(readRobotsMeta(html)).toBeNull();
|
|
77
|
+
expect(res.headers.get("X-Robots-Tag")).toBeNull();
|
|
78
|
+
expect(readCanonical(html)).toBe(`${SITE_URL}/archive`);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// `layout` changes the markup, not the posts or their order, so the two
|
|
82
|
+
// renderings are one page with one canonical.
|
|
83
|
+
it("consolidates layout and tracking params onto the bare path", async () => {
|
|
84
|
+
const { app, services } = setupApp();
|
|
85
|
+
await seed(services);
|
|
86
|
+
|
|
87
|
+
for (const query of ["?layout=grid", "?utm_source=nl"]) {
|
|
88
|
+
const html = await (await app.request(`/archive${query}`)).text();
|
|
89
|
+
expect(readRobotsMeta(html)).toBeNull();
|
|
90
|
+
expect(readCanonical(html)).toBe(`${SITE_URL}/archive`);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Pagination is genuinely different content, not a different rendering of
|
|
95
|
+
// the same content, so the canonical keeps it.
|
|
96
|
+
it("keeps the page number in the canonical", async () => {
|
|
97
|
+
const { app, services } = setupApp();
|
|
98
|
+
await seed(services);
|
|
99
|
+
|
|
100
|
+
const html = await (await app.request("/archive?page=2")).text();
|
|
101
|
+
expect(readCanonical(html)).toBe(`${SITE_URL}/archive?page=2`);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("keeps a reader's facet out of the index while leaving its links crawlable", async () => {
|
|
105
|
+
const { app, services } = setupApp();
|
|
106
|
+
await seed(services);
|
|
107
|
+
|
|
108
|
+
const res = await app.request("/archive?format=note");
|
|
109
|
+
const html = await res.text();
|
|
110
|
+
|
|
111
|
+
expect(readRobotsMeta(html)).toBe("noindex, follow");
|
|
112
|
+
expect(res.headers.get("X-Robots-Tag")).toBe("noindex, follow");
|
|
113
|
+
expect(readCanonical(html)).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// `sort` is a filter, not a presentation toggle: the year filter follows the
|
|
117
|
+
// sort axis, and under pagination a different order is a different page one.
|
|
118
|
+
it("treats sort as a facet rather than a rendering", async () => {
|
|
119
|
+
const { app, services } = setupApp();
|
|
120
|
+
await seed(services);
|
|
121
|
+
|
|
122
|
+
const html = await (await app.request("/archive?sort=updated")).text();
|
|
123
|
+
expect(readRobotsMeta(html)).toBe("noindex, follow");
|
|
124
|
+
expect(readCanonical(html)).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// A site that asked not to be indexed at all has made the stricter
|
|
128
|
+
// statement, and a page policy may only narrow further. `follow` would
|
|
129
|
+
// otherwise reopen the site's links on exactly the URLs it never wanted
|
|
130
|
+
// crawled.
|
|
131
|
+
it("lets a site-wide noindex override the facet policy", async () => {
|
|
132
|
+
const { app, services } = createTestApp({
|
|
133
|
+
authenticated: false,
|
|
134
|
+
demoMode: true,
|
|
135
|
+
});
|
|
136
|
+
app.route("/archive", archiveRoutes);
|
|
137
|
+
await seed(services);
|
|
138
|
+
|
|
139
|
+
const facet = await (await app.request("/archive?format=note")).text();
|
|
140
|
+
expect(readRobotsMeta(facet)).toBe("noindex, nofollow");
|
|
141
|
+
|
|
142
|
+
const bare = await (await app.request("/archive")).text();
|
|
143
|
+
expect(readRobotsMeta(bare)).toBe("noindex, nofollow");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Every member of an hreflang set has to be a canonical URL. Left alone, the
|
|
147
|
+
// self-referential alternate would carry the very params the canonical
|
|
148
|
+
// strips, giving one page two identities.
|
|
149
|
+
it("builds the hreflang set from the canonical, not the raw URL", async () => {
|
|
150
|
+
const { app, services } = setupApp();
|
|
151
|
+
await services.settings.set("SITE_LANGUAGE", "zh-Hans");
|
|
152
|
+
await services.settings.set("ADDITIONAL_LANGUAGES", "en");
|
|
153
|
+
await services.settings.set("MULTILINGUAL_ENABLED", "true");
|
|
154
|
+
await seed(services);
|
|
155
|
+
|
|
156
|
+
const html = await (await app.request("/archive?utm_source=nl")).text();
|
|
157
|
+
|
|
158
|
+
expect(readCanonical(html)).toBe(`${SITE_URL}/archive`);
|
|
159
|
+
expect(readAlternates(html)).toEqual([
|
|
160
|
+
`zh-Hans ${SITE_URL}/archive`,
|
|
161
|
+
`en ${SITE_URL}/en/archive`,
|
|
162
|
+
`x-default ${SITE_URL}/archive`,
|
|
163
|
+
]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// A facet has no canonical, so there is no set to build.
|
|
167
|
+
it("emits no hreflang set on a facet", async () => {
|
|
168
|
+
const { app, services } = setupApp();
|
|
169
|
+
await services.settings.set("SITE_LANGUAGE", "zh-Hans");
|
|
170
|
+
await services.settings.set("ADDITIONAL_LANGUAGES", "en");
|
|
171
|
+
await services.settings.set("MULTILINGUAL_ENABLED", "true");
|
|
172
|
+
await seed(services);
|
|
173
|
+
|
|
174
|
+
const facet = await (await app.request("/archive?format=note")).text();
|
|
175
|
+
expect(readAlternates(facet)).toEqual([]);
|
|
176
|
+
|
|
177
|
+
const bare = await (await app.request("/archive")).text();
|
|
178
|
+
expect(readAlternates(bare)).toContain(`en ${SITE_URL}/en/archive`);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// The author declared this path, so it is a page even though its stored
|
|
182
|
+
// query is a filter. Same seam the auth guard uses.
|
|
183
|
+
it("publishes a stored archive path despite its filter", async () => {
|
|
184
|
+
const { app, services } = setupPageApp();
|
|
185
|
+
await seed(services);
|
|
186
|
+
await services.paths.create({
|
|
187
|
+
path: "/every-note",
|
|
188
|
+
kind: "archive",
|
|
189
|
+
archiveQuery: "format=note&sort=updated",
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const res = await app.request("/every-note");
|
|
193
|
+
expect(res.status).toBe(200);
|
|
194
|
+
|
|
195
|
+
const html = await res.text();
|
|
196
|
+
expect(readRobotsMeta(html)).toBeNull();
|
|
197
|
+
expect(res.headers.get("X-Robots-Tag")).toBeNull();
|
|
198
|
+
expect(readCanonical(html)).toBe(`${SITE_URL}/every-note`);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -30,6 +30,12 @@ function setupApp(rssPublishDelaySeconds = 0) {
|
|
|
30
30
|
return { app, services };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function setupAuthedApp() {
|
|
34
|
+
const { app, services } = createTestApp({ authenticated: true });
|
|
35
|
+
app.route("/archive", archiveRoutes);
|
|
36
|
+
return { app, services };
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
afterEach(() => {
|
|
34
40
|
vi.restoreAllMocks();
|
|
35
41
|
});
|
|
@@ -60,6 +66,31 @@ describe("archive feed filter params", () => {
|
|
|
60
66
|
expect(xml).not.toContain("Recent Archive Post");
|
|
61
67
|
});
|
|
62
68
|
|
|
69
|
+
it("carries the number of entries ?limit= asks for", async () => {
|
|
70
|
+
const { app, services } = setupApp();
|
|
71
|
+
await services.posts.create({
|
|
72
|
+
format: "note",
|
|
73
|
+
title: "Older Archive Post",
|
|
74
|
+
bodyMarkdown: "Older",
|
|
75
|
+
status: "published",
|
|
76
|
+
publishedAt: 1_000,
|
|
77
|
+
});
|
|
78
|
+
await services.posts.create({
|
|
79
|
+
format: "note",
|
|
80
|
+
title: "Newer Archive Post",
|
|
81
|
+
bodyMarkdown: "Newer",
|
|
82
|
+
status: "published",
|
|
83
|
+
publishedAt: 2_000,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const xml = await fetchFeed(app, "?limit=1");
|
|
87
|
+
|
|
88
|
+
expect(xml).toContain("Newer Archive Post");
|
|
89
|
+
expect(xml).not.toContain("Older Archive Post");
|
|
90
|
+
// Not a filter, so the feed's own address does not carry it.
|
|
91
|
+
expect(xml).not.toContain("limit=1");
|
|
92
|
+
});
|
|
93
|
+
|
|
63
94
|
it("filters by title with the new param and the legacy fallback", async () => {
|
|
64
95
|
const { app, services } = setupApp();
|
|
65
96
|
await services.posts.create({
|
|
@@ -298,6 +329,20 @@ describe("archive page legacy param redirect", () => {
|
|
|
298
329
|
expect(res.headers.get("location")).toBe("/archive?visibility=hidden");
|
|
299
330
|
});
|
|
300
331
|
|
|
332
|
+
// `visibility=all` filtered nothing, exactly like an absent parameter — a
|
|
333
|
+
// second spelling of "nothing selected" that only the parser knew about.
|
|
334
|
+
it("drops the visibility=all chip state, which selected nothing", async () => {
|
|
335
|
+
const { app } = setupApp();
|
|
336
|
+
|
|
337
|
+
const res = await app.request("/archive?visibility=all");
|
|
338
|
+
expect(res.status).toBe(308);
|
|
339
|
+
expect(res.headers.get("location")).toBe("/archive");
|
|
340
|
+
|
|
341
|
+
const withOthers = await app.request("/archive?format=note&visibility=all");
|
|
342
|
+
expect(withOthers.status).toBe(308);
|
|
343
|
+
expect(withOthers.headers.get("location")).toBe("/archive?format=note");
|
|
344
|
+
});
|
|
345
|
+
|
|
301
346
|
it("drops a legacy param without overriding an explicit new one", async () => {
|
|
302
347
|
const { app } = setupApp();
|
|
303
348
|
|
|
@@ -305,6 +350,39 @@ describe("archive page legacy param redirect", () => {
|
|
|
305
350
|
expect(res.status).toBe(308);
|
|
306
351
|
expect(res.headers.get("location")).toBe("/archive?title=any");
|
|
307
352
|
});
|
|
353
|
+
|
|
354
|
+
// `view` was renamed to `layout` so `view` can name the saved-selection
|
|
355
|
+
// concept. Shared links and bookmarks predate the rename.
|
|
356
|
+
it("redirects the legacy view param to layout", async () => {
|
|
357
|
+
const { app } = setupApp();
|
|
358
|
+
|
|
359
|
+
const res = await app.request("/archive?view=grid");
|
|
360
|
+
expect(res.status).toBe(308);
|
|
361
|
+
expect(res.headers.get("location")).toBe("/archive?layout=grid");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("keeps layout when a URL carries both spellings", async () => {
|
|
365
|
+
const { app } = setupApp();
|
|
366
|
+
|
|
367
|
+
const res = await app.request("/archive?layout=list&view=grid");
|
|
368
|
+
expect(res.status).toBe(308);
|
|
369
|
+
expect(res.headers.get("location")).toBe("/archive?layout=list");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("drops a view value it cannot render", async () => {
|
|
373
|
+
const { app } = setupApp();
|
|
374
|
+
|
|
375
|
+
const res = await app.request("/archive?view=carousel");
|
|
376
|
+
expect(res.status).toBe(308);
|
|
377
|
+
expect(res.headers.get("location")).toBe("/archive");
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("leaves a canonical layout param alone", async () => {
|
|
381
|
+
const { app } = setupApp();
|
|
382
|
+
|
|
383
|
+
const res = await app.request("/archive?layout=grid");
|
|
384
|
+
expect(res.status).toBe(200);
|
|
385
|
+
});
|
|
308
386
|
});
|
|
309
387
|
|
|
310
388
|
describe("archive page ordering", () => {
|
|
@@ -354,7 +432,7 @@ describe("archive page ordering", () => {
|
|
|
354
432
|
publishedAt: Date.UTC(2026, 0, 1) / 1000,
|
|
355
433
|
});
|
|
356
434
|
|
|
357
|
-
const res = await app.request("/archive?
|
|
435
|
+
const res = await app.request("/archive?layout=list");
|
|
358
436
|
expect(res.status).toBe(200);
|
|
359
437
|
const html = await res.text();
|
|
360
438
|
|
|
@@ -408,16 +486,20 @@ describe("archive page sort=updated", () => {
|
|
|
408
486
|
);
|
|
409
487
|
});
|
|
410
488
|
|
|
489
|
+
// Month headers are a grid affordance, so the bucketing axis is asserted
|
|
490
|
+
// through the grid layout rather than the site default.
|
|
411
491
|
it("buckets months on the same axis it sorts by", async () => {
|
|
412
492
|
const { app, services } = setupApp();
|
|
413
493
|
await seedDivergingThreads(services);
|
|
414
494
|
|
|
415
|
-
const byPublished = await (
|
|
495
|
+
const byPublished = await (
|
|
496
|
+
await app.request("/archive?layout=grid")
|
|
497
|
+
).text();
|
|
416
498
|
expect(byPublished).toContain("March 2019");
|
|
417
499
|
expect(byPublished).not.toContain("August 2026");
|
|
418
500
|
|
|
419
501
|
const byActivity = await (
|
|
420
|
-
await app.request("/archive?sort=updated")
|
|
502
|
+
await app.request("/archive?layout=grid&sort=updated")
|
|
421
503
|
).text();
|
|
422
504
|
expect(byActivity).toContain("August 2026");
|
|
423
505
|
expect(byActivity).not.toContain("March 2019");
|
|
@@ -454,13 +536,46 @@ describe("archive page sort=updated", () => {
|
|
|
454
536
|
await seedDivergingThreads(services);
|
|
455
537
|
|
|
456
538
|
const byPublished = await (await app.request("/archive")).text();
|
|
457
|
-
expect(byPublished).toContain("<title>
|
|
539
|
+
expect(byPublished).toContain("<title>All posts - ");
|
|
458
540
|
expect(byPublished).not.toContain("Recently updated");
|
|
459
541
|
|
|
460
542
|
const byActivity = await (
|
|
461
543
|
await app.request("/archive?sort=updated")
|
|
462
544
|
).text();
|
|
463
|
-
expect(byActivity).toContain("<title>
|
|
545
|
+
expect(byActivity).toContain("<title>All posts - Recently updated - ");
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it("says how much a filter removed, and stays quiet when none did", async () => {
|
|
549
|
+
const { app, services } = setupApp();
|
|
550
|
+
await seedDivergingThreads(services);
|
|
551
|
+
|
|
552
|
+
// Two thread roots; asking for threads with replies leaves one of them.
|
|
553
|
+
const filtered = await (await app.request("/archive?replies=any")).text();
|
|
554
|
+
expect(filtered).toContain("of 2");
|
|
555
|
+
|
|
556
|
+
// Nothing filtered, so there is no baseline worth comparing against.
|
|
557
|
+
const unfiltered = await (await app.request("/archive")).text();
|
|
558
|
+
expect(unfiltered).not.toContain("of 2");
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it("names the active filter in the document title", async () => {
|
|
562
|
+
const { app, services } = setupApp();
|
|
563
|
+
await seedDivergingThreads(services);
|
|
564
|
+
|
|
565
|
+
const byFormat = await (await app.request("/archive?format=note")).text();
|
|
566
|
+
expect(byFormat).toContain("<title>Notes - ");
|
|
567
|
+
|
|
568
|
+
// Title presence absorbs the format it refines, as the filter chip does.
|
|
569
|
+
const untitled = await (
|
|
570
|
+
await app.request("/archive?format=note&title=none")
|
|
571
|
+
).text();
|
|
572
|
+
expect(untitled).toContain("<title>Untitled - ");
|
|
573
|
+
|
|
574
|
+
// A tab truncates from the right, so the description stops at two parts.
|
|
575
|
+
const capped = await (
|
|
576
|
+
await app.request("/archive?format=note&year=2019&replies=any")
|
|
577
|
+
).text();
|
|
578
|
+
expect(capped).toContain("<title>Notes, 2019 - ");
|
|
464
579
|
});
|
|
465
580
|
|
|
466
581
|
it("ignores an unknown sort value", async () => {
|
|
@@ -499,7 +614,9 @@ describe("archive page sort=updated", () => {
|
|
|
499
614
|
publishedAt: Date.UTC(2025, 0, 1) / 1000,
|
|
500
615
|
});
|
|
501
616
|
|
|
502
|
-
const html = await (
|
|
617
|
+
const html = await (
|
|
618
|
+
await app.request("/archive?layout=grid&sort=updated")
|
|
619
|
+
).text();
|
|
503
620
|
expect(html.indexOf("Quietly extended thread")).toBeLessThan(
|
|
504
621
|
html.indexOf("Untouched newer thread"),
|
|
505
622
|
);
|
|
@@ -514,3 +631,250 @@ describe("archive page sort=updated", () => {
|
|
|
514
631
|
expect(roots[0]?.title).toBe("Untouched newer thread");
|
|
515
632
|
});
|
|
516
633
|
});
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* `visibility` is the one archive dimension whose values are not all reachable
|
|
637
|
+
* by every reader. The rule: `private` is never silently dropped. On the page
|
|
638
|
+
* the reader's own URL is rewritten so the address bar keeps describing what
|
|
639
|
+
* rendered; a stored archive path 404s instead (see custom-archive-url.test.ts,
|
|
640
|
+
* which has no URL to rewrite).
|
|
641
|
+
*/
|
|
642
|
+
describe("archive visibility without a session", () => {
|
|
643
|
+
async function seedVisibilities(services: {
|
|
644
|
+
posts: {
|
|
645
|
+
create: (input: Record<string, unknown>) => Promise<unknown>;
|
|
646
|
+
};
|
|
647
|
+
}) {
|
|
648
|
+
await services.posts.create({
|
|
649
|
+
format: "note",
|
|
650
|
+
bodyMarkdown: "body on the stream",
|
|
651
|
+
status: "published",
|
|
652
|
+
});
|
|
653
|
+
await services.posts.create({
|
|
654
|
+
format: "note",
|
|
655
|
+
bodyMarkdown: "body off the stream",
|
|
656
|
+
status: "published",
|
|
657
|
+
visibility: "latest_hidden",
|
|
658
|
+
});
|
|
659
|
+
await services.posts.create({
|
|
660
|
+
format: "note",
|
|
661
|
+
bodyMarkdown: "body kept back",
|
|
662
|
+
status: "published",
|
|
663
|
+
visibility: "private",
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
it("filters by Hidden-from-Latest for a signed-out reader", async () => {
|
|
668
|
+
const { app, services } = setupApp();
|
|
669
|
+
await seedVisibilities(services);
|
|
670
|
+
|
|
671
|
+
const res = await app.request("/archive?visibility=hidden");
|
|
672
|
+
expect(res.status).toBe(200);
|
|
673
|
+
|
|
674
|
+
const html = await res.text();
|
|
675
|
+
expect(html).toContain("body off the stream");
|
|
676
|
+
expect(html).not.toContain("body on the stream");
|
|
677
|
+
expect(html).not.toContain("body kept back");
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it("strips private from a signed-out reader's URL, keeping the rest", async () => {
|
|
681
|
+
const { app } = setupApp();
|
|
682
|
+
|
|
683
|
+
const res = await app.request("/archive?visibility=private&format=note");
|
|
684
|
+
expect(res.status).toBe(302);
|
|
685
|
+
expect(res.headers.get("location")).toBe("/archive?format=note");
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// 302, not 308: the URL moved for this reader at this moment, and has to
|
|
689
|
+
// keep working once the author signs in.
|
|
690
|
+
it("uses a temporary redirect, and a permanent one for legacy spellings", async () => {
|
|
691
|
+
const { app } = setupApp();
|
|
692
|
+
|
|
693
|
+
const both = await app.request("/archive?visibility=private&hasTitle=1");
|
|
694
|
+
expect(both.status).toBe(302);
|
|
695
|
+
expect(both.headers.get("location")).toBe("/archive?title=any");
|
|
696
|
+
|
|
697
|
+
const legacyOnly = await app.request("/archive?hasTitle=1");
|
|
698
|
+
expect(legacyOnly.status).toBe(308);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// The redirect must answer from the param and the session alone. Firing only
|
|
702
|
+
// when private posts exist would tell any visitor whether this site keeps any.
|
|
703
|
+
it("redirects identically when the site holds no private posts", async () => {
|
|
704
|
+
const { app, services } = setupApp();
|
|
705
|
+
await services.posts.create({
|
|
706
|
+
format: "note",
|
|
707
|
+
bodyMarkdown: "the only post here",
|
|
708
|
+
status: "published",
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
const res = await app.request("/archive?visibility=private");
|
|
712
|
+
expect(res.status).toBe(302);
|
|
713
|
+
expect(res.headers.get("location")).toBe("/archive");
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it("leaves the author's own private filter alone", async () => {
|
|
717
|
+
const { app, services } = setupAuthedApp();
|
|
718
|
+
await seedVisibilities(services);
|
|
719
|
+
|
|
720
|
+
const res = await app.request("/archive?visibility=private");
|
|
721
|
+
expect(res.status).toBe(200);
|
|
722
|
+
|
|
723
|
+
const html = await res.text();
|
|
724
|
+
expect(html).toContain("body kept back");
|
|
725
|
+
expect(html).not.toContain("body on the stream");
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
// A feed carries no session, so it cannot serve this selection. The page
|
|
729
|
+
// withholds the link rather than promising a feed that answers 404.
|
|
730
|
+
it("offers no feed link while the private filter is active", async () => {
|
|
731
|
+
const { app, services } = setupAuthedApp();
|
|
732
|
+
await seedVisibilities(services);
|
|
733
|
+
|
|
734
|
+
const filtered = await app.request("/archive?visibility=private");
|
|
735
|
+
expect(await filtered.text()).not.toContain("/archive/feed");
|
|
736
|
+
|
|
737
|
+
const plain = await app.request("/archive");
|
|
738
|
+
expect(await plain.text()).toContain("/archive/feed");
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it("carries the visibility selection into the feed", async () => {
|
|
742
|
+
const { app, services } = setupApp();
|
|
743
|
+
await seedVisibilities(services);
|
|
744
|
+
|
|
745
|
+
const xml = await fetchFeed(app, "?visibility=hidden");
|
|
746
|
+
expect(xml).toContain("body off the stream");
|
|
747
|
+
expect(xml).not.toContain("body on the stream");
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
it("404s the feed for a selection it cannot serve", async () => {
|
|
751
|
+
const { app, services } = setupApp();
|
|
752
|
+
await seedVisibilities(services);
|
|
753
|
+
|
|
754
|
+
const res = await app.request("/archive/feed?visibility=private");
|
|
755
|
+
expect(res.status).toBe(404);
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
describe("archive collection selection", () => {
|
|
760
|
+
async function seedOneCollectedPost(services: {
|
|
761
|
+
posts: {
|
|
762
|
+
create: (input: Record<string, unknown>) => Promise<{ id: string }>;
|
|
763
|
+
};
|
|
764
|
+
collections: {
|
|
765
|
+
create: (input: Record<string, unknown>) => Promise<{ id: string }>;
|
|
766
|
+
addThread: (collectionId: string, threadId: string) => Promise<unknown>;
|
|
767
|
+
};
|
|
768
|
+
}) {
|
|
769
|
+
const collection = await services.collections.create({
|
|
770
|
+
slug: "recipes",
|
|
771
|
+
title: "Recipes",
|
|
772
|
+
});
|
|
773
|
+
const collected = await services.posts.create({
|
|
774
|
+
format: "note",
|
|
775
|
+
title: "Collected Post",
|
|
776
|
+
bodyMarkdown: "in the collection",
|
|
777
|
+
status: "published",
|
|
778
|
+
});
|
|
779
|
+
await services.collections.addThread(collection.id, collected.id);
|
|
780
|
+
await services.posts.create({
|
|
781
|
+
format: "note",
|
|
782
|
+
title: "Loose Post",
|
|
783
|
+
bodyMarkdown: "in no collection",
|
|
784
|
+
status: "published",
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
it("filters the page by a collection that exists", async () => {
|
|
789
|
+
const { app, services } = setupApp();
|
|
790
|
+
await seedOneCollectedPost(services);
|
|
791
|
+
|
|
792
|
+
const res = await app.request("/archive?collection=recipes");
|
|
793
|
+
expect(res.status).toBe(200);
|
|
794
|
+
const html = await res.text();
|
|
795
|
+
expect(html).toContain("Collected Post");
|
|
796
|
+
expect(html).not.toContain("Loose Post");
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// Rendering the whole archive here would answer a question nobody asked,
|
|
800
|
+
// under the name the reader typed — the failure `visibility=private` was
|
|
801
|
+
// fixed for. `/collections/{slug}` answers the same question with a 404.
|
|
802
|
+
it("404s the page for a collection that does not exist", async () => {
|
|
803
|
+
const { app, services } = setupApp();
|
|
804
|
+
await seedOneCollectedPost(services);
|
|
805
|
+
|
|
806
|
+
const res = await app.request("/archive?collection=nope");
|
|
807
|
+
expect(res.status).toBe(404);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it("404s the feed for a collection that does not exist", async () => {
|
|
811
|
+
const { app, services } = setupApp();
|
|
812
|
+
await seedOneCollectedPost(services);
|
|
813
|
+
|
|
814
|
+
expect((await app.request("/archive/feed?collection=recipes")).status).toBe(
|
|
815
|
+
200,
|
|
816
|
+
);
|
|
817
|
+
expect((await app.request("/archive/feed?collection=nope")).status).toBe(
|
|
818
|
+
404,
|
|
819
|
+
);
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
// The page read a single slug while `/api/public/archive` read a list, so one
|
|
823
|
+
// spelling meant two things depending on which surface you asked. Both now
|
|
824
|
+
// read the same vocabulary.
|
|
825
|
+
it("selects the union of several collections", async () => {
|
|
826
|
+
const { app, services } = setupApp();
|
|
827
|
+
const recipes = await services.collections.create({
|
|
828
|
+
slug: "recipes",
|
|
829
|
+
title: "Recipes",
|
|
830
|
+
});
|
|
831
|
+
const travel = await services.collections.create({
|
|
832
|
+
slug: "travel",
|
|
833
|
+
title: "Travel",
|
|
834
|
+
});
|
|
835
|
+
const inRecipes = await services.posts.create({
|
|
836
|
+
format: "note",
|
|
837
|
+
title: "Recipe Post",
|
|
838
|
+
bodyMarkdown: "in recipes",
|
|
839
|
+
status: "published",
|
|
840
|
+
});
|
|
841
|
+
const inTravel = await services.posts.create({
|
|
842
|
+
format: "note",
|
|
843
|
+
title: "Travel Post",
|
|
844
|
+
bodyMarkdown: "in travel",
|
|
845
|
+
status: "published",
|
|
846
|
+
});
|
|
847
|
+
await services.posts.create({
|
|
848
|
+
format: "note",
|
|
849
|
+
title: "Loose Post",
|
|
850
|
+
bodyMarkdown: "in no collection",
|
|
851
|
+
status: "published",
|
|
852
|
+
});
|
|
853
|
+
await services.collections.addThread(recipes.id, inRecipes.id);
|
|
854
|
+
await services.collections.addThread(travel.id, inTravel.id);
|
|
855
|
+
|
|
856
|
+
const res = await app.request("/archive?collection=recipes,travel");
|
|
857
|
+
expect(res.status).toBe(200);
|
|
858
|
+
const html = await res.text();
|
|
859
|
+
expect(html).toContain("Recipe Post");
|
|
860
|
+
expect(html).toContain("Travel Post");
|
|
861
|
+
expect(html).not.toContain("Loose Post");
|
|
862
|
+
|
|
863
|
+
// Hono decodes a query value as form-urlencoded, so the `+` that separates
|
|
864
|
+
// slugs in `/collections/{a+b}` arrives here as a space. It is read anyway;
|
|
865
|
+
// a comma is what gets written back.
|
|
866
|
+
const plusSpelling = await app.request(
|
|
867
|
+
"/archive?collection=recipes+travel",
|
|
868
|
+
);
|
|
869
|
+
expect(plusSpelling.status).toBe(200);
|
|
870
|
+
expect(await plusSpelling.text()).toContain("Travel Post");
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
it("404s when only one of several collections is missing", async () => {
|
|
874
|
+
const { app, services } = setupApp();
|
|
875
|
+
await seedOneCollectedPost(services);
|
|
876
|
+
|
|
877
|
+
const res = await app.request("/archive?collection=recipes,nope");
|
|
878
|
+
expect(res.status).toBe(404);
|
|
879
|
+
});
|
|
880
|
+
});
|
|
@@ -166,6 +166,22 @@ describe("Collection Routing", () => {
|
|
|
166
166
|
expect(xml).toContain("Film log");
|
|
167
167
|
});
|
|
168
168
|
|
|
169
|
+
it("leads the collection feed title with the site name", async () => {
|
|
170
|
+
// A reader's sidebar sorts by feed title, so every Jant feed leads with
|
|
171
|
+
// the site name — otherwise one site's feeds scatter across the alphabet.
|
|
172
|
+
const { app, services } = createCollectionRoutingTestApp();
|
|
173
|
+
await services.settings.set("SITE_NAME", "Jant");
|
|
174
|
+
await createCollectionWithPost(services, {
|
|
175
|
+
slug: "reading",
|
|
176
|
+
title: "Reading",
|
|
177
|
+
postTitle: "Book log",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const xml = await (await app.request("/reading/feed")).text();
|
|
181
|
+
|
|
182
|
+
expect(xml).toContain("<title>Jant - Reading</title>");
|
|
183
|
+
});
|
|
184
|
+
|
|
169
185
|
it("applies the RSS publication delay to Collection feeds", async () => {
|
|
170
186
|
const currentTime = 2_000_000;
|
|
171
187
|
vi.spyOn(Date, "now").mockReturnValue(currentTime * 1000);
|