@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/schemas.ts
CHANGED
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import {
|
|
13
|
+
ARCHIVE_LAYOUTS,
|
|
13
14
|
FORMATS,
|
|
14
15
|
STATUSES,
|
|
15
16
|
VISIBILITIES,
|
|
16
17
|
SORT_ORDERS,
|
|
17
18
|
COLLECTION_SORT_ORDERS,
|
|
19
|
+
SMART_COLLECTION_SORT_ORDERS,
|
|
18
20
|
NAV_ITEM_TYPES,
|
|
19
21
|
SYSTEM_NAV_KEY_VALUES,
|
|
20
22
|
MAX_MEDIA_ATTACHMENTS,
|
|
@@ -30,14 +32,20 @@ import {
|
|
|
30
32
|
type ConfigEditorDefinition,
|
|
31
33
|
type ConfigKey,
|
|
32
34
|
} from "../types.js";
|
|
33
|
-
import { isCjkSerifFont } from "../i18n/detect.js";
|
|
34
35
|
import {
|
|
36
|
+
formatLanguageList,
|
|
35
37
|
isLocale,
|
|
36
38
|
isValidContentLanguage,
|
|
37
39
|
normalizeContentLanguage,
|
|
40
|
+
parseLanguageList,
|
|
38
41
|
} from "../i18n/locales.js";
|
|
39
42
|
import { ValidationError } from "./errors.js";
|
|
40
43
|
import { createTypeIdSchema, ID_PREFIX } from "./ids.js";
|
|
44
|
+
import {
|
|
45
|
+
FILTER_DIMENSION_KEYS,
|
|
46
|
+
FILTER_DIMENSIONS,
|
|
47
|
+
type PostFilterSelection,
|
|
48
|
+
} from "./filter-dimensions.js";
|
|
41
49
|
import { normalizeSlug } from "./slug-format.js";
|
|
42
50
|
import { isReservedPath } from "./constants.js";
|
|
43
51
|
import { sanitizeUrl, normalizePath } from "./url.js";
|
|
@@ -69,6 +77,27 @@ export function normalizeEmail(email: string): string {
|
|
|
69
77
|
return email.trim().toLowerCase();
|
|
70
78
|
}
|
|
71
79
|
|
|
80
|
+
/**
|
|
81
|
+
* A display name to open an account under, before the author has given one.
|
|
82
|
+
*
|
|
83
|
+
* The first setup screen asks for credentials only, so better-auth needs a
|
|
84
|
+
* name it can create the row with; the second screen overwrites it with the
|
|
85
|
+
* site's name. This stands in only for the gap between the two, and for the
|
|
86
|
+
* setup that is abandoned in it — which is why it is the address the author
|
|
87
|
+
* just typed rather than a placeholder they would not recognise.
|
|
88
|
+
*
|
|
89
|
+
* @param email - The normalized address the account is being opened under
|
|
90
|
+
* @returns The part before the `@`, or the whole address when there is none
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* deriveAccountName("owner@example.com");
|
|
94
|
+
* // Returns: "owner"
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export function deriveAccountName(email: string): string {
|
|
98
|
+
return email.split("@")[0]?.trim() || email;
|
|
99
|
+
}
|
|
100
|
+
|
|
72
101
|
/** Trim, strip control characters, and collapse to undefined when empty. */
|
|
73
102
|
function sanitizeText(maxLength: number) {
|
|
74
103
|
return z
|
|
@@ -172,7 +201,10 @@ export const SystemNavKeySchema = z.enum(SYSTEM_NAV_KEY_VALUES);
|
|
|
172
201
|
export const RedirectTypeSchema = z.enum(["301", "302"]);
|
|
173
202
|
|
|
174
203
|
/**
|
|
175
|
-
* Custom URL target type enum schema
|
|
204
|
+
* Custom URL target type enum schema.
|
|
205
|
+
*
|
|
206
|
+
* Every kind a stored path can be, including `archive` — which is read and
|
|
207
|
+
* listed but no longer created. See {@link CreatableCustomUrlTargetTypeSchema}.
|
|
176
208
|
*/
|
|
177
209
|
export const CustomUrlTargetTypeSchema = z.enum([
|
|
178
210
|
"post",
|
|
@@ -181,6 +213,20 @@ export const CustomUrlTargetTypeSchema = z.enum([
|
|
|
181
213
|
"archive",
|
|
182
214
|
]);
|
|
183
215
|
|
|
216
|
+
/**
|
|
217
|
+
* The target kinds a new custom URL may take.
|
|
218
|
+
*
|
|
219
|
+
* `archive` is absent: hand-typing `format=note&title=none` into a text field
|
|
220
|
+
* is the problem smart collections exist to replace, and a form that stopped
|
|
221
|
+
* offering it while the endpoint still accepted it would not have stopped
|
|
222
|
+
* offering it. Existing archive paths keep working indefinitely.
|
|
223
|
+
*/
|
|
224
|
+
export const CreatableCustomUrlTargetTypeSchema = z.enum([
|
|
225
|
+
"post",
|
|
226
|
+
"collection",
|
|
227
|
+
"redirect",
|
|
228
|
+
]);
|
|
229
|
+
|
|
184
230
|
/**
|
|
185
231
|
* Rating schema (1-5 integer)
|
|
186
232
|
*/
|
|
@@ -193,9 +239,24 @@ export const RatingSchema = z.coerce
|
|
|
193
239
|
.or(z.literal("").transform(() => undefined))
|
|
194
240
|
.transform((v) => (v === 0 ? undefined : v));
|
|
195
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Any syntactically valid BCP 47 tag, normalized to canonical case.
|
|
244
|
+
*
|
|
245
|
+
* Deliberately open: Jant ships dashboard catalogs for a handful of locales but
|
|
246
|
+
* content can be written in any language.
|
|
247
|
+
*/
|
|
248
|
+
export const ContentLanguageSchema = z
|
|
249
|
+
.string()
|
|
250
|
+
.trim()
|
|
251
|
+
.refine(isValidContentLanguage, {
|
|
252
|
+
message: "Enter a valid BCP 47 language tag (e.g. en, zh-Hans, ja).",
|
|
253
|
+
})
|
|
254
|
+
.transform(normalizeContentLanguage);
|
|
255
|
+
|
|
196
256
|
const PostIdSchema = createTypeIdSchema(ID_PREFIX.post);
|
|
197
257
|
const MediaIdSchema = createTypeIdSchema(ID_PREFIX.media);
|
|
198
258
|
const CollectionIdSchema = createTypeIdSchema(ID_PREFIX.collection);
|
|
259
|
+
const SmartCollectionIdSchema = createTypeIdSchema(ID_PREFIX.smartCollection);
|
|
199
260
|
const CollectionDirectoryItemIdSchema = createTypeIdSchema(
|
|
200
261
|
ID_PREFIX.collectionDirectoryItem,
|
|
201
262
|
);
|
|
@@ -294,6 +355,12 @@ const PostFieldsSchema = z.object({
|
|
|
294
355
|
.optional(),
|
|
295
356
|
replyToId: PostIdSchema.optional(),
|
|
296
357
|
quietReply: z.boolean().optional(),
|
|
358
|
+
// Content language. The post service normalizes and enforces the
|
|
359
|
+
// thread-uniform rule; this only rejects tags that are not BCP 47 at all.
|
|
360
|
+
language: ContentLanguageSchema.optional().or(
|
|
361
|
+
z.literal("").transform(() => undefined),
|
|
362
|
+
),
|
|
363
|
+
translationOfId: PostIdSchema.optional(),
|
|
297
364
|
publishedAt: z.number().int().positive().optional(),
|
|
298
365
|
mediaIds: z.array(MediaIdSchema).max(MAX_MEDIA_ATTACHMENTS).optional(),
|
|
299
366
|
mediaAlts: z.record(MediaIdSchema, z.string()).optional(),
|
|
@@ -566,6 +633,14 @@ export const CreateNavItemSchema = z.discriminatedUnion("type", [
|
|
|
566
633
|
label: sanitizeText(100).pipe(z.string().min(1)).optional(),
|
|
567
634
|
placement: z.enum(["header", "more"]).optional(),
|
|
568
635
|
}),
|
|
636
|
+
// Placed exactly like a collection — the label follows the target's title
|
|
637
|
+
// and the URL follows its address; only which column holds the key differs.
|
|
638
|
+
z.object({
|
|
639
|
+
type: z.literal("smart_collection"),
|
|
640
|
+
smartCollectionId: SmartCollectionIdSchema,
|
|
641
|
+
label: sanitizeText(100).pipe(z.string().min(1)).optional(),
|
|
642
|
+
placement: z.enum(["header", "more"]).optional(),
|
|
643
|
+
}),
|
|
569
644
|
z.object({
|
|
570
645
|
type: z.literal("page"),
|
|
571
646
|
postId: PostIdSchema,
|
|
@@ -620,6 +695,19 @@ export const CreateCollectionDirectoryItemSchema = z.discriminatedUnion(
|
|
|
620
695
|
],
|
|
621
696
|
);
|
|
622
697
|
|
|
698
|
+
/**
|
|
699
|
+
* A row in the collections directory, as the drag surface names it.
|
|
700
|
+
*
|
|
701
|
+
* A placed row is named by its directory row id. A collection or smart
|
|
702
|
+
* collection that has never been placed is named by its own id, because that is
|
|
703
|
+
* all the directory has to render — moving one is what gives it a row.
|
|
704
|
+
*/
|
|
705
|
+
export const CollectionDirectoryRowIdSchema = z.union([
|
|
706
|
+
CollectionDirectoryItemIdSchema,
|
|
707
|
+
CollectionIdSchema,
|
|
708
|
+
SmartCollectionIdSchema,
|
|
709
|
+
]);
|
|
710
|
+
|
|
623
711
|
export const UpdateCollectionDirectoryItemSchema = z.object({
|
|
624
712
|
label: z.union([CollectionDirectoryLabelSchema, z.null()]).optional(),
|
|
625
713
|
url: CollectionDirectoryLinkUrlSchema.optional(),
|
|
@@ -633,6 +721,7 @@ export {
|
|
|
633
721
|
NavItemIdSchema,
|
|
634
722
|
PathIdSchema,
|
|
635
723
|
PostIdSchema,
|
|
724
|
+
SmartCollectionIdSchema,
|
|
636
725
|
};
|
|
637
726
|
|
|
638
727
|
/**
|
|
@@ -677,6 +766,61 @@ export const CreateCollectionSchema = z.object({
|
|
|
677
766
|
*/
|
|
678
767
|
export const UpdateCollectionSchema = CreateCollectionSchema.partial();
|
|
679
768
|
|
|
769
|
+
/**
|
|
770
|
+
* What a stored selection may hold — the validator a create or update request
|
|
771
|
+
* runs its conditions through.
|
|
772
|
+
*
|
|
773
|
+
* Assembled from the dimension registry rather than restated, so a new
|
|
774
|
+
* dimension is accepted by every endpoint the moment it is declared, and a
|
|
775
|
+
* dimension whose stored vocabulary is narrower than its URL one (`visibility`)
|
|
776
|
+
* is narrow everywhere at once.
|
|
777
|
+
*
|
|
778
|
+
* The registry itself carries plain predicates, not schemas, because the smart
|
|
779
|
+
* collection dialog compiles it into the browser bundle. The Zod wrapper lives
|
|
780
|
+
* here, on the server side of that line, where request bodies are validated and
|
|
781
|
+
* a rejected condition still has to say which one it was.
|
|
782
|
+
*/
|
|
783
|
+
export const PostFilterSelectionSchema = z
|
|
784
|
+
.object(
|
|
785
|
+
Object.fromEntries(
|
|
786
|
+
FILTER_DIMENSION_KEYS.map((key) => [
|
|
787
|
+
key,
|
|
788
|
+
z
|
|
789
|
+
.custom((value) => FILTER_DIMENSIONS[key].storage.isStorable(value), {
|
|
790
|
+
message: `Invalid value for the ${key} condition`,
|
|
791
|
+
})
|
|
792
|
+
.optional(),
|
|
793
|
+
]),
|
|
794
|
+
),
|
|
795
|
+
)
|
|
796
|
+
.strict() as unknown as z.ZodType<PostFilterSelection>;
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* API request body schema for creating a smart collection.
|
|
800
|
+
*
|
|
801
|
+
* The conditions come from the dimension registry rather than being restated
|
|
802
|
+
* here, so a new dimension is accepted the moment it is declared and a stored
|
|
803
|
+
* vocabulary narrower than the URL one (`visibility` never being `private`)
|
|
804
|
+
* is narrow at every entry point at once.
|
|
805
|
+
*/
|
|
806
|
+
export const CreateSmartCollectionSchema = z.object({
|
|
807
|
+
slug: CollectionSlugSchema,
|
|
808
|
+
title: CollectionTitleSchema,
|
|
809
|
+
description: CollectionDescriptionValueSchema.nullable().optional(),
|
|
810
|
+
// Omitted means no conditions, which is the honest spelling of "every post".
|
|
811
|
+
selection: PostFilterSelectionSchema.optional(),
|
|
812
|
+
sort: z.enum(SMART_COLLECTION_SORT_ORDERS).optional(),
|
|
813
|
+
layout: z.enum(ARCHIVE_LAYOUTS).nullable().optional(),
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
export const UpdateSmartCollectionSchema =
|
|
817
|
+
CreateSmartCollectionSchema.partial();
|
|
818
|
+
|
|
819
|
+
/** The body of a preview request: the conditions, and nothing else. */
|
|
820
|
+
export const SmartCollectionPreviewSchema = z.object({
|
|
821
|
+
selection: PostFilterSelectionSchema.optional(),
|
|
822
|
+
});
|
|
823
|
+
|
|
680
824
|
export const SiteNameSettingSchema = sanitizeSettingText(MAX_SITE_NAME_LENGTH);
|
|
681
825
|
export const SiteDescriptionSettingSchema = sanitizeSettingText(
|
|
682
826
|
MAX_SITE_DESCRIPTION_LENGTH,
|
|
@@ -808,11 +952,16 @@ export function normalizeEditableSettingValue(
|
|
|
808
952
|
);
|
|
809
953
|
}
|
|
810
954
|
break;
|
|
811
|
-
case "
|
|
812
|
-
if (
|
|
813
|
-
throw new ValidationError("
|
|
955
|
+
case "MULTILINGUAL_ENABLED":
|
|
956
|
+
if (normalized !== "true" && normalized !== "false") {
|
|
957
|
+
throw new ValidationError("Multilingual content is on or off.");
|
|
814
958
|
}
|
|
815
959
|
break;
|
|
960
|
+
case "ADDITIONAL_LANGUAGES":
|
|
961
|
+
// Re-serialized from the parsed form so a hand-written value cannot
|
|
962
|
+
// store blanks, duplicates, or non-canonical casing.
|
|
963
|
+
normalized = formatLanguageList(parseLanguageList(normalized));
|
|
964
|
+
break;
|
|
816
965
|
case "TIME_ZONE":
|
|
817
966
|
if (!isSupportedTimeZone(normalized)) {
|
|
818
967
|
throw new ValidationError("Choose a valid time zone.");
|
|
@@ -837,11 +986,10 @@ export const CreateCustomUrlSchema = z.object({
|
|
|
837
986
|
"Path must contain only lowercase alphanumeric characters, hyphens, slashes, and dots",
|
|
838
987
|
)
|
|
839
988
|
.transform((p) => (p.startsWith("/") ? p : `/${p}`)),
|
|
840
|
-
targetType:
|
|
989
|
+
targetType: CreatableCustomUrlTargetTypeSchema,
|
|
841
990
|
targetId: z.string().optional(),
|
|
842
991
|
toPath: z.string().optional(),
|
|
843
992
|
redirectType: RedirectTypeSchema.optional(),
|
|
844
|
-
archiveQuery: z.string().optional(),
|
|
845
993
|
});
|
|
846
994
|
|
|
847
995
|
// =============================================================================
|
|
@@ -849,10 +997,13 @@ export const CreateCustomUrlSchema = z.object({
|
|
|
849
997
|
// =============================================================================
|
|
850
998
|
|
|
851
999
|
/**
|
|
852
|
-
*
|
|
1000
|
+
* The first of the two screens a self-hosted site is set up through.
|
|
1001
|
+
*
|
|
1002
|
+
* Credentials and nothing else. Everything about the site itself waits for the
|
|
1003
|
+
* second screen, which is the same screen a hosted site shows — so the two
|
|
1004
|
+
* install kinds converge instead of keeping two forms that drift.
|
|
853
1005
|
*/
|
|
854
|
-
export const
|
|
855
|
-
siteName: z.string().min(1, "Site name is required"),
|
|
1006
|
+
export const SetupAccountSchema = z.object({
|
|
856
1007
|
email: z
|
|
857
1008
|
.string()
|
|
858
1009
|
.transform(normalizeEmail)
|
|
@@ -863,6 +1014,46 @@ export const SetupSchema = z.object({
|
|
|
863
1014
|
.max(128),
|
|
864
1015
|
});
|
|
865
1016
|
|
|
1017
|
+
/**
|
|
1018
|
+
* Setup on a site whose shell already exists and is already named.
|
|
1019
|
+
*
|
|
1020
|
+
* A control plane created the site and its owner, so the only thing left to
|
|
1021
|
+
* settle is the language its author writes in — the one fact no header, host,
|
|
1022
|
+
* or billing record can supply.
|
|
1023
|
+
*/
|
|
1024
|
+
export const SetupLanguageSchema = z.object({
|
|
1025
|
+
contentLanguage: ContentLanguageSchema,
|
|
1026
|
+
/**
|
|
1027
|
+
* The Jant Discover answer, from the checkbox on the setup screen.
|
|
1028
|
+
*
|
|
1029
|
+
* Optional, and absence is not `false`: an older client or a scripted setup
|
|
1030
|
+
* sends nothing, and that has to keep meaning "leave the deployment default
|
|
1031
|
+
* in force" rather than "the author said no".
|
|
1032
|
+
*/
|
|
1033
|
+
discover: z.boolean().optional(),
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* The second of the two self-hosted screens: the same questions the hosted
|
|
1038
|
+
* screen asks, plus the name only a self-hosted site is still missing.
|
|
1039
|
+
*
|
|
1040
|
+
* Which of the two applies is not a property of the form but of the site — a
|
|
1041
|
+
* site that already has a name is never asked for one — so the route picks the
|
|
1042
|
+
* schema by reading the stored name rather than trusting a field.
|
|
1043
|
+
*/
|
|
1044
|
+
export const SetupSiteSchema = SetupLanguageSchema.extend({
|
|
1045
|
+
// The message is set twice over on purpose: `.min(1)` catches a field left
|
|
1046
|
+
// blank, and the type-level `error` catches a form that carried no field at
|
|
1047
|
+
// all. Without the second, a scripted setup is told "expected string,
|
|
1048
|
+
// received undefined" — which names nothing the author can act on.
|
|
1049
|
+
siteName: z
|
|
1050
|
+
.string({ error: "Site name is required" })
|
|
1051
|
+
.min(1, "Site name is required"),
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
/** The answers the last setup screen collects from either install kind. */
|
|
1055
|
+
export type SetupLanguageAnswers = z.infer<typeof SetupLanguageSchema>;
|
|
1056
|
+
|
|
866
1057
|
/**
|
|
867
1058
|
* Sign-in form validation schema
|
|
868
1059
|
*/
|
|
@@ -29,7 +29,8 @@ export async function renderSiteHeaderHtml(c: Context<Env>): Promise<string> {
|
|
|
29
29
|
links={navData.links}
|
|
30
30
|
currentPath={navData.currentPath}
|
|
31
31
|
sitePathPrefix={navData.sitePathPrefix}
|
|
32
|
-
|
|
32
|
+
basePath={navData.basePath}
|
|
33
|
+
siteAvatarThumbUrl={navData.siteAvatarThumbUrl}
|
|
33
34
|
showHeaderAvatar={navData.showHeaderAvatar}
|
|
34
35
|
/>
|
|
35
36
|
</I18nProvider>,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone page served when a site resolves but is suspended.
|
|
3
|
+
*
|
|
4
|
+
* Kept deliberately self-contained (no theme, no site services, no i18n
|
|
5
|
+
* catalog): it renders before any site context exists, and the site whose
|
|
6
|
+
* theme we would otherwise use is exactly the one that is unavailable.
|
|
7
|
+
*
|
|
8
|
+
* The copy stays provider-neutral. Hosted deployments pass their brand across
|
|
9
|
+
* the boundary through `HOSTED_CONTROL_PLANE_PROVIDER_NAME` /
|
|
10
|
+
* `HOSTED_CONTROL_PLANE_BASE_URL`, which turn the last line into a real link
|
|
11
|
+
* back to the control plane; a self-hosted instance renders the same page
|
|
12
|
+
* without one.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { getHostedControlPlaneProviderLabel } from "./env.js";
|
|
16
|
+
import { getHostedControlPlaneDashboardUrl } from "./hosted-signin.js";
|
|
17
|
+
import { escapeHtml } from "./html.js";
|
|
18
|
+
|
|
19
|
+
export function renderSiteUnavailablePage(
|
|
20
|
+
env: object | undefined | null,
|
|
21
|
+
): string {
|
|
22
|
+
const providerLabel = getHostedControlPlaneProviderLabel(env);
|
|
23
|
+
const dashboardUrl = getHostedControlPlaneDashboardUrl(env);
|
|
24
|
+
|
|
25
|
+
const ownerLine =
|
|
26
|
+
dashboardUrl && providerLabel
|
|
27
|
+
? `<p>If it's your site, sign in to <a href="${escapeHtml(dashboardUrl)}">${escapeHtml(providerLabel)}</a> to bring it back online.</p>`
|
|
28
|
+
: `<p>If it's your site, sign in to your hosting provider to bring it back online.</p>`;
|
|
29
|
+
|
|
30
|
+
return `<!DOCTYPE html>
|
|
31
|
+
<html lang="en">
|
|
32
|
+
<head>
|
|
33
|
+
<meta charset="utf-8">
|
|
34
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
35
|
+
<meta name="robots" content="noindex">
|
|
36
|
+
<title>This site is offline</title>
|
|
37
|
+
<style>body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#fafafa;color:#111;padding:24px}div{max-width:460px;text-align:left}h1{font-size:1.25rem;font-weight:600;margin:0 0 12px}p{color:#4a4a4a;line-height:1.6;margin:12px 0}a{color:#0f766e}@media(prefers-color-scheme:dark){body{background:#111;color:#fafafa}p{color:#a1a1a1}a{color:#2dd4bf}}</style>
|
|
38
|
+
</head>
|
|
39
|
+
<body>
|
|
40
|
+
<div>
|
|
41
|
+
<h1>This site is offline</h1>
|
|
42
|
+
<p>There's nothing to read here right now — the site's hosting has stopped.</p>
|
|
43
|
+
${ownerLine}
|
|
44
|
+
</div>
|
|
45
|
+
</body>
|
|
46
|
+
</html>`;
|
|
47
|
+
}
|
package/src/lib/slug.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Handles three cases: user-provided slug, title-based slug, and random-only slug.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { slugify } from "./
|
|
8
|
+
import { slugify } from "./slugify.js";
|
|
9
9
|
import { generateRandomId } from "./nanoid.js";
|
|
10
10
|
import { isReservedPath } from "./constants.js";
|
|
11
11
|
import { ValidationError, ConflictError } from "./errors.js";
|
|
@@ -19,6 +19,21 @@ export interface SlugOptions {
|
|
|
19
19
|
title?: string;
|
|
20
20
|
/** Length of random IDs */
|
|
21
21
|
idLength: number;
|
|
22
|
+
/**
|
|
23
|
+
* URL form of the post's language (lowercased BCP 47 tag, e.g. `zh-hant`),
|
|
24
|
+
* set only for posts written in a non-primary language. Used as the first
|
|
25
|
+
* fallback when a title-based slug collides, so the transliterations of a
|
|
26
|
+
* translated pair land on `shu-ping` and `shu-ping-en` rather than a random
|
|
27
|
+
* suffix. Omitted for primary-language posts: the bare namespace belongs to
|
|
28
|
+
* them, and a language suffix among same-language posts distinguishes nothing.
|
|
29
|
+
*/
|
|
30
|
+
languageSuffix?: string;
|
|
31
|
+
/**
|
|
32
|
+
* URL prefixes currently served by language views. A slug matching one would
|
|
33
|
+
* be shadowed by that view and never resolve, so it is treated as reserved
|
|
34
|
+
* for as long as the language is configured.
|
|
35
|
+
*/
|
|
36
|
+
reservedPrefixes?: readonly string[];
|
|
22
37
|
/** Callback to check if a slug is available (checks post slugs + path_registry paths) */
|
|
23
38
|
isAvailable: (slug: string) => Promise<boolean>;
|
|
24
39
|
}
|
|
@@ -28,7 +43,7 @@ export interface SlugOptions {
|
|
|
28
43
|
*
|
|
29
44
|
* Resolution order:
|
|
30
45
|
* 1. User-provided slug → validate format, check reserved, check availability
|
|
31
|
-
* 2. Title exists → slugify(title),
|
|
46
|
+
* 2. Title exists → slugify(title), then -{languageSuffix}, then -{randomId}
|
|
32
47
|
* 3. No title → pure random ID
|
|
33
48
|
*
|
|
34
49
|
* @param opts - Slug generation options
|
|
@@ -42,16 +57,33 @@ export interface SlugOptions {
|
|
|
42
57
|
* // Title-based
|
|
43
58
|
* await generatePostSlug({ title: "Hello World", idLength: 5, isAvailable: check });
|
|
44
59
|
*
|
|
60
|
+
* // Title-based in a secondary language, colliding with an existing slug
|
|
61
|
+
* await generatePostSlug({
|
|
62
|
+
* title: "書評",
|
|
63
|
+
* languageSuffix: "en",
|
|
64
|
+
* idLength: 5,
|
|
65
|
+
* isAvailable: check,
|
|
66
|
+
* }); // => "shu-ping-en"
|
|
67
|
+
*
|
|
45
68
|
* // Random
|
|
46
69
|
* await generatePostSlug({ idLength: 5, isAvailable: check });
|
|
47
70
|
* ```
|
|
48
71
|
*/
|
|
49
72
|
export async function generatePostSlug(opts: SlugOptions): Promise<string> {
|
|
50
|
-
const {
|
|
73
|
+
const {
|
|
74
|
+
slug,
|
|
75
|
+
title,
|
|
76
|
+
idLength,
|
|
77
|
+
languageSuffix,
|
|
78
|
+
reservedPrefixes = [],
|
|
79
|
+
isAvailable,
|
|
80
|
+
} = opts;
|
|
81
|
+
const isReserved = (candidate: string) =>
|
|
82
|
+
isReservedPath(candidate, reservedPrefixes);
|
|
51
83
|
|
|
52
84
|
// Case 1: User-provided slug
|
|
53
85
|
if (slug) {
|
|
54
|
-
if (
|
|
86
|
+
if (isReserved(slug)) {
|
|
55
87
|
throw new ValidationError(
|
|
56
88
|
`Slug "${slug}" is reserved and cannot be used`,
|
|
57
89
|
);
|
|
@@ -66,15 +98,27 @@ export async function generatePostSlug(opts: SlugOptions): Promise<string> {
|
|
|
66
98
|
// Case 2: Title-based slug
|
|
67
99
|
if (title) {
|
|
68
100
|
const base = slugify(title);
|
|
69
|
-
if (base && !
|
|
101
|
+
if (base && !isReserved(base)) {
|
|
70
102
|
const available = await isAvailable(base);
|
|
71
103
|
if (available) return base;
|
|
104
|
+
|
|
105
|
+
// A readable language suffix beats a random one, and it is exactly the
|
|
106
|
+
// collision a translated pair produces.
|
|
107
|
+
if (languageSuffix) {
|
|
108
|
+
const languageCandidate = `${base}-${languageSuffix}`;
|
|
109
|
+
if (
|
|
110
|
+
!isReserved(languageCandidate) &&
|
|
111
|
+
(await isAvailable(languageCandidate))
|
|
112
|
+
) {
|
|
113
|
+
return languageCandidate;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
72
116
|
}
|
|
73
117
|
|
|
74
118
|
// Append random suffix on conflict or reserved base
|
|
75
119
|
for (let i = 0; i < MAX_RETRIES; i++) {
|
|
76
120
|
const candidate = `${base || generateRandomId(idLength)}-${generateRandomId(idLength)}`;
|
|
77
|
-
if (!
|
|
121
|
+
if (!isReserved(candidate) && (await isAvailable(candidate))) {
|
|
78
122
|
return candidate;
|
|
79
123
|
}
|
|
80
124
|
}
|
|
@@ -86,7 +130,7 @@ export async function generatePostSlug(opts: SlugOptions): Promise<string> {
|
|
|
86
130
|
// Case 3: Pure random
|
|
87
131
|
for (let i = 0; i < MAX_RETRIES; i++) {
|
|
88
132
|
const candidate = generateRandomId(idLength);
|
|
89
|
-
if (!
|
|
133
|
+
if (!isReserved(candidate) && (await isAvailable(candidate))) {
|
|
90
134
|
return candidate;
|
|
91
135
|
}
|
|
92
136
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slug generation from free text.
|
|
3
|
+
*
|
|
4
|
+
* Lives apart from `url.ts` on purpose: `limax` drags the whole `pinyin-pro`
|
|
5
|
+
* dictionary (≈390 KB minified) into any bundle that imports it. Keeping the
|
|
6
|
+
* dictionary behind this single module lets the browser load it lazily via
|
|
7
|
+
* `client/lazy-slugify.ts`, while `url.ts` stays cheap enough for the public
|
|
8
|
+
* bundle. The build guard in `vite.shared.ts` fails the client build if
|
|
9
|
+
* `pinyin-pro` ever lands in an entry chunk again.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import limax from "limax";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
HAN_RE,
|
|
16
|
+
HANGUL_RE,
|
|
17
|
+
KANA_RE,
|
|
18
|
+
hangulToLatin,
|
|
19
|
+
kanaToRomaji,
|
|
20
|
+
} from "./translit.js";
|
|
21
|
+
|
|
22
|
+
const HAN_GLOBAL_RE = /\p{Script=Han}/gu;
|
|
23
|
+
const CONTENT_CHAR_RE = /[\p{L}\p{N}]/u;
|
|
24
|
+
const ASCII_ALNUM_RE = /[a-zA-Z0-9]/;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Minimum fraction of a title's letters/digits the transliteration pipeline
|
|
28
|
+
* must handle for the result to be usable. Below this, too much meaning is
|
|
29
|
+
* lost (e.g. mixed kanji+kana Japanese where the semantic core is kanji, or
|
|
30
|
+
* scripts limax drops entirely) and `slugify` returns "" so callers fall back
|
|
31
|
+
* to their random-ID path instead of shipping a garbled slug.
|
|
32
|
+
*/
|
|
33
|
+
const SLUG_COVERAGE_THRESHOLD = 0.75;
|
|
34
|
+
|
|
35
|
+
/** Per-character limax probe cache: does this char transliterate to anything? */
|
|
36
|
+
const charCoverageCache = new Map<string, boolean>();
|
|
37
|
+
|
|
38
|
+
function isCharCovered(char: string): boolean {
|
|
39
|
+
if (ASCII_ALNUM_RE.test(char)) return true;
|
|
40
|
+
let covered = charCoverageCache.get(char);
|
|
41
|
+
if (covered === undefined) {
|
|
42
|
+
covered = limax(char, { tone: false }).length > 0;
|
|
43
|
+
charCoverageCache.set(char, covered);
|
|
44
|
+
}
|
|
45
|
+
return covered;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function slugCoverage(text: string, japanese: boolean): number {
|
|
49
|
+
let content = 0;
|
|
50
|
+
let covered = 0;
|
|
51
|
+
for (const char of text) {
|
|
52
|
+
if (!CONTENT_CHAR_RE.test(char)) continue;
|
|
53
|
+
content++;
|
|
54
|
+
if (KANA_RE.test(char) || HANGUL_RE.test(char)) {
|
|
55
|
+
covered++;
|
|
56
|
+
} else if (japanese && HAN_RE.test(char)) {
|
|
57
|
+
// Kanji in a kana-containing title is dropped, never pinyin-read.
|
|
58
|
+
} else if (isCharCovered(char)) {
|
|
59
|
+
covered++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return content === 0 ? 0 : covered / content;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Converts text to a URL-friendly slug.
|
|
67
|
+
*
|
|
68
|
+
* Transliterates i18n text to Latin before slugifying: Korean hangul via
|
|
69
|
+
* Revised Romanization, Japanese kana via Hepburn romaji (both in-house, see
|
|
70
|
+
* `translit.ts`), everything else (Han → pinyin, Cyrillic, accents, …) via
|
|
71
|
+
* limax. When the title contains kana it is treated as Japanese and Han
|
|
72
|
+
* characters are dropped rather than misread as Chinese pinyin; Han without
|
|
73
|
+
* kana is treated as Chinese.
|
|
74
|
+
*
|
|
75
|
+
* Returns `""` when transliteration would lose too much of the title (see
|
|
76
|
+
* `SLUG_COVERAGE_THRESHOLD`) — every caller treats an empty base as "no
|
|
77
|
+
* usable title" and falls back to a random ID, which beats publishing a
|
|
78
|
+
* garbled slug.
|
|
79
|
+
*
|
|
80
|
+
* @param text - The text to convert to a slug
|
|
81
|
+
* @returns The slugified string, or `""` when no usable slug can be derived
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* slugify("Hello World! This is a Test.");
|
|
86
|
+
* // Returns: "hello-world-this-is-a-test"
|
|
87
|
+
*
|
|
88
|
+
* slugify("书评");
|
|
89
|
+
* // Returns: "shu-ping"
|
|
90
|
+
*
|
|
91
|
+
* slugify("안녕하세요 세계");
|
|
92
|
+
* // Returns: "annyeonghaseyo-segye"
|
|
93
|
+
*
|
|
94
|
+
* slugify("ハンバーガー");
|
|
95
|
+
* // Returns: "hanbaga"
|
|
96
|
+
*
|
|
97
|
+
* slugify("日本語のタイトルです");
|
|
98
|
+
* // Returns: "" (kanji carries the meaning and can't be transliterated)
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export function slugify(text: string): string {
|
|
102
|
+
const japanese = KANA_RE.test(text);
|
|
103
|
+
let pre = text;
|
|
104
|
+
if (japanese) {
|
|
105
|
+
pre = kanaToRomaji(pre.replace(HAN_GLOBAL_RE, " "));
|
|
106
|
+
}
|
|
107
|
+
if (HANGUL_RE.test(pre)) {
|
|
108
|
+
pre = hangulToLatin(pre);
|
|
109
|
+
}
|
|
110
|
+
const base = limax(pre, { tone: false }).replace(/_/g, "-");
|
|
111
|
+
if (!base) return "";
|
|
112
|
+
return slugCoverage(text, japanese) >= SLUG_COVERAGE_THRESHOLD ? base : "";
|
|
113
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { escapeHtml } from "./html.js";
|
|
2
|
+
import { getJantDocsUrl } from "./jant-docs.js";
|
|
2
3
|
import {
|
|
3
4
|
getAuthSecret,
|
|
4
5
|
getHostedControlPlaneBaseUrl,
|
|
@@ -94,8 +95,7 @@ function getAuthSecretErrorHtml(kind: AuthSecretIssueKind): string {
|
|
|
94
95
|
return renderConfigurationErrorPage({
|
|
95
96
|
title: titleByKind[kind],
|
|
96
97
|
bodyHtml: `${leadByKind[kind]}${runtimeInstructions}`,
|
|
97
|
-
docsHref:
|
|
98
|
-
"https://github.com/jant-me/jant/blob/main/docs/configuration.md#required",
|
|
98
|
+
docsHref: getJantDocsUrl("configuration#required"),
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
101
|
|
|
@@ -219,8 +219,7 @@ function getHostBasedConfigurationErrorHtml(
|
|
|
219
219
|
return renderConfigurationErrorPage({
|
|
220
220
|
title: "Hosted configuration is incomplete",
|
|
221
221
|
bodyHtml: `<p>Jant is running with <code>SITE_RESOLUTION_MODE=host-based</code>, so hosted control-plane integration must be configured before the instance can serve requests.</p><ul>${itemsHtml}</ul>`,
|
|
222
|
-
docsHref:
|
|
223
|
-
"https://github.com/jant-me/jant/blob/main/docs/configuration.md#hosted-control-plane-integration-variables",
|
|
222
|
+
docsHref: getJantDocsUrl("configuration"),
|
|
224
223
|
});
|
|
225
224
|
}
|
|
226
225
|
|
|
@@ -243,8 +242,7 @@ export function getRuntimeConfigurationErrorPage(message: string): string {
|
|
|
243
242
|
return renderConfigurationErrorPage({
|
|
244
243
|
title: "Configuration Error",
|
|
245
244
|
bodyHtml: `<p>${escapeHtml(message)}</p><p>Update your environment or instance data, then restart Jant.</p>`,
|
|
246
|
-
docsHref:
|
|
247
|
-
"https://github.com/jant-me/jant/blob/main/docs/configuration.md#site-resolution",
|
|
245
|
+
docsHref: getJantDocsUrl("configuration"),
|
|
248
246
|
});
|
|
249
247
|
}
|
|
250
248
|
|
package/src/lib/storage.ts
CHANGED
|
@@ -432,6 +432,13 @@ export function createS3Driver(config: S3DriverConfig): StorageDriver {
|
|
|
432
432
|
// uploaded object checksums during finalize, so only enable response
|
|
433
433
|
// checksum verification when a request explicitly asks for it.
|
|
434
434
|
responseChecksumValidation: "WHEN_REQUIRED",
|
|
435
|
+
// Presigning runs with an empty body, so the SDK's default
|
|
436
|
+
// "WHEN_SUPPORTED" request checksums bake CRC32-of-nothing
|
|
437
|
+
// (x-amz-checksum-crc32=AAAAAA==) into every presigned PUT URL. The
|
|
438
|
+
// browser then uploads real bytes and the storage provider rejects
|
|
439
|
+
// the mismatch. Only send a request checksum when the caller asked
|
|
440
|
+
// for one (presignPut passes ChecksumSHA256 explicitly).
|
|
441
|
+
requestChecksumCalculation: "WHEN_REQUIRED",
|
|
435
442
|
});
|
|
436
443
|
return {
|
|
437
444
|
send: (cmd: unknown) => client.send(cmd as never),
|