@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,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language View Routes
|
|
3
|
+
*
|
|
4
|
+
* Every reader-facing surface, served a second time under a language prefix
|
|
5
|
+
* (`/en`, `/zh-hant`). The primary language keeps the unprefixed root, so this
|
|
6
|
+
* group only ever handles the other languages.
|
|
7
|
+
*
|
|
8
|
+
* Two things about how this is wired are load-bearing:
|
|
9
|
+
*
|
|
10
|
+
* 1. **The group is always mounted.** Which prefixes are languages is
|
|
11
|
+
* per-site configuration loaded by `withConfig()` on each request, and one
|
|
12
|
+
* app instance serves many sites in hosted mode — so the routing table
|
|
13
|
+
* cannot encode the language set. The decision is made per request instead.
|
|
14
|
+
*
|
|
15
|
+
* 2. **The decision lives inside each handler, not in middleware.** Hono
|
|
16
|
+
* middleware cannot decline a route: `next()` proceeds *into* the handlers
|
|
17
|
+
* that already matched. A gate that has to fall through to the
|
|
18
|
+
* `path_registry` catch-all therefore has to be the handler itself, which
|
|
19
|
+
* is what `langGet()` welds on at registration time.
|
|
20
|
+
*
|
|
21
|
+
* Mount this immediately before the catch-all page routes, after every static
|
|
22
|
+
* route group, so `/archive` reaches the archive rather than being read as a
|
|
23
|
+
* language named "archive".
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { Hono } from "hono";
|
|
27
|
+
import type { Context, Next } from "hono";
|
|
28
|
+
import type { Bindings } from "../../types.js";
|
|
29
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
30
|
+
import { resolveLanguageView } from "../../lib/view-language.js";
|
|
31
|
+
import {
|
|
32
|
+
redirectLegacyArchiveFeed,
|
|
33
|
+
renderArchiveFeed,
|
|
34
|
+
renderArchiveRoute,
|
|
35
|
+
} from "./archive.js";
|
|
36
|
+
import {
|
|
37
|
+
renderCollectionSelectionFeedRoute,
|
|
38
|
+
renderCollectionSelectionRoute,
|
|
39
|
+
} from "./collection.js";
|
|
40
|
+
import { renderCollectionsDirectory } from "./collections.js";
|
|
41
|
+
import { renderSubscribePage } from "./subscribe.js";
|
|
42
|
+
import {
|
|
43
|
+
redirectLegacyFeaturedFeed,
|
|
44
|
+
renderFeaturedFeed,
|
|
45
|
+
renderFeaturedPage,
|
|
46
|
+
} from "./featured.js";
|
|
47
|
+
import { renderHomePage } from "./home.js";
|
|
48
|
+
import {
|
|
49
|
+
redirectLatestToHome,
|
|
50
|
+
redirectLegacyLatestFeed,
|
|
51
|
+
renderLatestFeed,
|
|
52
|
+
} from "./latest.js";
|
|
53
|
+
import { renderRegisteredPath } from "./page.js";
|
|
54
|
+
import { renderSearchPage } from "./search.js";
|
|
55
|
+
import { renderMainFeed } from "../feed/feed.js";
|
|
56
|
+
|
|
57
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
58
|
+
|
|
59
|
+
export const languageRoutes = new Hono<Env>();
|
|
60
|
+
|
|
61
|
+
type ViewHandler = (c: Context<Env>) => Response | Promise<Response>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Register a surface under the language prefix, gated on the prefix actually
|
|
65
|
+
* being one of this site's languages.
|
|
66
|
+
*
|
|
67
|
+
* @param path - Path within the language prefix, such as `/archive`
|
|
68
|
+
* @param handler - The same handler the root namespace uses
|
|
69
|
+
*/
|
|
70
|
+
function langGet(path: string, handler: ViewHandler): void {
|
|
71
|
+
languageRoutes.get(path, async (c: Context<Env>, next: Next) => {
|
|
72
|
+
const decision = resolveLanguageView(c);
|
|
73
|
+
|
|
74
|
+
if (decision.kind === "pass") return next();
|
|
75
|
+
if (decision.kind === "redirect") return c.redirect(decision.to, 301);
|
|
76
|
+
|
|
77
|
+
c.set("viewLang", decision.lang);
|
|
78
|
+
// Drives `<html lang>`, the CJK font profile, and feed language tags. The
|
|
79
|
+
// message catalog deliberately stays on the base locale: public chrome is
|
|
80
|
+
// English on every view, exactly as it is at the root.
|
|
81
|
+
c.set("lang", decision.lang);
|
|
82
|
+
|
|
83
|
+
return handler(c);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
langGet("/", renderHomePage);
|
|
88
|
+
langGet("/feed", renderMainFeed);
|
|
89
|
+
|
|
90
|
+
langGet("/latest", redirectLatestToHome);
|
|
91
|
+
langGet("/latest/feed", renderLatestFeed);
|
|
92
|
+
langGet("/latest/feed/atom.xml", redirectLegacyLatestFeed);
|
|
93
|
+
|
|
94
|
+
langGet("/featured", renderFeaturedPage);
|
|
95
|
+
langGet("/featured/feed", renderFeaturedFeed);
|
|
96
|
+
langGet("/featured/feed/atom.xml", redirectLegacyFeaturedFeed);
|
|
97
|
+
|
|
98
|
+
langGet("/archive", renderArchiveRoute);
|
|
99
|
+
langGet("/archive/feed", renderArchiveFeed);
|
|
100
|
+
langGet("/archive/feed/atom.xml", redirectLegacyArchiveFeed);
|
|
101
|
+
|
|
102
|
+
langGet("/search", renderSearchPage);
|
|
103
|
+
|
|
104
|
+
langGet("/subscribe", renderSubscribePage);
|
|
105
|
+
|
|
106
|
+
langGet("/collections", renderCollectionsDirectory);
|
|
107
|
+
langGet("/collections/:slug", renderCollectionSelectionRoute);
|
|
108
|
+
langGet("/collections/:slug/feed", renderCollectionSelectionFeedRoute);
|
|
109
|
+
|
|
110
|
+
// Registered last: everything else resolves through the path registry, the
|
|
111
|
+
// same way the root namespace does. Anything that is not a language prefix
|
|
112
|
+
// falls through from here to the root catch-all.
|
|
113
|
+
langGet("/*", renderRegisteredPath);
|
|
@@ -5,22 +5,32 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Hono } from "hono";
|
|
8
|
+
import type { Context } from "hono";
|
|
8
9
|
import type { Bindings } from "../../types.js";
|
|
9
10
|
import type { AppVariables } from "../../types/app-context.js";
|
|
10
|
-
import {
|
|
11
|
+
import { toViewPath } from "../../lib/view-language.js";
|
|
11
12
|
import { defaultFeedRenderer } from "../../lib/feed.js";
|
|
12
|
-
import {
|
|
13
|
+
import { renderFeed } from "../../lib/feed-policy.js";
|
|
14
|
+
import { buildFeedData, parseFormatQuery } from "../feed/feed.js";
|
|
13
15
|
|
|
14
16
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
15
17
|
|
|
16
18
|
export const latestRoutes = new Hono<Env>();
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
/** `/latest` is an alias for the view's home timeline. */
|
|
21
|
+
export function redirectLatestToHome(c: Context<Env>): Response {
|
|
22
|
+
return c.redirect(toViewPath(c, "/"), 302);
|
|
23
|
+
}
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Render the latest Atom feed for the current view language.
|
|
27
|
+
*
|
|
28
|
+
* Accepts `?format=note|link|quote`.
|
|
29
|
+
*
|
|
30
|
+
* @param c - Hono context
|
|
31
|
+
* @returns Atom feed response
|
|
32
|
+
*/
|
|
33
|
+
export async function renderLatestFeed(c: Context<Env>): Promise<Response> {
|
|
24
34
|
const format = parseFormatQuery(c);
|
|
25
35
|
const feedData = await buildFeedData(c, {
|
|
26
36
|
kind: "latest",
|
|
@@ -28,16 +38,16 @@ latestRoutes.get("/feed", async (c) => {
|
|
|
28
38
|
format,
|
|
29
39
|
});
|
|
30
40
|
return renderFeed(defaultFeedRenderer(feedData));
|
|
31
|
-
}
|
|
41
|
+
}
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const sitePathPrefix = c.var.appConfig.sitePathPrefix;
|
|
43
|
+
/** Legacy atom.xml suffix → canonical /latest/feed, preserving `?format=`. */
|
|
44
|
+
export function redirectLegacyLatestFeed(c: Context<Env>): Response {
|
|
36
45
|
const qs = c.req.url.includes("?")
|
|
37
46
|
? c.req.url.slice(c.req.url.indexOf("?"))
|
|
38
47
|
: "";
|
|
39
|
-
return c.redirect(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
return c.redirect(`${toViewPath(c, "/latest/feed")}${qs}`, 308);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
latestRoutes.get("/", redirectLatestToHome);
|
|
52
|
+
latestRoutes.get("/feed", renderLatestFeed);
|
|
53
|
+
latestRoutes.get("/feed/atom.xml", redirectLegacyLatestFeed);
|
package/src/routes/pages/new.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import { buildPageTitle } from "../../lib/page-title.js";
|
|
|
8
8
|
import { renderPublicPage } from "../../lib/render.js";
|
|
9
9
|
import { getI18n } from "../../i18n/index.js";
|
|
10
10
|
import { ComposePage } from "../../ui/pages/ComposePage.js";
|
|
11
|
+
import { buildComposeLanguages, getViewLang } from "../../lib/view-language.js";
|
|
11
12
|
import { SETTINGS_KEYS } from "../../lib/constants.js";
|
|
12
13
|
|
|
13
14
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
@@ -43,6 +44,8 @@ newPostRoutes.get("/new", async (c) => {
|
|
|
43
44
|
uploadMaxFileSize={c.var.appConfig.uploadMaxFileSize}
|
|
44
45
|
closeHref="/"
|
|
45
46
|
slashCommandDiscovered={slashCommandDiscovered}
|
|
47
|
+
languages={buildComposeLanguages(c)}
|
|
48
|
+
contextLanguage={getViewLang(c)}
|
|
46
49
|
/>
|
|
47
50
|
),
|
|
48
51
|
});
|
|
@@ -24,15 +24,30 @@ import {
|
|
|
24
24
|
} from "../../lib/post-display.js";
|
|
25
25
|
import { buildArticleJsonLd } from "../../lib/structured-data.js";
|
|
26
26
|
import {
|
|
27
|
+
normalizePath,
|
|
27
28
|
toAbsoluteAssetUrl,
|
|
29
|
+
toAbsoluteSiteUrl,
|
|
28
30
|
toPublicHref,
|
|
29
31
|
toPublicPath,
|
|
30
32
|
} from "../../lib/url.js";
|
|
33
|
+
import {
|
|
34
|
+
buildLanguageSwitcher,
|
|
35
|
+
isPrefixedLanguageView,
|
|
36
|
+
toViewPath,
|
|
37
|
+
viewRelativePath,
|
|
38
|
+
type LanguageAlternate,
|
|
39
|
+
} from "../../lib/view-language.js";
|
|
40
|
+
import { getOrBuildEntry } from "../../i18n/supported-locales.js";
|
|
31
41
|
import { ABOUT_PAGE_SLUG } from "../../services/about-page.js";
|
|
32
42
|
import { isTextAttachment } from "../../services/media.js";
|
|
43
|
+
import type { ResolvedPath } from "../../services/path.js";
|
|
33
44
|
import type { Post } from "../../types.js";
|
|
34
45
|
import { renderArchivePage } from "./archive.js";
|
|
35
46
|
import { renderCollectionFeed, renderCollectionPage } from "./collection.js";
|
|
47
|
+
import {
|
|
48
|
+
renderSmartCollectionFeed,
|
|
49
|
+
renderSmartCollectionPage,
|
|
50
|
+
} from "./smart-collection.js";
|
|
36
51
|
|
|
37
52
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
38
53
|
|
|
@@ -55,10 +70,10 @@ interface TextPreviewAutoOpen {
|
|
|
55
70
|
* content at every reply URL. Point the canonical to the thread root so
|
|
56
71
|
* crawlers consolidate ranking on one URL.
|
|
57
72
|
*
|
|
58
|
-
* The root post is always at index 0 of `threadPostViews
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
73
|
+
* The root post is always at index 0 of `threadPostViews`: Thread order puts
|
|
74
|
+
* the root first even when a reply is older than it. When `threadPostViews` is
|
|
75
|
+
* undefined the post is not part of a multi-post thread, so the post itself is
|
|
76
|
+
* the root.
|
|
62
77
|
*/
|
|
63
78
|
function buildPostCanonicalHref(
|
|
64
79
|
postView: { permalink: string },
|
|
@@ -102,7 +117,9 @@ async function renderPostWithTextPreview(
|
|
|
102
117
|
post: Post,
|
|
103
118
|
autoOpen: TextPreviewAutoOpen,
|
|
104
119
|
) {
|
|
105
|
-
|
|
120
|
+
// A post page's chrome lives in the post's own language view (its URL
|
|
121
|
+
// carries no prefix, so `viewLang` can't say).
|
|
122
|
+
const navDataPromise = getNavigationData(c, { languageScope: post.language });
|
|
106
123
|
const display = await assemblePostPageDisplay(c, post, {
|
|
107
124
|
isAuthenticated: true,
|
|
108
125
|
});
|
|
@@ -147,6 +164,7 @@ async function renderPostWithTextPreview(
|
|
|
147
164
|
articleModifiedTime: display.articleModifiedTime,
|
|
148
165
|
jsonLd: buildPostJsonLd(c, display, meta, canonicalHref, navData.siteName),
|
|
149
166
|
navData,
|
|
167
|
+
composeContextLanguage: post.language,
|
|
150
168
|
content: (
|
|
151
169
|
<>
|
|
152
170
|
<PostPage
|
|
@@ -205,13 +223,82 @@ function canRenderDraftAboutEditor(
|
|
|
205
223
|
);
|
|
206
224
|
}
|
|
207
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Where each of a post's languages lives.
|
|
228
|
+
*
|
|
229
|
+
* A translated post is not the same URL in another language — it is a
|
|
230
|
+
* different post with its own address — so both the `hreflang` alternates and
|
|
231
|
+
* the language switcher are built from the translation group rather than from
|
|
232
|
+
* the current path.
|
|
233
|
+
*
|
|
234
|
+
* `pathByLanguage` always contains the post itself, so the switcher marks the
|
|
235
|
+
* language the reader is in even when nothing has been translated yet. The
|
|
236
|
+
* alternates stay empty in that case: a lone self-referential `hreflang` says
|
|
237
|
+
* nothing.
|
|
238
|
+
*
|
|
239
|
+
* @param c - Hono context
|
|
240
|
+
* @param post - The post being rendered
|
|
241
|
+
* @returns Head alternates and per-language destinations for the switcher
|
|
242
|
+
*/
|
|
243
|
+
async function buildTranslationLinks(
|
|
244
|
+
c: Context<Env>,
|
|
245
|
+
post: Post,
|
|
246
|
+
): Promise<{
|
|
247
|
+
alternates: LanguageAlternate[];
|
|
248
|
+
pathByLanguage: Map<string, string>;
|
|
249
|
+
}> {
|
|
250
|
+
const { siteUrl, sitePathPrefix } = c.var.appConfig;
|
|
251
|
+
const pathByLanguage = new Map<string, string>();
|
|
252
|
+
if (!post.language) return { alternates: [], pathByLanguage };
|
|
253
|
+
|
|
254
|
+
const translations = post.translationGroupId
|
|
255
|
+
? await c.var.services.posts.listTranslations(post.threadId)
|
|
256
|
+
: [];
|
|
257
|
+
// Only the versions a reader can actually open. A draft or private
|
|
258
|
+
// translation answers the same URL with a 404, and an `hreflang` pointing
|
|
259
|
+
// at a 404 is worse than no `hreflang` at all — the switcher then falls
|
|
260
|
+
// back to that language's home, which is always somewhere real (§5).
|
|
261
|
+
const siblings = translations.filter(
|
|
262
|
+
(other): other is Post & { language: string } =>
|
|
263
|
+
Boolean(other.language) &&
|
|
264
|
+
other.status === "published" &&
|
|
265
|
+
other.visibility !== "private",
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Prefer each post's custom URL when it has one, the same way permalinks do,
|
|
269
|
+
// so a link never points at a URL that immediately redirects.
|
|
270
|
+
const aliasesMap = await c.var.services.paths.getPostAliases([
|
|
271
|
+
post.id,
|
|
272
|
+
...siblings.map((other) => other.id),
|
|
273
|
+
]);
|
|
274
|
+
const pathFor = (target: Post) =>
|
|
275
|
+
`/${aliasesMap.get(target.id)?.[0] ?? target.slug}`;
|
|
276
|
+
|
|
277
|
+
const group = [post as Post & { language: string }, ...siblings];
|
|
278
|
+
for (const target of group) {
|
|
279
|
+
pathByLanguage.set(target.language, pathFor(target));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const alternates =
|
|
283
|
+
siblings.length > 0 && siteUrl
|
|
284
|
+
? group.map((target) => ({
|
|
285
|
+
hreflang: target.language,
|
|
286
|
+
href: toAbsoluteSiteUrl(pathFor(target), siteUrl, sitePathPrefix),
|
|
287
|
+
}))
|
|
288
|
+
: [];
|
|
289
|
+
|
|
290
|
+
return { alternates, pathByLanguage };
|
|
291
|
+
}
|
|
292
|
+
|
|
208
293
|
async function renderPost(
|
|
209
294
|
c: Context<Env>,
|
|
210
295
|
post: Post,
|
|
211
296
|
options: { allowDraft?: boolean; isPreview?: boolean } = {},
|
|
212
297
|
) {
|
|
213
|
-
// Start navData fetch immediately — it's independent of thread/media queries
|
|
214
|
-
|
|
298
|
+
// Start navData fetch immediately — it's independent of thread/media queries.
|
|
299
|
+
// The chrome is scoped to the post's language: a Japanese post's logo and
|
|
300
|
+
// nav lead into /ja, since the post URL itself carries no language.
|
|
301
|
+
const navDataPromise = getNavigationData(c, { languageScope: post.language });
|
|
215
302
|
const display = await assemblePostPageDisplay(c, post, {
|
|
216
303
|
// Private-post access is validated before renderPost() is called.
|
|
217
304
|
isAuthenticated: true,
|
|
@@ -224,6 +311,22 @@ async function renderPost(
|
|
|
224
311
|
|
|
225
312
|
const navData = await navDataPromise;
|
|
226
313
|
const meta = buildPostMeta(post, navData.siteName);
|
|
314
|
+
|
|
315
|
+
// A post is rendered in its own language, whatever surface led here — this
|
|
316
|
+
// drives `<html lang>`, screen-reader pronunciation, and the CJK font
|
|
317
|
+
// profile (Simplified, Traditional, Japanese and Korean share code points
|
|
318
|
+
// but not glyphs).
|
|
319
|
+
if (post.language) c.set("lang", post.language);
|
|
320
|
+
const { alternates, pathByLanguage } = await buildTranslationLinks(c, post);
|
|
321
|
+
// The reader-facing half of the same data: a line under the post, so someone
|
|
322
|
+
// who cannot read this language does not have to find the switcher.
|
|
323
|
+
const translationLinks = [...pathByLanguage]
|
|
324
|
+
.filter(([tag]) => tag !== post.language)
|
|
325
|
+
.map(([tag, path]) => ({
|
|
326
|
+
href: toPublicPath(path, c.var.appConfig.sitePathPrefix),
|
|
327
|
+
label: getOrBuildEntry(tag).native,
|
|
328
|
+
lang: tag,
|
|
329
|
+
}));
|
|
227
330
|
const postView =
|
|
228
331
|
options.isPreview && display.postView.status === "draft"
|
|
229
332
|
? {
|
|
@@ -270,6 +373,14 @@ async function renderPost(
|
|
|
270
373
|
? buildPageTitle(previewTitle, meta.title, navData.siteName)
|
|
271
374
|
: buildPageTitle(meta.title, navData.siteName),
|
|
272
375
|
description: meta.description,
|
|
376
|
+
...(alternates.length > 0 ? { alternateLanguages: alternates } : {}),
|
|
377
|
+
// "Take me to this language's site": the translation when one exists, and
|
|
378
|
+
// that language's home page when it does not (§5).
|
|
379
|
+
languageSwitcher: buildLanguageSwitcher(c, {
|
|
380
|
+
hrefByLanguage: pathByLanguage,
|
|
381
|
+
fallbackPath: "/",
|
|
382
|
+
...(post.language ? { currentLang: post.language } : {}),
|
|
383
|
+
}),
|
|
273
384
|
...(post.status === "published"
|
|
274
385
|
? {
|
|
275
386
|
canonicalHref,
|
|
@@ -290,6 +401,7 @@ async function renderPost(
|
|
|
290
401
|
}
|
|
291
402
|
: {}),
|
|
292
403
|
navData,
|
|
404
|
+
composeContextLanguage: post.language,
|
|
293
405
|
...(options.isPreview
|
|
294
406
|
? {
|
|
295
407
|
pageChrome: <DraftPreviewBar editHref={draftEditHref} />,
|
|
@@ -301,6 +413,7 @@ async function renderPost(
|
|
|
301
413
|
post={postView}
|
|
302
414
|
threadPosts={threadPostViews}
|
|
303
415
|
isPreview={options.isPreview}
|
|
416
|
+
translations={translationLinks}
|
|
304
417
|
/>
|
|
305
418
|
),
|
|
306
419
|
});
|
|
@@ -321,11 +434,48 @@ pageRoutes.get("/preview/:slug", requireAuth(), async (c) => {
|
|
|
321
434
|
return renderPost(c, post, { allowDraft: true, isPreview: true });
|
|
322
435
|
});
|
|
323
436
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
437
|
+
/**
|
|
438
|
+
* Resolve a stored path, reusing the lookup the stored-redirect middleware
|
|
439
|
+
* already made for this request's own address.
|
|
440
|
+
*
|
|
441
|
+
* Only that one address is reusable: a `/feed` suffix or a text deep link asks
|
|
442
|
+
* about a different path, and a language view strips its prefix before asking,
|
|
443
|
+
* so the stored form is compared rather than assumed.
|
|
444
|
+
*
|
|
445
|
+
* @param c - Hono context
|
|
446
|
+
* @param storedPath - Registry path form, without a leading slash
|
|
447
|
+
* @returns The resolved record, or null when nothing is registered there
|
|
448
|
+
*/
|
|
449
|
+
async function resolveStoredPath(
|
|
450
|
+
c: Context<Env>,
|
|
451
|
+
storedPath: string,
|
|
452
|
+
): Promise<ResolvedPath | null> {
|
|
453
|
+
const lookup = c.var.pathLookup;
|
|
454
|
+
if (lookup && lookup.path === normalizePath(storedPath)) {
|
|
455
|
+
return lookup.record;
|
|
456
|
+
}
|
|
457
|
+
return c.var.services.paths.resolve(storedPath);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Resolve a path through the path registry: posts, collections, their aliases,
|
|
462
|
+
* archive URLs, stored redirects, and text-attachment deep links.
|
|
463
|
+
*
|
|
464
|
+
* Serves both the root namespace and every language view. A language view
|
|
465
|
+
* resolves the same rows — the prefix is stripped first — but a post reached
|
|
466
|
+
* that way redirects to its one canonical address, because a post's URL never
|
|
467
|
+
* encodes a language.
|
|
468
|
+
*
|
|
469
|
+
* @param c - Hono context
|
|
470
|
+
* @returns The resolved page, a redirect, or 404
|
|
471
|
+
*/
|
|
472
|
+
export async function renderRegisteredPath(c: Context<Env>): Promise<Response> {
|
|
473
|
+
const fullPath = viewRelativePath(c).slice(1); // Remove leading /
|
|
327
474
|
if (!fullPath) return c.notFound();
|
|
328
475
|
const sitePathPrefix = c.var.appConfig.sitePathPrefix;
|
|
476
|
+
// Posts keep one language-neutral address; every other surface stays inside
|
|
477
|
+
// the view the reader is browsing.
|
|
478
|
+
const inLanguageView = isPrefixedLanguageView(c);
|
|
329
479
|
|
|
330
480
|
// Explicit application routes are registered before this catch-all. Any
|
|
331
481
|
// unmatched path under a reserved prefix is therefore an application 404,
|
|
@@ -334,10 +484,15 @@ pageRoutes.get("/*", async (c) => {
|
|
|
334
484
|
|
|
335
485
|
// Stored redirects outrank the normal post/collection/deep-link resolvers,
|
|
336
486
|
// but can never shadow an explicit or reserved application route.
|
|
337
|
-
const resolved = await c
|
|
487
|
+
const resolved = await resolveStoredPath(c, fullPath);
|
|
338
488
|
if (resolved?.kind === "redirect" && resolved.redirectToPath) {
|
|
489
|
+
const target = `/${resolved.redirectToPath}`;
|
|
490
|
+
// Follow the stored redirect within the current view; the hop that lands
|
|
491
|
+
// on a post then applies the canonical-address rule below.
|
|
339
492
|
return c.redirect(
|
|
340
|
-
toPublicHref(
|
|
493
|
+
toPublicHref(target) === target
|
|
494
|
+
? toViewPath(c, target)
|
|
495
|
+
: toPublicHref(target, sitePathPrefix),
|
|
341
496
|
resolved.redirectType ?? 301,
|
|
342
497
|
);
|
|
343
498
|
}
|
|
@@ -348,6 +503,17 @@ pageRoutes.get("/*", async (c) => {
|
|
|
348
503
|
|
|
349
504
|
const resolvedCollection =
|
|
350
505
|
await c.var.services.paths.resolve(collectionPath);
|
|
506
|
+
|
|
507
|
+
if (resolvedCollection?.smartCollectionId) {
|
|
508
|
+
const smartCollection = await c.var.services.smartCollections.getById(
|
|
509
|
+
resolvedCollection.smartCollectionId,
|
|
510
|
+
);
|
|
511
|
+
if (!smartCollection) return c.notFound();
|
|
512
|
+
|
|
513
|
+
const result = await renderSmartCollectionFeed(c, smartCollection.slug);
|
|
514
|
+
return result ?? c.notFound();
|
|
515
|
+
}
|
|
516
|
+
|
|
351
517
|
if (resolvedCollection?.collectionId) {
|
|
352
518
|
const collection = await c.var.services.collections.getById(
|
|
353
519
|
resolvedCollection.collectionId,
|
|
@@ -360,10 +526,7 @@ pageRoutes.get("/*", async (c) => {
|
|
|
360
526
|
collection.id,
|
|
361
527
|
);
|
|
362
528
|
if (alias) {
|
|
363
|
-
return c.redirect(
|
|
364
|
-
toPublicPath(`/${alias.path}/feed`, sitePathPrefix),
|
|
365
|
-
301,
|
|
366
|
-
);
|
|
529
|
+
return c.redirect(toViewPath(c, `/${alias.path}/feed`), 301);
|
|
367
530
|
}
|
|
368
531
|
|
|
369
532
|
const result = await renderCollectionFeed(c, collection.slug);
|
|
@@ -390,26 +553,31 @@ pageRoutes.get("/*", async (c) => {
|
|
|
390
553
|
const resolvedPost = await c.var.services.paths.resolve(slugPart);
|
|
391
554
|
|
|
392
555
|
if (resolvedPost?.postId) {
|
|
393
|
-
const
|
|
556
|
+
const loaded = await c.var.services.posts.getWithCanonicalAlias(
|
|
557
|
+
resolvedPost.postId,
|
|
558
|
+
);
|
|
559
|
+
const post = loaded?.post;
|
|
394
560
|
if (!post || post.status === "draft") return c.notFound();
|
|
395
561
|
|
|
396
|
-
if (post.visibility === "private") {
|
|
397
|
-
|
|
398
|
-
if (!navData.isAuthenticated) return c.notFound();
|
|
562
|
+
if (post.visibility === "private" && !c.var.isAuthenticated) {
|
|
563
|
+
return c.notFound();
|
|
399
564
|
}
|
|
400
565
|
|
|
401
566
|
// Redirect slug → alias if one exists (same pattern as post pages)
|
|
402
|
-
if (resolvedPost.kind === "slug") {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
567
|
+
if (resolvedPost.kind === "slug" && loaded.canonicalAlias) {
|
|
568
|
+
return c.redirect(
|
|
569
|
+
toPublicPath(
|
|
570
|
+
`${loaded.canonicalAlias}/text/${mediaId}`,
|
|
571
|
+
sitePathPrefix,
|
|
572
|
+
),
|
|
573
|
+
301,
|
|
406
574
|
);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// A text attachment belongs to a post, so it inherits that post's single
|
|
578
|
+
// canonical address rather than existing once per language view.
|
|
579
|
+
if (inLanguageView) {
|
|
580
|
+
return c.redirect(toPublicPath(`/${fullPath}`, sitePathPrefix), 301);
|
|
413
581
|
}
|
|
414
582
|
|
|
415
583
|
// Verify the media belongs to this post and is a Jant-composed text
|
|
@@ -453,31 +621,43 @@ pageRoutes.get("/*", async (c) => {
|
|
|
453
621
|
}
|
|
454
622
|
|
|
455
623
|
if (resolved.postId) {
|
|
456
|
-
const
|
|
624
|
+
const loaded = await c.var.services.posts.getWithCanonicalAlias(
|
|
625
|
+
resolved.postId,
|
|
626
|
+
);
|
|
627
|
+
const post = loaded?.post;
|
|
457
628
|
if (!post) return c.notFound();
|
|
458
629
|
|
|
459
630
|
const allowDraft = canRenderDraftAboutEditor(c, fullPath, post);
|
|
460
631
|
if (post.status === "draft" && !allowDraft) return c.notFound();
|
|
461
632
|
|
|
462
|
-
if (post.visibility === "private") {
|
|
463
|
-
|
|
464
|
-
if (!navData.isAuthenticated) return c.notFound();
|
|
633
|
+
if (post.visibility === "private" && !c.var.isAuthenticated) {
|
|
634
|
+
return c.notFound();
|
|
465
635
|
}
|
|
466
636
|
|
|
467
|
-
// If accessed via slug but an alias exists,
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
637
|
+
// If accessed via slug but an alias exists, the alias is canonical.
|
|
638
|
+
const canonicalPath =
|
|
639
|
+
resolved.kind === "slug" && loaded.canonicalAlias
|
|
640
|
+
? loaded.canonicalAlias
|
|
641
|
+
: `/${fullPath}`;
|
|
642
|
+
|
|
643
|
+
// One address per post: a language-prefixed URL is only ever a way in.
|
|
644
|
+
if (inLanguageView || canonicalPath !== `/${fullPath}`) {
|
|
645
|
+
return c.redirect(toPublicPath(canonicalPath, sitePathPrefix), 301);
|
|
476
646
|
}
|
|
477
647
|
|
|
478
648
|
return renderPost(c, post, { allowDraft });
|
|
479
649
|
}
|
|
480
650
|
|
|
651
|
+
if (resolved.smartCollectionId) {
|
|
652
|
+
const smartCollection = await c.var.services.smartCollections.getById(
|
|
653
|
+
resolved.smartCollectionId,
|
|
654
|
+
);
|
|
655
|
+
if (!smartCollection) return c.notFound();
|
|
656
|
+
|
|
657
|
+
const result = await renderSmartCollectionPage(c, smartCollection.slug);
|
|
658
|
+
return result ?? c.notFound();
|
|
659
|
+
}
|
|
660
|
+
|
|
481
661
|
if (resolved.collectionId) {
|
|
482
662
|
const collection = await c.var.services.collections.getById(
|
|
483
663
|
resolved.collectionId,
|
|
@@ -490,7 +670,7 @@ pageRoutes.get("/*", async (c) => {
|
|
|
490
670
|
collection.id,
|
|
491
671
|
);
|
|
492
672
|
if (alias) {
|
|
493
|
-
return c.redirect(
|
|
673
|
+
return c.redirect(toViewPath(c, `/${alias.path}`), 301);
|
|
494
674
|
}
|
|
495
675
|
|
|
496
676
|
const result = await renderCollectionPage(c, collection.slug);
|
|
@@ -509,4 +689,7 @@ pageRoutes.get("/*", async (c) => {
|
|
|
509
689
|
}
|
|
510
690
|
|
|
511
691
|
return c.notFound();
|
|
512
|
-
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Catch-all for path-registry backed post URLs, aliases, and redirects
|
|
695
|
+
pageRoutes.get("/*", renderRegisteredPath);
|
|
@@ -59,6 +59,47 @@ partialPageRoutes.get("/_/post-card/:postId", async (c) => {
|
|
|
59
59
|
);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* A Post rendered as its detail page, with every control taken off.
|
|
64
|
+
*
|
|
65
|
+
* For surfaces that show one Post inside another context — the composer
|
|
66
|
+
* writing a translation of it, so far. Those need the Post to *look* like
|
|
67
|
+
* itself, because its structure is part of what the author is working from,
|
|
68
|
+
* but they must not offer to act on it: the actions belong to the Post being
|
|
69
|
+
* written, not to the one being read.
|
|
70
|
+
*
|
|
71
|
+
* Not a variant of `/_/post-view`: that one renders the whole Thread and is
|
|
72
|
+
* what a page swaps itself for. This renders the root alone.
|
|
73
|
+
*/
|
|
74
|
+
partialPageRoutes.get("/_/post-preview/:postId", async (c) => {
|
|
75
|
+
const postId = parseIdParam(c.req.param("postId"), ID_PREFIX.post);
|
|
76
|
+
const postView = await assemblePostCardView(c, postId, {
|
|
77
|
+
isAuthenticated: c.var.isAuthenticated,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!postView) {
|
|
81
|
+
return c.notFound();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return c.html(
|
|
85
|
+
<I18nProvider c={c}>
|
|
86
|
+
<TimelineItemFromPost
|
|
87
|
+
post={postView}
|
|
88
|
+
mode="detail"
|
|
89
|
+
display={{
|
|
90
|
+
hideStatusBadges: true,
|
|
91
|
+
showFullBody: true,
|
|
92
|
+
// `hideTimestamp` is deliberately absent: a titled detail post puts
|
|
93
|
+
// its date in the header, and the card only drops the footer's copy
|
|
94
|
+
// when this is left undefined. Stating it either way prints the date
|
|
95
|
+
// twice.
|
|
96
|
+
footer: { hideActions: true, hideReply: true },
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
</I18nProvider>,
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
62
103
|
partialPageRoutes.get("/_/post-view/:postId", async (c) => {
|
|
63
104
|
const postId = parseIdParam(c.req.param("postId"), ID_PREFIX.post);
|
|
64
105
|
const display = await assemblePostPageDisplay(c, postId, {
|