@jant/core 0.6.16 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +60 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1526 -15
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +926 -73
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +16 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +42 -1
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +139 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +51 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +159 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +23 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +5 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +471 -369
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +178 -17
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post.test.ts +46 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +278 -19
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +221 -6
- package/src/services/post.ts +1021 -152
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1087 -7219
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +87 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language view routing.
|
|
3
|
+
*
|
|
4
|
+
* The path matrix here is the contract for how `/:lang` coexists with every
|
|
5
|
+
* other route group. Hono decides precedence by registration order and cannot
|
|
6
|
+
* "un-match" a route from middleware, so these are the tests that catch a
|
|
7
|
+
* reordering in `app.tsx` before it silently turns `/archive` into a language
|
|
8
|
+
* view — or `/en` into a 404 for a site with an `en` post.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
12
|
+
import { createTestApp } from "../../../__tests__/helpers/app.js";
|
|
13
|
+
import { i18nMiddleware } from "../../../i18n/index.js";
|
|
14
|
+
import { archiveRoutes } from "../archive.js";
|
|
15
|
+
import { collectionRoutes } from "../collection.js";
|
|
16
|
+
import { collectionsPageRoutes } from "../collections.js";
|
|
17
|
+
import { featuredRoutes } from "../featured.js";
|
|
18
|
+
import { homeRoutes } from "../home.js";
|
|
19
|
+
import { languageRoutes } from "../language.js";
|
|
20
|
+
import { latestRoutes } from "../latest.js";
|
|
21
|
+
import { pageRoutes } from "../page.js";
|
|
22
|
+
import { searchRoutes } from "../search.js";
|
|
23
|
+
import { feedRoutes } from "../../feed/feed.js";
|
|
24
|
+
import { publicPostsApiRoutes } from "../../api/public/posts.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Mount the public route groups in the same order `createApp()` does — the
|
|
28
|
+
* order is the behaviour under test.
|
|
29
|
+
*/
|
|
30
|
+
function createLanguageTestApp() {
|
|
31
|
+
const testApp = createTestApp();
|
|
32
|
+
const { app } = testApp;
|
|
33
|
+
|
|
34
|
+
app.use("*", async (c, next) => {
|
|
35
|
+
c.set("publicPath", c.req.path);
|
|
36
|
+
c.set("publicRequestUrl", c.req.url);
|
|
37
|
+
await next();
|
|
38
|
+
});
|
|
39
|
+
// The real middleware, because `<html lang>` is part of what is under test.
|
|
40
|
+
app.use("*", i18nMiddleware());
|
|
41
|
+
|
|
42
|
+
// API routes come first in `createApp()` too, ahead of the catch-all.
|
|
43
|
+
app.route("/api/public/posts", publicPostsApiRoutes);
|
|
44
|
+
app.route("/feed", feedRoutes);
|
|
45
|
+
app.route("/search", searchRoutes);
|
|
46
|
+
app.route("/archive", archiveRoutes);
|
|
47
|
+
app.route("/featured", featuredRoutes);
|
|
48
|
+
app.route("/latest", latestRoutes);
|
|
49
|
+
app.route("/collections", collectionsPageRoutes);
|
|
50
|
+
app.route("/collections", collectionRoutes);
|
|
51
|
+
app.route("/", homeRoutes);
|
|
52
|
+
app.route("/:lang", languageRoutes);
|
|
53
|
+
app.route("/", pageRoutes);
|
|
54
|
+
|
|
55
|
+
return testApp;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function enableMultilingual(
|
|
59
|
+
services: ReturnType<typeof createTestApp>["services"],
|
|
60
|
+
{ primary = "zh-Hans", additional = "en,zh-Hant" } = {},
|
|
61
|
+
) {
|
|
62
|
+
await services.settings.set("SITE_LANGUAGE", primary);
|
|
63
|
+
await services.settings.set("ADDITIONAL_LANGUAGES", additional);
|
|
64
|
+
await services.settings.set("MULTILINGUAL_ENABLED", "true");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function noteBody(text: string): string {
|
|
68
|
+
return JSON.stringify({
|
|
69
|
+
type: "doc",
|
|
70
|
+
content: [{ type: "paragraph", content: [{ type: "text", text }] }],
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe("language view routing", () => {
|
|
75
|
+
describe("path matrix", () => {
|
|
76
|
+
let app: ReturnType<typeof createLanguageTestApp>["app"];
|
|
77
|
+
|
|
78
|
+
beforeEach(async () => {
|
|
79
|
+
const testApp = createLanguageTestApp();
|
|
80
|
+
app = testApp.app;
|
|
81
|
+
await enableMultilingual(testApp.services);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it.each([
|
|
85
|
+
"/",
|
|
86
|
+
"/archive",
|
|
87
|
+
"/archive/feed",
|
|
88
|
+
"/feed",
|
|
89
|
+
"/latest/feed",
|
|
90
|
+
"/featured",
|
|
91
|
+
"/search",
|
|
92
|
+
"/collections",
|
|
93
|
+
])("keeps %s on the root view", async (path) => {
|
|
94
|
+
const res = await app.request(path);
|
|
95
|
+
expect(res.status, path).toBe(200);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it.each([
|
|
99
|
+
"/en",
|
|
100
|
+
"/en/archive",
|
|
101
|
+
"/en/archive/feed",
|
|
102
|
+
"/en/feed",
|
|
103
|
+
"/en/latest/feed",
|
|
104
|
+
"/en/featured",
|
|
105
|
+
"/en/search",
|
|
106
|
+
"/en/collections",
|
|
107
|
+
"/zh-hant",
|
|
108
|
+
"/zh-hant/archive",
|
|
109
|
+
])("serves %s as a language view", async (path) => {
|
|
110
|
+
const res = await app.request(path);
|
|
111
|
+
expect(res.status, path).toBe(200);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it.each(["/fr", "/hello", "/hello/text/med_whatever"])(
|
|
115
|
+
"leaves %s to the path registry",
|
|
116
|
+
async (path) => {
|
|
117
|
+
// Nothing is registered at these paths, so the catch-all 404s — the
|
|
118
|
+
// point is that it is the catch-all answering, not a language view.
|
|
119
|
+
const res = await app.request(path);
|
|
120
|
+
expect(res.status, path).toBe(404);
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("the primary language's own prefix", () => {
|
|
126
|
+
it("redirects to the root it aliases", async () => {
|
|
127
|
+
const { app, services } = createLanguageTestApp();
|
|
128
|
+
await enableMultilingual(services);
|
|
129
|
+
|
|
130
|
+
const res = await app.request("/zh-hans");
|
|
131
|
+
expect(res.status).toBe(301);
|
|
132
|
+
expect(res.headers.get("location")).toBe("/");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("strips the prefix from sub-paths and keeps the query", async () => {
|
|
136
|
+
const { app, services } = createLanguageTestApp();
|
|
137
|
+
await enableMultilingual(services);
|
|
138
|
+
|
|
139
|
+
const res = await app.request("/zh-hans/archive?format=note");
|
|
140
|
+
expect(res.status).toBe(301);
|
|
141
|
+
expect(res.headers.get("location")).toBe("/archive?format=note");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("after multilingual is switched off", () => {
|
|
146
|
+
beforeEach(() => {});
|
|
147
|
+
|
|
148
|
+
it("redirects a configured prefix instead of 404ing", async () => {
|
|
149
|
+
const { app, services } = createLanguageTestApp();
|
|
150
|
+
await enableMultilingual(services);
|
|
151
|
+
await services.settings.set("MULTILINGUAL_ENABLED", "false");
|
|
152
|
+
|
|
153
|
+
const res = await app.request("/en/archive");
|
|
154
|
+
expect(res.status).toBe(301);
|
|
155
|
+
expect(res.headers.get("location")).toBe("/archive");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("keeps feed subscribers working", async () => {
|
|
159
|
+
const { app, services } = createLanguageTestApp();
|
|
160
|
+
await enableMultilingual(services);
|
|
161
|
+
await services.settings.set("MULTILINGUAL_ENABLED", "false");
|
|
162
|
+
|
|
163
|
+
const res = await app.request("/en/feed");
|
|
164
|
+
expect(res.status).toBe(301);
|
|
165
|
+
expect(res.headers.get("location")).toBe("/feed");
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("a site that never configured a second language", () => {
|
|
170
|
+
it("serves a post whose slug looks like a language tag", async () => {
|
|
171
|
+
const { app, services } = createLanguageTestApp();
|
|
172
|
+
await services.posts.create({
|
|
173
|
+
format: "note",
|
|
174
|
+
slug: "en",
|
|
175
|
+
title: "Enderby",
|
|
176
|
+
body: noteBody("A post that happens to live at /en"),
|
|
177
|
+
status: "published",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const res = await app.request("/en");
|
|
181
|
+
expect(res.status).toBe(200);
|
|
182
|
+
expect(await res.text()).toContain("Enderby");
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("language views filter content", () => {
|
|
188
|
+
async function seedTwoLanguages() {
|
|
189
|
+
const testApp = createLanguageTestApp();
|
|
190
|
+
await enableMultilingual(testApp.services, {
|
|
191
|
+
primary: "zh-Hans",
|
|
192
|
+
additional: "en",
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const zh = await testApp.services.posts.create({
|
|
196
|
+
format: "note",
|
|
197
|
+
title: "中文文章",
|
|
198
|
+
body: noteBody("中文正文"),
|
|
199
|
+
language: "zh-Hans",
|
|
200
|
+
status: "published",
|
|
201
|
+
});
|
|
202
|
+
const en = await testApp.services.posts.create({
|
|
203
|
+
format: "note",
|
|
204
|
+
title: "English post",
|
|
205
|
+
body: noteBody("English body"),
|
|
206
|
+
language: "en",
|
|
207
|
+
status: "published",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return { ...testApp, zh, en };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
it("shows only the view's language on its home page", async () => {
|
|
214
|
+
const { app } = await seedTwoLanguages();
|
|
215
|
+
|
|
216
|
+
const html = await (await app.request("/en")).text();
|
|
217
|
+
expect(html).toContain("English post");
|
|
218
|
+
expect(html).not.toContain("中文文章");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("shows the primary language at the root", async () => {
|
|
222
|
+
const { app } = await seedTwoLanguages();
|
|
223
|
+
|
|
224
|
+
const html = await (await app.request("/")).text();
|
|
225
|
+
expect(html).toContain("中文文章");
|
|
226
|
+
expect(html).not.toContain("English post");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("filters the archive", async () => {
|
|
230
|
+
const { app } = await seedTwoLanguages();
|
|
231
|
+
|
|
232
|
+
const html = await (await app.request("/en/archive")).text();
|
|
233
|
+
expect(html).toContain("English post");
|
|
234
|
+
expect(html).not.toContain("中文文章");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("filters the feed and declares the view's language", async () => {
|
|
238
|
+
const { app } = await seedTwoLanguages();
|
|
239
|
+
|
|
240
|
+
const xml = await (await app.request("/en/latest/feed")).text();
|
|
241
|
+
expect(xml).toContain("English post");
|
|
242
|
+
expect(xml).not.toContain("中文文章");
|
|
243
|
+
expect(xml).toContain('xml:lang="en"');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("leaves the root feed on the primary language", async () => {
|
|
247
|
+
const { app } = await seedTwoLanguages();
|
|
248
|
+
|
|
249
|
+
const xml = await (await app.request("/latest/feed")).text();
|
|
250
|
+
expect(xml).toContain('xml:lang="zh-Hans"');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// A directory holding one of a site's feeds has no other way to learn the
|
|
254
|
+
// site publishes a second language: the feed is already filtered to one.
|
|
255
|
+
it("links each language's copy of the feed from every feed", async () => {
|
|
256
|
+
const { app } = await seedTwoLanguages();
|
|
257
|
+
|
|
258
|
+
const xml = await (await app.request("/en/latest/feed")).text();
|
|
259
|
+
|
|
260
|
+
expect(xml).toContain(
|
|
261
|
+
'<link href="http://localhost:3000/latest/feed" rel="alternate" type="application/atom+xml" hreflang="zh-Hans"/>',
|
|
262
|
+
);
|
|
263
|
+
expect(xml).toContain(
|
|
264
|
+
'<link href="http://localhost:3000/en/latest/feed" rel="alternate" type="application/atom+xml" hreflang="en"/>',
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// `x-default` is defined for web pages a search engine ranks. A feed reader
|
|
269
|
+
// has no use for it, and Atom has no notion of a default alternate.
|
|
270
|
+
it("omits x-default from feed alternates", async () => {
|
|
271
|
+
const { app } = await seedTwoLanguages();
|
|
272
|
+
|
|
273
|
+
const xml = await (await app.request("/en/latest/feed")).text();
|
|
274
|
+
|
|
275
|
+
expect(xml).not.toContain("x-default");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("declares Discover in each language's feed, pointing at that language", async () => {
|
|
279
|
+
const { app, services } = await seedTwoLanguages();
|
|
280
|
+
// Discover is opt-in, so the declaration under test only exists once the
|
|
281
|
+
// site has actually asked to be listed.
|
|
282
|
+
await services.settings.set("DISCOVER", "latest");
|
|
283
|
+
|
|
284
|
+
const root = await (await app.request("/latest/feed")).text();
|
|
285
|
+
const english = await (await app.request("/en/latest/feed")).text();
|
|
286
|
+
|
|
287
|
+
// Under `latest` the declaration names the featured feed too, and that
|
|
288
|
+
// one belongs to the same language as the feed carrying it. The status
|
|
289
|
+
// endpoint lives outside language views, so the view travels as `lang`.
|
|
290
|
+
expect(root).toContain(
|
|
291
|
+
'<jant:discover feed="http://localhost:3000/latest/feed" featured="http://localhost:3000/featured/feed" status="http://localhost:3000/api/discover/posts?lang=zh-Hans">latest</jant:discover>',
|
|
292
|
+
);
|
|
293
|
+
expect(english).toContain(
|
|
294
|
+
'<jant:discover feed="http://localhost:3000/en/latest/feed" featured="http://localhost:3000/en/featured/feed" status="http://localhost:3000/api/discover/posts?lang=en">latest</jant:discover>',
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("keeps in-view links inside the view", async () => {
|
|
299
|
+
const { app } = await seedTwoLanguages();
|
|
300
|
+
|
|
301
|
+
const html = await (await app.request("/en/archive")).text();
|
|
302
|
+
// Filter, pagination and feed URLs must not throw the reader back to the
|
|
303
|
+
// root. The language switcher is the one link that leaves on purpose, so
|
|
304
|
+
// this looks at the page body rather than the whole document.
|
|
305
|
+
const body = html.slice(html.indexOf("<main"));
|
|
306
|
+
expect(body).toContain('href="/en/archive');
|
|
307
|
+
expect(body).not.toMatch(/href="\/archive[?"/]/);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("advertises each language's copy of the surface with hreflang", async () => {
|
|
311
|
+
const { app } = await seedTwoLanguages();
|
|
312
|
+
|
|
313
|
+
const html = await (await app.request("/en/archive")).text();
|
|
314
|
+
const head = html.slice(0, html.indexOf("</head>"));
|
|
315
|
+
|
|
316
|
+
expect(head).toContain(
|
|
317
|
+
'<link rel="alternate" hreflang="zh-Hans" href="http://localhost:3000/archive"/>',
|
|
318
|
+
);
|
|
319
|
+
expect(head).toContain(
|
|
320
|
+
'<link rel="alternate" hreflang="en" href="http://localhost:3000/en/archive"/>',
|
|
321
|
+
);
|
|
322
|
+
// A set without a self-referential entry is ignored by search engines, and
|
|
323
|
+
// the renderer used to drop this one for sharing the canonical link's URL.
|
|
324
|
+
expect(head).toContain(
|
|
325
|
+
'<link rel="alternate" hreflang="x-default" href="http://localhost:3000/archive"/>',
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("renders the view language on the html element", async () => {
|
|
330
|
+
const { app } = await seedTwoLanguages();
|
|
331
|
+
|
|
332
|
+
expect(await (await app.request("/en/archive")).text()).toContain(
|
|
333
|
+
'<html lang="en"',
|
|
334
|
+
);
|
|
335
|
+
expect(await (await app.request("/archive")).text()).toContain(
|
|
336
|
+
'<html lang="zh-Hans"',
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("renders a post in its own language whatever view led there", async () => {
|
|
341
|
+
const { app, en } = await seedTwoLanguages();
|
|
342
|
+
|
|
343
|
+
expect(await (await app.request(`/${en.slug}`)).text()).toContain(
|
|
344
|
+
'<html lang="en"',
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("offers a language switcher in the site header", async () => {
|
|
349
|
+
const { app } = await seedTwoLanguages();
|
|
350
|
+
|
|
351
|
+
const html = await (await app.request("/en")).text();
|
|
352
|
+
expect(html).toContain('id="site-nav-lang-trigger"');
|
|
353
|
+
expect(html).toContain('href="/" hreflang="zh-Hans"');
|
|
354
|
+
expect(html).toContain('aria-current="true"');
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("names the language in the header once the reader is off the primary one", async () => {
|
|
358
|
+
const { app, en } = await seedTwoLanguages();
|
|
359
|
+
|
|
360
|
+
// The root is the site as it comes; the globe stands alone there.
|
|
361
|
+
const primary = await (await app.request("/")).text();
|
|
362
|
+
const primaryHeader = primary.slice(primary.indexOf("site-header-lang"));
|
|
363
|
+
expect(primaryHeader).not.toContain("site-header-lang-name");
|
|
364
|
+
expect(primaryHeader).toContain(`aria-label="Language"`);
|
|
365
|
+
|
|
366
|
+
// Every other view says which one it is, on the list surfaces…
|
|
367
|
+
const view = await (await app.request("/en")).text();
|
|
368
|
+
expect(view).toContain('<span class="site-header-lang-name" lang="en">');
|
|
369
|
+
expect(view).toContain(`aria-label="Language: English"`);
|
|
370
|
+
|
|
371
|
+
// …and on a post, whose chrome belongs to the post's own language.
|
|
372
|
+
const post = await (await app.request(`/${en.slug}`)).text();
|
|
373
|
+
expect(post).toContain('<span class="site-header-lang-name" lang="en">');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("repeats the switcher in the mobile drawer, where the header hides it", async () => {
|
|
377
|
+
const { app } = await seedTwoLanguages();
|
|
378
|
+
|
|
379
|
+
const html = await (await app.request("/en")).text();
|
|
380
|
+
const drawer = html.slice(html.indexOf('id="site-nav-drawer"'));
|
|
381
|
+
expect(drawer).toContain('class="site-nav-drawer-section-label">Language');
|
|
382
|
+
expect(drawer).toContain('hreflang="zh-Hans"');
|
|
383
|
+
expect(drawer).toContain('hreflang="en"');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("switches to the translation from a post, not to its own path", async () => {
|
|
387
|
+
const { app, services, zh, en } = await seedTwoLanguages();
|
|
388
|
+
await services.posts.linkTranslation(zh.id, en.id);
|
|
389
|
+
|
|
390
|
+
const html = await (await app.request(`/${zh.slug}`)).text();
|
|
391
|
+
// The English entry points at the translation…
|
|
392
|
+
expect(html).toContain(`href="/${en.slug}" hreflang="en"`);
|
|
393
|
+
// …and the head advertises the whole set, this post included: an hreflang
|
|
394
|
+
// group without a self-referential entry is ignored.
|
|
395
|
+
expect(html).toContain(
|
|
396
|
+
`<link rel="alternate" hreflang="zh-Hans" href="http://localhost:3000/${zh.slug}"/>`,
|
|
397
|
+
);
|
|
398
|
+
expect(html).toContain(
|
|
399
|
+
`<link rel="alternate" hreflang="en" href="http://localhost:3000/${en.slug}"/>`,
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("offers the translation to a reader who cannot read this one", async () => {
|
|
404
|
+
const { app, services, zh, en } = await seedTwoLanguages();
|
|
405
|
+
await services.posts.linkTranslation(zh.id, en.id);
|
|
406
|
+
|
|
407
|
+
const html = await (await app.request(`/${zh.slug}`)).text();
|
|
408
|
+
const start = html.indexOf("data-post-translations");
|
|
409
|
+
const line = html.slice(start, html.indexOf("</p>", start));
|
|
410
|
+
|
|
411
|
+
expect(html).toContain("Also available in");
|
|
412
|
+
expect(line).toContain(`href="/${en.slug}"`);
|
|
413
|
+
expect(line).toContain("English");
|
|
414
|
+
// Its own language is not an alternative to itself.
|
|
415
|
+
expect(line).not.toContain("简体中文");
|
|
416
|
+
// One quiet sentence after the post, not a banner above it.
|
|
417
|
+
expect(html.indexOf("<article")).toBeLessThan(start);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("says nothing on a post with no translations", async () => {
|
|
421
|
+
const { app, zh } = await seedTwoLanguages();
|
|
422
|
+
|
|
423
|
+
expect(await (await app.request(`/${zh.slug}`)).text()).not.toContain(
|
|
424
|
+
"Also available in",
|
|
425
|
+
);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it.each([
|
|
429
|
+
["draft", { status: "draft" as const }],
|
|
430
|
+
["private", { visibility: "private" as const }],
|
|
431
|
+
])("never points a reader at a %s translation", async (_label, patch) => {
|
|
432
|
+
const { app, services, zh, en } = await seedTwoLanguages();
|
|
433
|
+
await services.posts.linkTranslation(zh.id, en.id);
|
|
434
|
+
await services.posts.update(en.id, patch);
|
|
435
|
+
|
|
436
|
+
const html = await (await app.request(`/${zh.slug}`)).text();
|
|
437
|
+
|
|
438
|
+
// Both answer a reader with a 404, so neither the line nor the head
|
|
439
|
+
// may advertise them.
|
|
440
|
+
expect(html).not.toContain("Also available in");
|
|
441
|
+
expect(html).not.toContain('rel="alternate" hreflang=');
|
|
442
|
+
// The switcher still offers English — that language's home, which is
|
|
443
|
+
// always somewhere real.
|
|
444
|
+
expect(html).toContain('href="/en" hreflang="en"');
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("sends a language with no translation to that language's home", async () => {
|
|
448
|
+
const { app, zh } = await seedTwoLanguages();
|
|
449
|
+
|
|
450
|
+
const html = await (await app.request(`/${zh.slug}`)).text();
|
|
451
|
+
expect(html).toContain('href="/en" hreflang="en"');
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("serves each post at one language-neutral address", async () => {
|
|
455
|
+
const { app, en } = await seedTwoLanguages();
|
|
456
|
+
|
|
457
|
+
const direct = await app.request(`/${en.slug}`);
|
|
458
|
+
expect(direct.status).toBe(200);
|
|
459
|
+
|
|
460
|
+
const prefixed = await app.request(`/en/${en.slug}`);
|
|
461
|
+
expect(prefixed.status).toBe(301);
|
|
462
|
+
expect(prefixed.headers.get("location")).toBe(`/${en.slug}`);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("redirects even when the post is in another language", async () => {
|
|
466
|
+
const { app, zh } = await seedTwoLanguages();
|
|
467
|
+
|
|
468
|
+
const res = await app.request(`/en/${zh.slug}`);
|
|
469
|
+
expect(res.status).toBe(301);
|
|
470
|
+
expect(res.headers.get("location")).toBe(`/${zh.slug}`);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe("collections in a language view", () => {
|
|
475
|
+
async function seedCollection() {
|
|
476
|
+
const testApp = createLanguageTestApp();
|
|
477
|
+
await enableMultilingual(testApp.services, {
|
|
478
|
+
primary: "zh-Hans",
|
|
479
|
+
additional: "en",
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const collection = await testApp.services.collections.create({
|
|
483
|
+
slug: "reading",
|
|
484
|
+
title: "Reading",
|
|
485
|
+
});
|
|
486
|
+
const zh = await testApp.services.posts.create({
|
|
487
|
+
format: "note",
|
|
488
|
+
title: "读书笔记",
|
|
489
|
+
body: noteBody("中文正文"),
|
|
490
|
+
language: "zh-Hans",
|
|
491
|
+
status: "published",
|
|
492
|
+
});
|
|
493
|
+
const en = await testApp.services.posts.create({
|
|
494
|
+
format: "note",
|
|
495
|
+
title: "Book log",
|
|
496
|
+
body: noteBody("English body"),
|
|
497
|
+
language: "en",
|
|
498
|
+
status: "published",
|
|
499
|
+
});
|
|
500
|
+
await testApp.services.collections.addThread(collection.id, zh.id);
|
|
501
|
+
await testApp.services.collections.addThread(collection.id, en.id);
|
|
502
|
+
|
|
503
|
+
return { ...testApp, collection, zh, en };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
it("renders the collection page filtered to the view", async () => {
|
|
507
|
+
const { app } = await seedCollection();
|
|
508
|
+
|
|
509
|
+
const res = await app.request("/en/reading");
|
|
510
|
+
expect(res.status).toBe(200);
|
|
511
|
+
const html = await res.text();
|
|
512
|
+
expect(html).toContain("Book log");
|
|
513
|
+
expect(html).not.toContain("读书笔记");
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it("lists every collection in the directory, whatever the view", async () => {
|
|
517
|
+
const { app } = await seedCollection();
|
|
518
|
+
|
|
519
|
+
const html = await (await app.request("/en/collections")).text();
|
|
520
|
+
expect(html).toContain("Reading");
|
|
521
|
+
// The directory is site skeleton, but its links stay in the view.
|
|
522
|
+
expect(html).toContain('href="/en/reading"');
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("filters the collection feed", async () => {
|
|
526
|
+
const { app } = await seedCollection();
|
|
527
|
+
|
|
528
|
+
const xml = await (await app.request("/en/reading/feed")).text();
|
|
529
|
+
expect(xml).toContain("Book log");
|
|
530
|
+
expect(xml).not.toContain("读书笔记");
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("gives an empty language view a way out", async () => {
|
|
534
|
+
const testApp = createLanguageTestApp();
|
|
535
|
+
await enableMultilingual(testApp.services, {
|
|
536
|
+
primary: "zh-Hans",
|
|
537
|
+
additional: "en",
|
|
538
|
+
});
|
|
539
|
+
const collection = await testApp.services.collections.create({
|
|
540
|
+
slug: "reading",
|
|
541
|
+
title: "Reading",
|
|
542
|
+
});
|
|
543
|
+
const zh = await testApp.services.posts.create({
|
|
544
|
+
format: "note",
|
|
545
|
+
title: "读书笔记",
|
|
546
|
+
body: noteBody("中文正文"),
|
|
547
|
+
language: "zh-Hans",
|
|
548
|
+
status: "published",
|
|
549
|
+
});
|
|
550
|
+
await testApp.services.collections.addThread(collection.id, zh.id);
|
|
551
|
+
|
|
552
|
+
const html = await (await testApp.app.request("/en/reading")).text();
|
|
553
|
+
expect(html).toContain("Nothing in English here yet.");
|
|
554
|
+
expect(html).toContain('href="/reading"');
|
|
555
|
+
expect(html).toContain("Read it in 简体中文");
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("keeps the plain empty state when the collection is empty everywhere", async () => {
|
|
559
|
+
const testApp = createLanguageTestApp();
|
|
560
|
+
await enableMultilingual(testApp.services, {
|
|
561
|
+
primary: "zh-Hans",
|
|
562
|
+
additional: "en",
|
|
563
|
+
});
|
|
564
|
+
await testApp.services.collections.create({
|
|
565
|
+
slug: "reading",
|
|
566
|
+
title: "Reading",
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
const html = await (await testApp.app.request("/en/reading")).text();
|
|
570
|
+
expect(html).not.toContain("Nothing in English here yet.");
|
|
571
|
+
expect(html).toContain("This collection is empty.");
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
describe("the public JSON API", () => {
|
|
576
|
+
it("filters by an explicit language tag", async () => {
|
|
577
|
+
const testApp = createLanguageTestApp();
|
|
578
|
+
await enableMultilingual(testApp.services, {
|
|
579
|
+
primary: "zh-Hans",
|
|
580
|
+
additional: "en",
|
|
581
|
+
});
|
|
582
|
+
await testApp.services.posts.create({
|
|
583
|
+
format: "note",
|
|
584
|
+
title: "中文文章",
|
|
585
|
+
body: noteBody("中文正文"),
|
|
586
|
+
language: "zh-Hans",
|
|
587
|
+
status: "published",
|
|
588
|
+
});
|
|
589
|
+
await testApp.services.posts.create({
|
|
590
|
+
format: "note",
|
|
591
|
+
title: "English post",
|
|
592
|
+
body: noteBody("English body"),
|
|
593
|
+
language: "en",
|
|
594
|
+
status: "published",
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
const res = await testApp.app.request("/api/public/posts?lang=en");
|
|
598
|
+
const body = (await res.json()) as {
|
|
599
|
+
posts: { title: string; language: string }[];
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
expect(body.posts).toHaveLength(1);
|
|
603
|
+
expect(body.posts[0]?.title).toBe("English post");
|
|
604
|
+
expect(body.posts[0]?.language).toBe("en");
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it("rejects a malformed language tag rather than ignoring it", async () => {
|
|
608
|
+
const testApp = createLanguageTestApp();
|
|
609
|
+
|
|
610
|
+
const res = await testApp.app.request(
|
|
611
|
+
"/api/public/posts?lang=not%20a%20tag",
|
|
612
|
+
);
|
|
613
|
+
expect(res.status).toBe(400);
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
describe("a page in the navigation", () => {
|
|
618
|
+
async function seedAboutPage({ withTranslation = true } = {}) {
|
|
619
|
+
const testApp = createLanguageTestApp();
|
|
620
|
+
await enableMultilingual(testApp.services, {
|
|
621
|
+
primary: "zh-Hans",
|
|
622
|
+
additional: "en",
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
const zh = await testApp.services.posts.create({
|
|
626
|
+
format: "note",
|
|
627
|
+
title: "关于",
|
|
628
|
+
slug: "about",
|
|
629
|
+
body: noteBody("中文介绍"),
|
|
630
|
+
language: "zh-Hans",
|
|
631
|
+
status: "published",
|
|
632
|
+
visibility: "latest_hidden",
|
|
633
|
+
});
|
|
634
|
+
const en = withTranslation
|
|
635
|
+
? await testApp.services.posts.create({
|
|
636
|
+
format: "note",
|
|
637
|
+
title: "About",
|
|
638
|
+
body: noteBody("English introduction"),
|
|
639
|
+
language: "en",
|
|
640
|
+
status: "published",
|
|
641
|
+
visibility: "latest_hidden",
|
|
642
|
+
translationOfId: zh.id,
|
|
643
|
+
})
|
|
644
|
+
: null;
|
|
645
|
+
await testApp.services.navItems.create({ type: "page", postId: zh.id });
|
|
646
|
+
|
|
647
|
+
return { ...testApp, zh, en };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
it("sends an English reader to the English version", async () => {
|
|
651
|
+
const { app, en } = await seedAboutPage();
|
|
652
|
+
|
|
653
|
+
const html = await (await app.request("/en")).text();
|
|
654
|
+
|
|
655
|
+
expect(html).toContain(`href="/${en?.slug}"`);
|
|
656
|
+
expect(html).toContain(">About<");
|
|
657
|
+
expect(html).not.toContain(`href="/about"`);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it("keeps the primary view on the page written for it", async () => {
|
|
661
|
+
const { app } = await seedAboutPage();
|
|
662
|
+
|
|
663
|
+
const html = await (await app.request("/")).text();
|
|
664
|
+
|
|
665
|
+
expect(html).toContain(`href="/about"`);
|
|
666
|
+
expect(html).toContain(">关于<");
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it("falls back to the one version that exists", async () => {
|
|
670
|
+
const { app } = await seedAboutPage({ withTranslation: false });
|
|
671
|
+
|
|
672
|
+
const html = await (await app.request("/en")).text();
|
|
673
|
+
|
|
674
|
+
// No English version to reach, so the link — and its label, which warns
|
|
675
|
+
// what it leads to — stay in the primary language.
|
|
676
|
+
expect(html).toContain(`href="/about"`);
|
|
677
|
+
expect(html).toContain(">关于<");
|
|
678
|
+
});
|
|
679
|
+
});
|