@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
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Hono } from "hono";
|
|
6
|
+
import type { Context } from "hono";
|
|
6
7
|
import type { Bindings, SearchResult } from "../../types.js";
|
|
7
8
|
import type { AppVariables } from "../../types/app-context.js";
|
|
8
9
|
import { SearchPage } from "../../ui/pages/SearchPage.js";
|
|
@@ -10,17 +11,28 @@ import { getNavigationData } from "../../lib/navigation.js";
|
|
|
10
11
|
import { buildPageTitle } from "../../lib/page-title.js";
|
|
11
12
|
import { renderPublicPage } from "../../lib/render.js";
|
|
12
13
|
import { createMediaContext, toSearchResultViews } from "../../lib/view.js";
|
|
14
|
+
import {
|
|
15
|
+
buildSurfaceAlternates,
|
|
16
|
+
getViewLang,
|
|
17
|
+
} from "../../lib/view-language.js";
|
|
13
18
|
|
|
14
19
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
15
20
|
|
|
16
21
|
export const searchRoutes = new Hono<Env>();
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Render the search page for the current view language.
|
|
25
|
+
*
|
|
26
|
+
* @param c - Hono context
|
|
27
|
+
* @returns Search page response
|
|
28
|
+
*/
|
|
29
|
+
export async function renderSearchPage(c: Context<Env>): Promise<Response> {
|
|
19
30
|
const query = c.req.query("q") || "";
|
|
20
31
|
const pageParam = c.req.query("page");
|
|
21
32
|
const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1;
|
|
22
33
|
|
|
23
|
-
|
|
34
|
+
// The chrome does not gate the query, so it loads alongside it.
|
|
35
|
+
const navDataPromise = getNavigationData(c);
|
|
24
36
|
|
|
25
37
|
// Only search if there's a query
|
|
26
38
|
let results: SearchResult[] = [];
|
|
@@ -35,6 +47,7 @@ searchRoutes.get("/", async (c) => {
|
|
|
35
47
|
limit: pageSize + 1,
|
|
36
48
|
offset: (page - 1) * pageSize,
|
|
37
49
|
status: ["published"],
|
|
50
|
+
lang: getViewLang(c) ?? undefined,
|
|
38
51
|
});
|
|
39
52
|
|
|
40
53
|
hasMore = results.length > pageSize;
|
|
@@ -51,7 +64,8 @@ searchRoutes.get("/", async (c) => {
|
|
|
51
64
|
// Transform to View Models
|
|
52
65
|
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
53
66
|
const postIds = results.map((r) => r.post.id);
|
|
54
|
-
const [aliasesMap, collectionsMap] = await Promise.all([
|
|
67
|
+
const [navData, aliasesMap, collectionsMap] = await Promise.all([
|
|
68
|
+
navDataPromise,
|
|
55
69
|
c.var.services.paths.getPostAliases(postIds),
|
|
56
70
|
c.var.services.collections.getCollectionsByPostIds(postIds),
|
|
57
71
|
]);
|
|
@@ -72,6 +86,7 @@ searchRoutes.get("/", async (c) => {
|
|
|
72
86
|
query ? `Search: ${query}` : "Search",
|
|
73
87
|
navData.siteName,
|
|
74
88
|
),
|
|
89
|
+
alternateLanguages: buildSurfaceAlternates(c),
|
|
75
90
|
navData,
|
|
76
91
|
content: (
|
|
77
92
|
<SearchPage
|
|
@@ -80,9 +95,11 @@ searchRoutes.get("/", async (c) => {
|
|
|
80
95
|
error={error}
|
|
81
96
|
hasMore={hasMore}
|
|
82
97
|
page={page}
|
|
83
|
-
|
|
98
|
+
basePath={navData.basePath}
|
|
84
99
|
isAuthenticated={navData.isAuthenticated}
|
|
85
100
|
/>
|
|
86
101
|
),
|
|
87
102
|
});
|
|
88
|
-
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
searchRoutes.get("/", renderSearchPage);
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Collection Page Route
|
|
3
|
+
*
|
|
4
|
+
* A smart collection is always public, so this file carries no page guard, no
|
|
5
|
+
* feed guard, and no "can this reader see it" branch. What it does carry is the
|
|
6
|
+
* ordinary per-post visibility floor, identical to a manual collection page:
|
|
7
|
+
* an author signed in may see more threads than an anonymous reader, on the
|
|
8
|
+
* page and in the count alike.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Context } from "hono";
|
|
12
|
+
import type { Bindings, SmartCollection } from "../../types.js";
|
|
13
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
14
|
+
import { SmartCollectionPage } from "../../ui/pages/SmartCollectionPage.js";
|
|
15
|
+
import { getNavigationData } from "../../lib/navigation.js";
|
|
16
|
+
import { formatPageLabel, parsePageNumber } from "../../lib/pagination.js";
|
|
17
|
+
import { buildPageTitle } from "../../lib/page-title.js";
|
|
18
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
19
|
+
import { assembleTimelineItems } from "../../lib/timeline.js";
|
|
20
|
+
import { defaultFeedRenderer } from "../../lib/feed.js";
|
|
21
|
+
import {
|
|
22
|
+
buildFeedDiscoveryFields,
|
|
23
|
+
getFeedEntryUpdatedAt,
|
|
24
|
+
getFeedLimit,
|
|
25
|
+
getRssPublishedBefore,
|
|
26
|
+
renderFeed,
|
|
27
|
+
} from "../../lib/feed-policy.js";
|
|
28
|
+
import { toPlainText as markdownToPlainText } from "../../lib/markdown.js";
|
|
29
|
+
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
30
|
+
import { createMediaContext, toPostViews } from "../../lib/view.js";
|
|
31
|
+
import { toAbsoluteSiteUrl } from "../../lib/url.js";
|
|
32
|
+
import {
|
|
33
|
+
buildSurfaceAlternates,
|
|
34
|
+
getViewLang,
|
|
35
|
+
toViewPath,
|
|
36
|
+
viewBasePath,
|
|
37
|
+
} from "../../lib/view-language.js";
|
|
38
|
+
import { getCollectionPagePath } from "../../lib/collection-paths.js";
|
|
39
|
+
import { buildCollectionVocabulary } from "../../lib/filter-dimensions.js";
|
|
40
|
+
import {
|
|
41
|
+
buildSmartCollectionArchiveHref,
|
|
42
|
+
describeSmartCollection,
|
|
43
|
+
} from "../../ui/shared/smart-collection-labels.js";
|
|
44
|
+
import { getI18n } from "../../i18n/index.js";
|
|
45
|
+
import { SMART_COLLECTION_SORT_ORDERS } from "../../types.js";
|
|
46
|
+
import type { SmartCollectionSortOrder } from "../../types.js";
|
|
47
|
+
import { supportsCollectionRatingSort } from "../../lib/collection-sort.js";
|
|
48
|
+
|
|
49
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The collection vocabulary, loaded only when a condition names one.
|
|
53
|
+
*
|
|
54
|
+
* The condition line has to name the collection it filters by, and the archive
|
|
55
|
+
* link has to spell its slug — but most smart collections name none, and the
|
|
56
|
+
* lookup is a round trip.
|
|
57
|
+
*/
|
|
58
|
+
async function loadConditionVocabulary(
|
|
59
|
+
c: Context<Env>,
|
|
60
|
+
smartCollection: SmartCollection,
|
|
61
|
+
) {
|
|
62
|
+
const ids = smartCollection.selection.collection ?? [];
|
|
63
|
+
if (ids.length === 0) return buildCollectionVocabulary([]);
|
|
64
|
+
return buildCollectionVocabulary(await c.var.services.collections.list());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readSort(
|
|
68
|
+
value: string | undefined,
|
|
69
|
+
fallback: SmartCollectionSortOrder,
|
|
70
|
+
showRatingSort: boolean,
|
|
71
|
+
): SmartCollectionSortOrder {
|
|
72
|
+
// `updated` was a fourth order before `newest` came to mean the same thing.
|
|
73
|
+
// Links to it are still in the wild, and they still resolve to what the
|
|
74
|
+
// reader chose, so the accept-old rule costs one line here.
|
|
75
|
+
const named = value === "updated" ? "newest" : value;
|
|
76
|
+
const requested = (
|
|
77
|
+
SMART_COLLECTION_SORT_ORDERS as readonly string[]
|
|
78
|
+
).includes(named ?? "")
|
|
79
|
+
? (named as SmartCollectionSortOrder)
|
|
80
|
+
: fallback;
|
|
81
|
+
// Silently falls back rather than showing an order that would look arbitrary
|
|
82
|
+
// on a set where almost nothing is rated — the same rule a collection page
|
|
83
|
+
// applies to its own rating sort.
|
|
84
|
+
return requested === "rating_desc" && !showRatingSort ? "newest" : requested;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Render a smart collection page.
|
|
89
|
+
*
|
|
90
|
+
* @param c - Hono context
|
|
91
|
+
* @param slug - The smart collection's address
|
|
92
|
+
* @returns The rendered page, or null when no smart collection lives there
|
|
93
|
+
*/
|
|
94
|
+
export async function renderSmartCollectionPage(
|
|
95
|
+
c: Context<Env>,
|
|
96
|
+
slug: string,
|
|
97
|
+
): Promise<Response | null> {
|
|
98
|
+
const page = parsePageNumber(c.req.query("page"));
|
|
99
|
+
const paginatedPageTitle = formatPageLabel(page);
|
|
100
|
+
|
|
101
|
+
const [smartCollection, navData] = await Promise.all([
|
|
102
|
+
c.var.services.smartCollections.getBySlug(slug),
|
|
103
|
+
getNavigationData(c),
|
|
104
|
+
]);
|
|
105
|
+
if (!smartCollection) return null;
|
|
106
|
+
|
|
107
|
+
const canonicalPagePath = getCollectionPagePath(smartCollection.slug);
|
|
108
|
+
const viewer = {
|
|
109
|
+
isAuthenticated: navData.isAuthenticated,
|
|
110
|
+
lang: getViewLang(c) ?? undefined,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// The rating order is offered only where it would say something. One cheap
|
|
114
|
+
// bounded count answers that, exactly as the collection page does it.
|
|
115
|
+
const ratedCount = await c.var.services.posts.countUpTo(
|
|
116
|
+
{
|
|
117
|
+
...c.var.services.smartCollections.toPostFilters(smartCollection, viewer),
|
|
118
|
+
hasRating: true,
|
|
119
|
+
},
|
|
120
|
+
2,
|
|
121
|
+
);
|
|
122
|
+
const showRatingSort = supportsCollectionRatingSort(ratedCount);
|
|
123
|
+
const defaultSort = readSort(smartCollection.sort, "newest", showRatingSort);
|
|
124
|
+
// The reader's `?sort=` wins over the stored default, the same way it does on
|
|
125
|
+
// a collection page. Condition params in the URL are ignored: membership is
|
|
126
|
+
// edited in the dialog, not by hand in the address bar.
|
|
127
|
+
const currentSort = readSort(
|
|
128
|
+
c.req.query("sort"),
|
|
129
|
+
defaultSort,
|
|
130
|
+
showRatingSort,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const pageSize = c.var.appConfig.pageSize;
|
|
134
|
+
const filters = c.var.services.smartCollections.toPostFilters(
|
|
135
|
+
{ ...smartCollection, sort: currentSort },
|
|
136
|
+
viewer,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const [totalThreadCount, posts, collectionVocabulary] = await Promise.all([
|
|
140
|
+
c.var.services.posts.count(filters),
|
|
141
|
+
c.var.services.posts.list({
|
|
142
|
+
...filters,
|
|
143
|
+
limit: pageSize,
|
|
144
|
+
offset: (page - 1) * pageSize,
|
|
145
|
+
}),
|
|
146
|
+
loadConditionVocabulary(c, smartCollection),
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
const totalPages = Math.max(1, Math.ceil(totalThreadCount / pageSize));
|
|
150
|
+
const items = await assembleTimelineItems(c, posts);
|
|
151
|
+
|
|
152
|
+
const i18n = getI18n(c);
|
|
153
|
+
const dimensionCtx = { collections: collectionVocabulary };
|
|
154
|
+
const feedHref = c.var.appConfig.rssFeedsEnabled
|
|
155
|
+
? `${canonicalPagePath}/feed`
|
|
156
|
+
: undefined;
|
|
157
|
+
|
|
158
|
+
return renderPublicPage(c, {
|
|
159
|
+
title:
|
|
160
|
+
page > 1
|
|
161
|
+
? buildPageTitle(
|
|
162
|
+
smartCollection.title,
|
|
163
|
+
paginatedPageTitle,
|
|
164
|
+
navData.siteName,
|
|
165
|
+
)
|
|
166
|
+
: buildPageTitle(smartCollection.title, navData.siteName),
|
|
167
|
+
description: smartCollection.description
|
|
168
|
+
? markdownToPlainText(smartCollection.description)
|
|
169
|
+
: undefined,
|
|
170
|
+
// A page the author declared, so it is always indexable and always has a
|
|
171
|
+
// canonical and an hreflang set — unlike an archive URL a reader assembled.
|
|
172
|
+
alternateLanguages: buildSurfaceAlternates(c),
|
|
173
|
+
pageFeed: feedHref
|
|
174
|
+
? { href: toViewPath(c, feedHref), title: smartCollection.title }
|
|
175
|
+
: undefined,
|
|
176
|
+
navData,
|
|
177
|
+
content: (
|
|
178
|
+
<SmartCollectionPage
|
|
179
|
+
smartCollection={smartCollection}
|
|
180
|
+
items={items}
|
|
181
|
+
totalThreadCount={totalThreadCount}
|
|
182
|
+
currentPage={page}
|
|
183
|
+
totalPages={totalPages}
|
|
184
|
+
pagePath={canonicalPagePath}
|
|
185
|
+
baseUrl={
|
|
186
|
+
currentSort === defaultSort
|
|
187
|
+
? toViewPath(c, canonicalPagePath)
|
|
188
|
+
: toViewPath(c, `${canonicalPagePath}?sort=${currentSort}`)
|
|
189
|
+
}
|
|
190
|
+
currentSort={currentSort}
|
|
191
|
+
defaultSort={defaultSort}
|
|
192
|
+
showRatingSort={showRatingSort}
|
|
193
|
+
conditionSummary={describeSmartCollection(
|
|
194
|
+
smartCollection.selection,
|
|
195
|
+
i18n,
|
|
196
|
+
dimensionCtx,
|
|
197
|
+
)}
|
|
198
|
+
conditionHref={buildSmartCollectionArchiveHref(
|
|
199
|
+
smartCollection.selection,
|
|
200
|
+
dimensionCtx,
|
|
201
|
+
)}
|
|
202
|
+
isAuthenticated={navData.isAuthenticated}
|
|
203
|
+
isInNavigation={navData.links.some(
|
|
204
|
+
(item) =>
|
|
205
|
+
item.type === "smart_collection" &&
|
|
206
|
+
item.smartCollectionId === smartCollection.id,
|
|
207
|
+
)}
|
|
208
|
+
sitePathPrefix={navData.sitePathPrefix}
|
|
209
|
+
basePath={navData.basePath}
|
|
210
|
+
feedHref={feedHref}
|
|
211
|
+
/>
|
|
212
|
+
),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Render a smart collection's Atom feed.
|
|
218
|
+
*
|
|
219
|
+
* No auth guard: a feed is anonymous by construction, and a smart collection
|
|
220
|
+
* can never name a set only its author can see.
|
|
221
|
+
*
|
|
222
|
+
* @param c - Hono context
|
|
223
|
+
* @param slug - The smart collection's address
|
|
224
|
+
* @returns The feed response, or null when no smart collection lives there
|
|
225
|
+
*/
|
|
226
|
+
export async function renderSmartCollectionFeed(
|
|
227
|
+
c: Context<Env>,
|
|
228
|
+
slug: string,
|
|
229
|
+
): Promise<Response | null> {
|
|
230
|
+
const smartCollection = await c.var.services.smartCollections.getBySlug(slug);
|
|
231
|
+
if (!smartCollection) return null;
|
|
232
|
+
|
|
233
|
+
const { appConfig, services } = c.var;
|
|
234
|
+
const canonicalFeedPath = `${getCollectionPagePath(
|
|
235
|
+
smartCollection.slug,
|
|
236
|
+
)}/feed`;
|
|
237
|
+
const publishedBefore = getRssPublishedBefore(
|
|
238
|
+
appConfig.rssPublishDelaySeconds,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const filters = services.smartCollections.toPostFilters(smartCollection, {
|
|
242
|
+
isAuthenticated: false,
|
|
243
|
+
lang: getViewLang(c) ?? undefined,
|
|
244
|
+
});
|
|
245
|
+
const posts = await services.posts.list({
|
|
246
|
+
...filters,
|
|
247
|
+
// The publication delay never moves: a just-published post is not announced
|
|
248
|
+
// early even when a year condition bounds the same column more tightly.
|
|
249
|
+
publishedBefore:
|
|
250
|
+
filters.publishedBefore === undefined
|
|
251
|
+
? publishedBefore
|
|
252
|
+
: Math.min(filters.publishedBefore, publishedBefore),
|
|
253
|
+
limit: getFeedLimit(c),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const rootIds = posts.filter((p) => p.threadId === p.id).map((p) => p.id);
|
|
257
|
+
const postIds = posts.map((post) => post.id);
|
|
258
|
+
const [threadMap, rawMediaMap, aliasesMap] = await Promise.all([
|
|
259
|
+
services.posts.getPublishedThreads(rootIds, { publishedBefore }),
|
|
260
|
+
services.media.getByPostIds(postIds),
|
|
261
|
+
services.paths.getPostAliases(postIds),
|
|
262
|
+
]);
|
|
263
|
+
|
|
264
|
+
const replyIds: string[] = [];
|
|
265
|
+
for (const [rootId, thread] of threadMap) {
|
|
266
|
+
for (const reply of thread) {
|
|
267
|
+
if (reply.id !== rootId) replyIds.push(reply.id);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const replyMediaMap =
|
|
271
|
+
replyIds.length > 0
|
|
272
|
+
? await services.media.getByPostIds(replyIds)
|
|
273
|
+
: new Map<string, never[]>();
|
|
274
|
+
|
|
275
|
+
const mediaCtx = createMediaContext(appConfig);
|
|
276
|
+
const allRawMedia = new Map(rawMediaMap);
|
|
277
|
+
for (const [id, media] of replyMediaMap) {
|
|
278
|
+
allRawMedia.set(id, media);
|
|
279
|
+
}
|
|
280
|
+
const mediaMap = buildMediaMap(
|
|
281
|
+
allRawMedia,
|
|
282
|
+
mediaCtx.r2PublicUrl,
|
|
283
|
+
mediaCtx.imageTransformUrl,
|
|
284
|
+
mediaCtx.s3PublicUrl,
|
|
285
|
+
mediaCtx.localPublicUrl,
|
|
286
|
+
mediaCtx.sitePathPrefix,
|
|
287
|
+
);
|
|
288
|
+
const aliasMap = new Map<string, string>();
|
|
289
|
+
for (const [id, aliases] of aliasesMap) {
|
|
290
|
+
if (aliases[0]) aliasMap.set(id, aliases[0]);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const postViews = toPostViews(
|
|
294
|
+
posts.map((post) => ({
|
|
295
|
+
...post,
|
|
296
|
+
mediaAttachments: mediaMap.get(post.id) ?? [],
|
|
297
|
+
})),
|
|
298
|
+
mediaCtx,
|
|
299
|
+
undefined,
|
|
300
|
+
aliasMap,
|
|
301
|
+
).map((postView, index) => {
|
|
302
|
+
const post = posts[index] as (typeof posts)[number];
|
|
303
|
+
const thread = threadMap.get(post.id);
|
|
304
|
+
const replies =
|
|
305
|
+
thread && thread.length > 1
|
|
306
|
+
? toPostViews(
|
|
307
|
+
thread
|
|
308
|
+
.filter((reply) => reply.id !== post.id)
|
|
309
|
+
.map((reply) => ({
|
|
310
|
+
...reply,
|
|
311
|
+
mediaAttachments: mediaMap.get(reply.id) ?? [],
|
|
312
|
+
})),
|
|
313
|
+
mediaCtx,
|
|
314
|
+
)
|
|
315
|
+
: undefined;
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
...postView,
|
|
319
|
+
feedUpdatedAt: getFeedEntryUpdatedAt(post, thread),
|
|
320
|
+
threadReplies: replies,
|
|
321
|
+
};
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const feedData = {
|
|
325
|
+
...buildFeedDiscoveryFields(c),
|
|
326
|
+
siteName: appConfig.siteName,
|
|
327
|
+
siteDescription: markdownToPlainText(appConfig.siteDescription),
|
|
328
|
+
siteUrl: appConfig.siteUrl,
|
|
329
|
+
siteLanguage: getViewLang(c) ?? appConfig.siteLanguage,
|
|
330
|
+
title: buildPageTitle(appConfig.siteName, smartCollection.title),
|
|
331
|
+
selfUrl: toAbsoluteSiteUrl(
|
|
332
|
+
`${viewBasePath(c)}${canonicalFeedPath}`,
|
|
333
|
+
appConfig.siteUrl,
|
|
334
|
+
appConfig.sitePathPrefix,
|
|
335
|
+
),
|
|
336
|
+
posts: postViews,
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
return renderFeed(defaultFeedRenderer(feedData));
|
|
340
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscribe Page Route
|
|
3
|
+
*
|
|
4
|
+
* `/subscribe` — how to follow this site, for a reader who does not already
|
|
5
|
+
* know. The nav's feed entry points here rather than at `/feed`, which hands a
|
|
6
|
+
* browser a screen of Atom XML.
|
|
7
|
+
*
|
|
8
|
+
* Three feeds, always, on every site: the main feed, the opposite end of the
|
|
9
|
+
* same list, and the complete record. Nothing here is conditioned on what the
|
|
10
|
+
* site happens to contain — every Jant site's subscribe page looks the same, so
|
|
11
|
+
* the page needs no service calls and no database round-trip at all.
|
|
12
|
+
*
|
|
13
|
+
* Collection feeds are deliberately absent. Wanting one collection and not the
|
|
14
|
+
* rest is a rare intent on a single-author site, and it already has a better
|
|
15
|
+
* home: the feed icon on the collection you are reading. Listing them here
|
|
16
|
+
* would tax every reader's attention for a few readers' benefit. The full route
|
|
17
|
+
* table lives in `docs/feeds.md`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { Hono } from "hono";
|
|
21
|
+
import type { Context } from "hono";
|
|
22
|
+
import { msg } from "@lingui/core/macro";
|
|
23
|
+
import type { Bindings } from "../../types.js";
|
|
24
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
25
|
+
import { getI18n } from "../../i18n/index.js";
|
|
26
|
+
import { getNavigationData } from "../../lib/navigation.js";
|
|
27
|
+
import { buildPageTitle } from "../../lib/page-title.js";
|
|
28
|
+
import { renderPublicPage } from "../../lib/render.js";
|
|
29
|
+
import { toAbsoluteSiteUrl, toPublicPath } from "../../lib/url.js";
|
|
30
|
+
import {
|
|
31
|
+
buildSurfaceAlternates,
|
|
32
|
+
viewBasePath,
|
|
33
|
+
} from "../../lib/view-language.js";
|
|
34
|
+
import { SubscribePage } from "../../ui/pages/SubscribePage.js";
|
|
35
|
+
import type { SubscribeFeed } from "../../ui/pages/SubscribePage.js";
|
|
36
|
+
|
|
37
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
38
|
+
|
|
39
|
+
export const subscribeRoutes = new Hono<Env>();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Render the subscribe page for the current view language.
|
|
43
|
+
*
|
|
44
|
+
* @param c - Hono context
|
|
45
|
+
* @returns The page, or 404 when feeds are switched off site-wide
|
|
46
|
+
*/
|
|
47
|
+
export async function renderSubscribePage(c: Context<Env>): Promise<Response> {
|
|
48
|
+
const { appConfig } = c.var;
|
|
49
|
+
|
|
50
|
+
// With feeds off there is nothing to subscribe to, and the nav entry is
|
|
51
|
+
// filtered out too — so the page 404s rather than listing dead addresses.
|
|
52
|
+
if (!appConfig.rssFeedsEnabled) return c.notFound();
|
|
53
|
+
|
|
54
|
+
const navData = await getNavigationData(c);
|
|
55
|
+
const i18n = getI18n(c);
|
|
56
|
+
|
|
57
|
+
// Readers paste these into a reader, so they have to be whole addresses, and
|
|
58
|
+
// they have to stay inside the language view they were read from.
|
|
59
|
+
const siteUrl =
|
|
60
|
+
appConfig.siteUrl ||
|
|
61
|
+
new URL(toPublicPath("/", appConfig.sitePathPrefix), c.req.url).toString();
|
|
62
|
+
const feedUrl = (path: string): string =>
|
|
63
|
+
toAbsoluteSiteUrl(
|
|
64
|
+
`${viewBasePath(c)}${path}`,
|
|
65
|
+
siteUrl,
|
|
66
|
+
appConfig.sitePathPrefix,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// The page each feed is the feed of. Relative, unlike the addresses: these
|
|
70
|
+
// are links to follow here and now, not strings to paste elsewhere.
|
|
71
|
+
const pageUrl = (path: string): string =>
|
|
72
|
+
toPublicPath(`${viewBasePath(c)}${path}`, appConfig.sitePathPrefix);
|
|
73
|
+
|
|
74
|
+
const latestLabel = i18n._(
|
|
75
|
+
msg({
|
|
76
|
+
message: "Latest",
|
|
77
|
+
comment: "@context: Subscribe page name for the latest public posts feed",
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
const latestDescription = i18n._(
|
|
81
|
+
msg({
|
|
82
|
+
message: "The same posts as the home page.",
|
|
83
|
+
comment:
|
|
84
|
+
"@context: Subscribe page description of the latest posts feed. The home page and this feed run the same query, so they carry the same posts.",
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
// `/latest` redirects to `/` — the home page is the canonical latest
|
|
88
|
+
// timeline, and its query is the one this feed runs.
|
|
89
|
+
const latestPageUrl = pageUrl("/");
|
|
90
|
+
const featuredLabel = i18n._(
|
|
91
|
+
msg({
|
|
92
|
+
message: "Featured",
|
|
93
|
+
comment: "@context: Subscribe page name for the featured posts feed",
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
const featuredDescription = i18n._(
|
|
97
|
+
msg({
|
|
98
|
+
message: "Only the posts marked as featured.",
|
|
99
|
+
comment:
|
|
100
|
+
"@context: Subscribe page description of the featured posts feed",
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
const featuredPageUrl = pageUrl("/featured");
|
|
104
|
+
|
|
105
|
+
const mainIsLatest = appConfig.mainRssFeed === "latest";
|
|
106
|
+
|
|
107
|
+
const mainFeed: SubscribeFeed = {
|
|
108
|
+
label: i18n._(
|
|
109
|
+
msg({
|
|
110
|
+
message: "Main feed",
|
|
111
|
+
comment: "@context: Subscribe page name for the site's main feed",
|
|
112
|
+
}),
|
|
113
|
+
),
|
|
114
|
+
url: feedUrl("/feed"),
|
|
115
|
+
// Named for what it currently carries. `/feed` is either end of the list
|
|
116
|
+
// depending on the site's setting, and "main feed" alone says neither.
|
|
117
|
+
// The description and the page it links to are the same pair the row
|
|
118
|
+
// below uses for that feed, so the two rows can never disagree.
|
|
119
|
+
description: mainIsLatest ? latestDescription : featuredDescription,
|
|
120
|
+
pageUrl: mainIsLatest ? latestPageUrl : featuredPageUrl,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// The other end of the same list. Offering `/latest/feed` beside a `/feed`
|
|
124
|
+
// that already returns the latest posts would be two names for one feed —
|
|
125
|
+
// exactly the confusion this page exists to end. The stable aliases are an
|
|
126
|
+
// author's concern and stay in General settings and the docs.
|
|
127
|
+
const otherEnd: SubscribeFeed = mainIsLatest
|
|
128
|
+
? {
|
|
129
|
+
label: featuredLabel,
|
|
130
|
+
url: feedUrl("/featured/feed"),
|
|
131
|
+
description: featuredDescription,
|
|
132
|
+
pageUrl: featuredPageUrl,
|
|
133
|
+
}
|
|
134
|
+
: {
|
|
135
|
+
label: latestLabel,
|
|
136
|
+
url: feedUrl("/latest/feed"),
|
|
137
|
+
description: latestDescription,
|
|
138
|
+
pageUrl: latestPageUrl,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const archiveFeed: SubscribeFeed = {
|
|
142
|
+
label: i18n._(
|
|
143
|
+
msg({
|
|
144
|
+
message: "All",
|
|
145
|
+
comment:
|
|
146
|
+
"@context: Subscribe page name for the full archive feed. Matches the Archive page's own nav label.",
|
|
147
|
+
}),
|
|
148
|
+
),
|
|
149
|
+
url: feedUrl("/archive/feed"),
|
|
150
|
+
// Says what it adds over the others rather than sounding like the default
|
|
151
|
+
// choice, so a reader who wants everything recognizes it and a reader who
|
|
152
|
+
// wants the main line knows to skip it.
|
|
153
|
+
description: i18n._(
|
|
154
|
+
msg({
|
|
155
|
+
message:
|
|
156
|
+
"Every published post, including those kept off the home page.",
|
|
157
|
+
comment:
|
|
158
|
+
"@context: Subscribe page description of the full archive feed. Says what it adds over the other two rather than sounding like the default pick.",
|
|
159
|
+
}),
|
|
160
|
+
),
|
|
161
|
+
pageUrl: pageUrl("/archive"),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return renderPublicPage(c, {
|
|
165
|
+
title: buildPageTitle(
|
|
166
|
+
i18n._(
|
|
167
|
+
msg({
|
|
168
|
+
message: "Subscribe",
|
|
169
|
+
comment: "@context: Browser page title for the subscribe page",
|
|
170
|
+
}),
|
|
171
|
+
),
|
|
172
|
+
navData.siteName,
|
|
173
|
+
),
|
|
174
|
+
alternateLanguages: buildSurfaceAlternates(c),
|
|
175
|
+
navData,
|
|
176
|
+
content: (
|
|
177
|
+
<SubscribePage mainFeed={mainFeed} otherFeeds={[otherEnd, archiveFeed]} />
|
|
178
|
+
),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
subscribeRoutes.get("/", renderSubscribePage);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { hasSessionCookie } from "../index.js";
|
|
3
|
+
|
|
4
|
+
describe("hasSessionCookie", () => {
|
|
5
|
+
it.each([
|
|
6
|
+
["__Secure-better-auth.session_token=abc", true],
|
|
7
|
+
["better-auth.session_token=abc", true],
|
|
8
|
+
["theme=dark; __Secure-better-auth.session_token=abc; lang=en", true],
|
|
9
|
+
["theme=dark", false],
|
|
10
|
+
["", false],
|
|
11
|
+
[null, false],
|
|
12
|
+
[undefined, false],
|
|
13
|
+
// A cookie merely *named* after the session must not count — only a value
|
|
14
|
+
// assignment does, so a stray marker cookie can't force primary reads.
|
|
15
|
+
["mysession_token", false],
|
|
16
|
+
])("hasSessionCookie(%s) → %s", (cookieHeader, expected) => {
|
|
17
|
+
expect(hasSessionCookie(cookieHeader)).toBe(expected);
|
|
18
|
+
});
|
|
19
|
+
});
|