@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,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Announcing a site to its directory, from whichever route just gave it a
|
|
3
|
+
* reason to.
|
|
4
|
+
*
|
|
5
|
+
* Two routes have one: the settings page, whenever the owner turns Discover on
|
|
6
|
+
* or off, and first-run setup, when they answer the same question for the first
|
|
7
|
+
* time. Both send the same thing to the same place, so they send it through the
|
|
8
|
+
* same function rather than through two that drift.
|
|
9
|
+
*
|
|
10
|
+
* The transport lives in `lib/discover-ping.ts` and the protocol in
|
|
11
|
+
* `lib/discover.ts`. What is here is the part that needs a request: the
|
|
12
|
+
* deployment's endpoint, the site's own address, and the runtime's hook for
|
|
13
|
+
* work that outlives the response.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Context } from "hono";
|
|
17
|
+
import type { Bindings } from "../types.js";
|
|
18
|
+
import type { AppVariables } from "../types/app-context.js";
|
|
19
|
+
import { runDeferred } from "../lib/deferred.js";
|
|
20
|
+
import {
|
|
21
|
+
getDiscoverFeedPath,
|
|
22
|
+
parseDiscoverSetting,
|
|
23
|
+
resolveDiscoverMode,
|
|
24
|
+
} from "../lib/discover.js";
|
|
25
|
+
import { getDiscoverDefault, getDiscoverPingUrl } from "../lib/env.js";
|
|
26
|
+
import { toAbsoluteSiteUrl } from "../lib/url.js";
|
|
27
|
+
|
|
28
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Tell the configured directory where this site's feed is, in the background.
|
|
32
|
+
*
|
|
33
|
+
* Never awaited by its caller. A directory takes up to twelve seconds to give
|
|
34
|
+
* up on, and neither a settings save nor the last screen of setup may hang on
|
|
35
|
+
* one — nor fail because one is down. The outcome is recorded either way, so
|
|
36
|
+
* the owner can tell "announced" from "never got through"; the settings page's
|
|
37
|
+
* status block is where that shows, and it carries a Retry.
|
|
38
|
+
*
|
|
39
|
+
* @param c - Request context, for the runtime's background-work hook
|
|
40
|
+
* @param storedValue - The stored Discover choice. Passed in rather than read
|
|
41
|
+
* from `c.var.allSettings`, which is the snapshot taken before the request
|
|
42
|
+
* ran — on the save that triggers this it still holds the previous answer.
|
|
43
|
+
* @returns Whether an announcement was started at all
|
|
44
|
+
*/
|
|
45
|
+
export function announceInBackground(
|
|
46
|
+
c: Context<Env>,
|
|
47
|
+
storedValue: string | undefined,
|
|
48
|
+
): boolean {
|
|
49
|
+
const { appConfig } = c.var;
|
|
50
|
+
const endpoint = getDiscoverPingUrl(c.env);
|
|
51
|
+
if (!endpoint) return false;
|
|
52
|
+
|
|
53
|
+
// Read the mode back through the same derivation the feed uses, so a site
|
|
54
|
+
// that cannot actually be polled — feeds switched off, `noindex` set, demo
|
|
55
|
+
// mode — never announces an address that would answer 404.
|
|
56
|
+
const mode = resolveDiscoverMode({
|
|
57
|
+
storedValue,
|
|
58
|
+
defaultValue: getDiscoverDefault(c.env),
|
|
59
|
+
demoMode: appConfig.demoMode,
|
|
60
|
+
noindex: appConfig.noindex,
|
|
61
|
+
rssFeedsEnabled: appConfig.rssFeedsEnabled,
|
|
62
|
+
});
|
|
63
|
+
// A site that has just switched Discover off resolves to `none` and so has
|
|
64
|
+
// no feed of its own to name — but that is exactly the site with something
|
|
65
|
+
// to say, and the declaration a directory needs to read sits in every feed,
|
|
66
|
+
// not only the one it was polling. `/latest/feed` is the address that is
|
|
67
|
+
// always served, so the stop is sent there.
|
|
68
|
+
//
|
|
69
|
+
// Only for an owner's own `off`, never for the other ways a site resolves to
|
|
70
|
+
// `none`: a demo site or one with feeds switched off would be naming an
|
|
71
|
+
// address that answers 404.
|
|
72
|
+
const feedPath =
|
|
73
|
+
getDiscoverFeedPath(mode) ??
|
|
74
|
+
(parseDiscoverSetting(storedValue) === "off" &&
|
|
75
|
+
appConfig.rssFeedsEnabled &&
|
|
76
|
+
!appConfig.demoMode
|
|
77
|
+
? getDiscoverFeedPath("latest")
|
|
78
|
+
: null);
|
|
79
|
+
if (!feedPath) return false;
|
|
80
|
+
|
|
81
|
+
// Deferred, so the caller answers without waiting on a directory. The helper
|
|
82
|
+
// is what keeps the orphaned promise from taking a Node process down with
|
|
83
|
+
// it: `announceToDiscover` already resolves rather than throws, and this is
|
|
84
|
+
// what holds if it ever stops being.
|
|
85
|
+
runDeferred(c, "Discover announcement", async () => {
|
|
86
|
+
await c.var.services.settings.announceToDiscover({
|
|
87
|
+
endpoint,
|
|
88
|
+
feedUrl: toAbsoluteSiteUrl(
|
|
89
|
+
feedPath,
|
|
90
|
+
appConfig.siteUrl,
|
|
91
|
+
appConfig.sitePathPrefix,
|
|
92
|
+
),
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
@@ -6,6 +6,7 @@ import type { AppVariables } from "../../../types/app-context.js";
|
|
|
6
6
|
import {
|
|
7
7
|
createTestDatabase,
|
|
8
8
|
DEFAULT_TEST_SITE_ID,
|
|
9
|
+
enforceD1BoundParameterLimit,
|
|
9
10
|
} from "../../../__tests__/helpers/db.js";
|
|
10
11
|
import { posts as postTable } from "../../../db/schema.js";
|
|
11
12
|
import { createPostService } from "../../../services/post.js";
|
|
@@ -22,7 +23,7 @@ import type { Database } from "../../../db/index.js";
|
|
|
22
23
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
23
24
|
|
|
24
25
|
function createFeedTestApp(envOverrides: Partial<Bindings> = {}) {
|
|
25
|
-
const { db } = createTestDatabase();
|
|
26
|
+
const { db, sqlite } = createTestDatabase();
|
|
26
27
|
const pathService = createPathService(db as never, DEFAULT_TEST_SITE_ID);
|
|
27
28
|
|
|
28
29
|
const services = {
|
|
@@ -65,7 +66,12 @@ function createFeedTestApp(envOverrides: Partial<Bindings> = {}) {
|
|
|
65
66
|
app.route("/latest", latestRoutes);
|
|
66
67
|
app.route("/featured", featuredRoutes);
|
|
67
68
|
|
|
68
|
-
return { app, services, db: db as unknown as Database };
|
|
69
|
+
return { app, services, db: db as unknown as Database, sqlite };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Count the `<entry>` elements in an Atom document. */
|
|
73
|
+
function countEntries(xml: string): number {
|
|
74
|
+
return xml.match(/<entry>/g)?.length ?? 0;
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
afterEach(() => {
|
|
@@ -597,6 +603,132 @@ describe("Atom Feed Routes", () => {
|
|
|
597
603
|
});
|
|
598
604
|
});
|
|
599
605
|
|
|
606
|
+
describe("Discover fields", () => {
|
|
607
|
+
it("gives every entry the post's own ID beside its permalink id", async () => {
|
|
608
|
+
const { app, services } = createFeedTestApp();
|
|
609
|
+
const post = await services.posts.create({
|
|
610
|
+
format: "note",
|
|
611
|
+
title: "Identified",
|
|
612
|
+
bodyMarkdown: "Body",
|
|
613
|
+
status: "published",
|
|
614
|
+
slug: "identified",
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const xml = await (await app.request("/latest/feed")).text();
|
|
618
|
+
|
|
619
|
+
expect(xml).toContain(
|
|
620
|
+
`<id>http://localhost:${DEFAULT_APP_PORT}/identified</id>\n <jant:id>${post.id}</jant:id>`,
|
|
621
|
+
);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it("names the status endpoint in the declaration while the site is listed", async () => {
|
|
625
|
+
const { app, services } = createFeedTestApp();
|
|
626
|
+
await services.settings.set("DISCOVER", "latest");
|
|
627
|
+
|
|
628
|
+
const xml = await (await app.request("/latest/feed")).text();
|
|
629
|
+
|
|
630
|
+
expect(xml).toContain(
|
|
631
|
+
` status="http://localhost:${DEFAULT_APP_PORT}/api/discover/posts">latest</jant:discover>`,
|
|
632
|
+
);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it("names no status endpoint under none", async () => {
|
|
636
|
+
const { app, services } = createFeedTestApp();
|
|
637
|
+
await services.settings.set("DISCOVER", "off");
|
|
638
|
+
|
|
639
|
+
const xml = await (await app.request("/latest/feed")).text();
|
|
640
|
+
|
|
641
|
+
expect(xml).toContain("<jant:discover>none</jant:discover>");
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
describe("?limit=", () => {
|
|
646
|
+
async function createNotes(
|
|
647
|
+
services: ReturnType<typeof createFeedTestApp>["services"],
|
|
648
|
+
count: number,
|
|
649
|
+
options: { featured?: boolean } = {},
|
|
650
|
+
) {
|
|
651
|
+
for (let i = 0; i < count; i++) {
|
|
652
|
+
await services.posts.create({
|
|
653
|
+
format: "note",
|
|
654
|
+
title: `Post ${i}`,
|
|
655
|
+
bodyMarkdown: `Body ${i}`,
|
|
656
|
+
status: "published",
|
|
657
|
+
featured: options.featured,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
it("carries more than RSS_FEED_LIMIT when asked", async () => {
|
|
663
|
+
const { app, services } = createFeedTestApp({ RSS_FEED_LIMIT: "2" });
|
|
664
|
+
await createNotes(services, 4);
|
|
665
|
+
|
|
666
|
+
const xml = await (await app.request("/latest/feed?limit=3")).text();
|
|
667
|
+
|
|
668
|
+
expect(countEntries(xml)).toBe(3);
|
|
669
|
+
expect(xml).toContain("Post 3");
|
|
670
|
+
expect(xml).toContain("Post 1");
|
|
671
|
+
expect(xml).not.toContain("Post 0");
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("carries fewer when asked", async () => {
|
|
675
|
+
const { app, services } = createFeedTestApp();
|
|
676
|
+
await createNotes(services, 3);
|
|
677
|
+
|
|
678
|
+
const xml = await (await app.request("/latest/feed?limit=1")).text();
|
|
679
|
+
|
|
680
|
+
expect(countEntries(xml)).toBe(1);
|
|
681
|
+
expect(xml).toContain("Post 2");
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it("keeps the site's length for a value it cannot read", async () => {
|
|
685
|
+
const { app, services } = createFeedTestApp({ RSS_FEED_LIMIT: "2" });
|
|
686
|
+
await createNotes(services, 4);
|
|
687
|
+
|
|
688
|
+
for (const query of ["limit=abc", "limit=0", "limit=-1", "limit=1.5"]) {
|
|
689
|
+
const res = await app.request(`/latest/feed?${query}`);
|
|
690
|
+
expect(res.status).toBe(200);
|
|
691
|
+
expect(countEntries(await res.text())).toBe(2);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it("applies to the featured and main feeds", async () => {
|
|
696
|
+
const { app, services } = createFeedTestApp({ RSS_FEED_LIMIT: "1" });
|
|
697
|
+
await createNotes(services, 3, { featured: true });
|
|
698
|
+
|
|
699
|
+
const featured = await (
|
|
700
|
+
await app.request("/featured/feed?limit=2")
|
|
701
|
+
).text();
|
|
702
|
+
const main = await (await app.request("/feed?limit=2")).text();
|
|
703
|
+
|
|
704
|
+
expect(countEntries(featured)).toBe(2);
|
|
705
|
+
expect(countEntries(main)).toBe(2);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it("leaves the feed's self link on the address without it", async () => {
|
|
709
|
+
const { app, services } = createFeedTestApp();
|
|
710
|
+
await createNotes(services, 1);
|
|
711
|
+
|
|
712
|
+
const xml = await (await app.request("/latest/feed?limit=10")).text();
|
|
713
|
+
|
|
714
|
+
expect(xml).toContain(
|
|
715
|
+
`<link href="http://localhost:${DEFAULT_APP_PORT}/latest/feed" rel="self"/>`,
|
|
716
|
+
);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("renders past a hundred entries within D1's bound-parameter limit", async () => {
|
|
720
|
+
const { app, services, sqlite } = createFeedTestApp();
|
|
721
|
+
await createNotes(services, 120, { featured: true });
|
|
722
|
+
enforceD1BoundParameterLimit(sqlite);
|
|
723
|
+
|
|
724
|
+
for (const path of ["/latest/feed", "/featured/feed"]) {
|
|
725
|
+
const res = await app.request(`${path}?limit=120`);
|
|
726
|
+
expect(res.status).toBe(200);
|
|
727
|
+
expect(countEntries(await res.text())).toBe(120);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
|
|
600
732
|
describe("RSS_PUBLISH_DELAY_SECONDS env var", () => {
|
|
601
733
|
it("includes the exact 300-second boundary and excludes newer Posts", async () => {
|
|
602
734
|
const currentTime = 2_000_000;
|
|
@@ -702,6 +834,37 @@ describe("Atom Feed Routes", () => {
|
|
|
702
834
|
expect(xml).toContain("<hr/>");
|
|
703
835
|
});
|
|
704
836
|
|
|
837
|
+
it("names a reply by its alias, the way the site does", async () => {
|
|
838
|
+
const { app, services } = createFeedTestApp();
|
|
839
|
+
|
|
840
|
+
const root = await services.posts.create({
|
|
841
|
+
format: "note",
|
|
842
|
+
title: "Aliased Thread Root",
|
|
843
|
+
bodyMarkdown: "Root content",
|
|
844
|
+
status: "published",
|
|
845
|
+
featured: true,
|
|
846
|
+
});
|
|
847
|
+
const reply = await services.posts.create({
|
|
848
|
+
format: "note",
|
|
849
|
+
bodyMarkdown: "Reply content",
|
|
850
|
+
status: "published",
|
|
851
|
+
replyToId: root.id,
|
|
852
|
+
});
|
|
853
|
+
await services.paths.create({
|
|
854
|
+
path: "/notes/the-reply",
|
|
855
|
+
kind: "alias",
|
|
856
|
+
postId: reply.id,
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
for (const path of ["/latest/feed", "/featured/feed"]) {
|
|
860
|
+
const xml = await (await app.request(path)).text();
|
|
861
|
+
// The row, the fold's `latest`, and the inline reply link all name
|
|
862
|
+
// the reply by the address the site uses, not by its slug.
|
|
863
|
+
expect(xml).toContain('href="http://localhost:3000/notes/the-reply"');
|
|
864
|
+
expect(xml).not.toContain(`/${reply.slug}"`);
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
|
|
705
868
|
it("delays new replies inside an already eligible Thread", async () => {
|
|
706
869
|
const currentTime = 2_000_000;
|
|
707
870
|
const dateNow = vi.spyOn(Date, "now").mockReturnValue(currentTime * 1000);
|
package/src/routes/feed/feed.ts
CHANGED
|
@@ -28,9 +28,13 @@ import type {
|
|
|
28
28
|
import type { AppVariables } from "../../types/app-context.js";
|
|
29
29
|
import { defaultFeedRenderer } from "../../lib/feed.js";
|
|
30
30
|
import {
|
|
31
|
+
buildFeedDiscoveryFields,
|
|
32
|
+
featuredFeedSelection,
|
|
31
33
|
getFeedEntryUpdatedAt,
|
|
34
|
+
getFeedLimit,
|
|
32
35
|
getRssPublishedBefore,
|
|
33
|
-
|
|
36
|
+
latestFeedSelection,
|
|
37
|
+
renderFeed,
|
|
34
38
|
} from "../../lib/feed-policy.js";
|
|
35
39
|
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
36
40
|
import { getI18n } from "../../i18n/index.js";
|
|
@@ -38,6 +42,7 @@ import { FORMATS } from "../../types/constants.js";
|
|
|
38
42
|
|
|
39
43
|
import { createMediaContext, toPostViews } from "../../lib/view.js";
|
|
40
44
|
import { toAbsoluteSiteUrl, toPublicPath } from "../../lib/url.js";
|
|
45
|
+
import { getViewLang, viewBasePath } from "../../lib/view-language.js";
|
|
41
46
|
import { toPlainText } from "../../lib/markdown.js";
|
|
42
47
|
|
|
43
48
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
@@ -82,12 +87,18 @@ async function loadMediaMap(
|
|
|
82
87
|
|
|
83
88
|
/**
|
|
84
89
|
* Build thread replies as PostView[] for a given root post from a thread map.
|
|
90
|
+
*
|
|
91
|
+
* Replies take the alias map too. A reply's permalink is what `<jant:post>`
|
|
92
|
+
* rows, the fold's `gap` and `latest`, and every `media:content` name it by,
|
|
93
|
+
* and a consumer keys on that address — so it has to be the address the site
|
|
94
|
+
* itself uses, which is the reply's first alias when it has one.
|
|
85
95
|
*/
|
|
86
96
|
function buildThreadReplies(
|
|
87
97
|
rootId: string,
|
|
88
98
|
threadMap: Map<string, Post[]>,
|
|
89
99
|
mediaMap: ReturnType<typeof buildMediaMap>,
|
|
90
100
|
mediaCtx: ReturnType<typeof createMediaContext>,
|
|
101
|
+
aliasMap: Map<string, string>,
|
|
91
102
|
) {
|
|
92
103
|
const thread = threadMap.get(rootId);
|
|
93
104
|
if (!thread || thread.length <= 1) return undefined;
|
|
@@ -100,9 +111,20 @@ function buildThreadReplies(
|
|
|
100
111
|
mediaAttachments: mediaMap.get(r.id) ?? [],
|
|
101
112
|
})),
|
|
102
113
|
mediaCtx,
|
|
114
|
+
undefined,
|
|
115
|
+
aliasMap,
|
|
103
116
|
);
|
|
104
117
|
}
|
|
105
118
|
|
|
119
|
+
/** The first alias of each post that has one: the address the site uses. */
|
|
120
|
+
function toAliasMap(aliasesMap: Map<string, string[]>): Map<string, string> {
|
|
121
|
+
const aliasMap = new Map<string, string>();
|
|
122
|
+
for (const [id, aliases] of aliasesMap) {
|
|
123
|
+
if (aliases[0]) aliasMap.set(id, aliases[0]);
|
|
124
|
+
}
|
|
125
|
+
return aliasMap;
|
|
126
|
+
}
|
|
127
|
+
|
|
106
128
|
/**
|
|
107
129
|
* Collect all reply IDs from a thread map (excluding root posts).
|
|
108
130
|
*/
|
|
@@ -126,13 +148,12 @@ async function buildLatestFeedData(
|
|
|
126
148
|
format?: Format,
|
|
127
149
|
): Promise<{ posts: Post[]; postViews: FeedPostView[] }> {
|
|
128
150
|
const posts = await c.var.services.posts.list({
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
151
|
+
...latestFeedSelection({
|
|
152
|
+
lang: getViewLang(c) ?? undefined,
|
|
153
|
+
publishedBefore,
|
|
154
|
+
}),
|
|
133
155
|
format,
|
|
134
156
|
ignorePinnedSort: true,
|
|
135
|
-
publishedBefore,
|
|
136
157
|
limit: feedLimit,
|
|
137
158
|
});
|
|
138
159
|
|
|
@@ -146,17 +167,19 @@ async function buildLatestFeedData(
|
|
|
146
167
|
c.var.services.paths.getPostAliases(postIds),
|
|
147
168
|
]);
|
|
148
169
|
|
|
149
|
-
//
|
|
170
|
+
// Replies are only known once the threads are, so their media and aliases
|
|
171
|
+
// come in a second round.
|
|
150
172
|
const replyIds = collectReplyIds(threadMap);
|
|
151
|
-
const replyMediaMap =
|
|
152
|
-
replyIds.length > 0
|
|
173
|
+
const [replyMediaMap, replyAliasesMap] =
|
|
174
|
+
replyIds.length > 0
|
|
175
|
+
? await Promise.all([
|
|
176
|
+
loadMediaMap(c, replyIds, mediaCtx),
|
|
177
|
+
c.var.services.paths.getPostAliases(replyIds),
|
|
178
|
+
])
|
|
179
|
+
: [mediaMap, new Map<string, string[]>()];
|
|
153
180
|
// Merge reply media into main map
|
|
154
181
|
const mergedMediaMap = new Map([...mediaMap, ...replyMediaMap]);
|
|
155
|
-
|
|
156
|
-
const aliasMap = new Map<string, string>();
|
|
157
|
-
for (const [id, aliases] of aliasesMap) {
|
|
158
|
-
if (aliases[0]) aliasMap.set(id, aliases[0]);
|
|
159
|
-
}
|
|
182
|
+
const aliasMap = toAliasMap(new Map([...aliasesMap, ...replyAliasesMap]));
|
|
160
183
|
|
|
161
184
|
const postViews = toPostViews(
|
|
162
185
|
posts.map((p) => ({
|
|
@@ -177,6 +200,7 @@ async function buildLatestFeedData(
|
|
|
177
200
|
threadMap,
|
|
178
201
|
mergedMediaMap,
|
|
179
202
|
mediaCtx,
|
|
203
|
+
aliasMap,
|
|
180
204
|
),
|
|
181
205
|
};
|
|
182
206
|
});
|
|
@@ -197,9 +221,10 @@ async function buildFeaturedFeedData(
|
|
|
197
221
|
publishedBefore: number,
|
|
198
222
|
): Promise<{ posts: Post[]; postViews: FeedPostView[] }> {
|
|
199
223
|
const rootIds = await c.var.services.posts.listFeaturedThreadRootIds({
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
224
|
+
...featuredFeedSelection({
|
|
225
|
+
lang: getViewLang(c) ?? undefined,
|
|
226
|
+
publishedBefore,
|
|
227
|
+
}),
|
|
203
228
|
limit: feedLimit,
|
|
204
229
|
});
|
|
205
230
|
|
|
@@ -224,7 +249,7 @@ async function buildFeaturedFeedData(
|
|
|
224
249
|
const postIds = posts.map((p) => p.id);
|
|
225
250
|
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
226
251
|
|
|
227
|
-
// Collect all post IDs (roots + replies) for media loading
|
|
252
|
+
// Collect all post IDs (roots + replies) for media and alias loading
|
|
228
253
|
const allPostIds = new Set(postIds);
|
|
229
254
|
for (const thread of threadMap.values()) {
|
|
230
255
|
for (const post of thread) {
|
|
@@ -234,13 +259,9 @@ async function buildFeaturedFeedData(
|
|
|
234
259
|
|
|
235
260
|
const [mediaMap, aliasesMap] = await Promise.all([
|
|
236
261
|
loadMediaMap(c, [...allPostIds], mediaCtx),
|
|
237
|
-
c.var.services.paths.getPostAliases(
|
|
262
|
+
c.var.services.paths.getPostAliases([...allPostIds]),
|
|
238
263
|
]);
|
|
239
|
-
|
|
240
|
-
const aliasMap = new Map<string, string>();
|
|
241
|
-
for (const [id, aliases] of aliasesMap) {
|
|
242
|
-
if (aliases[0]) aliasMap.set(id, aliases[0]);
|
|
243
|
-
}
|
|
264
|
+
const aliasMap = toAliasMap(aliasesMap);
|
|
244
265
|
|
|
245
266
|
const postViews = toPostViews(
|
|
246
267
|
posts.map((p) => ({
|
|
@@ -257,7 +278,13 @@ async function buildFeaturedFeedData(
|
|
|
257
278
|
return {
|
|
258
279
|
...postView,
|
|
259
280
|
feedUpdatedAt: getFeedEntryUpdatedAt(post, thread),
|
|
260
|
-
threadReplies: buildThreadReplies(
|
|
281
|
+
threadReplies: buildThreadReplies(
|
|
282
|
+
post.id,
|
|
283
|
+
threadMap,
|
|
284
|
+
mediaMap,
|
|
285
|
+
mediaCtx,
|
|
286
|
+
aliasMap,
|
|
287
|
+
),
|
|
261
288
|
};
|
|
262
289
|
});
|
|
263
290
|
|
|
@@ -284,8 +311,9 @@ export async function buildFeedData(
|
|
|
284
311
|
const siteName = appConfig.siteName;
|
|
285
312
|
const siteDescription = toPlainText(appConfig.siteDescription);
|
|
286
313
|
const siteUrl = appConfig.siteUrl;
|
|
287
|
-
|
|
288
|
-
const
|
|
314
|
+
// A language view's feed is that language's feed, so it declares it.
|
|
315
|
+
const siteLanguage = getViewLang(c) ?? appConfig.siteLanguage;
|
|
316
|
+
const feedLimit = getFeedLimit(c);
|
|
289
317
|
const publishedBefore = getRssPublishedBefore(
|
|
290
318
|
appConfig.rssPublishDelaySeconds,
|
|
291
319
|
);
|
|
@@ -297,6 +325,7 @@ export async function buildFeedData(
|
|
|
297
325
|
: await buildLatestFeedData(c, feedLimit, publishedBefore, opts.format);
|
|
298
326
|
|
|
299
327
|
return {
|
|
328
|
+
...buildFeedDiscoveryFields(c),
|
|
300
329
|
siteName,
|
|
301
330
|
siteDescription,
|
|
302
331
|
siteUrl,
|
|
@@ -318,10 +347,11 @@ export async function buildFeedData(
|
|
|
318
347
|
}),
|
|
319
348
|
)}`,
|
|
320
349
|
selfUrl: toAbsoluteSiteUrl(
|
|
321
|
-
opts.selfPath
|
|
350
|
+
`${viewBasePath(c)}${opts.selfPath}`,
|
|
322
351
|
siteUrl,
|
|
323
352
|
appConfig.sitePathPrefix,
|
|
324
353
|
),
|
|
354
|
+
siteIconUrl: appConfig.siteAvatarUrl || undefined,
|
|
325
355
|
posts: postViews,
|
|
326
356
|
};
|
|
327
357
|
}
|
|
@@ -338,21 +368,20 @@ export function parseFormatQuery(c: Context<Env>): Format | undefined {
|
|
|
338
368
|
return undefined;
|
|
339
369
|
}
|
|
340
370
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Atom — /feed
|
|
351
|
-
feedRoutes.get("/", async (c) => {
|
|
371
|
+
/**
|
|
372
|
+
* Render the site's main Atom feed for the current view language.
|
|
373
|
+
*
|
|
374
|
+
* @param c - Hono context
|
|
375
|
+
* @returns Atom feed response
|
|
376
|
+
*/
|
|
377
|
+
export async function renderMainFeed(c: Context<Env>): Promise<Response> {
|
|
352
378
|
const kind = c.var.appConfig.mainRssFeed === "latest" ? "latest" : "featured";
|
|
353
379
|
const feedData = await buildFeedData(c, { kind, selfPath: "/feed" });
|
|
354
380
|
return renderFeed(defaultFeedRenderer(feedData));
|
|
355
|
-
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Atom — /feed
|
|
384
|
+
feedRoutes.get("/", renderMainFeed);
|
|
356
385
|
|
|
357
386
|
// Legacy — /feed/latest moved to the canonical /latest/feed. Kept
|
|
358
387
|
// indefinitely as a 308 so old subscribers don't break; preserves the
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { Hono } from "hono";
|
|
18
|
-
import type { Bindings } from "../../types.js";
|
|
18
|
+
import type { Bindings, Post } from "../../types.js";
|
|
19
19
|
import type { AppVariables } from "../../types/app-context.js";
|
|
20
20
|
import {
|
|
21
21
|
renderSitemapIndex,
|
|
@@ -82,8 +82,12 @@ sitemapRoutes.get("/sitemap.xml", async (c) => {
|
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
// The shard carries both kinds, so it is listed when either exists.
|
|
86
|
+
const [collections, smartCollections] = await Promise.all([
|
|
87
|
+
c.var.services.collections.list(),
|
|
88
|
+
c.var.services.smartCollections.list(),
|
|
89
|
+
]);
|
|
90
|
+
if (collections.length > 0 || smartCollections.length > 0) {
|
|
87
91
|
entries.push({
|
|
88
92
|
loc: absoluteUrl("/sitemap-collections.xml", siteUrl, sitePathPrefix),
|
|
89
93
|
});
|
|
@@ -125,16 +129,66 @@ sitemapRoutes.get("/:file{sitemap-posts-[0-9]+\\.xml}", async (c) => {
|
|
|
125
129
|
limit: SITEMAP_SHARD_SIZE,
|
|
126
130
|
});
|
|
127
131
|
|
|
132
|
+
// `entry.alias` already includes a leading "/" (see `paths.getPostAliases`);
|
|
133
|
+
// slugs are stored raw. Prepending "/" to an alias would create "//path",
|
|
134
|
+
// which `new URL()` reads as protocol-relative and hijacks the hostname.
|
|
135
|
+
const pathOf = (entry: (typeof shardEntries)[number]) =>
|
|
136
|
+
entry.alias ?? `/${entry.slug}`;
|
|
137
|
+
|
|
138
|
+
// A post's URL is language-neutral and listed once. Translations are
|
|
139
|
+
// announced with `xhtml:link` alternates instead, which is what the sitemap
|
|
140
|
+
// protocol asks for — and the only place a crawler can learn about a
|
|
141
|
+
// translation whose own URL lives in a different shard.
|
|
142
|
+
const translated = shardEntries.filter(
|
|
143
|
+
(entry) => entry.translationGroupId && entry.language,
|
|
144
|
+
);
|
|
145
|
+
const translationsMap =
|
|
146
|
+
translated.length > 0
|
|
147
|
+
? await c.var.services.posts.getTranslationsMap(
|
|
148
|
+
translated.map((entry) => entry.id),
|
|
149
|
+
)
|
|
150
|
+
: new Map<string, Post[]>();
|
|
151
|
+
const siblingIds = [...translationsMap.values()]
|
|
152
|
+
.flat()
|
|
153
|
+
.map((post) => post.id);
|
|
154
|
+
const siblingAliases =
|
|
155
|
+
siblingIds.length > 0
|
|
156
|
+
? await c.var.services.paths.getPostAliases(siblingIds)
|
|
157
|
+
: new Map<string, string[]>();
|
|
158
|
+
|
|
128
159
|
const urls: SitemapUrlEntry[] = shardEntries.map((entry) => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
160
|
+
const path = pathOf(entry);
|
|
161
|
+
const siblings: Post[] = translationsMap.get(entry.id) ?? [];
|
|
162
|
+
const alternates =
|
|
163
|
+
entry.language && siblings.length > 0
|
|
164
|
+
? [
|
|
165
|
+
{
|
|
166
|
+
hreflang: entry.language,
|
|
167
|
+
href: absoluteUrl(path, siteUrl, sitePathPrefix),
|
|
168
|
+
},
|
|
169
|
+
...siblings.flatMap((sibling) =>
|
|
170
|
+
sibling.language
|
|
171
|
+
? [
|
|
172
|
+
{
|
|
173
|
+
hreflang: sibling.language,
|
|
174
|
+
href: absoluteUrl(
|
|
175
|
+
siblingAliases.get(sibling.id)?.[0] ??
|
|
176
|
+
`/${sibling.slug}`,
|
|
177
|
+
siteUrl,
|
|
178
|
+
sitePathPrefix,
|
|
179
|
+
),
|
|
180
|
+
},
|
|
181
|
+
]
|
|
182
|
+
: [],
|
|
183
|
+
),
|
|
184
|
+
]
|
|
185
|
+
: undefined;
|
|
186
|
+
|
|
134
187
|
return {
|
|
135
188
|
loc: absoluteUrl(path, siteUrl, sitePathPrefix),
|
|
136
189
|
lastmod: toIsoDate(entry.updatedAt),
|
|
137
190
|
priority: entry.featuredAt ? "0.8" : "0.6",
|
|
191
|
+
...(alternates ? { alternates } : {}),
|
|
138
192
|
};
|
|
139
193
|
});
|
|
140
194
|
|
|
@@ -156,7 +210,10 @@ sitemapRoutes.get("/sitemap-collections.xml", async (c) => {
|
|
|
156
210
|
const { appConfig } = c.var;
|
|
157
211
|
const { siteUrl, sitePathPrefix } = appConfig;
|
|
158
212
|
|
|
159
|
-
const collections = await
|
|
213
|
+
const [collections, smartCollections] = await Promise.all([
|
|
214
|
+
c.var.services.collections.list(),
|
|
215
|
+
c.var.services.smartCollections.list(),
|
|
216
|
+
]);
|
|
160
217
|
|
|
161
218
|
// Resolve each collection's canonical URL (alias if one exists, else slug).
|
|
162
219
|
// The `/collections` directory itself lives in `/sitemap-pages.xml`, since
|
|
@@ -176,6 +233,19 @@ sitemapRoutes.get("/sitemap-collections.xml", async (c) => {
|
|
|
176
233
|
}),
|
|
177
234
|
);
|
|
178
235
|
|
|
236
|
+
// Smart collections are here unconditionally. A page a reader assembled out
|
|
237
|
+
// of archive parameters is one facet of a combinatorial family and stays out
|
|
238
|
+
// of the index; a smart collection is a page the author declared and gave an
|
|
239
|
+
// address, so it belongs in the sitemap like any other — and unlike an
|
|
240
|
+
// archive URL, it can never be one only its author can see.
|
|
241
|
+
for (const smartCollection of smartCollections) {
|
|
242
|
+
urls.push({
|
|
243
|
+
loc: absoluteUrl(`/${smartCollection.slug}`, siteUrl, sitePathPrefix),
|
|
244
|
+
lastmod: toIsoDate(smartCollection.updatedAt),
|
|
245
|
+
priority: "0.7",
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
179
249
|
return xmlResponse(renderSitemapUrlSet(urls), CACHE_SHORT);
|
|
180
250
|
});
|
|
181
251
|
|
|
@@ -206,12 +276,26 @@ sitemapRoutes.get("/sitemap-pages.xml", async (c) => {
|
|
|
206
276
|
changefreq: "daily",
|
|
207
277
|
});
|
|
208
278
|
|
|
279
|
+
// The subscribe page exists only while feeds do, and its content never
|
|
280
|
+
// changes on its own — it lists the same three addresses for the life of the
|
|
281
|
+
// site.
|
|
282
|
+
if (appConfig.rssFeedsEnabled) {
|
|
283
|
+
urls.push({
|
|
284
|
+
loc: absoluteUrl("/subscribe", siteUrl, sitePathPrefix),
|
|
285
|
+
priority: "0.3",
|
|
286
|
+
changefreq: "yearly",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
209
290
|
// Include the collections directory landing page when at least one
|
|
210
291
|
// collection exists. When there are no collections, `/collections` still
|
|
211
292
|
// renders (as an empty directory), but indexing an empty aggregate page
|
|
212
293
|
// adds no value.
|
|
213
|
-
const collections = await
|
|
214
|
-
|
|
294
|
+
const [collections, smartCollections] = await Promise.all([
|
|
295
|
+
c.var.services.collections.list(),
|
|
296
|
+
c.var.services.smartCollections.list(),
|
|
297
|
+
]);
|
|
298
|
+
if (collections.length > 0 || smartCollections.length > 0) {
|
|
215
299
|
urls.push({
|
|
216
300
|
loc: absoluteUrl("/collections", siteUrl, sitePathPrefix),
|
|
217
301
|
priority: "0.5",
|