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