@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
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-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
|
>
|
|
@@ -12,15 +12,18 @@
|
|
|
12
12
|
import type { Bindings } from "../types/bindings.js";
|
|
13
13
|
import type { AppConfig } from "../types/config.js";
|
|
14
14
|
import { CONFIG_FIELDS } from "../types/config.js";
|
|
15
|
-
import type { FeedKind } from "../types/constants.js";
|
|
15
|
+
import type { ArchiveLayout, FeedKind } from "../types/constants.js";
|
|
16
16
|
import { ASSET_BASE_SEGMENT, getPublicAssetBasePath } from "./asset-path.js";
|
|
17
|
+
import { resolveDiscoverMode } from "./discover.js";
|
|
17
18
|
import {
|
|
18
19
|
getAuthSecret,
|
|
19
20
|
getConfiguredSingleSiteUrl,
|
|
20
21
|
getConfiguredStorageDriver,
|
|
22
|
+
getDiscoverDefault,
|
|
21
23
|
getEnvString,
|
|
22
24
|
} from "./env.js";
|
|
23
|
-
import {
|
|
25
|
+
import { parseLanguageList } from "../i18n/locales.js";
|
|
26
|
+
import { getPublicUrlForProvider, getMediaUrl, getImageUrl } from "./image.js";
|
|
24
27
|
import { normalizeTimeZone } from "./timezones.js";
|
|
25
28
|
import { getSiteOrigin, getSitePathPrefix, normalizeSiteUrl } from "./url.js";
|
|
26
29
|
|
|
@@ -147,6 +150,13 @@ function parseFeedKind(value: string, fallback: FeedKind): FeedKind {
|
|
|
147
150
|
return value === "latest" || value === "featured" ? value : fallback;
|
|
148
151
|
}
|
|
149
152
|
|
|
153
|
+
function parseArchiveLayout(
|
|
154
|
+
value: string,
|
|
155
|
+
fallback: ArchiveLayout,
|
|
156
|
+
): ArchiveLayout {
|
|
157
|
+
return value === "list" || value === "grid" ? value : fallback;
|
|
158
|
+
}
|
|
159
|
+
|
|
150
160
|
/**
|
|
151
161
|
* Build a complete AppConfig from environment bindings and DB settings.
|
|
152
162
|
*
|
|
@@ -198,6 +208,7 @@ export function resolveConfig(
|
|
|
198
208
|
// Resolve avatar URL from storage key
|
|
199
209
|
const siteAvatar = allSettings["SITE_AVATAR"] ?? "";
|
|
200
210
|
let siteAvatarUrl = "";
|
|
211
|
+
let siteAvatarThumbUrl = "";
|
|
201
212
|
if (siteAvatar) {
|
|
202
213
|
const publicUrl = getPublicUrlForProvider(
|
|
203
214
|
storageDriver,
|
|
@@ -206,6 +217,17 @@ export function resolveConfig(
|
|
|
206
217
|
localPublicUrl,
|
|
207
218
|
);
|
|
208
219
|
siteAvatarUrl = getMediaUrl(siteAvatar, publicUrl, sitePathPrefix);
|
|
220
|
+
// The header and drawer marks render this at 32px and 24px, on every page,
|
|
221
|
+
// in parallel with the render-blocking stylesheet. An untransformed avatar
|
|
222
|
+
// is whatever the author uploaded — commonly a 512px PNG — so it competes
|
|
223
|
+
// for the critical path with tens of times the bytes it can use. 128px
|
|
224
|
+
// still covers a high-DPR header and a theme that enlarges the mark.
|
|
225
|
+
siteAvatarThumbUrl = getImageUrl(siteAvatarUrl, imageTransformUrl, {
|
|
226
|
+
width: 128,
|
|
227
|
+
quality: 80,
|
|
228
|
+
format: "auto",
|
|
229
|
+
fit: "scale-down",
|
|
230
|
+
});
|
|
209
231
|
}
|
|
210
232
|
|
|
211
233
|
// Description is "explicit" when set in DB or ENV (not just the default)
|
|
@@ -216,6 +238,16 @@ export function resolveConfig(
|
|
|
216
238
|
? !!dbDescription
|
|
217
239
|
: !!envDescription;
|
|
218
240
|
|
|
241
|
+
// The owner's stored choice and the deployment's default stay apart, and
|
|
242
|
+
// the registry default must not stand in for either: an absent row is what
|
|
243
|
+
// lets `noindex` decide, and an absent binding is what makes Discover
|
|
244
|
+
// opt-in for a self-hosted site.
|
|
245
|
+
const discoverStoredValue = allSettings["DISCOVER"];
|
|
246
|
+
const discoverDefaultValue = getDiscoverDefault(env);
|
|
247
|
+
const noindex = demoMode || resolve("NOINDEX", allSettings, env) === "true";
|
|
248
|
+
const rssFeedsEnabled =
|
|
249
|
+
resolve("RSS_FEEDS_ENABLED", allSettings, env) === "true";
|
|
250
|
+
|
|
219
251
|
return {
|
|
220
252
|
// Site identity (DB > ENV > Default)
|
|
221
253
|
siteName: resolve("SITE_NAME", allSettings, env),
|
|
@@ -223,19 +255,34 @@ export function resolveConfig(
|
|
|
223
255
|
siteDescriptionExplicit,
|
|
224
256
|
siteLanguage: resolve("SITE_LANGUAGE", allSettings, env),
|
|
225
257
|
dashboardLanguage: resolve("DASHBOARD_LANGUAGE", allSettings, env),
|
|
226
|
-
|
|
258
|
+
multilingualEnabled:
|
|
259
|
+
resolve("MULTILINGUAL_ENABLED", allSettings, env) === "true",
|
|
260
|
+
additionalLanguages: parseLanguageList(
|
|
261
|
+
resolve("ADDITIONAL_LANGUAGES", allSettings, env),
|
|
262
|
+
),
|
|
227
263
|
mainRssFeed: parseFeedKind(
|
|
228
264
|
resolve("MAIN_RSS_FEED", allSettings, env),
|
|
229
265
|
"featured",
|
|
230
266
|
),
|
|
267
|
+
archiveDefaultLayout: parseArchiveLayout(
|
|
268
|
+
resolve("ARCHIVE_DEFAULT_LAYOUT", allSettings, env),
|
|
269
|
+
"list",
|
|
270
|
+
),
|
|
231
271
|
timeZone: normalizeTimeZone(resolve("TIME_ZONE", allSettings, env)),
|
|
232
272
|
siteFooter: resolve("SITE_FOOTER", allSettings, env),
|
|
233
273
|
showJantBrandingOnHome:
|
|
234
274
|
resolve("SHOW_JANT_BRANDING_ON_HOME", allSettings, env) === "true",
|
|
235
|
-
noindex
|
|
275
|
+
noindex,
|
|
276
|
+
discover: resolveDiscoverMode({
|
|
277
|
+
storedValue: discoverStoredValue,
|
|
278
|
+
defaultValue: discoverDefaultValue,
|
|
279
|
+
demoMode,
|
|
280
|
+
noindex,
|
|
281
|
+
rssFeedsEnabled,
|
|
282
|
+
}),
|
|
236
283
|
publicApiEnabled:
|
|
237
284
|
resolve("PUBLIC_API_ENABLED", allSettings, env) === "true",
|
|
238
|
-
rssFeedsEnabled
|
|
285
|
+
rssFeedsEnabled,
|
|
239
286
|
|
|
240
287
|
// Infrastructure (ENV only)
|
|
241
288
|
siteUrl,
|
|
@@ -318,6 +365,7 @@ export function resolveConfig(
|
|
|
318
365
|
siteAvatar,
|
|
319
366
|
showHeaderAvatar: allSettings["SHOW_HEADER_AVATAR"] === "true",
|
|
320
367
|
siteAvatarUrl,
|
|
368
|
+
siteAvatarThumbUrl,
|
|
321
369
|
faviconVersion: allSettings["SITE_FAVICON_VERSION"] ?? "",
|
|
322
370
|
|
|
323
371
|
// Rate limiting (ENV only). Defaults are conservative enough for a
|