@jant/core 0.6.16 → 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 +60 -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-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
- 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-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
- 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__/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 +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 +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-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.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 +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/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-toolbar.ts +22 -9
- 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__/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 +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 +1526 -15
- 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__/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 +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__/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 +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 +926 -73
- 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 +16 -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/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-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 +42 -1
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +139 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +51 -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__/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 +159 -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 +23 -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 +5 -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__/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/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-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/archive.tsx +471 -369
- 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 +178 -17
- 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 +108 -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.test.ts +46 -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 +331 -46
- 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 +278 -19
- 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 +221 -6
- package/src/services/post.ts +1021 -152
- package/src/services/search.ts +39 -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 -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 +1087 -7219
- 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 +87 -1
- package/src/types/operations.ts +64 -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 +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/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/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/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View language: which language's browsing surface a request is asking for.
|
|
3
|
+
*
|
|
4
|
+
* A multilingual site keeps its primary language at the root (`/`, `/archive`,
|
|
5
|
+
* `/feed`) and gives every other language a URL prefix (`/en`, `/zh-hant`).
|
|
6
|
+
* The prefix selects a *view* — it filters the timeline, feeds and collection
|
|
7
|
+
* pages to that language — and it does not change any post's own address.
|
|
8
|
+
*
|
|
9
|
+
* Everything a handler needs to stay inside its view goes through
|
|
10
|
+
* `toViewPath()`, so a page rendered at `/en` links to `/en?page=2` rather
|
|
11
|
+
* than throwing the reader back to the primary language on the second page.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Context } from "hono";
|
|
15
|
+
import type { Bindings } from "../types/bindings.js";
|
|
16
|
+
import type { AppVariables } from "../types/app-context.js";
|
|
17
|
+
import type {
|
|
18
|
+
LanguageAlternate,
|
|
19
|
+
LanguageSwitcherOption,
|
|
20
|
+
} from "../types/views.js";
|
|
21
|
+
import { toLanguagePrefix } from "../i18n/locales.js";
|
|
22
|
+
import { getOrBuildEntry } from "../i18n/supported-locales.js";
|
|
23
|
+
import { isPerLanguageSurface } from "./per-language-surfaces.js";
|
|
24
|
+
import { toAbsoluteSiteUrl, toPublicPath } from "./url.js";
|
|
25
|
+
|
|
26
|
+
type ViewContext = Context<{ Bindings: Bindings; Variables: AppVariables }>;
|
|
27
|
+
|
|
28
|
+
export type { LanguageAlternate, LanguageSwitcherOption };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What the first path segment of a request means.
|
|
32
|
+
*
|
|
33
|
+
* - `pass` — not a language prefix; the request belongs to another route.
|
|
34
|
+
* - `redirect` — a language prefix that has no view right now (the primary
|
|
35
|
+
* language's own prefix, or any prefix left over from before multilingual
|
|
36
|
+
* was switched off). Old links and feed subscriptions keep working.
|
|
37
|
+
* - `view` — serve this language's view.
|
|
38
|
+
*/
|
|
39
|
+
export type LanguageViewDecision =
|
|
40
|
+
| { kind: "pass" }
|
|
41
|
+
| { kind: "redirect"; to: string }
|
|
42
|
+
| { kind: "view"; lang: string };
|
|
43
|
+
|
|
44
|
+
const PASS: LanguageViewDecision = { kind: "pass" };
|
|
45
|
+
|
|
46
|
+
/** First segment of an internal path, without its slashes. */
|
|
47
|
+
function firstSegment(path: string): string {
|
|
48
|
+
const rest = path.startsWith("/") ? path.slice(1) : path;
|
|
49
|
+
const slash = rest.indexOf("/");
|
|
50
|
+
return slash === -1 ? rest : rest.slice(0, slash);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The same path with its first segment removed, always rooted at `/`. */
|
|
54
|
+
function stripFirstSegment(path: string): string {
|
|
55
|
+
const rest = path.startsWith("/") ? path.slice(1) : path;
|
|
56
|
+
const slash = rest.indexOf("/");
|
|
57
|
+
return slash === -1 ? "/" : rest.slice(slash);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Query string of the current request, including `?`, or an empty string. */
|
|
61
|
+
function queryString(c: ViewContext): string {
|
|
62
|
+
const index = c.req.url.indexOf("?");
|
|
63
|
+
return index === -1 ? "" : c.req.url.slice(index);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Decide what the language prefix in the current URL means.
|
|
68
|
+
*
|
|
69
|
+
* A site that has never configured a second language gets `pass` for every
|
|
70
|
+
* request — its posts are free to use any first segment they like, including
|
|
71
|
+
* ones that happen to look like language tags (`/is`, `/it`, `/no`).
|
|
72
|
+
*
|
|
73
|
+
* @param c - Request context, after `withConfig()`
|
|
74
|
+
* @returns How the request should be handled
|
|
75
|
+
* @example
|
|
76
|
+
* // On a site serving zh-Hans (primary) and en:
|
|
77
|
+
* resolveLanguageView(c); // GET /en/archive → { kind: "view", lang: "en" }
|
|
78
|
+
* resolveLanguageView(c); // GET /zh-hans → { kind: "redirect", to: "/" }
|
|
79
|
+
* resolveLanguageView(c); // GET /hello → { kind: "pass" }
|
|
80
|
+
*/
|
|
81
|
+
export function resolveLanguageView(c: ViewContext): LanguageViewDecision {
|
|
82
|
+
const { siteLanguage, additionalLanguages, multilingualEnabled } =
|
|
83
|
+
c.var.appConfig;
|
|
84
|
+
|
|
85
|
+
const primaryPrefix = toLanguagePrefix(siteLanguage);
|
|
86
|
+
const configured = additionalLanguages.filter(
|
|
87
|
+
(tag) => toLanguagePrefix(tag) !== primaryPrefix,
|
|
88
|
+
);
|
|
89
|
+
if (configured.length === 0) return PASS;
|
|
90
|
+
|
|
91
|
+
const segment = firstSegment(c.req.path).toLowerCase();
|
|
92
|
+
if (!segment) return PASS;
|
|
93
|
+
|
|
94
|
+
const redirect = (): LanguageViewDecision => ({
|
|
95
|
+
kind: "redirect",
|
|
96
|
+
to: `${toPublicPath(
|
|
97
|
+
stripFirstSegment(c.req.path),
|
|
98
|
+
c.var.appConfig.sitePathPrefix,
|
|
99
|
+
)}${queryString(c)}`,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// The primary language is served from the root, so its own prefix is only
|
|
103
|
+
// ever an alias for it (§4.3).
|
|
104
|
+
if (segment === primaryPrefix) return redirect();
|
|
105
|
+
|
|
106
|
+
const match = configured.find((tag) => toLanguagePrefix(tag) === segment);
|
|
107
|
+
if (!match) return PASS;
|
|
108
|
+
|
|
109
|
+
return multilingualEnabled ? { kind: "view", lang: match } : redirect();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Every URL prefix a path on this site can arrive under.
|
|
114
|
+
*
|
|
115
|
+
* The site's own languages, the primary one included: its prefix has no view
|
|
116
|
+
* of its own but still redirects to the root, so `/zh-hans/hello` names the
|
|
117
|
+
* same page as `/hello`. Empty on a site with one language, where a first
|
|
118
|
+
* segment that looks like a language tag is just a post's slug.
|
|
119
|
+
*
|
|
120
|
+
* Use it to resolve an address the author pasted; requests themselves are
|
|
121
|
+
* decided by {@link resolveLanguageView}, which knows view from redirect.
|
|
122
|
+
*
|
|
123
|
+
* @param config - Site language settings
|
|
124
|
+
* @returns Prefixes without slashes, e.g. `["zh-hans", "en"]`
|
|
125
|
+
*/
|
|
126
|
+
export function languageUrlPrefixes(config: {
|
|
127
|
+
siteLanguage: string;
|
|
128
|
+
additionalLanguages: readonly string[];
|
|
129
|
+
}): string[] {
|
|
130
|
+
const primaryPrefix = toLanguagePrefix(config.siteLanguage);
|
|
131
|
+
const others = config.additionalLanguages
|
|
132
|
+
.map((tag) => toLanguagePrefix(tag))
|
|
133
|
+
.filter((prefix) => prefix !== primaryPrefix);
|
|
134
|
+
|
|
135
|
+
return others.length === 0 ? [] : [primaryPrefix, ...new Set(others)];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Whether the current URL carries a language prefix.
|
|
140
|
+
*
|
|
141
|
+
* Distinct from `getViewLang()`: the root URLs of a multilingual site show the
|
|
142
|
+
* primary language, so they *have* a view language while having no prefix.
|
|
143
|
+
*
|
|
144
|
+
* @param c - Request context
|
|
145
|
+
* @returns True when the request came in under `/en`-style URLs
|
|
146
|
+
*/
|
|
147
|
+
export function isPrefixedLanguageView(c: ViewContext): boolean {
|
|
148
|
+
return c.var.viewLang !== undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The language the current view shows, for filtering content.
|
|
153
|
+
*
|
|
154
|
+
* On a multilingual site the root is the primary language's view, so this
|
|
155
|
+
* returns the primary language there rather than "no filter" — otherwise `/`
|
|
156
|
+
* would show every language while `/en` showed one, and the primary language
|
|
157
|
+
* would have no view of its own.
|
|
158
|
+
*
|
|
159
|
+
* @param c - Request context
|
|
160
|
+
* @returns Canonical BCP 47 tag, or `null` when the site is single-language
|
|
161
|
+
*/
|
|
162
|
+
export function getViewLang(c: ViewContext): string | null {
|
|
163
|
+
if (c.var.viewLang) return c.var.viewLang;
|
|
164
|
+
|
|
165
|
+
const { siteLanguage, additionalLanguages, multilingualEnabled } =
|
|
166
|
+
c.var.appConfig;
|
|
167
|
+
if (!multilingualEnabled) return null;
|
|
168
|
+
|
|
169
|
+
const primaryPrefix = toLanguagePrefix(siteLanguage);
|
|
170
|
+
const hasSecondLanguage = additionalLanguages.some(
|
|
171
|
+
(tag) => toLanguagePrefix(tag) !== primaryPrefix,
|
|
172
|
+
);
|
|
173
|
+
return hasSecondLanguage ? siteLanguage : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* URL prefix of the current view, for building in-view links.
|
|
178
|
+
*
|
|
179
|
+
* @param c - Request context
|
|
180
|
+
* @returns `/en`-style prefix, or an empty string on the root view
|
|
181
|
+
*/
|
|
182
|
+
export function viewBasePath(c: ViewContext): string {
|
|
183
|
+
const lang = c.var.viewLang;
|
|
184
|
+
return lang ? `/${toLanguagePrefix(lang)}` : "";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* URL prefix of the view a given language is served under.
|
|
189
|
+
*
|
|
190
|
+
* A post page belongs to its own language's site: its chrome — logo, nav,
|
|
191
|
+
* search — links into that language's view, so a reader on a Japanese post
|
|
192
|
+
* stays among Japanese surfaces. Post pages render at language-neutral URLs,
|
|
193
|
+
* so this is derived from the post's language rather than the request path.
|
|
194
|
+
*
|
|
195
|
+
* @param c - Request context
|
|
196
|
+
* @param lang - Content language of the page, or null/undefined when unknown
|
|
197
|
+
* @returns `/ja`-style prefix for an active non-primary language, otherwise
|
|
198
|
+
* an empty string (the root view)
|
|
199
|
+
* @example
|
|
200
|
+
* languageScopeBasePath(c, "ja"); // "/ja" on a zh-Hans + ja site
|
|
201
|
+
* languageScopeBasePath(c, "zh-Hans"); // "" — the primary lives at the root
|
|
202
|
+
*/
|
|
203
|
+
export function languageScopeBasePath(
|
|
204
|
+
c: ViewContext,
|
|
205
|
+
lang: string | null | undefined,
|
|
206
|
+
): string {
|
|
207
|
+
if (!lang) return "";
|
|
208
|
+
const { siteLanguage } = c.var.appConfig;
|
|
209
|
+
const prefix = toLanguagePrefix(lang);
|
|
210
|
+
if (prefix === toLanguagePrefix(siteLanguage)) return "";
|
|
211
|
+
const active = getViewLanguages(c).some(
|
|
212
|
+
(tag) => toLanguagePrefix(tag) === prefix,
|
|
213
|
+
);
|
|
214
|
+
return active ? `/${prefix}` : "";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Build a public path that stays inside the current language view.
|
|
219
|
+
*
|
|
220
|
+
* Use this for every in-page link a handler generates — pagination, filter
|
|
221
|
+
* chips, feed autodiscovery, JSON-LD. Post permalinks are deliberately *not*
|
|
222
|
+
* view-scoped: a post has one address whatever view it was reached from.
|
|
223
|
+
*
|
|
224
|
+
* @param c - Request context
|
|
225
|
+
* @param path - Internal app path such as `/archive`
|
|
226
|
+
* @returns Public path, prefixed by the view language and the site path prefix
|
|
227
|
+
* @example
|
|
228
|
+
* toViewPath(c, "/"); // "/" at the root, "/en" under the English view
|
|
229
|
+
* toViewPath(c, "/archive"); // "/archive" or "/en/archive"
|
|
230
|
+
*/
|
|
231
|
+
export function toViewPath(c: ViewContext, path = "/"): string {
|
|
232
|
+
const base = viewBasePath(c);
|
|
233
|
+
const { sitePathPrefix } = c.var.appConfig;
|
|
234
|
+
if (!base) return toPublicPath(path, sitePathPrefix);
|
|
235
|
+
const suffix = path === "/" || path === "" ? "" : path;
|
|
236
|
+
return toPublicPath(`${base}${suffix}`, sitePathPrefix);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The request path with the language prefix removed.
|
|
241
|
+
*
|
|
242
|
+
* Handlers that resolve a path against the site's own content — the
|
|
243
|
+
* `path_registry` catch-all above all — work on this, so `/en/hello` and
|
|
244
|
+
* `/hello` resolve to the same row.
|
|
245
|
+
*
|
|
246
|
+
* @param c - Request context
|
|
247
|
+
* @returns Internal path rooted at `/`, without the view prefix
|
|
248
|
+
*/
|
|
249
|
+
export function viewRelativePath(c: ViewContext): string {
|
|
250
|
+
return c.var.viewLang ? stripFirstSegment(c.req.path) : c.req.path;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// The table of per-language surfaces lives in its own module so the client can
|
|
254
|
+
// import it too — a link built in the browser needs the same answer.
|
|
255
|
+
export { isPerLanguageSurface };
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Build a path inside an arbitrary language's view.
|
|
259
|
+
*
|
|
260
|
+
* Used by the language switcher, which links out of the current view.
|
|
261
|
+
*
|
|
262
|
+
* @param c - Request context
|
|
263
|
+
* @param lang - Target language tag, or `null` for the primary view
|
|
264
|
+
* @param path - Internal app path such as `/archive`
|
|
265
|
+
* @returns Public path in that language's view
|
|
266
|
+
*/
|
|
267
|
+
export function toLanguagePath(
|
|
268
|
+
c: ViewContext,
|
|
269
|
+
lang: string | null,
|
|
270
|
+
path = "/",
|
|
271
|
+
): string {
|
|
272
|
+
const { sitePathPrefix, siteLanguage } = c.var.appConfig;
|
|
273
|
+
const isPrimary =
|
|
274
|
+
!lang || toLanguagePrefix(lang) === toLanguagePrefix(siteLanguage);
|
|
275
|
+
if (isPrimary) return toPublicPath(path, sitePathPrefix);
|
|
276
|
+
const suffix = path === "/" || path === "" ? "" : path;
|
|
277
|
+
return toPublicPath(`/${toLanguagePrefix(lang)}${suffix}`, sitePathPrefix);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Every language the site serves a view for, primary first.
|
|
282
|
+
*
|
|
283
|
+
* @param c - Request context
|
|
284
|
+
* @returns Canonical tags, or an empty array when multilingual is off
|
|
285
|
+
*/
|
|
286
|
+
export function getViewLanguages(c: ViewContext): string[] {
|
|
287
|
+
const { siteLanguage, additionalLanguages, multilingualEnabled } =
|
|
288
|
+
c.var.appConfig;
|
|
289
|
+
if (!multilingualEnabled) return [];
|
|
290
|
+
const primaryPrefix = toLanguagePrefix(siteLanguage);
|
|
291
|
+
const rest = additionalLanguages.filter(
|
|
292
|
+
(tag) => toLanguagePrefix(tag) !== primaryPrefix,
|
|
293
|
+
);
|
|
294
|
+
return rest.length === 0 ? [] : [siteLanguage, ...rest];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The site's languages as the composer offers them.
|
|
299
|
+
*
|
|
300
|
+
* Empty on a single-language site, which is what makes the composer show no
|
|
301
|
+
* language UI at all — an author who never turned multilingual content on
|
|
302
|
+
* should never meet it.
|
|
303
|
+
*
|
|
304
|
+
* @param c - Request context
|
|
305
|
+
* @returns Tag and native label per language, primary first
|
|
306
|
+
*/
|
|
307
|
+
export function buildComposeLanguages(
|
|
308
|
+
c: ViewContext,
|
|
309
|
+
): Array<{ tag: string; label: string }> {
|
|
310
|
+
return getViewLanguages(c).map((tag) => ({
|
|
311
|
+
tag,
|
|
312
|
+
label: getOrBuildEntry(tag).native,
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Build the site's language switcher for the current page.
|
|
318
|
+
*
|
|
319
|
+
* The switcher means "take me to this language's site", not "translate this
|
|
320
|
+
* page": from a list surface it goes to that surface in the other language,
|
|
321
|
+
* from a post it goes to the translation when one exists, and from anywhere
|
|
322
|
+
* without a counterpart in that language — a post with no translation, a
|
|
323
|
+
* settings page — it goes to that language's home. It is never disabled and
|
|
324
|
+
* never warns: a reader who lands on a language's home page has still
|
|
325
|
+
* arrived somewhere, and it never links to a 404.
|
|
326
|
+
*
|
|
327
|
+
* @param c - Request context
|
|
328
|
+
* @param options - Per-language destinations, and where to go without one
|
|
329
|
+
* @returns Switcher entries in configured order, or an empty array when the
|
|
330
|
+
* site serves one language
|
|
331
|
+
* @example
|
|
332
|
+
* buildLanguageSwitcher(c); // list surfaces: same path, each language
|
|
333
|
+
* buildLanguageSwitcher(c, {
|
|
334
|
+
* hrefByLanguage: new Map([["en", "/my-post"]]),
|
|
335
|
+
* fallbackPath: "/",
|
|
336
|
+
* }); // a post: its translations, else that language's home
|
|
337
|
+
*/
|
|
338
|
+
export function buildLanguageSwitcher(
|
|
339
|
+
c: ViewContext,
|
|
340
|
+
options: {
|
|
341
|
+
hrefByLanguage?: ReadonlyMap<string, string>;
|
|
342
|
+
fallbackPath?: string;
|
|
343
|
+
/** Language to mark as current. Defaults to the view's own language. */
|
|
344
|
+
currentLang?: string;
|
|
345
|
+
} = {},
|
|
346
|
+
): LanguageSwitcherOption[] {
|
|
347
|
+
const languages = getViewLanguages(c);
|
|
348
|
+
if (languages.length === 0) return [];
|
|
349
|
+
|
|
350
|
+
const currentPath = viewRelativePath(c);
|
|
351
|
+
const fallbackPath =
|
|
352
|
+
options.fallbackPath ??
|
|
353
|
+
(isPerLanguageSurface(currentPath) ? currentPath : "/");
|
|
354
|
+
const currentPrefix = toLanguagePrefix(
|
|
355
|
+
options.currentLang || getViewLang(c) || c.var.appConfig.siteLanguage,
|
|
356
|
+
);
|
|
357
|
+
const primaryPrefix = toLanguagePrefix(c.var.appConfig.siteLanguage);
|
|
358
|
+
|
|
359
|
+
return languages.map((lang) => {
|
|
360
|
+
const override = options.hrefByLanguage?.get(lang);
|
|
361
|
+
return {
|
|
362
|
+
lang,
|
|
363
|
+
label: getOrBuildEntry(lang).native,
|
|
364
|
+
href: override
|
|
365
|
+
? toPublicPath(override, c.var.appConfig.sitePathPrefix)
|
|
366
|
+
: toLanguagePath(c, lang, fallbackPath),
|
|
367
|
+
isCurrent: toLanguagePrefix(lang) === currentPrefix,
|
|
368
|
+
isPrimary: toLanguagePrefix(lang) === primaryPrefix,
|
|
369
|
+
};
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* `hreflang` alternates for the surface the current request is rendering.
|
|
375
|
+
*
|
|
376
|
+
* List surfaces exist once per language at the same path, so every language's
|
|
377
|
+
* copy of the current path is an alternate of it.
|
|
378
|
+
*
|
|
379
|
+
* An `x-default` alternate pointing at the primary language closes the set,
|
|
380
|
+
* so a reader whose language the site does not publish lands somewhere
|
|
381
|
+
* deliberate. Surfaces where it has no defined meaning — Atom feeds — opt out
|
|
382
|
+
* with `xDefault: false`.
|
|
383
|
+
*
|
|
384
|
+
* Returns an empty array on a single-language site, and when the site has no
|
|
385
|
+
* absolute URL configured — an hreflang `href` must be absolute to be honoured.
|
|
386
|
+
*
|
|
387
|
+
* The request's own query string rides along by default, so a filtered surface
|
|
388
|
+
* points at the same filter in each language. A page that also emits a
|
|
389
|
+
* `rel="canonical"` must pass `query` instead: every member of an hreflang set
|
|
390
|
+
* has to be a canonical URL, so the self-referential alternate cannot carry
|
|
391
|
+
* params the canonical strips — the two tags would give one page two
|
|
392
|
+
* identities.
|
|
393
|
+
*
|
|
394
|
+
* @param c - Request context
|
|
395
|
+
* @param options - `query` overrides the request's own query string, leading
|
|
396
|
+
* `?` included; `xDefault: false` omits the trailing `x-default` entry
|
|
397
|
+
* @returns Alternates for this surface, primary first
|
|
398
|
+
* @example
|
|
399
|
+
* // Rendering /en/archive on a zh-Hans + en site:
|
|
400
|
+
* buildSurfaceAlternates(c);
|
|
401
|
+
* // [{hreflang: "zh-Hans", href: "https://…/archive"},
|
|
402
|
+
* // {hreflang: "en", href: "https://…/en/archive"},
|
|
403
|
+
* // {hreflang: "x-default", href: "https://…/archive"}]
|
|
404
|
+
* @example
|
|
405
|
+
* // On /archive?utm_source=nl, whose canonical is /archive:
|
|
406
|
+
* buildSurfaceAlternates(c, { query: "" });
|
|
407
|
+
*/
|
|
408
|
+
export function buildSurfaceAlternates(
|
|
409
|
+
c: ViewContext,
|
|
410
|
+
options?: { query?: string; xDefault?: boolean },
|
|
411
|
+
): LanguageAlternate[] {
|
|
412
|
+
const languages = getViewLanguages(c);
|
|
413
|
+
const { siteUrl, sitePathPrefix } = c.var.appConfig;
|
|
414
|
+
if (languages.length === 0 || !siteUrl) return [];
|
|
415
|
+
|
|
416
|
+
const path = viewRelativePath(c);
|
|
417
|
+
const query = options?.query ?? queryString(c);
|
|
418
|
+
const primaryPrefix = toLanguagePrefix(c.var.appConfig.siteLanguage);
|
|
419
|
+
|
|
420
|
+
const alternates = languages.map((lang) => {
|
|
421
|
+
const prefix = toLanguagePrefix(lang);
|
|
422
|
+
const internal =
|
|
423
|
+
prefix === primaryPrefix ? path : `/${prefix}${path === "/" ? "" : path}`;
|
|
424
|
+
return {
|
|
425
|
+
hreflang: lang,
|
|
426
|
+
href: `${toAbsoluteSiteUrl(internal, siteUrl, sitePathPrefix)}${query}`,
|
|
427
|
+
};
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// A reader whose language the site does not publish lands on the primary one.
|
|
431
|
+
const primary = alternates[0];
|
|
432
|
+
if (primary && options?.xDefault !== false) {
|
|
433
|
+
alternates.push({ hreflang: "x-default", href: primary.href });
|
|
434
|
+
}
|
|
435
|
+
return alternates;
|
|
436
|
+
}
|
package/src/lib/view.ts
CHANGED
|
@@ -32,10 +32,19 @@ import {
|
|
|
32
32
|
formatDate,
|
|
33
33
|
formatTime,
|
|
34
34
|
formatRelativeTime,
|
|
35
|
+
formatYearMonthLabel,
|
|
35
36
|
} from "./time.js";
|
|
36
37
|
import { getCollectionPagePath } from "./collection-paths.js";
|
|
37
38
|
import { getMediaUrl, getImageUrl, getPublicUrlForProvider } from "./image.js";
|
|
38
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
extractSummaryHtml,
|
|
41
|
+
extractBodyText,
|
|
42
|
+
ARTICLE_SUMMARY_MAX_BLOCKS,
|
|
43
|
+
ARTICLE_SUMMARY_MAX_CHARS,
|
|
44
|
+
NOTE_SUMMARY_MAX_BLOCKS,
|
|
45
|
+
NOTE_SUMMARY_MAX_CHARS,
|
|
46
|
+
NOTE_SUMMARY_MIN_HIDDEN_CHARS,
|
|
47
|
+
} from "./summary.js";
|
|
39
48
|
import { renderTiptapDocumentAroundBoundary } from "./tiptap-render.js";
|
|
40
49
|
import { highlightText } from "./search-snippet.js";
|
|
41
50
|
import { isFullUrl, toPublicPath, toSameSitePath } from "./url.js";
|
|
@@ -140,15 +149,6 @@ export function toMediaView(media: Media, ctx: MediaContext): MediaView {
|
|
|
140
149
|
// Post Conversions
|
|
141
150
|
// =============================================================================
|
|
142
151
|
|
|
143
|
-
/** Feed summary limits for titled, article-style posts (the excerpt is a teaser). */
|
|
144
|
-
const ARTICLE_SUMMARY_MAX_BLOCKS = 5;
|
|
145
|
-
const ARTICLE_SUMMARY_MAX_CHARS = 500;
|
|
146
|
-
/** Larger feed summary limits for untitled notes — the body itself is the content. */
|
|
147
|
-
const NOTE_SUMMARY_MAX_BLOCKS = 10;
|
|
148
|
-
const NOTE_SUMMARY_MAX_CHARS = 1500;
|
|
149
|
-
/** Don't truncate an untitled note just to hide a tail under this many chars. */
|
|
150
|
-
const NOTE_SUMMARY_MIN_HIDDEN_CHARS = 200;
|
|
151
|
-
|
|
152
152
|
/**
|
|
153
153
|
* Splice a zero-width marker into rendered body HTML at a summary boundary.
|
|
154
154
|
*
|
|
@@ -401,6 +401,9 @@ export function toPostView(
|
|
|
401
401
|
collections,
|
|
402
402
|
replyToId: post.replyToId ?? undefined,
|
|
403
403
|
threadRootId: post.replyToId ? post.threadId : undefined,
|
|
404
|
+
// Language is a Thread property, carried on the root. Repeating it on every
|
|
405
|
+
// reply would invite per-reply controls that the data model does not allow.
|
|
406
|
+
language: post.replyToId ? undefined : (post.language ?? undefined),
|
|
404
407
|
isLastInThread: isLastInThread ?? true,
|
|
405
408
|
body: post.body ?? undefined,
|
|
406
409
|
};
|
|
@@ -507,6 +510,31 @@ export function toPostViewsFromPosts(
|
|
|
507
510
|
* @param currentPath - Current URL path for active state
|
|
508
511
|
* @param isAuthenticated - Whether the user is logged in (affects system settings item)
|
|
509
512
|
*/
|
|
513
|
+
/**
|
|
514
|
+
* System nav destinations that exist once per language view.
|
|
515
|
+
*
|
|
516
|
+
* `settings` is absent on purpose: the dashboard is a single, language-neutral
|
|
517
|
+
* surface, so a reader browsing `/en` still signs in at `/signin`.
|
|
518
|
+
*/
|
|
519
|
+
const LANGUAGE_SCOPED_SYSTEM_KEYS = new Set<string>([
|
|
520
|
+
"latest",
|
|
521
|
+
"featured",
|
|
522
|
+
"collections",
|
|
523
|
+
"archive",
|
|
524
|
+
"rss",
|
|
525
|
+
]);
|
|
526
|
+
|
|
527
|
+
/** Whether a nav item points at a surface that a language view re-serves. */
|
|
528
|
+
function isLanguageScopedNavItem(item: NavItem): boolean {
|
|
529
|
+
if (item.type === "system") {
|
|
530
|
+
return LANGUAGE_SCOPED_SYSTEM_KEYS.has(item.systemKey ?? "");
|
|
531
|
+
}
|
|
532
|
+
// Collections keep their slug but filter by language; posts and free-form
|
|
533
|
+
// links have one address each. A smart collection is a collection in this
|
|
534
|
+
// respect too — `/en/quotes` is the English half of the same conditions.
|
|
535
|
+
return item.type === "collection" || item.type === "smart_collection";
|
|
536
|
+
}
|
|
537
|
+
|
|
510
538
|
export function toNavItemView(
|
|
511
539
|
item: NavItem,
|
|
512
540
|
currentPath: string,
|
|
@@ -514,6 +542,7 @@ export function toNavItemView(
|
|
|
514
542
|
sitePathPrefix = "",
|
|
515
543
|
collectionFreshness?: Map<string, number>,
|
|
516
544
|
siteOrigin = "",
|
|
545
|
+
basePath = sitePathPrefix,
|
|
517
546
|
): NavItemView {
|
|
518
547
|
let url = item.url;
|
|
519
548
|
let label = item.label;
|
|
@@ -546,14 +575,15 @@ export function toNavItemView(
|
|
|
546
575
|
// so strip it back to a path and skip external-link affordances.
|
|
547
576
|
const sameSitePath = toSameSitePath(url, siteOrigin);
|
|
548
577
|
const isExternal = sameSitePath === null && isFullUrl(url);
|
|
578
|
+
const linkPrefix = isLanguageScopedNavItem(item) ? basePath : sitePathPrefix;
|
|
549
579
|
const publicUrl = isExternal
|
|
550
580
|
? url
|
|
551
|
-
: toPublicPath(sameSitePath ?? url,
|
|
581
|
+
: toPublicPath(sameSitePath ?? url, linkPrefix);
|
|
552
582
|
|
|
553
583
|
let isActive = false;
|
|
554
584
|
if (!isExternal) {
|
|
555
|
-
if (publicUrl ===
|
|
556
|
-
isActive = currentPath === (
|
|
585
|
+
if (publicUrl === linkPrefix || publicUrl === "/") {
|
|
586
|
+
isActive = currentPath === (linkPrefix || "/");
|
|
557
587
|
} else {
|
|
558
588
|
isActive =
|
|
559
589
|
currentPath === publicUrl || currentPath.startsWith(`${publicUrl}/`);
|
|
@@ -571,9 +601,11 @@ export function toNavItemView(
|
|
|
571
601
|
type: item.type as NavItemType,
|
|
572
602
|
systemKey: item.systemKey,
|
|
573
603
|
collectionId: item.collectionId,
|
|
604
|
+
smartCollectionId: item.smartCollectionId,
|
|
574
605
|
postId: item.postId,
|
|
575
606
|
label,
|
|
576
607
|
url: publicUrl,
|
|
608
|
+
targetTitle: item.targetTitle,
|
|
577
609
|
placement: item.placement as NavItemPlacement,
|
|
578
610
|
isActive,
|
|
579
611
|
isExternal,
|
|
@@ -596,6 +628,7 @@ export function toNavItemViews(
|
|
|
596
628
|
sitePathPrefix = "",
|
|
597
629
|
collectionFreshness?: Map<string, number>,
|
|
598
630
|
siteOrigin = "",
|
|
631
|
+
basePath = sitePathPrefix,
|
|
599
632
|
): NavItemView[] {
|
|
600
633
|
return items.map((item) =>
|
|
601
634
|
toNavItemView(
|
|
@@ -605,6 +638,7 @@ export function toNavItemViews(
|
|
|
605
638
|
sitePathPrefix,
|
|
606
639
|
collectionFreshness,
|
|
607
640
|
siteOrigin,
|
|
641
|
+
basePath,
|
|
608
642
|
),
|
|
609
643
|
);
|
|
610
644
|
}
|
|
@@ -703,11 +737,8 @@ export function toArchiveGroups(
|
|
|
703
737
|
const [year, month] = yearMonth.split("-");
|
|
704
738
|
if (!year || !month) continue;
|
|
705
739
|
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
year: "numeric",
|
|
709
|
-
month: "long",
|
|
710
|
-
});
|
|
740
|
+
const label = formatYearMonthLabel(yearMonth);
|
|
741
|
+
if (!label) continue;
|
|
711
742
|
|
|
712
743
|
groups.push({
|
|
713
744
|
year,
|
|
@@ -737,11 +768,8 @@ export function toArchiveGroupsWithMedia(
|
|
|
737
768
|
const [year, month] = yearMonth.split("-");
|
|
738
769
|
if (!year || !month) continue;
|
|
739
770
|
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
year: "numeric",
|
|
743
|
-
month: "long",
|
|
744
|
-
});
|
|
771
|
+
const label = formatYearMonthLabel(yearMonth);
|
|
772
|
+
if (!label) continue;
|
|
745
773
|
|
|
746
774
|
groups.push({
|
|
747
775
|
year,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-scoped viewer state for Hono JSX renders.
|
|
3
|
+
*
|
|
4
|
+
* Whether the visitor is the signed-in author decides what a Post card should
|
|
5
|
+
* even emit. The post menu trigger, the reply trigger, and three of the four
|
|
6
|
+
* status badges only ever become visible under `body[data-authenticated]` or
|
|
7
|
+
* an article state an anonymous reader cannot reach, so for a logged-out
|
|
8
|
+
* visitor they are markup that can never be seen — several hundred DOM nodes
|
|
9
|
+
* of invisible chrome on a full timeline, paid for in style and layout on
|
|
10
|
+
* every load.
|
|
11
|
+
*
|
|
12
|
+
* The flag is bound once per render, alongside the i18n instance and from the
|
|
13
|
+
* same source (`c.var.isAuthenticated`), rather than threaded through the six
|
|
14
|
+
* component layers between a page and `PostFooter`. It must stay that same
|
|
15
|
+
* flag: `BaseLayout` writes it into `data-authenticated`, and a gate that
|
|
16
|
+
* disagrees with the CSS reveal either hides the author's own controls or
|
|
17
|
+
* brings the dead markup back.
|
|
18
|
+
*
|
|
19
|
+
* Renders are synchronous and single-threaded per request, so a module-level
|
|
20
|
+
* binding is safe here — the same reasoning the i18n context runs on.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Whether the current render is for the signed-in author.
|
|
25
|
+
*
|
|
26
|
+
* Defaults to `false` so a render that never bound a viewer emits the reader's
|
|
27
|
+
* markup. That is the safe direction: the author's controls are hidden until
|
|
28
|
+
* something says otherwise, rather than leaking into an anonymous page.
|
|
29
|
+
*/
|
|
30
|
+
let currentIsAuthor = false;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Bind the viewer for the render that is about to run.
|
|
34
|
+
*
|
|
35
|
+
* Called by the render provider, not by components.
|
|
36
|
+
*
|
|
37
|
+
* @param isAuthor - Whether this request carries the author's session
|
|
38
|
+
* @returns Nothing
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* bindViewer(c.get("isAuthenticated") === true);
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function bindViewer(isAuthor: boolean): void {
|
|
46
|
+
currentIsAuthor = isAuthor;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read the current render's viewer.
|
|
51
|
+
*
|
|
52
|
+
* @returns `isAuthor` — whether the signed-in author is looking at this page
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* const { isAuthor } = useViewer();
|
|
57
|
+
* return isAuthor ? <PostMenuTriggerButton /> : null;
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function useViewer(): { isAuthor: boolean } {
|
|
61
|
+
return { isAuthor: currentIsAuthor };
|
|
62
|
+
}
|