@jant/core 0.6.15 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +67 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C-oi_t8W.js → app-K_Aa1MMn.js} +52623 -13193
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-CsFx6F_M.js → github-sync-BPAvT999.js} +2343 -110
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +524 -2
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +57 -8
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +88 -1
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +79 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +3283 -389
- package/src/client/components/__tests__/jant-compose-editor.test.ts +581 -86
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +34 -89
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +121 -3
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +106 -49
- package/src/client/components/jant-compose-dialog.ts +2757 -1194
- package/src/client/components/jant-compose-editor.ts +866 -397
- package/src/client/components/jant-compose-fullscreen.ts +53 -47
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1142 -268
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +336 -242
- package/src/client/compose-launch.ts +89 -2
- package/src/client/compose-shortcuts.ts +9 -7
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/post-refresh.ts +135 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +143 -38
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +28 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +28 -0
- package/src/db/pg/schema.ts +213 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +242 -27
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +568 -168
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +562 -162
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +562 -162
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1691 -12
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/markdown.test.ts +16 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +88 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +93 -34
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +1021 -66
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +21 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +6 -1
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-display.ts +42 -9
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +81 -10
- package/src/lib/tiptap-render.ts +5 -4
- package/src/lib/translit.ts +288 -0
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +188 -24
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +79 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +172 -9
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/__tests__/posts.test.ts +38 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +30 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +11 -2
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +657 -1
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/preview.test.ts +3 -1
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +507 -357
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +183 -19
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +139 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post-timeline.test.ts +142 -11
- package/src/services/__tests__/post.test.ts +323 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +330 -47
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +281 -24
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +225 -7
- package/src/services/post.ts +1318 -252
- package/src/services/search.ts +48 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1585
- package/src/styles/site-media.css +20 -6
- package/src/styles/tokens.css +144 -13
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1224 -7276
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +92 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +168 -30
- package/src/types/views.ts +61 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +215 -21
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +103 -17
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +401 -301
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +122 -3
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/DraftPreviewBar.tsx +25 -9
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +55 -23
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +8 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-BxCOR3Uc.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-pLre2DM_.js +0 -1
- package/dist/client/_assets/client-B_eGKN5p.css +0 -2
- package/dist/client/_assets/client-DxY5BFe8.js +0 -296
- package/dist/client/_assets/client-auth-CakPESv9.js +0 -5533
- package/dist/env-OHRKGcMj.js +0 -281
- package/dist/github-api-BgSiE71w.js +0 -176
- package/dist/github-app-BbklkFmU.js +0 -275
- package/dist/github-sync-Cq1pzzOI.js +0 -431
- package/dist/github-sync-ppWXN3jb.js +0 -4
- package/dist/url-BMYO-Zlt.js +0 -395
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -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
|
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
|
}
|