@jant/core 0.6.16 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +60 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1526 -15
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +926 -73
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +16 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +42 -1
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +139 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +51 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +159 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +23 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +5 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +471 -369
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +178 -17
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post.test.ts +46 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +278 -19
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +221 -6
- package/src/services/post.ts +1021 -152
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1087 -7219
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +87 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jant Discover
|
|
3
|
+
*
|
|
4
|
+
* Discover is a public directory of Jant blogs: a crawler polls the Atom
|
|
5
|
+
* feeds of sites that opt in and lists their posts — the ones marked
|
|
6
|
+
* Featured on its home, link and quote posts on lists of their own. Core owns
|
|
7
|
+
* the protocol, not the directory — this module holds the site setting's
|
|
8
|
+
* effective-mode rules and the identifiers the feed declaration is built
|
|
9
|
+
* from. The declaration is specified in `docs/feeds.md`; how a directory
|
|
10
|
+
* behaves is that directory's own business, documented where it lives.
|
|
11
|
+
*
|
|
12
|
+
* `featured` survives as a stored value: older releases offered it as a
|
|
13
|
+
* choice, and a site that made it still declares it, which a directory reads
|
|
14
|
+
* as "only this feed". Nothing writes it any more.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* XML namespace for Jant's own feed extensions.
|
|
19
|
+
*
|
|
20
|
+
* A permanent identifier, not an environment-dependent URL: a feed served
|
|
21
|
+
* from any host declares this exact URI, the same policy published markdown
|
|
22
|
+
* links follow. Feed readers ignore elements in namespaces they do not know,
|
|
23
|
+
* so subscribers see nothing.
|
|
24
|
+
*/
|
|
25
|
+
export const DISCOVER_NAMESPACE_URI = "https://jant.me/ns";
|
|
26
|
+
|
|
27
|
+
/** Stored value of the `DISCOVER` site setting. */
|
|
28
|
+
export type DiscoverSetting = "latest" | "featured" | "off";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Effective Discover mode, as declared in the feed.
|
|
32
|
+
*
|
|
33
|
+
* `none` rather than `off` because it is what a crawler reads, and the
|
|
34
|
+
* element's absence already means "this site predates Discover" — the two
|
|
35
|
+
* states are different and must stay tellable apart.
|
|
36
|
+
*/
|
|
37
|
+
export type DiscoverMode = "latest" | "featured" | "none";
|
|
38
|
+
|
|
39
|
+
const DISCOVER_SETTINGS: readonly DiscoverSetting[] = [
|
|
40
|
+
"latest",
|
|
41
|
+
"featured",
|
|
42
|
+
"off",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse a stored or environment-supplied Discover setting.
|
|
47
|
+
*
|
|
48
|
+
* @param raw - Raw setting value, from the DB or an environment binding
|
|
49
|
+
* @returns The setting, or `null` when absent or unrecognized
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* parseDiscoverSetting("featured"); // "featured"
|
|
53
|
+
* parseDiscoverSetting("yes"); // null
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function parseDiscoverSetting(
|
|
57
|
+
raw: string | undefined | null,
|
|
58
|
+
): DiscoverSetting | null {
|
|
59
|
+
const value = raw?.trim();
|
|
60
|
+
if (!value) return null;
|
|
61
|
+
return DISCOVER_SETTINGS.includes(value as DiscoverSetting)
|
|
62
|
+
? (value as DiscoverSetting)
|
|
63
|
+
: null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Derive the effective Discover mode from the site's configuration.
|
|
68
|
+
*
|
|
69
|
+
* The rules, in the order they are applied:
|
|
70
|
+
*
|
|
71
|
+
* 1. A demo site is never listed. Demos exist to be thrown away, and the
|
|
72
|
+
* same lock already applies to search indexing.
|
|
73
|
+
* 2. A site with feeds turned off has nothing to poll — every feed path
|
|
74
|
+
* 404s — so it cannot honestly declare that it is listed.
|
|
75
|
+
* 3. The owner's stored choice wins over anything derived, including
|
|
76
|
+
* `noindex`. Someone who ticked the box meant it.
|
|
77
|
+
* 4. Without a stored choice, `noindex` implies `none`: hiding from search
|
|
78
|
+
* engines and being surfaced by a directory contradict each other, and the
|
|
79
|
+
* quieter reading is the safe one.
|
|
80
|
+
* 5. Then the deployment default, from the `DISCOVER` binding. Hosted Jant
|
|
81
|
+
* sets it, so every blog it serves takes part until its owner says
|
|
82
|
+
* otherwise. It sits below `noindex` on purpose: a deployment saying
|
|
83
|
+
* "list my sites" is a default, not the answer of the person who hid this
|
|
84
|
+
* one from search engines.
|
|
85
|
+
* 6. Otherwise `none`. A site nobody configured has not opted in, and the
|
|
86
|
+
* declaration is the consent record a directory reads — a site listed on
|
|
87
|
+
* the strength of a default its owner never chose was never asked.
|
|
88
|
+
*
|
|
89
|
+
* @param input - Resolved site configuration relevant to Discover
|
|
90
|
+
* @returns The mode this site's feeds should declare
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* resolveDiscoverMode({
|
|
94
|
+
* storedValue: null, defaultValue: null,
|
|
95
|
+
* demoMode: false, noindex: false, rssFeedsEnabled: true,
|
|
96
|
+
* }); // "none" — self-hosted, never opted in
|
|
97
|
+
* resolveDiscoverMode({
|
|
98
|
+
* storedValue: null, defaultValue: "latest",
|
|
99
|
+
* demoMode: false, noindex: false, rssFeedsEnabled: true,
|
|
100
|
+
* }); // "latest" — a deployment that lists its sites by default
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export function resolveDiscoverMode(input: {
|
|
104
|
+
/** The owner's own choice, stored in this site's settings. */
|
|
105
|
+
storedValue: string | undefined | null;
|
|
106
|
+
/**
|
|
107
|
+
* What this deployment lists by default, from the `DISCOVER` binding.
|
|
108
|
+
*
|
|
109
|
+
* Absent for a self-hosted site, which is what makes Discover opt-in
|
|
110
|
+
* there; hosted Jant sets it so its fleet is listed without every owner
|
|
111
|
+
* having to ask.
|
|
112
|
+
*/
|
|
113
|
+
defaultValue?: string | undefined | null;
|
|
114
|
+
demoMode: boolean;
|
|
115
|
+
/** Effective `noindex`, as resolved onto `AppConfig`. */
|
|
116
|
+
noindex: boolean;
|
|
117
|
+
rssFeedsEnabled: boolean;
|
|
118
|
+
}): DiscoverMode {
|
|
119
|
+
if (input.demoMode) return "none";
|
|
120
|
+
if (!input.rssFeedsEnabled) return "none";
|
|
121
|
+
|
|
122
|
+
const stored = parseDiscoverSetting(input.storedValue);
|
|
123
|
+
if (stored) return stored === "off" ? "none" : stored;
|
|
124
|
+
|
|
125
|
+
if (input.noindex) return "none";
|
|
126
|
+
|
|
127
|
+
const fallback = parseDiscoverSetting(input.defaultValue);
|
|
128
|
+
if (fallback) return fallback === "off" ? "none" : fallback;
|
|
129
|
+
|
|
130
|
+
return "none";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Site-relative path of the feed a Discover crawler should poll.
|
|
135
|
+
*
|
|
136
|
+
* The declaration names the feed to poll rather than leaving a crawler to
|
|
137
|
+
* guess, so a site that lists only its featured posts is polled at
|
|
138
|
+
* `/featured/feed` and never at `/latest/feed`.
|
|
139
|
+
*
|
|
140
|
+
* @param mode - Effective Discover mode
|
|
141
|
+
* @returns Site-relative feed path, or `null` when the site is not listed
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* getDiscoverFeedPath("featured"); // "/featured/feed"
|
|
145
|
+
* getDiscoverFeedPath("none"); // null
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export function getDiscoverFeedPath(mode: DiscoverMode): string | null {
|
|
149
|
+
switch (mode) {
|
|
150
|
+
case "latest":
|
|
151
|
+
return "/latest/feed";
|
|
152
|
+
case "featured":
|
|
153
|
+
return "/featured/feed";
|
|
154
|
+
case "none":
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Site-relative path of the endpoint a directory asks about posts it holds.
|
|
161
|
+
*
|
|
162
|
+
* A feed shows a site's newest posts, so a post missing from it may have been
|
|
163
|
+
* taken out or may only have been pushed past the feed's length — and the
|
|
164
|
+
* permalink cannot settle it either, because a post hidden from Latest or
|
|
165
|
+
* unfeatured is still a live page. This answers the question outright: for
|
|
166
|
+
* each post, whether it is in the Latest feed and whether its Thread is in the
|
|
167
|
+
* featured feed. The declaration names it in its `status` attribute, so a
|
|
168
|
+
* directory never builds the address itself.
|
|
169
|
+
*/
|
|
170
|
+
export const DISCOVER_STATUS_PATH = "/api/discover/posts";
|
|
171
|
+
|
|
172
|
+
/** Most post ids one status request may ask about. */
|
|
173
|
+
export const DISCOVER_STATUS_MAX_IDS = 50;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Public posts a blog needs before the jant.me directory will list it.
|
|
177
|
+
*
|
|
178
|
+
* Not core's rule — the directory's, published on the Discover page — but
|
|
179
|
+
* core states it so a site owner can see where they stand without asking
|
|
180
|
+
* anybody. A directory of your own may decide differently.
|
|
181
|
+
*
|
|
182
|
+
* One post, and no minimum history at all. The directory used to ask for a
|
|
183
|
+
* week of it as well, and dropped that: for a self-hosted blog the age can
|
|
184
|
+
* only be read off the oldest post the feed still carries, which is strict
|
|
185
|
+
* against an honest blog and no obstacle whatsoever to one that backdates.
|
|
186
|
+
* What keeps day-one throwaways out is the opt-in itself.
|
|
187
|
+
*/
|
|
188
|
+
export const DISCOVER_MIN_PUBLIC_POSTS = 1;
|
|
189
|
+
|
|
190
|
+
/** Longest a directory waits before reading a newly announced feed. */
|
|
191
|
+
export const DISCOVER_FIRST_READ_MAX_HOURS = 6;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* How long the jant.me directory holds a post it has read before showing it.
|
|
195
|
+
*
|
|
196
|
+
* The directory's rule — its `PUBLIC_DELAY_SECONDS` — stated here for the same
|
|
197
|
+
* reason as `DISCOVER_MIN_PUBLIC_POSTS`: the help line under the Discover
|
|
198
|
+
* checkbox tells an author how long they have to edit a post before it shows.
|
|
199
|
+
* A tuning value on the directory's side; this moves with it.
|
|
200
|
+
*/
|
|
201
|
+
export const DISCOVER_PUBLIC_DELAY_HOURS = 24;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The directory's names for its two format lists.
|
|
205
|
+
*
|
|
206
|
+
* Proper names that stay in English in every locale, as the directory's own
|
|
207
|
+
* tabs spell them. They are placeholder values rather than catalog entries: a
|
|
208
|
+
* catalog entry for a word that must not be translated is one a later
|
|
209
|
+
* translation pass can quietly "fix", and the help line links each list by
|
|
210
|
+
* finding this exact run of text.
|
|
211
|
+
*/
|
|
212
|
+
export const DISCOVER_LIST_NAMES = {
|
|
213
|
+
links: "Links",
|
|
214
|
+
quotes: "Quotes",
|
|
215
|
+
} as const;
|
|
216
|
+
|
|
217
|
+
/** Where a site stands against the directory's threshold. */
|
|
218
|
+
export interface DiscoverMaturity {
|
|
219
|
+
publicPostCount: number;
|
|
220
|
+
/** The threshold is met. */
|
|
221
|
+
established: boolean;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Measure a site against the directory's threshold.
|
|
226
|
+
*
|
|
227
|
+
* @param input - The site's own public-post evidence
|
|
228
|
+
* @returns The count to show, and whether the threshold is met
|
|
229
|
+
* @example
|
|
230
|
+
* ```ts
|
|
231
|
+
* measureDiscoverMaturity({ publicPostCount: 5 });
|
|
232
|
+
* // { publicPostCount: 5, established: true }
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
export function measureDiscoverMaturity(input: {
|
|
236
|
+
publicPostCount: number;
|
|
237
|
+
}): DiscoverMaturity {
|
|
238
|
+
return {
|
|
239
|
+
publicPostCount: input.publicPostCount,
|
|
240
|
+
established: input.publicPostCount >= DISCOVER_MIN_PUBLIC_POSTS,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** The directory's public pages that the Discover help line links to. */
|
|
245
|
+
export interface DiscoverPageUrls {
|
|
246
|
+
/** The directory's home: the posts blogs have marked Featured. */
|
|
247
|
+
home: string;
|
|
248
|
+
/** Every link post, under the list name `Links`. */
|
|
249
|
+
links: string;
|
|
250
|
+
/** Every quote post, under the list name `Quotes`. */
|
|
251
|
+
quotes: string;
|
|
252
|
+
/** How blogs are listed: joining, review, and what takes a blog off. */
|
|
253
|
+
rules: string;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The directory's public pages, derived from its base address.
|
|
258
|
+
*
|
|
259
|
+
* What "Jant Discover" means is best answered by the list itself, so the name
|
|
260
|
+
* links to the home rather than to a page describing it; the rules page is
|
|
261
|
+
* linked separately, for an author asking how their own posts get there.
|
|
262
|
+
* Derived for the same reason as the submission form: a site announcing to a
|
|
263
|
+
* directory of its own must link to that one, not to jant.me. The paths are
|
|
264
|
+
* jant.me's, where the three lists sit flat and the rules sit under
|
|
265
|
+
* `/discover`.
|
|
266
|
+
*
|
|
267
|
+
* @param directoryBaseUrl - The directory this deployment belongs to, from
|
|
268
|
+
* `getDiscoverDirectoryBaseUrl`
|
|
269
|
+
* @returns Absolute URLs of the pages, or `null` when there is no directory
|
|
270
|
+
* @example
|
|
271
|
+
* ```ts
|
|
272
|
+
* getDiscoverPageUrls("https://jant.me/");
|
|
273
|
+
* // {
|
|
274
|
+
* // home: "https://jant.me/discover",
|
|
275
|
+
* // links: "https://jant.me/links",
|
|
276
|
+
* // quotes: "https://jant.me/quotes",
|
|
277
|
+
* // rules: "https://jant.me/discover/about",
|
|
278
|
+
* // }
|
|
279
|
+
* ```
|
|
280
|
+
*/
|
|
281
|
+
export function getDiscoverPageUrls(
|
|
282
|
+
directoryBaseUrl: string | undefined | null,
|
|
283
|
+
): DiscoverPageUrls | null {
|
|
284
|
+
if (!directoryBaseUrl) return null;
|
|
285
|
+
try {
|
|
286
|
+
const at = (path: string) => new URL(path, directoryBaseUrl).toString();
|
|
287
|
+
return {
|
|
288
|
+
home: at("/discover"),
|
|
289
|
+
links: at("/links"),
|
|
290
|
+
quotes: at("/quotes"),
|
|
291
|
+
rules: at("/discover/about"),
|
|
292
|
+
};
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The directory's manual submission form, derived from its base address.
|
|
300
|
+
*
|
|
301
|
+
* Never hardcoded: a site announcing to a directory of its own must not be
|
|
302
|
+
* sent to somebody else's form. Both paths are part of the same directory, so
|
|
303
|
+
* one address is enough to find the other.
|
|
304
|
+
*
|
|
305
|
+
* @param directoryBaseUrl - The directory this deployment belongs to, from
|
|
306
|
+
* `getDiscoverDirectoryBaseUrl`
|
|
307
|
+
* @returns Absolute URL of the form, or `null` when there is no directory
|
|
308
|
+
* @example
|
|
309
|
+
* ```ts
|
|
310
|
+
* getDiscoverSubmitUrl("https://jant.me/");
|
|
311
|
+
* // "https://jant.me/discover/submit"
|
|
312
|
+
* ```
|
|
313
|
+
*/
|
|
314
|
+
export function getDiscoverSubmitUrl(
|
|
315
|
+
directoryBaseUrl: string | undefined | null,
|
|
316
|
+
): string | null {
|
|
317
|
+
if (!directoryBaseUrl) return null;
|
|
318
|
+
try {
|
|
319
|
+
return new URL("/discover/submit", directoryBaseUrl).toString();
|
|
320
|
+
} catch {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** One run of a sentence, and where it links when it is a link. */
|
|
326
|
+
export interface TextRun {
|
|
327
|
+
text: string;
|
|
328
|
+
href?: string;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Split a sentence into runs, making each given term a link.
|
|
333
|
+
*
|
|
334
|
+
* The Discover help line reads as one sentence in every locale, so each link is
|
|
335
|
+
* found by searching the translated line for its translated term rather than by
|
|
336
|
+
* gluing fragments together. A term the translation drops or rewrites stays
|
|
337
|
+
* plain text — a sentence short one link, never a broken one. Each term links
|
|
338
|
+
* once, at its first occurrence no other link has claimed; longer terms are
|
|
339
|
+
* placed first, so a term inside another cannot take its place.
|
|
340
|
+
*
|
|
341
|
+
* @param text - The translated sentence
|
|
342
|
+
* @param links - Each run of text to link, and where it goes
|
|
343
|
+
* @returns The whole sentence as consecutive runs, in order
|
|
344
|
+
* @example
|
|
345
|
+
* ```ts
|
|
346
|
+
* linkTerms("See Links and Quotes.", [
|
|
347
|
+
* { term: "Links", href: "/links" },
|
|
348
|
+
* { term: "Quotes", href: "/quotes" },
|
|
349
|
+
* ]);
|
|
350
|
+
* // [
|
|
351
|
+
* // { text: "See " },
|
|
352
|
+
* // { text: "Links", href: "/links" },
|
|
353
|
+
* // { text: " and " },
|
|
354
|
+
* // { text: "Quotes", href: "/quotes" },
|
|
355
|
+
* // { text: "." },
|
|
356
|
+
* // ]
|
|
357
|
+
* ```
|
|
358
|
+
*/
|
|
359
|
+
export function linkTerms(
|
|
360
|
+
text: string,
|
|
361
|
+
links: readonly { term: string; href: string }[],
|
|
362
|
+
): TextRun[] {
|
|
363
|
+
const claimed: { start: number; end: number; href: string }[] = [];
|
|
364
|
+
const ordered = links
|
|
365
|
+
.filter((link) => link.term && link.href)
|
|
366
|
+
.sort((a, b) => b.term.length - a.term.length);
|
|
367
|
+
|
|
368
|
+
for (const { term, href } of ordered) {
|
|
369
|
+
for (
|
|
370
|
+
let start = text.indexOf(term);
|
|
371
|
+
start !== -1;
|
|
372
|
+
start = text.indexOf(term, start + 1)
|
|
373
|
+
) {
|
|
374
|
+
const end = start + term.length;
|
|
375
|
+
if (claimed.some((run) => start < run.end && run.start < end)) continue;
|
|
376
|
+
claimed.push({ start, end, href });
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
claimed.sort((a, b) => a.start - b.start);
|
|
382
|
+
const runs: TextRun[] = [];
|
|
383
|
+
let at = 0;
|
|
384
|
+
for (const { start, end, href } of claimed) {
|
|
385
|
+
if (start > at) runs.push({ text: text.slice(at, start) });
|
|
386
|
+
runs.push({ text: text.slice(start, end), href });
|
|
387
|
+
at = end;
|
|
388
|
+
}
|
|
389
|
+
if (at < text.length) runs.push({ text: text.slice(at) });
|
|
390
|
+
return runs;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* The Discover help line as runs, each page it names linking to that page.
|
|
395
|
+
*
|
|
396
|
+
* The directory's name goes to its home, the two list names to the lists, and
|
|
397
|
+
* the rules to the page of rules. What Discover is, is best answered by the
|
|
398
|
+
* list itself, which is why the name links there rather than to a page about
|
|
399
|
+
* it, and why every link sits in the sentence that explains the list rather
|
|
400
|
+
* than on the checkbox label. The settings page and the setup screen both
|
|
401
|
+
* render their line through here, so the two can never link different words;
|
|
402
|
+
* setup's line is the settings line's opening sentence, so only the name is
|
|
403
|
+
* there to find.
|
|
404
|
+
*
|
|
405
|
+
* @param intro - The translated help line
|
|
406
|
+
* @param terms - The translated directory and rules-page names, as `intro`
|
|
407
|
+
* carries them
|
|
408
|
+
* @param pages - The directory's pages, or `null` when there is no directory
|
|
409
|
+
* @returns The line as runs; a single plain run when there is nothing to link to
|
|
410
|
+
* @example
|
|
411
|
+
* ```ts
|
|
412
|
+
* discoverIntroRuns(
|
|
413
|
+
* "Jant Discover is a directory. See the Discover community rules.",
|
|
414
|
+
* { name: "Jant Discover", rules: "Discover community rules" },
|
|
415
|
+
* getDiscoverPageUrls("https://jant.me/"),
|
|
416
|
+
* );
|
|
417
|
+
* // [
|
|
418
|
+
* // { text: "Jant Discover", href: "https://jant.me/discover" },
|
|
419
|
+
* // { text: " is a directory. See the " },
|
|
420
|
+
* // { text: "Discover community rules", href: "https://jant.me/discover/about" },
|
|
421
|
+
* // { text: "." },
|
|
422
|
+
* // ]
|
|
423
|
+
* ```
|
|
424
|
+
*/
|
|
425
|
+
export function discoverIntroRuns(
|
|
426
|
+
intro: string,
|
|
427
|
+
terms: { name: string; rules: string },
|
|
428
|
+
pages: DiscoverPageUrls | null,
|
|
429
|
+
): TextRun[] {
|
|
430
|
+
if (!pages) return [{ text: intro }];
|
|
431
|
+
return linkTerms(intro, [
|
|
432
|
+
{ term: terms.name, href: pages.home },
|
|
433
|
+
{ term: DISCOVER_LIST_NAMES.links, href: pages.links },
|
|
434
|
+
{ term: DISCOVER_LIST_NAMES.quotes, href: pages.quotes },
|
|
435
|
+
{ term: terms.rules, href: pages.rules },
|
|
436
|
+
]);
|
|
437
|
+
}
|
package/src/lib/env.ts
CHANGED
|
@@ -5,6 +5,21 @@ type EnvSource = object | undefined | null;
|
|
|
5
5
|
|
|
6
6
|
export const DEFAULT_APP_PORT = 3000;
|
|
7
7
|
|
|
8
|
+
/** Jant's own directory, for a deployment that belongs to no other. */
|
|
9
|
+
const DEFAULT_DISCOVER_DIRECTORY_URL = "https://jant.me";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Where a directory receives announcements.
|
|
13
|
+
*
|
|
14
|
+
* The same path at every directory, which is what lets one base address stand
|
|
15
|
+
* for the whole of it — and a long-lived public contract at Jant's own:
|
|
16
|
+
* shipped self-hosted versions POST here for years, so it never moves.
|
|
17
|
+
*/
|
|
18
|
+
const DISCOVER_PING_PATH = "/api/discover/ping";
|
|
19
|
+
|
|
20
|
+
/** Where a deployment announces when nothing else names a directory. */
|
|
21
|
+
export const DEFAULT_DISCOVER_PING_URL = `${DEFAULT_DISCOVER_DIRECTORY_URL}${DISCOVER_PING_PATH}`;
|
|
22
|
+
|
|
8
23
|
function toEnvRecord(env: EnvSource): Record<string, unknown> {
|
|
9
24
|
return (env ?? {}) as Record<string, unknown>;
|
|
10
25
|
}
|
|
@@ -200,6 +215,125 @@ export function getHostedControlPlaneInternalToken(
|
|
|
200
215
|
return getEnvString(env, "HOSTED_CONTROL_PLANE_INTERNAL_TOKEN");
|
|
201
216
|
}
|
|
202
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Which directory this deployment belongs to, resolved against one base.
|
|
220
|
+
*
|
|
221
|
+
* `DISCOVER_PING_URL` is read presence-aware rather than through
|
|
222
|
+
* `getEnvString`, because the two states that matter are spelled differently:
|
|
223
|
+
* an **absent** binding means "work it out", while a binding set to the
|
|
224
|
+
* **empty string** means "announce nowhere". `getEnvString` collapses both
|
|
225
|
+
* into undefined, which would make the documented way of switching the ping
|
|
226
|
+
* off silently fall back to switching it on.
|
|
227
|
+
*
|
|
228
|
+
* With nothing configured, a deployment that hosts blogs belongs to its own
|
|
229
|
+
* control plane's directory. Naming that twice is what let the two drift: a
|
|
230
|
+
* hosted deployment pointed at one control plane announced its blogs to
|
|
231
|
+
* jant.me, which knows nothing about them, and the failure was recorded where
|
|
232
|
+
* nobody reads it.
|
|
233
|
+
*
|
|
234
|
+
* @param env - Worker bindings or `process.env`
|
|
235
|
+
* @param controlPlaneBaseUrl - The control plane to fall back to, public or
|
|
236
|
+
* internal depending on who is asking
|
|
237
|
+
* @returns A URL on the directory's origin, or undefined when there is none
|
|
238
|
+
*/
|
|
239
|
+
function resolveDiscoverDirectory(
|
|
240
|
+
env: EnvSource,
|
|
241
|
+
controlPlaneBaseUrl: string | undefined,
|
|
242
|
+
): string | undefined {
|
|
243
|
+
const record = toEnvRecord(env);
|
|
244
|
+
if (Object.hasOwn(record, "DISCOVER_PING_URL")) {
|
|
245
|
+
const configured = normalizeEnvScalar(record["DISCOVER_PING_URL"]);
|
|
246
|
+
return configured?.trim() || undefined;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (controlPlaneBaseUrl) {
|
|
250
|
+
try {
|
|
251
|
+
return new URL(DISCOVER_PING_PATH, controlPlaneBaseUrl).toString();
|
|
252
|
+
} catch {
|
|
253
|
+
// A base URL this malformed fails louder elsewhere; announcing to Jant's
|
|
254
|
+
// own directory is a better answer here than taking a settings save down.
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return DEFAULT_DISCOVER_PING_URL;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Where a site announces itself when its owner turns Jant Discover on.
|
|
263
|
+
*
|
|
264
|
+
* Server-to-server, so a hosted deployment reaches its control plane by the
|
|
265
|
+
* internal address like every other core→cloud call — the ping carries no
|
|
266
|
+
* secret, but the internal route is the one that works without depending on
|
|
267
|
+
* the public hostname resolving and its certificate being trusted from inside.
|
|
268
|
+
*
|
|
269
|
+
* @param env - Worker bindings or `process.env`
|
|
270
|
+
* @returns The endpoint to announce to, or undefined when announcing is off
|
|
271
|
+
* @example
|
|
272
|
+
* ```ts
|
|
273
|
+
* getDiscoverPingUrl({}); // the default directory
|
|
274
|
+
* getDiscoverPingUrl({ DISCOVER_PING_URL: "" }); // undefined — off
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
export function getDiscoverPingUrl(env: EnvSource): string | undefined {
|
|
278
|
+
return resolveDiscoverDirectory(
|
|
279
|
+
env,
|
|
280
|
+
getHostedControlPlaneInternalBaseUrl(env),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* The directory a site owner is sent to, for the links on the settings page.
|
|
286
|
+
*
|
|
287
|
+
* The public counterpart of `getDiscoverPingUrl`: same directory, the address
|
|
288
|
+
* a browser can open. Both derive from the same configuration, so the page can
|
|
289
|
+
* never link to one directory while announcing to another.
|
|
290
|
+
*
|
|
291
|
+
* @param env - Worker bindings or `process.env`
|
|
292
|
+
* @returns The directory's base URL, or undefined when there is no directory
|
|
293
|
+
* @example
|
|
294
|
+
* ```ts
|
|
295
|
+
* getDiscoverDirectoryBaseUrl({}); // "https://jant.me/"
|
|
296
|
+
* getDiscoverDirectoryBaseUrl({ DISCOVER_PING_URL: "" }); // undefined
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
export function getDiscoverDirectoryBaseUrl(
|
|
300
|
+
env: EnvSource,
|
|
301
|
+
): string | undefined {
|
|
302
|
+
const resolved = resolveDiscoverDirectory(
|
|
303
|
+
env,
|
|
304
|
+
getHostedControlPlaneBaseUrl(env),
|
|
305
|
+
);
|
|
306
|
+
if (!resolved) return undefined;
|
|
307
|
+
try {
|
|
308
|
+
return new URL("/", resolved).toString();
|
|
309
|
+
} catch {
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* What this deployment lists in Jant Discover by default.
|
|
316
|
+
*
|
|
317
|
+
* Absent for a self-hosted site, and that is the point: with nothing stored
|
|
318
|
+
* and nothing configured, a site declares `none` and stays out of every
|
|
319
|
+
* directory until its owner turns it on. Hosted Jant sets `DISCOVER=latest`,
|
|
320
|
+
* so the blogs it serves are listed without each owner having to ask.
|
|
321
|
+
*
|
|
322
|
+
* A default, not a choice — the site's own stored setting overrides it, and
|
|
323
|
+
* so does `noindex`. See `resolveDiscoverMode`.
|
|
324
|
+
*
|
|
325
|
+
* @param env - Worker bindings or `process.env`
|
|
326
|
+
* @returns The raw binding value, for `resolveDiscoverMode` to parse
|
|
327
|
+
* @example
|
|
328
|
+
* ```ts
|
|
329
|
+
* getDiscoverDefault({}); // undefined — opt-in
|
|
330
|
+
* getDiscoverDefault({ DISCOVER: "latest" }); // "latest"
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
333
|
+
export function getDiscoverDefault(env: EnvSource): string | undefined {
|
|
334
|
+
return getEnvString(env, "DISCOVER");
|
|
335
|
+
}
|
|
336
|
+
|
|
203
337
|
export function getStorageDriverEnv(env: EnvSource): string | undefined {
|
|
204
338
|
return getEnvString(env, "STORAGE_DRIVER");
|
|
205
339
|
}
|
package/src/lib/errors.ts
CHANGED
|
@@ -60,6 +60,24 @@ export class ConflictError extends DomainError {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* A content language cannot be dropped while posts still carry it — 409.
|
|
65
|
+
*
|
|
66
|
+
* Thrown when removing a language, and when turning multilingual on with a
|
|
67
|
+
* language list that leaves stamped posts behind. Carries the language and
|
|
68
|
+
* post count so the settings route can compose a localized message; the
|
|
69
|
+
* plain-English `message` serves API consumers.
|
|
70
|
+
*/
|
|
71
|
+
export class LanguageInUseError extends DomainError {
|
|
72
|
+
constructor(
|
|
73
|
+
message: string,
|
|
74
|
+
public readonly language: string,
|
|
75
|
+
public readonly postCount: number,
|
|
76
|
+
) {
|
|
77
|
+
super(message, 409, "LANGUAGE_IN_USE");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
63
81
|
/** Hosted media quota exceeded — 409 */
|
|
64
82
|
export class MediaQuotaExceededError extends DomainError {
|
|
65
83
|
constructor(
|
|
@@ -90,6 +108,21 @@ export class ConfigurationError extends DomainError {
|
|
|
90
108
|
}
|
|
91
109
|
}
|
|
92
110
|
|
|
111
|
+
/**
|
|
112
|
+
* The requested site exists but is not being served — 503.
|
|
113
|
+
*
|
|
114
|
+
* Distinct from `NotFoundError` on purpose: a suspended site has a real
|
|
115
|
+
* hostname and real data behind it, and it can come back. 404 tells both the
|
|
116
|
+
* visitor and search engines that nothing was ever here, so a suspended site
|
|
117
|
+
* would quietly lose its index entries during a window it can still be
|
|
118
|
+
* restored from.
|
|
119
|
+
*/
|
|
120
|
+
export class SiteUnavailableError extends DomainError {
|
|
121
|
+
constructor(message = "This site is not being served right now.") {
|
|
122
|
+
super(message, 503, "SITE_UNAVAILABLE");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
93
126
|
// =============================================================================
|
|
94
127
|
// Route Helpers
|
|
95
128
|
// =============================================================================
|