@jant/core 0.6.15 → 0.7.0
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/import-site.js +67 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
- 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-CD3FhrOB.js +257 -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-CgaL2jCs.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-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- 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__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -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__/mise-config.test.ts +22 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- 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 +524 -2
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
- 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 +88 -1
- 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__/compose-format-convert.test.ts +0 -19
- 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 +79 -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 +3283 -389
- package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
- 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 +541 -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-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +121 -3
- 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 +106 -49
- package/src/client/components/jant-compose-dialog.ts +2757 -1194
- package/src/client/components/jant-compose-editor.ts +866 -397
- package/src/client/components/jant-compose-fullscreen.ts +53 -47
- 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 +1142 -268
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -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 +336 -242
- package/src/client/compose-launch.ts +89 -2
- package/src/client/compose-shortcuts.ts +9 -7
- 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/post-refresh.ts +135 -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__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +143 -38
- 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__/backfill-thread-activity.test.ts +234 -0
- 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/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0031_many_ego.sql +3 -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/0031_snapshot.json +2562 -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 +28 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -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/0029_snapshot.json +3289 -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 +28 -0
- package/src/db/pg/schema.ts +213 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +242 -27
- package/src/db/thread-activity.ts +101 -0
- 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 +568 -168
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +562 -162
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +562 -162
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- 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 +1691 -12
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -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__/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.test.ts +16 -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 +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +88 -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__/translit.test.ts +87 -0
- 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 +93 -34
- 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 +437 -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 +1021 -66
- package/src/lib/filter-dimensions.ts +1349 -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 +21 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- 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-display.ts +42 -9
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- 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 +81 -10
- package/src/lib/tiptap-render.ts +5 -4
- package/src/lib/translit.ts +288 -0
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +188 -24
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +79 -23
- 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-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- 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__/palette.test.ts +44 -0
- package/src/routes/api/__tests__/post-translations.test.ts +829 -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/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 +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +172 -9
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +30 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- 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 +11 -2
- 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__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- 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 +657 -1
- 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__/featured.test.ts +6 -2
- 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-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/preview.test.ts +3 -1
- 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/archive.tsx +507 -357
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- 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 +183 -19
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- 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/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +139 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +323 -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 +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +330 -47
- package/src/services/custom-url.ts +27 -22
- 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/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- 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/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +281 -24
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +225 -7
- package/src/services/post.ts +1318 -252
- package/src/services/search.ts +48 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- 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 -1585
- package/src/styles/site-media.css +20 -6
- package/src/styles/tokens.css +144 -13
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1224 -7276
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +92 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +168 -30
- package/src/types/views.ts +61 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +215 -21
- 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 +275 -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/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +103 -17
- 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 +401 -301
- 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 +122 -3
- 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/DraftPreviewBar.tsx +25 -9
- 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 +55 -23
- 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/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +8 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-BxCOR3Uc.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-pLre2DM_.js +0 -1
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
- package/dist/client/_assets/client-DxY5BFe8.js +0 -296
- package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
- package/dist/env-OHRKGcMj.js +0 -281
- package/dist/github-api-BgSiE71w.js +0 -176
- package/dist/github-app-BbklkFmU.js +0 -275
- package/dist/github-sync-Cq1pzzOI.js +0 -431
- package/dist/github-sync-ppWXN3jb.js +0 -4
- package/dist/url-BMYO-Zlt.js +0 -395
- 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/ui/pages/CollectionEditorPage.tsx +0 -175
package/src/lib/navigation.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import type { Context } from "hono";
|
|
8
8
|
import type { Collection, NavItem, NavItemView } from "../types.js";
|
|
9
|
+
import { isFeedNavKey } from "../types/constants.js";
|
|
9
10
|
import { toNavItemViews } from "./view.js";
|
|
11
|
+
import { languageScopeBasePath, viewBasePath } from "./view-language.js";
|
|
10
12
|
import { render as renderMarkdown, toPlainText } from "./markdown.js";
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -15,7 +17,15 @@ import { render as renderMarkdown, toPlainText } from "./markdown.js";
|
|
|
15
17
|
export interface NavigationData {
|
|
16
18
|
links: NavItemView[];
|
|
17
19
|
currentPath: string;
|
|
20
|
+
/** Deployment path prefix, such as `/blog`. */
|
|
18
21
|
sitePathPrefix: string;
|
|
22
|
+
/**
|
|
23
|
+
* Public path prefix for reader-facing surfaces. Equals `sitePathPrefix`
|
|
24
|
+
* outside a language view, and carries the language prefix inside one
|
|
25
|
+
* (`/blog/en`). Pass this — not `sitePathPrefix` — to anything building
|
|
26
|
+
* links to the home page, archive, search, feeds or collections.
|
|
27
|
+
*/
|
|
28
|
+
basePath: string;
|
|
19
29
|
siteName: string;
|
|
20
30
|
/** Plain-text description for meta tags and RSS/Atom feeds */
|
|
21
31
|
siteDescription: string;
|
|
@@ -23,11 +33,37 @@ export interface NavigationData {
|
|
|
23
33
|
siteDescriptionHtml?: string;
|
|
24
34
|
isAuthenticated: boolean;
|
|
25
35
|
collections: Collection[];
|
|
26
|
-
|
|
36
|
+
/** Header/drawer mark, already sized down — not the canonical asset. */
|
|
37
|
+
siteAvatarThumbUrl?: string;
|
|
27
38
|
showHeaderAvatar?: boolean;
|
|
28
39
|
siteFooterHtml?: string;
|
|
29
40
|
}
|
|
30
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Collect the collections and smart collections already placed in navigation.
|
|
44
|
+
*
|
|
45
|
+
* The directory offers both kinds the same "Add to Navigation" action, so it
|
|
46
|
+
* needs one list to check against; TypeID prefixes (`col_`, `smc_`) keep the
|
|
47
|
+
* two apart without a second field.
|
|
48
|
+
*
|
|
49
|
+
* @param links - Navigation items as rendered for this request
|
|
50
|
+
* @returns TypeIDs of every collection and smart collection in navigation
|
|
51
|
+
* @example
|
|
52
|
+
* const placed = collectNavigationCollectionIds(navData.links);
|
|
53
|
+
* // ["col_01abc", "smc_01xyz"]
|
|
54
|
+
*/
|
|
55
|
+
export function collectNavigationCollectionIds(links: NavItemView[]): string[] {
|
|
56
|
+
return links.flatMap((item) => {
|
|
57
|
+
if (item.type === "collection" && item.collectionId) {
|
|
58
|
+
return [item.collectionId];
|
|
59
|
+
}
|
|
60
|
+
if (item.type === "smart_collection" && item.smartCollectionId) {
|
|
61
|
+
return [item.smartCollectionId];
|
|
62
|
+
}
|
|
63
|
+
return [];
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
31
67
|
/**
|
|
32
68
|
* Fetch navigation data for public pages.
|
|
33
69
|
*
|
|
@@ -49,20 +85,45 @@ export interface NavigationData {
|
|
|
49
85
|
*/
|
|
50
86
|
export async function getNavigationData(
|
|
51
87
|
c: Context,
|
|
52
|
-
options?: {
|
|
88
|
+
options?: {
|
|
89
|
+
preloadedItems?: NavItem[];
|
|
90
|
+
includeCollections?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Content language whose view the page's chrome should live in. Post
|
|
93
|
+
* pages pass their post's language here: their URLs carry no language
|
|
94
|
+
* prefix, so `viewLang` is never set on them, yet a Japanese post's
|
|
95
|
+
* logo and nav should lead to `/ja`, not to the primary view.
|
|
96
|
+
*/
|
|
97
|
+
languageScope?: string | null;
|
|
98
|
+
},
|
|
53
99
|
): Promise<NavigationData> {
|
|
100
|
+
const appConfig = c.var.appConfig;
|
|
101
|
+
const langBase =
|
|
102
|
+
options?.languageScope !== undefined
|
|
103
|
+
? languageScopeBasePath(c, options.languageScope)
|
|
104
|
+
: viewBasePath(c);
|
|
105
|
+
// An empty prefix means the primary language's view, whose nav items already
|
|
106
|
+
// point where they should — so the whole primary-language site, multilingual
|
|
107
|
+
// or not, never pays for the translation lookup.
|
|
108
|
+
const scopeLanguage = langBase
|
|
109
|
+
? (options?.languageScope ?? c.var.viewLang ?? null)
|
|
110
|
+
: null;
|
|
111
|
+
|
|
54
112
|
// Callers that already fetched nav items can pass them in to avoid a
|
|
55
113
|
// redundant DB round-trip.
|
|
56
114
|
const savedItems =
|
|
57
|
-
options?.preloadedItems ??
|
|
115
|
+
options?.preloadedItems ??
|
|
116
|
+
(await c.var.services.navItems.list({ language: scopeLanguage }));
|
|
58
117
|
const currentPath = c.var.publicPath;
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
//
|
|
118
|
+
// Keep the saved feed items untouched so their placement and custom labels
|
|
119
|
+
// come back when feeds are re-enabled. Only the rendered projection is
|
|
120
|
+
// filtered. Both entries go: `rss` addresses a feed that is off, and
|
|
121
|
+
// `subscribe` a page that 404s without one.
|
|
62
122
|
const items = appConfig.rssFeedsEnabled
|
|
63
123
|
? savedItems
|
|
64
124
|
: savedItems.filter(
|
|
65
|
-
(item: NavItem) =>
|
|
125
|
+
(item: NavItem) =>
|
|
126
|
+
item.type !== "system" || !isFeedNavKey(item.systemKey),
|
|
66
127
|
);
|
|
67
128
|
|
|
68
129
|
const siteName = appConfig.siteName;
|
|
@@ -80,8 +141,10 @@ export async function getNavigationData(
|
|
|
80
141
|
})
|
|
81
142
|
: undefined;
|
|
82
143
|
|
|
83
|
-
// Avatar URL and display flag come from appConfig
|
|
84
|
-
|
|
144
|
+
// Avatar URL and display flag come from appConfig. The chrome takes the
|
|
145
|
+
// sized variant: the full-resolution asset belongs to the social card and
|
|
146
|
+
// the settings editor, not to a 32px mark on every page.
|
|
147
|
+
const siteAvatarThumbUrl = appConfig.siteAvatarThumbUrl || undefined;
|
|
85
148
|
const showHeaderAvatar = appConfig.showHeaderAvatar;
|
|
86
149
|
|
|
87
150
|
// Render footer markdown
|
|
@@ -107,6 +170,8 @@ export async function getNavigationData(
|
|
|
107
170
|
? await c.var.services.navItems.getCollectionFreshness(collectionNavIds)
|
|
108
171
|
: undefined;
|
|
109
172
|
|
|
173
|
+
const basePath = `${appConfig.sitePathPrefix}${langBase}`;
|
|
174
|
+
|
|
110
175
|
const links = toNavItemViews(
|
|
111
176
|
items,
|
|
112
177
|
currentPath,
|
|
@@ -114,6 +179,7 @@ export async function getNavigationData(
|
|
|
114
179
|
appConfig.sitePathPrefix,
|
|
115
180
|
collectionFreshness,
|
|
116
181
|
appConfig.siteOrigin,
|
|
182
|
+
basePath,
|
|
117
183
|
);
|
|
118
184
|
|
|
119
185
|
// Only load collections when authenticated (for compose dialog)
|
|
@@ -125,13 +191,14 @@ export async function getNavigationData(
|
|
|
125
191
|
links,
|
|
126
192
|
currentPath,
|
|
127
193
|
sitePathPrefix: appConfig.sitePathPrefix,
|
|
194
|
+
basePath,
|
|
128
195
|
siteName,
|
|
129
196
|
siteDescription,
|
|
130
197
|
siteDescriptionHtml,
|
|
131
198
|
isAuthenticated,
|
|
132
199
|
collections,
|
|
133
|
-
|
|
134
|
-
showHeaderAvatar: showHeaderAvatar && !!
|
|
200
|
+
siteAvatarThumbUrl,
|
|
201
|
+
showHeaderAvatar: showHeaderAvatar && !!siteAvatarThumbUrl,
|
|
135
202
|
siteFooterHtml,
|
|
136
203
|
};
|
|
137
204
|
}
|
package/src/lib/pagination.ts
CHANGED
|
@@ -38,10 +38,18 @@ export function formatPageLabel(
|
|
|
38
38
|
return `Page ${String(currentPage)}`;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/** Slots a numbered pagination control fills once there are more pages. */
|
|
42
|
+
const PAGE_SLOT_COUNT = 7;
|
|
43
|
+
|
|
41
44
|
/**
|
|
42
45
|
* Computes which page numbers to display in a numbered pagination control.
|
|
43
|
-
*
|
|
44
|
-
*
|
|
46
|
+
*
|
|
47
|
+
* Seven or fewer pages are all shown. Past that, the control always fills
|
|
48
|
+
* exactly seven slots — the first page, the last page, and a three-page
|
|
49
|
+
* window around the current page — so it keeps the same width, and the
|
|
50
|
+
* Previous/Next links stay put, as the reader moves through pages. Skipped
|
|
51
|
+
* pages are represented by 0 (ellipsis marker). An ellipsis always stands in
|
|
52
|
+
* for at least two pages; where it would hide just one, that page is shown.
|
|
45
53
|
*
|
|
46
54
|
* @param currentPage - The current active page (1-indexed)
|
|
47
55
|
* @param totalPages - Total number of pages
|
|
@@ -50,36 +58,32 @@ export function formatPageLabel(
|
|
|
50
58
|
* @example
|
|
51
59
|
* ```ts
|
|
52
60
|
* getPageNumbers(1, 5) // [1, 2, 3, 4, 5]
|
|
53
|
-
* getPageNumbers(1,
|
|
54
|
-
* getPageNumbers(10,
|
|
61
|
+
* getPageNumbers(1, 19) // [1, 2, 3, 4, 5, 0, 19]
|
|
62
|
+
* getPageNumbers(10, 19) // [1, 0, 9, 10, 11, 0, 19]
|
|
63
|
+
* getPageNumbers(19, 19) // [1, 0, 15, 16, 17, 18, 19]
|
|
55
64
|
* ```
|
|
56
65
|
*/
|
|
57
66
|
export function getPageNumbers(
|
|
58
67
|
currentPage: number,
|
|
59
68
|
totalPages: number,
|
|
60
69
|
): number[] {
|
|
61
|
-
if (totalPages <=
|
|
70
|
+
if (totalPages <= PAGE_SLOT_COUNT) {
|
|
62
71
|
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
63
72
|
}
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (currentPage < totalPages) pages.add(currentPage + 1);
|
|
71
|
-
|
|
72
|
-
const sorted = [...pages].sort((a, b) => a - b);
|
|
73
|
-
|
|
74
|
-
// Insert 0 for gaps
|
|
75
|
-
const result: number[] = [];
|
|
76
|
-
for (let i = 0; i < sorted.length; i++) {
|
|
77
|
-
const current = sorted[i] as number;
|
|
78
|
-
if (i > 0 && current - (sorted[i - 1] as number) > 1) {
|
|
79
|
-
result.push(0); // ellipsis marker
|
|
80
|
-
}
|
|
81
|
-
result.push(current);
|
|
82
|
-
}
|
|
74
|
+
// The second slot holds page 2 or an ellipsis, and the second-to-last slot
|
|
75
|
+
// holds page totalPages - 1 or an ellipsis, so the window around the current
|
|
76
|
+
// page starts no earlier than page 3 and ends no later than totalPages - 2.
|
|
77
|
+
const windowStart = Math.min(Math.max(currentPage - 1, 3), totalPages - 4);
|
|
78
|
+
const windowEnd = windowStart + 2;
|
|
83
79
|
|
|
84
|
-
return
|
|
80
|
+
return [
|
|
81
|
+
1,
|
|
82
|
+
windowStart === 3 ? 2 : 0,
|
|
83
|
+
windowStart,
|
|
84
|
+
windowStart + 1,
|
|
85
|
+
windowEnd,
|
|
86
|
+
windowEnd === totalPages - 2 ? totalPages - 1 : 0,
|
|
87
|
+
totalPages,
|
|
88
|
+
];
|
|
85
89
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which reader surfaces exist once per language.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the `langGet()` table in `routes/pages/language.tsx` — the two must
|
|
5
|
+
* change together. Kept in its own module, free of server imports, because the
|
|
6
|
+
* client needs the same answer: `viewPath()` uses it to decide whether a link
|
|
7
|
+
* carries the current view's language prefix, and getting it wrong in either
|
|
8
|
+
* direction is a bug you can see — a missing prefix drops the reader into
|
|
9
|
+
* another language, an extra one lands on a 404.
|
|
10
|
+
*
|
|
11
|
+
* Everything not listed here — the dashboard, auth, collection editors, and
|
|
12
|
+
* post permalinks — has one address site-wide.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Surfaces served per language at exactly this path. */
|
|
16
|
+
const PER_LANGUAGE_SURFACES = new Set([
|
|
17
|
+
"/",
|
|
18
|
+
"/feed",
|
|
19
|
+
"/latest",
|
|
20
|
+
"/featured",
|
|
21
|
+
"/archive",
|
|
22
|
+
"/search",
|
|
23
|
+
"/subscribe",
|
|
24
|
+
"/collections",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Surfaces whose sub-paths are all per-language too. `/collections/` is absent
|
|
29
|
+
* on purpose: aggregate selections and their feeds are, but nothing else
|
|
30
|
+
* nested under it is.
|
|
31
|
+
*/
|
|
32
|
+
const PER_LANGUAGE_SURFACE_PREFIXES = ["/latest/", "/featured/", "/archive/"];
|
|
33
|
+
|
|
34
|
+
const COLLECTIONS_PREFIX = "/collections/";
|
|
35
|
+
|
|
36
|
+
/** `/collections/{selection}` and its feed, and nothing else under it. */
|
|
37
|
+
function isCollectionSelectionSurface(path: string): boolean {
|
|
38
|
+
const rest = path.slice(COLLECTIONS_PREFIX.length);
|
|
39
|
+
if (!rest) return false;
|
|
40
|
+
|
|
41
|
+
const segments = rest.split("/");
|
|
42
|
+
if (segments[0] === "") return false;
|
|
43
|
+
if (segments.length === 1) return true;
|
|
44
|
+
return segments.length === 2 && segments[1] === "feed";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether a path exists in every language's view.
|
|
49
|
+
*
|
|
50
|
+
* @param path - Internal app path, without any language prefix. A query string
|
|
51
|
+
* or fragment is ignored, so callers can pass a built link as-is.
|
|
52
|
+
* @returns True when `/ja{path}` is a page rather than a 404
|
|
53
|
+
* @example
|
|
54
|
+
* isPerLanguageSurface("/archive?media=any"); // true — /ja/archive exists
|
|
55
|
+
* isPerLanguageSurface("/collections/a+b/feed"); // true — /ja/collections/a+b/feed exists
|
|
56
|
+
* isPerLanguageSurface("/settings/language"); // false — the dash is one place
|
|
57
|
+
*/
|
|
58
|
+
export function isPerLanguageSurface(path: string): boolean {
|
|
59
|
+
const bare = path.split(/[?#]/, 1)[0] ?? path;
|
|
60
|
+
if (PER_LANGUAGE_SURFACES.has(bare)) return true;
|
|
61
|
+
if (bare.startsWith(COLLECTIONS_PREFIX)) {
|
|
62
|
+
return isCollectionSelectionSurface(bare);
|
|
63
|
+
}
|
|
64
|
+
return PER_LANGUAGE_SURFACE_PREFIXES.some((prefix) =>
|
|
65
|
+
bare.startsWith(prefix),
|
|
66
|
+
);
|
|
67
|
+
}
|
package/src/lib/post-display.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { Context } from "hono";
|
|
|
9
9
|
import type { Bindings, Post, PostView } from "../types.js";
|
|
10
10
|
import type { AppVariables } from "../types/app-context.js";
|
|
11
11
|
import { buildMediaMap } from "./media-helpers.js";
|
|
12
|
-
import { createMediaContext, toPostView } from "./view.js";
|
|
12
|
+
import { createMediaContext, resolveDraftTailId, toPostView } from "./view.js";
|
|
13
13
|
|
|
14
14
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
15
15
|
|
|
@@ -82,7 +82,12 @@ function resolvePostSocialImage(
|
|
|
82
82
|
interface PostDisplayOptions {
|
|
83
83
|
isAuthenticated?: boolean;
|
|
84
84
|
allowDraft?: boolean;
|
|
85
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* Force draft members into the assembled thread. The author already sees
|
|
87
|
+
* them whenever they are signed in; this only covers the draft-preview
|
|
88
|
+
* route, where the request is authenticated but `isAuthenticated` has
|
|
89
|
+
* already been forced to `true` for private-post access.
|
|
90
|
+
*/
|
|
86
91
|
includeDraftThread?: boolean;
|
|
87
92
|
}
|
|
88
93
|
|
|
@@ -120,11 +125,16 @@ export async function assemblePostCardView(
|
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
123
|
-
const [rawMediaMap, collectionsMap, lastPostMap, aliasesMap] =
|
|
128
|
+
const [rawMediaMap, collectionsMap, lastPostMap, draftTailMap, aliasesMap] =
|
|
124
129
|
await Promise.all([
|
|
125
130
|
c.var.services.media.getByPostIds([post.id]),
|
|
126
131
|
c.var.services.collections.getCollectionsByPostIds([post.id]),
|
|
127
|
-
c.var.services.posts.
|
|
132
|
+
c.var.services.posts.getThreadTailIds([post.threadId]),
|
|
133
|
+
c.var.isAuthenticated
|
|
134
|
+
? c.var.services.posts.getThreadTailIds([post.threadId], {
|
|
135
|
+
includeDrafts: true,
|
|
136
|
+
})
|
|
137
|
+
: Promise.resolve(new Map<string, string>()),
|
|
128
138
|
c.var.services.paths.getPostAliases([post.id]),
|
|
129
139
|
]);
|
|
130
140
|
|
|
@@ -144,6 +154,13 @@ export async function assemblePostCardView(
|
|
|
144
154
|
aliasesMap.get(post.id)?.[0],
|
|
145
155
|
);
|
|
146
156
|
|
|
157
|
+
const draftTailId = resolveDraftTailId(
|
|
158
|
+
post.threadId,
|
|
159
|
+
lastPostMap,
|
|
160
|
+
draftTailMap,
|
|
161
|
+
);
|
|
162
|
+
if (draftTailId) view.draftTailId = draftTailId;
|
|
163
|
+
|
|
147
164
|
return view;
|
|
148
165
|
}
|
|
149
166
|
|
|
@@ -170,12 +187,19 @@ export async function assemblePostPageDisplay(
|
|
|
170
187
|
}
|
|
171
188
|
|
|
172
189
|
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
190
|
+
// Drafts render for the author so the thread they see ends where the server
|
|
191
|
+
// thinks it ends — otherwise a trailing draft silently blocks replies while
|
|
192
|
+
// staying invisible. Read the real auth state rather than the option, which
|
|
193
|
+
// callers force to `true` to bypass the private-post check.
|
|
194
|
+
const includeDrafts = Boolean(
|
|
195
|
+
options?.includeDraftThread || c.var.isAuthenticated,
|
|
196
|
+
);
|
|
173
197
|
const threadPosts = (
|
|
174
198
|
await c.var.services.posts.getThread(post.threadId)
|
|
175
199
|
).filter(
|
|
176
200
|
(threadPost) =>
|
|
177
201
|
threadPost.status === "published" ||
|
|
178
|
-
(
|
|
202
|
+
(includeDrafts && threadPost.status === "draft"),
|
|
179
203
|
);
|
|
180
204
|
|
|
181
205
|
const allPostIds =
|
|
@@ -219,13 +243,22 @@ export async function assemblePostPageDisplay(
|
|
|
219
243
|
)
|
|
220
244
|
: undefined;
|
|
221
245
|
|
|
222
|
-
|
|
246
|
+
// Page metadata describes what a reader gets, so it never draws on drafts:
|
|
247
|
+
// an unpublished reply must not supply the og:image or bump the modified
|
|
248
|
+
// time. Falls back to the post itself when nothing in the thread is
|
|
249
|
+
// published (the draft-preview route, which is noindex anyway).
|
|
250
|
+
const publishedViews = threadPostViews?.filter(
|
|
251
|
+
(view) => view.status === "published",
|
|
252
|
+
);
|
|
253
|
+
const metaViews = publishedViews?.length ? publishedViews : [postView];
|
|
254
|
+
|
|
255
|
+
const socialImage = resolvePostSocialImage(postView, metaViews);
|
|
223
256
|
|
|
224
|
-
|
|
225
|
-
const
|
|
257
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- metaViews is never empty
|
|
258
|
+
const rootView = metaViews[0]!;
|
|
226
259
|
// ISO 8601 strings from toISOString() are all UTC and zero-padded, so
|
|
227
260
|
// lexical comparison matches chronological order.
|
|
228
|
-
const articleModifiedTime =
|
|
261
|
+
const articleModifiedTime = metaViews
|
|
229
262
|
.map((p) => p.updatedAt)
|
|
230
263
|
.reduce((latest, t) => (t > latest ? t : latest));
|
|
231
264
|
|
package/src/lib/post-meta.ts
CHANGED
|
@@ -97,6 +97,25 @@ function getDescriptionCandidate(post: Post): string {
|
|
|
97
97
|
return "";
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* What to call a Post in a list, when it may have no title of its own.
|
|
102
|
+
*
|
|
103
|
+
* Notes are usually untitled, so falling back to the slug would show readers
|
|
104
|
+
* and authors a URL fragment where they expect a sentence. The chain is the
|
|
105
|
+
* same one browser titles and Open Graph tags already use: title, then the
|
|
106
|
+
* quoted text, then the summary, then the opening of the body, then a link's
|
|
107
|
+
* domain.
|
|
108
|
+
*
|
|
109
|
+
* @param post - The Post to name
|
|
110
|
+
* @returns A short plain-text label, or an empty string when there is nothing
|
|
111
|
+
* to derive one from
|
|
112
|
+
* @example
|
|
113
|
+
* getPostDisplayTitle(untitledNote); // "An untitled note about espresso."
|
|
114
|
+
*/
|
|
115
|
+
export function getPostDisplayTitle(post: Post): string {
|
|
116
|
+
return getTitleCandidate(post);
|
|
117
|
+
}
|
|
118
|
+
|
|
100
119
|
export interface PostMeta {
|
|
101
120
|
title: string;
|
|
102
121
|
description?: string;
|
package/src/lib/render.tsx
CHANGED
|
@@ -9,9 +9,20 @@ import type { Context } from "hono";
|
|
|
9
9
|
import type { Child } from "hono/jsx";
|
|
10
10
|
import type { SiteLayoutProps } from "../types.js";
|
|
11
11
|
import { SETTINGS_KEYS } from "./constants.js";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
BaseLayout,
|
|
14
|
+
type PageFeedLink,
|
|
15
|
+
type ToastProps,
|
|
16
|
+
} from "../ui/layouts/BaseLayout.js";
|
|
13
17
|
import { SiteLayout } from "../ui/layouts/SiteLayout.js";
|
|
14
18
|
import type { NavigationData } from "./navigation.js";
|
|
19
|
+
import {
|
|
20
|
+
buildComposeLanguages,
|
|
21
|
+
buildLanguageSwitcher,
|
|
22
|
+
getViewLang,
|
|
23
|
+
type LanguageAlternate,
|
|
24
|
+
type LanguageSwitcherOption,
|
|
25
|
+
} from "./view-language.js";
|
|
15
26
|
|
|
16
27
|
export interface RenderPublicPageOptions {
|
|
17
28
|
/** Page title for <title> tag */
|
|
@@ -45,6 +56,23 @@ export interface RenderPublicPageOptions {
|
|
|
45
56
|
* thread root).
|
|
46
57
|
*/
|
|
47
58
|
canonicalHref?: string;
|
|
59
|
+
/**
|
|
60
|
+
* `hreflang` alternates for this page. List surfaces pass
|
|
61
|
+
* `buildSurfaceAlternates(c)`; a post passes its translations. Pages that
|
|
62
|
+
* exist once for the whole site pass nothing.
|
|
63
|
+
*/
|
|
64
|
+
alternateLanguages?: LanguageAlternate[];
|
|
65
|
+
/**
|
|
66
|
+
* The feed this page publishes, for `<link rel="alternate">` autodiscovery.
|
|
67
|
+
* Set on pages with a feed of their own — a collection, the archive — so a
|
|
68
|
+
* reader's extension finds that feed rather than only the site-wide ones.
|
|
69
|
+
*/
|
|
70
|
+
pageFeed?: PageFeedLink;
|
|
71
|
+
/**
|
|
72
|
+
* Language switcher entries. Defaults to the current surface in each of the
|
|
73
|
+
* site's languages; a post overrides it with its translations.
|
|
74
|
+
*/
|
|
75
|
+
languageSwitcher?: LanguageSwitcherOption[];
|
|
48
76
|
/** Navigation data (from getNavigationData) */
|
|
49
77
|
navData: NavigationData;
|
|
50
78
|
/** Page content JSX to render inside SiteLayout */
|
|
@@ -57,14 +85,24 @@ export interface RenderPublicPageOptions {
|
|
|
57
85
|
toast?: ToastProps;
|
|
58
86
|
/** Whether to render the shared compose dialog shell */
|
|
59
87
|
showComposeDialog?: boolean;
|
|
60
|
-
/**
|
|
61
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Override the site-wide crawler setting for this page. `true` means
|
|
90
|
+
* `noindex, nofollow`; `"follow"` keeps the URL unindexed while leaving its
|
|
91
|
+
* links crawlable. See `BaseLayoutProps["noindex"]`.
|
|
92
|
+
*/
|
|
93
|
+
noindex?: boolean | "follow";
|
|
62
94
|
/** Whether to render the site header */
|
|
63
95
|
showHeader?: boolean;
|
|
64
96
|
/** Whether to render the home branding credit after the site footer */
|
|
65
97
|
showHomeBranding?: boolean;
|
|
66
98
|
/** When set, the mobile compose FAB pre-selects this collection. */
|
|
67
99
|
composeCollectionId?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Content language of the page the composer opens from, for its default.
|
|
102
|
+
* List surfaces leave this unset and get the view language; a post page
|
|
103
|
+
* passes its post's language, since its URL carries no view prefix.
|
|
104
|
+
*/
|
|
105
|
+
composeContextLanguage?: string | null;
|
|
68
106
|
}
|
|
69
107
|
|
|
70
108
|
/**
|
|
@@ -99,6 +137,9 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
|
|
|
99
137
|
articleModifiedTime,
|
|
100
138
|
jsonLd,
|
|
101
139
|
canonicalHref,
|
|
140
|
+
alternateLanguages,
|
|
141
|
+
pageFeed,
|
|
142
|
+
languageSwitcher,
|
|
102
143
|
navData,
|
|
103
144
|
content,
|
|
104
145
|
pageChrome,
|
|
@@ -109,6 +150,7 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
|
|
|
109
150
|
showHeader,
|
|
110
151
|
showHomeBranding,
|
|
111
152
|
composeCollectionId,
|
|
153
|
+
composeContextLanguage,
|
|
112
154
|
} = options;
|
|
113
155
|
|
|
114
156
|
// Use siteDescription as meta description fallback when not explicitly provided
|
|
@@ -123,9 +165,10 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
|
|
|
123
165
|
links: navData.links,
|
|
124
166
|
currentPath: navData.currentPath,
|
|
125
167
|
sitePathPrefix: navData.sitePathPrefix,
|
|
168
|
+
basePath: navData.basePath,
|
|
126
169
|
isAuthenticated: navData.isAuthenticated,
|
|
127
170
|
collections: navData.collections,
|
|
128
|
-
|
|
171
|
+
siteAvatarThumbUrl: navData.siteAvatarThumbUrl,
|
|
129
172
|
showHeaderAvatar: navData.showHeaderAvatar,
|
|
130
173
|
siteDescriptionHtml: navData.siteDescriptionHtml,
|
|
131
174
|
siteFooterHtml: navData.siteFooterHtml,
|
|
@@ -141,11 +184,14 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
|
|
|
141
184
|
allSettings[SETTINGS_KEYS.DISCOVERY_SLASH_COMMAND_AT],
|
|
142
185
|
),
|
|
143
186
|
composeCollectionId,
|
|
187
|
+
languageSwitcher: languageSwitcher ?? buildLanguageSwitcher(c),
|
|
188
|
+
composeLanguages: buildComposeLanguages(c),
|
|
189
|
+
composeContextLanguage: composeContextLanguage ?? getViewLang(c),
|
|
144
190
|
};
|
|
145
191
|
const faviconUrl = appConfig.siteAvatarUrl || undefined;
|
|
146
192
|
const faviconVersion = appConfig.faviconVersion || undefined;
|
|
147
|
-
|
|
148
|
-
|
|
193
|
+
// `noindex` is passed straight through: BaseLayout reads the site-wide
|
|
194
|
+
// setting from this same context and is the single place the two resolve.
|
|
149
195
|
return c.html(
|
|
150
196
|
<BaseLayout
|
|
151
197
|
title={title}
|
|
@@ -162,9 +208,11 @@ export function renderPublicPage(c: Context, options: RenderPublicPageOptions) {
|
|
|
162
208
|
articleModifiedTime={articleModifiedTime}
|
|
163
209
|
jsonLd={jsonLd}
|
|
164
210
|
canonicalHref={canonicalHref}
|
|
211
|
+
alternateLanguages={alternateLanguages}
|
|
212
|
+
pageFeed={pageFeed}
|
|
165
213
|
faviconUrl={faviconUrl}
|
|
166
214
|
faviconVersion={faviconVersion}
|
|
167
|
-
noindex={
|
|
215
|
+
noindex={noindex}
|
|
168
216
|
isAuthenticated={navData.isAuthenticated}
|
|
169
217
|
toast={toast}
|
|
170
218
|
>
|