@jant/core 0.6.14 → 0.6.16
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/bin/commands/import-site.js +88 -13
- package/bin/commands/migrate.js +55 -0
- package/bin/commands/posts/rebuild-html.js +184 -0
- package/bin/commands/site/snapshot/import.js +14 -10
- package/bin/lib/d1-query.js +23 -5
- package/bin/lib/migration-runner.js +29 -7
- package/bin/lib/node-database.js +32 -0
- package/bin/lib/site-snapshot.js +347 -46
- package/bin/lib/sql-export.js +1 -1
- package/bin/lib/thread-collection-migration.js +326 -0
- package/dist/app-C75Zc8dD.js +6 -0
- package/dist/{app-2i7-WQHg.js → app-C7_4oab9.js} +7774 -6425
- package/dist/client/.vite/manifest.json +8 -8
- package/dist/client/_assets/chunks/{url-pLre2DM_.js → url-CU9pEbk5.js} +1 -1
- package/dist/client/_assets/{client-BvID5ucz.js → client-Bq0vre8Y.js} +59 -38
- package/dist/client/_assets/client-D3VZyG4L.css +2 -0
- package/dist/client/_assets/client-auth-F2rb9WfF.js +5571 -0
- package/dist/{env-OHRKGcMj.js → env-BPYViDJJ.js} +1 -1
- package/dist/{export-DWn149b7.js → export-Dl5KCiEs.js} +1348 -128
- package/dist/{github-api-BgSiE71w.js → github-api-BNF35Lkx.js} +1 -1
- package/dist/{github-app-BbklkFmU.js → github-app-DdiD-bC4.js} +1 -1
- package/dist/{github-sync-C0zyF6XS.js → github-sync-Bqg62IvV.js} +3 -3
- package/dist/{github-sync-Ckk0sJxK.js → github-sync-HdK2EgvJ.js} +3 -3
- package/dist/index.js +5 -5
- package/dist/node.js +7 -7
- package/dist/{url-BMYO-Zlt.js → url-CbLAtlZe.js} +517 -12
- package/package.json +11 -10
- package/src/__tests__/bin/posts-rebuild-html.test.ts +137 -0
- package/src/__tests__/dev-navigation-defaults.test.ts +22 -0
- package/src/__tests__/export-hugo-build.test.ts +63 -4
- package/src/__tests__/export-import-roundtrip.test.ts +6 -4
- package/src/__tests__/export-service.test.ts +142 -9
- package/src/__tests__/helpers/app.ts +3 -0
- package/src/__tests__/helpers/export-fixtures.ts +2 -0
- package/src/__tests__/import-site-command.test.ts +65 -0
- package/src/__tests__/mise-config.test.ts +22 -0
- package/src/__tests__/vite-config.test.ts +45 -0
- package/src/app.tsx +16 -3
- package/src/client/__tests__/collection-created-notice.test.ts +36 -0
- package/src/client/__tests__/collection-form-bridge.test.ts +10 -1
- package/src/client/__tests__/collection-page-actions.test.ts +71 -2
- package/src/client/__tests__/compose-bridge.test.ts +316 -2
- package/src/client/__tests__/compose-shortcuts.test.ts +20 -0
- package/src/client/__tests__/feed-video-player.test.ts +16 -1
- package/src/client/__tests__/footnote-rail.test.ts +472 -0
- package/src/client/__tests__/hydrate-partial.test.ts +3 -0
- package/src/client/__tests__/toast.test.ts +82 -1
- package/src/client/collection-created-notice.ts +66 -0
- package/src/client/collection-form-bridge.ts +17 -0
- package/src/client/collection-navigation.ts +44 -0
- package/src/client/collection-page-actions.ts +57 -1
- package/src/client/components/__tests__/compose-format-convert.test.ts +0 -19
- package/src/client/components/__tests__/jant-collection-directory.test.ts +155 -6
- package/src/client/components/__tests__/jant-command-palette.test.ts +51 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +2268 -206
- package/src/client/components/__tests__/jant-compose-editor.test.ts +147 -59
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +114 -64
- package/src/client/components/__tests__/jant-config-editor.test.ts +405 -0
- package/src/client/components/__tests__/jant-media-lightbox.test.ts +121 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +538 -2
- package/src/client/components/__tests__/jant-post-form.test.ts +2 -2
- package/src/client/components/__tests__/jant-post-menu.test.ts +47 -5
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-manager-types.ts +9 -3
- package/src/client/components/compose-format-convert.ts +5 -1
- package/src/client/components/compose-types.ts +55 -3
- package/src/client/components/jant-collection-directory.ts +165 -30
- package/src/client/components/jant-command-palette.ts +14 -7
- package/src/client/components/jant-compose-dialog.ts +1542 -694
- package/src/client/components/jant-compose-editor.ts +237 -99
- package/src/client/components/jant-compose-fullscreen.ts +27 -9
- package/src/client/components/jant-config-editor.ts +722 -0
- package/src/client/components/jant-media-lightbox.ts +100 -18
- package/src/client/components/jant-nav-manager.ts +1283 -54
- package/src/client/components/jant-post-form.ts +3 -3
- package/src/client/components/jant-post-menu.ts +177 -95
- package/src/client/components/nav-manager-types.ts +51 -2
- package/src/client/components/post-form-types.ts +1 -1
- package/src/client/compose-bridge.ts +147 -123
- package/src/client/compose-launch.ts +13 -0
- package/src/client/compose-shortcuts.ts +5 -1
- package/src/client/feed-video-player.ts +62 -2
- package/src/client/footnote-rail.ts +478 -0
- package/src/client/hydrate-partial.ts +2 -0
- package/src/client/media-lightbox-events.ts +63 -0
- package/src/client/post-refresh.ts +135 -0
- package/src/client/site-header-fragment.ts +38 -0
- package/src/client/tiptap/__tests__/blockquote-editing.test.ts +201 -0
- package/src/client/tiptap/__tests__/clear-formatting.test.ts +168 -0
- package/src/client/tiptap/__tests__/link-input-rules.test.ts +121 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +447 -0
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +727 -4
- package/src/client/tiptap/__tests__/table-controls.test.ts +263 -0
- package/src/client/tiptap/__tests__/table-size-picker.test.ts +179 -0
- package/src/client/tiptap/bubble-menu.ts +88 -10
- package/src/client/tiptap/continuous-lists.ts +102 -0
- package/src/client/tiptap/create-editor.ts +3 -0
- package/src/client/tiptap/extensions.ts +14 -5
- package/src/client/tiptap/link-input-rules.ts +4 -4
- package/src/client/tiptap/markdown-clipboard.ts +608 -11
- package/src/client/tiptap/slash-commands.ts +8 -1
- package/src/client/tiptap/structural-keymap.ts +211 -0
- package/src/client/tiptap/tab-indent.ts +8 -22
- package/src/client/tiptap/table-control-labels.ts +30 -0
- package/src/client/tiptap/table-controls.ts +537 -0
- package/src/client/tiptap/table-size-picker.ts +254 -0
- package/src/client/toast.ts +133 -39
- package/src/client-auth.ts +1 -0
- package/src/client.ts +1 -0
- package/src/db/__tests__/backfill-thread-activity.test.ts +234 -0
- package/src/db/__tests__/d1-query.test.ts +43 -1
- package/src/db/__tests__/migration-runner.test.ts +63 -0
- package/src/db/__tests__/migrations.test.ts +469 -0
- package/src/db/__tests__/thread-activity.test.ts +113 -0
- package/src/db/__tests__/thread-collection-migration.test.ts +364 -0
- package/src/db/backfills/0005_split_thread_activity_from_quiet_replies.sql +68 -0
- package/src/db/migrations/0027_old_blue_blade.sql +39 -0
- package/src/db/migrations/0028_superb_post.sql +2 -0
- package/src/db/migrations/0029_boring_magma.sql +1 -0
- package/src/db/migrations/0030_typical_vivisector.sql +50 -0
- package/src/db/migrations/0031_many_ego.sql +3 -0
- package/src/db/migrations/meta/0027_snapshot.json +2511 -0
- package/src/db/migrations/meta/0028_snapshot.json +2511 -0
- package/src/db/migrations/meta/0029_snapshot.json +2519 -0
- package/src/db/migrations/meta/0030_snapshot.json +2541 -0
- package/src/db/migrations/meta/0031_snapshot.json +2562 -0
- package/src/db/migrations/meta/_journal.json +35 -0
- package/src/db/migrations/pg/0025_moaning_epoch.sql +39 -0
- package/src/db/migrations/pg/0026_talented_killer_shrike.sql +2 -0
- package/src/db/migrations/pg/0027_short_cannonball.sql +1 -0
- package/src/db/migrations/pg/0028_perfect_sharon_carter.sql +27 -0
- package/src/db/migrations/pg/0029_rare_hairball.sql +3 -0
- package/src/db/migrations/pg/meta/0025_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0026_snapshot.json +3204 -0
- package/src/db/migrations/pg/meta/0027_snapshot.json +3211 -0
- package/src/db/migrations/pg/meta/0028_snapshot.json +3248 -0
- package/src/db/migrations/pg/meta/0029_snapshot.json +3289 -0
- package/src/db/migrations/pg/meta/_journal.json +35 -0
- package/src/db/pg/schema.ts +46 -20
- package/src/db/rehearsal-fixtures/demo-current.json +39 -4
- package/src/db/rehearsal-fixtures/demo-current.sql +121 -6
- package/src/db/schema.ts +48 -20
- package/src/db/thread-activity.ts +101 -0
- package/src/i18n/__tests__/detect.test.ts +8 -2
- package/src/i18n/detect.ts +14 -5
- package/src/i18n/locales/public/en.po +222 -51
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +216 -45
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +216 -45
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +513 -10
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +515 -12
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +515 -12
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/api-settings.test.ts +299 -0
- package/src/lib/__tests__/constants.test.ts +5 -0
- package/src/lib/__tests__/feed-policy.test.ts +25 -0
- package/src/lib/__tests__/feed.test.ts +168 -0
- package/src/lib/__tests__/footnotes-upgrade.test.ts +181 -0
- package/src/lib/__tests__/markdown-roundtrip-embed.test.ts +61 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +32 -4
- package/src/lib/__tests__/markdown.test.ts +24 -6
- package/src/lib/__tests__/navigation.test.ts +59 -2
- package/src/lib/__tests__/post-body-html.test.ts +176 -0
- package/src/lib/__tests__/post-display.test.ts +64 -0
- package/src/lib/__tests__/resolve-config.test.ts +113 -1
- package/src/lib/__tests__/slug.test.ts +18 -0
- package/src/lib/__tests__/summary.test.ts +82 -0
- package/src/lib/__tests__/timeline.test.ts +109 -70
- package/src/lib/__tests__/timezones.test.ts +31 -3
- package/src/lib/__tests__/tiptap-render.test.ts +268 -9
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +6 -0
- package/src/lib/__tests__/translit.test.ts +87 -0
- package/src/lib/__tests__/url.test.ts +53 -0
- package/src/lib/__tests__/view.test.ts +53 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +18 -1
- package/src/lib/api-settings.ts +239 -12
- package/src/lib/collection-sort.ts +7 -5
- package/src/lib/constants.ts +2 -0
- package/src/lib/feed-path.ts +25 -0
- package/src/lib/feed-policy.ts +64 -0
- package/src/lib/feed.ts +113 -11
- package/src/lib/footnotes.ts +264 -0
- package/src/lib/github-sync-site-config.ts +2 -0
- package/src/lib/hugo-markdown.ts +15 -3
- package/src/lib/jant-branding.ts +1 -0
- package/src/lib/link-preview.ts +25 -0
- package/src/lib/markdown-manager.ts +193 -5
- package/src/lib/markdown.ts +10 -3
- package/src/lib/navigation.ts +16 -3
- package/src/lib/post-body-html.ts +131 -0
- package/src/lib/post-display.ts +47 -8
- package/src/lib/render.tsx +9 -2
- package/src/lib/resolve-config.ts +100 -16
- package/src/lib/schemas.ts +133 -1
- package/src/lib/settings-paths.ts +5 -0
- package/src/lib/site-skill.ts +78 -0
- package/src/lib/summary.ts +16 -5
- package/src/lib/timeline.ts +137 -131
- package/src/lib/timezones.ts +54 -14
- package/src/lib/tiptap-render.ts +359 -83
- package/src/lib/translit.ts +288 -0
- package/src/lib/url.ts +123 -12
- package/src/lib/view.ts +58 -20
- package/src/lib/worker-response-cache.ts +11 -18
- package/src/middleware/__tests__/public-content-access.test.ts +91 -0
- package/src/middleware/config.ts +2 -2
- package/src/middleware/public-content-access.ts +87 -0
- package/src/node/__tests__/cli-export.test.ts +14 -0
- package/src/node/__tests__/cli-migrate.test.ts +6 -0
- package/src/node/__tests__/cli-site-snapshot.test.ts +205 -2
- package/src/node/__tests__/cli-snapshot-meta.test.ts +100 -0
- package/src/node/index.ts +2 -1
- package/src/preset.css +16 -2
- package/src/routes/__tests__/compose.test.ts +51 -0
- package/src/routes/__tests__/site-skill.test.ts +112 -0
- package/src/routes/api/__tests__/collections.test.ts +91 -19
- package/src/routes/api/__tests__/mcp.test.ts +65 -1
- package/src/routes/api/__tests__/nav-items.test.ts +91 -0
- package/src/routes/api/__tests__/palette.test.ts +44 -0
- package/src/routes/api/__tests__/public-access.test.ts +81 -0
- package/src/routes/api/__tests__/settings.test.ts +217 -0
- package/src/routes/api/collections.ts +22 -26
- package/src/routes/api/export.ts +2 -0
- package/src/routes/api/internal/__tests__/post-body-html.test.ts +163 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +35 -2
- package/src/routes/api/internal/post-body-html.ts +43 -0
- package/src/routes/api/internal/sites.ts +24 -0
- package/src/routes/api/nav-items.ts +24 -1
- package/src/routes/api/posts.ts +14 -10
- package/src/routes/api/public/__tests__/archive.test.ts +35 -0
- package/src/routes/api/public/__tests__/posts.test.ts +246 -0
- package/src/routes/api/public/archive.ts +4 -0
- package/src/routes/api/public/posts.ts +48 -24
- package/src/routes/api/search.ts +3 -0
- package/src/routes/api/settings.ts +33 -0
- package/src/routes/auth/__tests__/setup.test.ts +7 -4
- package/src/routes/compose.tsx +8 -3
- package/src/routes/dash/settings.tsx +45 -2
- package/src/routes/feed/__tests__/feed.test.ts +210 -6
- package/src/routes/feed/__tests__/sitemap.test.ts +68 -1
- package/src/routes/feed/feed.ts +23 -14
- package/src/routes/pages/__tests__/archive-params.test.ts +384 -3
- package/src/routes/pages/__tests__/collection-routing.test.ts +63 -5
- package/src/routes/pages/__tests__/collections.test.ts +9 -9
- package/src/routes/pages/__tests__/featured.test.ts +6 -2
- package/src/routes/pages/__tests__/preview.test.ts +176 -0
- package/src/routes/pages/__tests__/search.test.ts +69 -0
- package/src/routes/pages/archive.tsx +90 -22
- package/src/routes/pages/collection.tsx +32 -19
- package/src/routes/pages/collections.tsx +6 -1
- package/src/routes/pages/page.tsx +86 -11
- package/src/routes/site-skill.ts +18 -0
- package/src/services/__tests__/auth.test.ts +7 -5
- package/src/services/__tests__/collection.test.ts +190 -44
- package/src/services/__tests__/navigation.test.ts +229 -21
- package/src/services/__tests__/post-timeline.test.ts +249 -39
- package/src/services/__tests__/post.test.ts +844 -5
- package/src/services/__tests__/search.test.ts +48 -1
- package/src/services/__tests__/settings.test.ts +8 -0
- package/src/services/__tests__/site-admin.test.ts +121 -1
- package/src/services/auth.ts +2 -2
- package/src/services/bootstrap.ts +3 -2
- package/src/services/collection.ts +224 -167
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +46 -25
- package/src/services/export-theme/layouts/_default/list.html +18 -4
- package/src/services/export-theme/layouts/_default/rss.xml +8 -12
- package/src/services/export-theme/layouts/featured/list.html +5 -4
- package/src/services/export-theme/layouts/partials/featured-thread.html +56 -0
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/media-gallery.html +1 -0
- package/src/services/export-theme/styles/main.css +30 -2
- package/src/services/export.ts +94 -85
- package/src/services/mcp.ts +37 -27
- package/src/services/media.ts +1 -1
- package/src/services/navigation.ts +185 -46
- package/src/services/path.ts +4 -1
- package/src/services/post.ts +1142 -342
- package/src/services/search.ts +17 -2
- package/src/services/settings.ts +2 -10
- package/src/services/site-admin.ts +63 -43
- package/src/style-cjk-jp.css +1 -1
- package/src/style-cjk-kr.css +1 -1
- package/src/style-cjk-tc.css +1 -1
- package/src/style-cjk.css +1 -1
- package/src/styles/components.css +540 -14
- package/src/styles/site-media.css +111 -19
- package/src/styles/tokens.css +76 -10
- package/src/styles/ui.css +2227 -1347
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +243 -12
- package/src/types/constants.ts +22 -1
- package/src/types/entities.ts +14 -8
- package/src/types/operations.ts +18 -10
- package/src/types/props.ts +20 -2
- package/src/types/raw-assets.d.ts +5 -0
- package/src/types/views.ts +22 -4
- package/src/ui/__tests__/font-themes.test.ts +42 -30
- package/src/ui/compose/ComposeDialog.tsx +109 -19
- package/src/ui/dash/appearance/NavigationContent.tsx +332 -4
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +23 -0
- package/src/ui/dash/settings/ConfigEditorContent.tsx +578 -0
- package/src/ui/dash/settings/GeneralContent.tsx +15 -5
- package/src/ui/dash/settings/SettingsRootContent.tsx +21 -1
- package/src/ui/dash/settings/__tests__/ConfigEditorContent.test.tsx +98 -0
- package/src/ui/dash/settings/__tests__/SettingsRootContent.test.tsx +15 -0
- package/src/ui/feed/CuratedThreadPreview.tsx +16 -4
- package/src/ui/feed/LinkCard.tsx +2 -12
- package/src/ui/feed/LinkPreview.tsx +2 -7
- package/src/ui/feed/NoteCard.tsx +10 -20
- package/src/ui/feed/PostStatusBadges.tsx +42 -3
- package/src/ui/feed/QuoteCard.tsx +2 -12
- package/src/ui/feed/ThreadPreview.tsx +26 -34
- package/src/ui/feed/TimelineFeed.tsx +2 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +197 -56
- package/src/ui/feed/__tests__/timeline-cards.test.ts +178 -3
- package/src/ui/feed/thread-preview-state.ts +5 -7
- package/src/ui/font-themes.ts +74 -26
- package/src/ui/layouts/BaseLayout.tsx +22 -8
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +64 -1
- package/src/ui/pages/ArchivePage.tsx +131 -24
- package/src/ui/pages/CollectionPage.tsx +101 -22
- package/src/ui/pages/CollectionsPage.tsx +3 -1
- package/src/ui/pages/PostPage.tsx +39 -12
- package/src/ui/pages/SearchPage.tsx +6 -14
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +70 -0
- package/src/ui/pages/__tests__/PostPage.test.tsx +88 -0
- package/src/ui/shared/CollectionDirectory.tsx +13 -9
- package/src/ui/shared/CollectionsManager.tsx +17 -7
- package/src/ui/shared/DraftPreviewBar.tsx +49 -0
- package/src/ui/shared/HomePageBranding.tsx +2 -2
- package/src/ui/shared/MediaGallery.tsx +1 -0
- package/src/ui/shared/PostFooter.tsx +38 -13
- package/src/ui/shared/__tests__/home-page-branding.test.tsx +1 -1
- package/src/ui/shared/__tests__/media-gallery.test.ts +1 -0
- package/src/ui/shared/__tests__/navigation-labels.test.ts +2 -2
- package/src/ui/shared/__tests__/post-footer.test.ts +15 -0
- package/src/ui/shared/collection-management-labels.ts +33 -2
- package/src/ui/shared/custom-icons.ts +5 -0
- package/src/ui/shared/navigation-labels.ts +2 -4
- package/src/ui/shared/post-article-attributes.ts +52 -0
- package/dist/app-DZDlmh7C.js +0 -6
- package/dist/client/_assets/client-Cevfpm8H.css +0 -2
- package/dist/client/_assets/client-auth-v3yvhbgZ.js +0 -4736
- package/src/client/tiptap/wrapping-input-rules.ts +0 -102
|
@@ -18,7 +18,7 @@ import { renderPublicPage } from "../../lib/render.js";
|
|
|
18
18
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
19
19
|
import { buildPageTitle } from "../../lib/page-title.js";
|
|
20
20
|
import { AdminBreadcrumb } from "../../ui/shared/AdminBreadcrumb.js";
|
|
21
|
-
import {
|
|
21
|
+
import { getTimeZoneOptions } from "../../lib/timezones.js";
|
|
22
22
|
import { ValidationError } from "../../lib/errors.js";
|
|
23
23
|
import { SETTINGS_KEYS } from "../../lib/constants.js";
|
|
24
24
|
import { getAvailableThemes } from "../../lib/theme.js";
|
|
@@ -39,6 +39,7 @@ import { FontThemeContent } from "../../ui/dash/appearance/FontThemeContent.js";
|
|
|
39
39
|
import { AdvancedContent } from "../../ui/dash/appearance/AdvancedContent.js";
|
|
40
40
|
import { CodeInjectionContent } from "../../ui/dash/appearance/CodeInjectionContent.js";
|
|
41
41
|
import { ApiTokensContent } from "../../ui/dash/settings/ApiTokensContent.js";
|
|
42
|
+
import { ConfigEditorContent } from "../../ui/dash/settings/ConfigEditorContent.js";
|
|
42
43
|
import { DeleteAccountContent } from "../../ui/dash/settings/DeleteAccountContent.js";
|
|
43
44
|
import {
|
|
44
45
|
GitHubSyncContent,
|
|
@@ -78,6 +79,7 @@ import {
|
|
|
78
79
|
triggerGitHubSyncInline,
|
|
79
80
|
} from "../../lib/github-sync-trigger.js";
|
|
80
81
|
import { buildSyncSiteConfig } from "../../lib/github-sync-site-config.js";
|
|
82
|
+
import { buildConfigEditorFields } from "../../lib/api-settings.js";
|
|
81
83
|
import {
|
|
82
84
|
readGitHubSyncStatus,
|
|
83
85
|
renderStatusCardHtml,
|
|
@@ -142,6 +144,7 @@ function breadcrumbLabel(
|
|
|
142
144
|
| "fontTheme"
|
|
143
145
|
| "customCss"
|
|
144
146
|
| "codeInjection"
|
|
147
|
+
| "config"
|
|
145
148
|
| "account"
|
|
146
149
|
| "sessions"
|
|
147
150
|
| "password"
|
|
@@ -187,6 +190,13 @@ function breadcrumbLabel(
|
|
|
187
190
|
comment: "@context: Breadcrumb label",
|
|
188
191
|
}),
|
|
189
192
|
);
|
|
193
|
+
case "config":
|
|
194
|
+
return i18n._(
|
|
195
|
+
msg({
|
|
196
|
+
message: "Config Editor",
|
|
197
|
+
comment: "@context: Breadcrumb label",
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
190
200
|
case "account":
|
|
191
201
|
return i18n._(
|
|
192
202
|
msg({
|
|
@@ -366,7 +376,7 @@ settingsRoutes.get("/general", async (c) => {
|
|
|
366
376
|
showJantBrandingOnHome={appConfig.showJantBrandingOnHome}
|
|
367
377
|
noindex={appConfig.noindex}
|
|
368
378
|
demoMode={appConfig.demoMode}
|
|
369
|
-
timezones={
|
|
379
|
+
timezones={getTimeZoneOptions(appConfig.timeZone)}
|
|
370
380
|
aboutPage={aboutPage}
|
|
371
381
|
aboutEditUrl={aboutEditPath(c)}
|
|
372
382
|
aboutCreateUrl={publicPath(c, "/settings/general/about-page")}
|
|
@@ -808,6 +818,7 @@ settingsRoutes.get("/navigation", async (c) => {
|
|
|
808
818
|
directoryData={directoryData}
|
|
809
819
|
suggestedLinks={suggestedLinks}
|
|
810
820
|
mainRssFeed={c.var.appConfig.mainRssFeed}
|
|
821
|
+
rssFeedsEnabled={c.var.appConfig.rssFeedsEnabled}
|
|
811
822
|
siteName={navData.siteName}
|
|
812
823
|
sitePathPrefix={c.var.appConfig.sitePathPrefix}
|
|
813
824
|
/>
|
|
@@ -1051,6 +1062,38 @@ settingsRoutes.post("/code-injection", async (c) => {
|
|
|
1051
1062
|
);
|
|
1052
1063
|
});
|
|
1053
1064
|
|
|
1065
|
+
// ===========================================================================
|
|
1066
|
+
// Config Editor — explicitly approved runtime settings only.
|
|
1067
|
+
// ===========================================================================
|
|
1068
|
+
|
|
1069
|
+
settingsRoutes.get("/config", async (c) => {
|
|
1070
|
+
const navData = await getNavigationData(c);
|
|
1071
|
+
const fields = buildConfigEditorFields(
|
|
1072
|
+
c.var.allSettings,
|
|
1073
|
+
c.env,
|
|
1074
|
+
c.var.appConfig.demoMode,
|
|
1075
|
+
);
|
|
1076
|
+
|
|
1077
|
+
return renderPublicPage(c, {
|
|
1078
|
+
title: buildPageTitle("Config Editor", navData.siteName),
|
|
1079
|
+
navData,
|
|
1080
|
+
content: (
|
|
1081
|
+
<>
|
|
1082
|
+
<AdminBreadcrumb
|
|
1083
|
+
parent={breadcrumbLabel(c, "settings")}
|
|
1084
|
+
parentHref={publicPath(c, "/settings")}
|
|
1085
|
+
current={breadcrumbLabel(c, "config")}
|
|
1086
|
+
/>
|
|
1087
|
+
<ConfigEditorContent
|
|
1088
|
+
fields={fields}
|
|
1089
|
+
endpoint={publicPath(c, "/api/settings")}
|
|
1090
|
+
sitePathPrefix={c.var.appConfig.sitePathPrefix}
|
|
1091
|
+
/>
|
|
1092
|
+
</>
|
|
1093
|
+
),
|
|
1094
|
+
});
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1054
1097
|
// ===========================================================================
|
|
1055
1098
|
// Account sub-menu
|
|
1056
1099
|
// ===========================================================================
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it,
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { eq } from "drizzle-orm";
|
|
3
3
|
import { Hono } from "hono";
|
|
4
4
|
import type { Bindings } from "../../../types.js";
|
|
@@ -43,6 +43,7 @@ function createFeedTestApp(envOverrides: Partial<Bindings> = {}) {
|
|
|
43
43
|
const env = {
|
|
44
44
|
SITE_ORIGIN: `http://localhost:${DEFAULT_APP_PORT}`,
|
|
45
45
|
SITE_PATH_PREFIX: "",
|
|
46
|
+
RSS_PUBLISH_DELAY_SECONDS: 0,
|
|
46
47
|
...envOverrides,
|
|
47
48
|
} as Bindings;
|
|
48
49
|
c.env = env;
|
|
@@ -67,6 +68,10 @@ function createFeedTestApp(envOverrides: Partial<Bindings> = {}) {
|
|
|
67
68
|
return { app, services, db: db as unknown as Database };
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
vi.restoreAllMocks();
|
|
73
|
+
});
|
|
74
|
+
|
|
70
75
|
describe("Atom Feed Routes", () => {
|
|
71
76
|
describe("/feed — site main feed", () => {
|
|
72
77
|
it("defaults to featured posts", async () => {
|
|
@@ -170,9 +175,10 @@ describe("Atom Feed Routes", () => {
|
|
|
170
175
|
expect(res.headers.get("Content-Type")).toBe(
|
|
171
176
|
"application/atom+xml; charset=utf-8",
|
|
172
177
|
);
|
|
178
|
+
expect(res.headers.get("Cache-Control")).toBe("public, max-age=60");
|
|
173
179
|
});
|
|
174
180
|
|
|
175
|
-
it("orders
|
|
181
|
+
it("orders Featured Threads by selected Post publication time", async () => {
|
|
176
182
|
const { app, services, db } = createFeedTestApp();
|
|
177
183
|
|
|
178
184
|
const olderPublished = await services.posts.create({
|
|
@@ -203,11 +209,9 @@ describe("Atom Feed Routes", () => {
|
|
|
203
209
|
expect(res.status).toBe(200);
|
|
204
210
|
|
|
205
211
|
const xml = await res.text();
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
xml.indexOf("Newer published"),
|
|
212
|
+
expect(xml.indexOf("Newer published")).toBeLessThan(
|
|
213
|
+
xml.indexOf("Older published"),
|
|
209
214
|
);
|
|
210
|
-
// <published> should reflect original publishedAt, not featuredAt
|
|
211
215
|
expect(xml).toContain("<published>1970-01-01T00:16:40.000Z</published>");
|
|
212
216
|
expect(xml).toContain("<published>1970-01-01T00:33:20.000Z</published>");
|
|
213
217
|
});
|
|
@@ -352,6 +356,45 @@ describe("Atom Feed Routes", () => {
|
|
|
352
356
|
expect(xml).toContain("Featured Post");
|
|
353
357
|
expect(xml).not.toContain("Regular Post");
|
|
354
358
|
});
|
|
359
|
+
|
|
360
|
+
it("deduplicates by Thread and orders from a Featured Child publication", async () => {
|
|
361
|
+
const { app, services } = createFeedTestApp();
|
|
362
|
+
|
|
363
|
+
const olderRoot = await services.posts.create({
|
|
364
|
+
format: "note",
|
|
365
|
+
title: "Older Thread Root",
|
|
366
|
+
bodyMarkdown: "Older root body",
|
|
367
|
+
status: "published",
|
|
368
|
+
publishedAt: 1000,
|
|
369
|
+
});
|
|
370
|
+
await services.posts.create({
|
|
371
|
+
format: "note",
|
|
372
|
+
title: "Featured Child",
|
|
373
|
+
bodyMarkdown: "Selected child body",
|
|
374
|
+
status: "published",
|
|
375
|
+
replyToId: olderRoot.id,
|
|
376
|
+
featured: true,
|
|
377
|
+
publishedAt: 3000,
|
|
378
|
+
});
|
|
379
|
+
await services.posts.create({
|
|
380
|
+
format: "note",
|
|
381
|
+
title: "Newer Root",
|
|
382
|
+
bodyMarkdown: "Newer root body",
|
|
383
|
+
status: "published",
|
|
384
|
+
featured: true,
|
|
385
|
+
publishedAt: 2000,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const res = await app.request("/featured/feed");
|
|
389
|
+
expect(res.status).toBe(200);
|
|
390
|
+
|
|
391
|
+
const xml = await res.text();
|
|
392
|
+
expect(xml.match(/<entry>/g)).toHaveLength(2);
|
|
393
|
+
expect(xml).toContain("Selected child body");
|
|
394
|
+
expect(xml.indexOf("Older Thread Root")).toBeLessThan(
|
|
395
|
+
xml.indexOf("Newer Root"),
|
|
396
|
+
);
|
|
397
|
+
});
|
|
355
398
|
});
|
|
356
399
|
|
|
357
400
|
describe("legacy feed path redirects", () => {
|
|
@@ -432,6 +475,33 @@ describe("Atom Feed Routes", () => {
|
|
|
432
475
|
});
|
|
433
476
|
});
|
|
434
477
|
|
|
478
|
+
it("emits canonical v3 footnotes from a stale stored projection", async () => {
|
|
479
|
+
const { app, services, db } = createFeedTestApp();
|
|
480
|
+
const post = await services.posts.create({
|
|
481
|
+
format: "note",
|
|
482
|
+
title: "Footnote feed",
|
|
483
|
+
bodyMarkdown: "Feed body[^1]\n\n[^1]: Feed definition",
|
|
484
|
+
status: "published",
|
|
485
|
+
});
|
|
486
|
+
await db
|
|
487
|
+
.update(postTable)
|
|
488
|
+
.set({
|
|
489
|
+
bodyHtml: '<span class="sidenote">legacy</span>',
|
|
490
|
+
bodyHtmlVersion: 1,
|
|
491
|
+
})
|
|
492
|
+
.where(eq(postTable.id, post.id));
|
|
493
|
+
|
|
494
|
+
const response = await app.request("/latest/feed");
|
|
495
|
+
const xml = await response.text();
|
|
496
|
+
|
|
497
|
+
expect(response.status).toBe(200);
|
|
498
|
+
expect(xml).toContain('role="doc-noteref"');
|
|
499
|
+
expect(xml).toContain('role="doc-endnotes"');
|
|
500
|
+
expect(xml).toMatch(/id="fn-[a-z0-9]{13}-1"/);
|
|
501
|
+
expect(xml).not.toContain(`fn-${post.id}`);
|
|
502
|
+
expect(xml).not.toContain("legacy");
|
|
503
|
+
});
|
|
504
|
+
|
|
435
505
|
describe("RSS_FEED_LIMIT env var", () => {
|
|
436
506
|
it("defaults to 50 when RSS_FEED_LIMIT is not set", async () => {
|
|
437
507
|
const { app, services } = createFeedTestApp();
|
|
@@ -527,6 +597,85 @@ describe("Atom Feed Routes", () => {
|
|
|
527
597
|
});
|
|
528
598
|
});
|
|
529
599
|
|
|
600
|
+
describe("RSS_PUBLISH_DELAY_SECONDS env var", () => {
|
|
601
|
+
it("includes the exact 300-second boundary and excludes newer Posts", async () => {
|
|
602
|
+
const currentTime = 2_000_000;
|
|
603
|
+
vi.spyOn(Date, "now").mockReturnValue(currentTime * 1000);
|
|
604
|
+
const { app, services } = createFeedTestApp({
|
|
605
|
+
RSS_PUBLISH_DELAY_SECONDS: 300,
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
await services.posts.create({
|
|
609
|
+
format: "note",
|
|
610
|
+
title: "Boundary Post",
|
|
611
|
+
bodyMarkdown: "Eligible at exactly five minutes",
|
|
612
|
+
status: "published",
|
|
613
|
+
publishedAt: currentTime - 300,
|
|
614
|
+
});
|
|
615
|
+
await services.posts.create({
|
|
616
|
+
format: "note",
|
|
617
|
+
title: "Recent Post",
|
|
618
|
+
bodyMarkdown: "Still inside the edit window",
|
|
619
|
+
status: "published",
|
|
620
|
+
publishedAt: currentTime - 299,
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
const xml = await (await app.request("/latest/feed")).text();
|
|
624
|
+
|
|
625
|
+
expect(xml).toContain("Boundary Post");
|
|
626
|
+
expect(xml).not.toContain("Recent Post");
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it("applies the delay to Featured Posts", async () => {
|
|
630
|
+
const currentTime = 2_000_000;
|
|
631
|
+
vi.spyOn(Date, "now").mockReturnValue(currentTime * 1000);
|
|
632
|
+
const { app, services } = createFeedTestApp({
|
|
633
|
+
RSS_PUBLISH_DELAY_SECONDS: 300,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
await services.posts.create({
|
|
637
|
+
format: "note",
|
|
638
|
+
title: "Eligible Featured Post",
|
|
639
|
+
bodyMarkdown: "Old enough",
|
|
640
|
+
status: "published",
|
|
641
|
+
featured: true,
|
|
642
|
+
publishedAt: currentTime - 300,
|
|
643
|
+
});
|
|
644
|
+
await services.posts.create({
|
|
645
|
+
format: "note",
|
|
646
|
+
title: "Recent Featured Post",
|
|
647
|
+
bodyMarkdown: "Too new",
|
|
648
|
+
status: "published",
|
|
649
|
+
featured: true,
|
|
650
|
+
publishedAt: currentTime,
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
const xml = await (await app.request("/featured/feed")).text();
|
|
654
|
+
|
|
655
|
+
expect(xml).toContain("Eligible Featured Post");
|
|
656
|
+
expect(xml).not.toContain("Recent Featured Post");
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it("lets zero disable the delay", async () => {
|
|
660
|
+
const currentTime = 2_000_000;
|
|
661
|
+
vi.spyOn(Date, "now").mockReturnValue(currentTime * 1000);
|
|
662
|
+
const { app, services } = createFeedTestApp({
|
|
663
|
+
RSS_PUBLISH_DELAY_SECONDS: 0,
|
|
664
|
+
});
|
|
665
|
+
await services.posts.create({
|
|
666
|
+
format: "note",
|
|
667
|
+
title: "Immediate Post",
|
|
668
|
+
bodyMarkdown: "No edit window",
|
|
669
|
+
status: "published",
|
|
670
|
+
publishedAt: currentTime,
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
const xml = await (await app.request("/latest/feed")).text();
|
|
674
|
+
|
|
675
|
+
expect(xml).toContain("Immediate Post");
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
|
|
530
679
|
describe("thread content in feed", () => {
|
|
531
680
|
it("includes thread replies inline with hr separators", async () => {
|
|
532
681
|
const { app, services } = createFeedTestApp();
|
|
@@ -552,5 +701,60 @@ describe("Atom Feed Routes", () => {
|
|
|
552
701
|
expect(xml).toContain("Reply content");
|
|
553
702
|
expect(xml).toContain("<hr/>");
|
|
554
703
|
});
|
|
704
|
+
|
|
705
|
+
it("delays new replies inside an already eligible Thread", async () => {
|
|
706
|
+
const currentTime = 2_000_000;
|
|
707
|
+
const dateNow = vi.spyOn(Date, "now").mockReturnValue(currentTime * 1000);
|
|
708
|
+
const { app, services } = createFeedTestApp({
|
|
709
|
+
RSS_PUBLISH_DELAY_SECONDS: 300,
|
|
710
|
+
});
|
|
711
|
+
const root = await services.posts.create({
|
|
712
|
+
format: "note",
|
|
713
|
+
title: "Eligible Thread Root",
|
|
714
|
+
bodyMarkdown: "Root content",
|
|
715
|
+
status: "published",
|
|
716
|
+
publishedAt: currentTime - 600,
|
|
717
|
+
});
|
|
718
|
+
await services.posts.create({
|
|
719
|
+
format: "note",
|
|
720
|
+
bodyMarkdown: "Recent reply content",
|
|
721
|
+
status: "published",
|
|
722
|
+
replyToId: root.id,
|
|
723
|
+
publishedAt: currentTime,
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
const beforeDelay = await (await app.request("/latest/feed")).text();
|
|
727
|
+
expect(beforeDelay).toContain("Root content");
|
|
728
|
+
expect(beforeDelay).not.toContain("Recent reply content");
|
|
729
|
+
|
|
730
|
+
dateNow.mockReturnValue((currentTime + 300) * 1000);
|
|
731
|
+
const afterDelay = await (await app.request("/latest/feed")).text();
|
|
732
|
+
expect(afterDelay).toContain("Recent reply content");
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it("reflects eligible content edits in the Atom entry updated timestamp", async () => {
|
|
736
|
+
const currentTime = 2_000_000;
|
|
737
|
+
vi.spyOn(Date, "now").mockReturnValue(currentTime * 1000);
|
|
738
|
+
const { app, services } = createFeedTestApp({
|
|
739
|
+
RSS_PUBLISH_DELAY_SECONDS: 300,
|
|
740
|
+
});
|
|
741
|
+
const root = await services.posts.create({
|
|
742
|
+
format: "note",
|
|
743
|
+
title: "Edited Post",
|
|
744
|
+
bodyMarkdown: "Original copy",
|
|
745
|
+
status: "published",
|
|
746
|
+
publishedAt: currentTime - 300,
|
|
747
|
+
});
|
|
748
|
+
await services.posts.update(root.id, {
|
|
749
|
+
bodyMarkdown: "Corrected copy",
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
const xml = await (await app.request("/latest/feed")).text();
|
|
753
|
+
const entry = xml.match(/<entry>[\s\S]*?<\/entry>/)?.[0];
|
|
754
|
+
|
|
755
|
+
expect(entry).toContain("Corrected copy");
|
|
756
|
+
expect(entry).not.toContain("Original copy");
|
|
757
|
+
expect(entry).toContain("<updated>1970-01-24T03:33:20.000Z</updated>");
|
|
758
|
+
});
|
|
555
759
|
});
|
|
556
760
|
});
|
|
@@ -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
|
@@ -27,9 +27,13 @@ import type {
|
|
|
27
27
|
} from "../../types.js";
|
|
28
28
|
import type { AppVariables } from "../../types/app-context.js";
|
|
29
29
|
import { defaultFeedRenderer } from "../../lib/feed.js";
|
|
30
|
+
import {
|
|
31
|
+
getFeedEntryUpdatedAt,
|
|
32
|
+
getRssPublishedBefore,
|
|
33
|
+
RSS_FEED_CACHE_CONTROL,
|
|
34
|
+
} from "../../lib/feed-policy.js";
|
|
30
35
|
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
31
36
|
import { getI18n } from "../../i18n/index.js";
|
|
32
|
-
import { toISOString } from "../../lib/time.js";
|
|
33
37
|
import { FORMATS } from "../../types/constants.js";
|
|
34
38
|
|
|
35
39
|
import { createMediaContext, toPostViews } from "../../lib/view.js";
|
|
@@ -118,6 +122,7 @@ function collectReplyIds(threadMap: Map<string, Post[]>): string[] {
|
|
|
118
122
|
async function buildLatestFeedData(
|
|
119
123
|
c: Context<Env>,
|
|
120
124
|
feedLimit: number,
|
|
125
|
+
publishedBefore: number,
|
|
121
126
|
format?: Format,
|
|
122
127
|
): Promise<{ posts: Post[]; postViews: FeedPostView[] }> {
|
|
123
128
|
const posts = await c.var.services.posts.list({
|
|
@@ -127,6 +132,7 @@ async function buildLatestFeedData(
|
|
|
127
132
|
excludePrivate: true,
|
|
128
133
|
format,
|
|
129
134
|
ignorePinnedSort: true,
|
|
135
|
+
publishedBefore,
|
|
130
136
|
limit: feedLimit,
|
|
131
137
|
});
|
|
132
138
|
|
|
@@ -135,7 +141,7 @@ async function buildLatestFeedData(
|
|
|
135
141
|
|
|
136
142
|
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
137
143
|
const [threadMap, mediaMap, aliasesMap] = await Promise.all([
|
|
138
|
-
c.var.services.posts.getPublishedThreads(rootIds),
|
|
144
|
+
c.var.services.posts.getPublishedThreads(rootIds, { publishedBefore }),
|
|
139
145
|
loadMediaMap(c, postIds, mediaCtx),
|
|
140
146
|
c.var.services.paths.getPostAliases(postIds),
|
|
141
147
|
]);
|
|
@@ -162,9 +168,10 @@ async function buildLatestFeedData(
|
|
|
162
168
|
aliasMap,
|
|
163
169
|
).map((postView, index) => {
|
|
164
170
|
const post = posts[index] as (typeof posts)[number];
|
|
171
|
+
const thread = threadMap.get(post.id);
|
|
165
172
|
return {
|
|
166
173
|
...postView,
|
|
167
|
-
feedUpdatedAt:
|
|
174
|
+
feedUpdatedAt: getFeedEntryUpdatedAt(post, thread),
|
|
168
175
|
threadReplies: buildThreadReplies(
|
|
169
176
|
post.id,
|
|
170
177
|
threadMap,
|
|
@@ -187,10 +194,12 @@ async function buildLatestFeedData(
|
|
|
187
194
|
async function buildFeaturedFeedData(
|
|
188
195
|
c: Context<Env>,
|
|
189
196
|
feedLimit: number,
|
|
197
|
+
publishedBefore: number,
|
|
190
198
|
): Promise<{ posts: Post[]; postViews: FeedPostView[] }> {
|
|
191
199
|
const rootIds = await c.var.services.posts.listFeaturedThreadRootIds({
|
|
192
200
|
status: "published",
|
|
193
201
|
excludePrivate: true,
|
|
202
|
+
publishedBefore,
|
|
194
203
|
limit: feedLimit,
|
|
195
204
|
});
|
|
196
205
|
|
|
@@ -198,7 +207,9 @@ async function buildFeaturedFeedData(
|
|
|
198
207
|
return { posts: [], postViews: [] };
|
|
199
208
|
}
|
|
200
209
|
|
|
201
|
-
const threadMap = await c.var.services.posts.getPublishedThreads(rootIds
|
|
210
|
+
const threadMap = await c.var.services.posts.getPublishedThreads(rootIds, {
|
|
211
|
+
publishedBefore,
|
|
212
|
+
});
|
|
202
213
|
|
|
203
214
|
// Extract root posts in the same order as rootIds
|
|
204
215
|
const posts: Post[] = [];
|
|
@@ -241,16 +252,11 @@ async function buildFeaturedFeedData(
|
|
|
241
252
|
aliasMap,
|
|
242
253
|
).map((postView, index) => {
|
|
243
254
|
const post = posts[index] as (typeof posts)[number];
|
|
244
|
-
|
|
245
|
-
// feedUpdatedAt = max(lastActivityAt, featuredAt)
|
|
246
|
-
const lastActivity = toISOString(post.lastActivityAt);
|
|
247
|
-
const featuredIso = post.featuredAt ? toISOString(post.featuredAt) : null;
|
|
248
|
-
const feedUpdatedAt =
|
|
249
|
-
featuredIso && featuredIso > lastActivity ? featuredIso : lastActivity;
|
|
255
|
+
const thread = threadMap.get(post.id);
|
|
250
256
|
|
|
251
257
|
return {
|
|
252
258
|
...postView,
|
|
253
|
-
feedUpdatedAt,
|
|
259
|
+
feedUpdatedAt: getFeedEntryUpdatedAt(post, thread),
|
|
254
260
|
threadReplies: buildThreadReplies(post.id, threadMap, mediaMap, mediaCtx),
|
|
255
261
|
};
|
|
256
262
|
});
|
|
@@ -280,12 +286,15 @@ export async function buildFeedData(
|
|
|
280
286
|
const siteUrl = appConfig.siteUrl;
|
|
281
287
|
const siteLanguage = appConfig.siteLanguage;
|
|
282
288
|
const feedLimit = appConfig.rssFeedLimit;
|
|
289
|
+
const publishedBefore = getRssPublishedBefore(
|
|
290
|
+
appConfig.rssPublishDelaySeconds,
|
|
291
|
+
);
|
|
283
292
|
const kind = opts.kind;
|
|
284
293
|
|
|
285
294
|
const { postViews } =
|
|
286
295
|
kind === "featured"
|
|
287
|
-
? await buildFeaturedFeedData(c, feedLimit)
|
|
288
|
-
: await buildLatestFeedData(c, feedLimit, opts.format);
|
|
296
|
+
? await buildFeaturedFeedData(c, feedLimit, publishedBefore)
|
|
297
|
+
: await buildLatestFeedData(c, feedLimit, publishedBefore, opts.format);
|
|
289
298
|
|
|
290
299
|
return {
|
|
291
300
|
siteName,
|
|
@@ -333,7 +342,7 @@ export function renderFeed(xml: string) {
|
|
|
333
342
|
return new Response(xml, {
|
|
334
343
|
headers: {
|
|
335
344
|
"Content-Type": "application/atom+xml; charset=utf-8",
|
|
336
|
-
"Cache-Control":
|
|
345
|
+
"Cache-Control": RSS_FEED_CACHE_CONTROL,
|
|
337
346
|
},
|
|
338
347
|
});
|
|
339
348
|
}
|