blume 2.1.0 → 2.1.1
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/CHANGELOG.md +94 -0
- package/dist/cli/{chunk-5cg2rzgf.js → chunk-073rdvgb.js} +19 -48
- package/dist/cli/{chunk-5cg2rzgf.js.map → chunk-073rdvgb.js.map} +4 -5
- package/dist/cli/{chunk-ga0pf4aj.js → chunk-0n9xkp3d.js} +12 -3
- package/dist/cli/{chunk-ga0pf4aj.js.map → chunk-0n9xkp3d.js.map} +3 -3
- package/dist/cli/chunk-150jk7km.js +52 -0
- package/dist/cli/chunk-150jk7km.js.map +10 -0
- package/dist/cli/{chunk-rekqz4qg.js → chunk-313vnn40.js} +16 -15
- package/dist/cli/chunk-313vnn40.js.map +10 -0
- package/dist/cli/{chunk-gp84za0h.js → chunk-3v3ffbrj.js} +2 -2
- package/dist/cli/{chunk-9xr4dgee.js → chunk-3x6x3pph.js} +29 -16
- package/dist/cli/chunk-3x6x3pph.js.map +10 -0
- package/dist/cli/{chunk-0gz36akr.js → chunk-42t4jxdx.js} +21 -5
- package/dist/cli/chunk-42t4jxdx.js.map +12 -0
- package/dist/cli/{chunk-ta27feev.js → chunk-54ztw1nj.js} +21 -2
- package/dist/cli/chunk-54ztw1nj.js.map +10 -0
- package/dist/cli/{chunk-jnwj5kjj.js → chunk-5vjtgs6e.js} +20 -7
- package/dist/cli/{chunk-jnwj5kjj.js.map → chunk-5vjtgs6e.js.map} +3 -3
- package/dist/cli/chunk-66w9j7p6.js +322 -0
- package/dist/cli/chunk-66w9j7p6.js.map +14 -0
- package/dist/cli/{chunk-nd1821sk.js → chunk-7ppf1rva.js} +32 -18
- package/dist/cli/chunk-7ppf1rva.js.map +10 -0
- package/dist/cli/{chunk-p804f3pe.js → chunk-8y2kmjv8.js} +6 -6
- package/dist/cli/{chunk-2f7n4qzt.js → chunk-c1y538kd.js} +1 -1
- package/dist/cli/{chunk-1n228xqb.js → chunk-c887fj0s.js} +2 -2
- package/dist/cli/{chunk-y5h4r2y0.js → chunk-cfyg47zg.js} +15 -5
- package/dist/cli/chunk-cfyg47zg.js.map +10 -0
- package/dist/cli/{chunk-cmtcnh9f.js → chunk-dzwehpy7.js} +2 -2
- package/dist/cli/{chunk-h2a48yhd.js → chunk-e6atmn4h.js} +119 -15
- package/dist/cli/{chunk-h2a48yhd.js.map → chunk-e6atmn4h.js.map} +10 -10
- package/dist/cli/{chunk-5ffywsmb.js → chunk-e8xw9qkh.js} +8 -6
- package/dist/cli/{chunk-5ffywsmb.js.map → chunk-e8xw9qkh.js.map} +3 -3
- package/dist/cli/{chunk-ntv4qrt7.js → chunk-e9q7a2ak.js} +14 -14
- package/dist/cli/{chunk-xkz5xbtv.js → chunk-ehweg4fr.js} +2366 -1114
- package/dist/cli/chunk-ehweg4fr.js.map +218 -0
- package/dist/cli/{chunk-zf748avv.js → chunk-ep6nd0rn.js} +176 -281
- package/dist/cli/{chunk-zf748avv.js.map → chunk-ep6nd0rn.js.map} +11 -12
- package/dist/cli/{chunk-yxmk4pec.js → chunk-fdrkhpjy.js} +16 -15
- package/dist/cli/{chunk-yxmk4pec.js.map → chunk-fdrkhpjy.js.map} +3 -3
- package/dist/cli/{chunk-5wj2qwn2.js → chunk-ft9vfys6.js} +4 -4
- package/dist/cli/{chunk-4h8z9vv5.js → chunk-gb420vvz.js} +89 -46
- package/dist/cli/chunk-gb420vvz.js.map +17 -0
- package/dist/cli/{chunk-713mngdq.js → chunk-hebn0tpx.js} +76 -10
- package/dist/cli/chunk-hebn0tpx.js.map +14 -0
- package/dist/cli/{chunk-2n4ps1b3.js → chunk-htrwgabr.js} +40 -22
- package/dist/cli/chunk-htrwgabr.js.map +15 -0
- package/dist/cli/{chunk-zhvd6adm.js → chunk-hx3xqnk9.js} +13 -13
- package/dist/cli/chunk-j134vrp2.js +65 -0
- package/dist/cli/chunk-j134vrp2.js.map +10 -0
- package/dist/cli/{chunk-y4ykbark.js → chunk-kma9f6sf.js} +2 -2
- package/dist/cli/{chunk-py62fewh.js → chunk-mrcccgnh.js} +3 -3
- package/dist/cli/{chunk-2f3ndkpw.js → chunk-n0v7m8ry.js} +3 -3
- package/dist/cli/{chunk-43zcf4gz.js → chunk-ncj7w596.js} +522 -353
- package/dist/cli/chunk-ncj7w596.js.map +35 -0
- package/dist/cli/{chunk-9geymqns.js → chunk-nq1d20w2.js} +14 -14
- package/dist/cli/{chunk-g2z9x01v.js → chunk-nrns10mj.js} +3 -3
- package/dist/cli/{chunk-e7vvk5ya.js → chunk-sds923fa.js} +3 -3
- package/dist/cli/{chunk-cy5bm0j0.js → chunk-t2m5j4pr.js} +2 -2
- package/dist/cli/{chunk-cy5bm0j0.js.map → chunk-t2m5j4pr.js.map} +1 -1
- package/dist/cli/{chunk-yj17vs39.js → chunk-tdqhyxfz.js} +5 -5
- package/dist/cli/{chunk-q3wxxq39.js → chunk-tphac74f.js} +5 -5
- package/dist/cli/{chunk-6bm0xpgd.js → chunk-w1r4vase.js} +2 -2
- package/dist/cli/index.js +24 -19
- package/dist/cli/index.js.map +3 -3
- package/dist/types/ai/ask-context.d.ts +6 -2
- package/dist/types/ai/component-markdown.d.ts +2 -0
- package/dist/types/ai/openapi-components.d.ts +4 -1
- package/dist/types/astro/integration.d.ts +5 -4
- package/dist/types/astro/markdown-negotiation.d.ts +3 -0
- package/dist/types/astro/pages.d.ts +10 -0
- package/dist/types/cli/init/scaffold.d.ts +1 -1
- package/dist/types/components/content/api-field.d.ts +4 -1
- package/dist/types/components/content/api-page.d.ts +6 -1
- package/dist/types/components/openapi/code-samples.d.ts +29 -0
- package/dist/types/components/openapi/helpers.d.ts +22 -1
- package/dist/types/components/openapi/security.d.ts +10 -0
- package/dist/types/components/openapi/snippets.d.ts +60 -0
- package/dist/types/core/changelog-index.d.ts +8 -0
- package/dist/types/core/config-input.d.ts +30 -3
- package/dist/types/core/data.d.ts +4 -0
- package/dist/types/core/i18n-ui.d.ts +2 -0
- package/dist/types/core/links.d.ts +8 -0
- package/dist/types/core/probe.d.ts +2 -0
- package/dist/types/core/project-graph.d.ts +6 -0
- package/dist/types/core/redirect-patterns.d.ts +11 -3
- package/dist/types/core/schema.d.ts +119 -0
- package/dist/types/core/sources/cache.d.ts +3 -0
- package/dist/types/core/sources/contentful-rich-text.d.ts +11 -2
- package/dist/types/core/sources/contentful.d.ts +17 -1
- package/dist/types/core/sources/lexical.d.ts +5 -0
- package/dist/types/core/sources/normalize.d.ts +20 -1
- package/dist/types/core/sources/remote.d.ts +37 -10
- package/dist/types/core/sources/sanity.d.ts +1 -1
- package/dist/types/core/types.d.ts +17 -0
- package/dist/types/deploy/adapter-output.d.ts +2 -1
- package/dist/types/deploy/adapters/cloudflare.d.ts +7 -5
- package/dist/types/deploy/adapters/node.d.ts +58 -23
- package/dist/types/deploy/adapters/registry.d.ts +10 -0
- package/dist/types/deploy/adapters/types.d.ts +3 -2
- package/dist/types/deploy/platforms/cloudflare.d.ts +10 -7
- package/dist/types/deploy/platforms/node.d.ts +3 -1
- package/dist/types/deploy/platforms/types.d.ts +18 -7
- package/dist/types/deploy/platforms/vercel.d.ts +20 -1
- package/dist/types/deploy/redirects.d.ts +17 -1
- package/dist/types/deploy/sitemap.d.ts +2 -1
- package/dist/types/deploy/vercel-negotiation.d.ts +42 -21
- package/dist/types/openapi/checks.d.ts +27 -0
- package/dist/types/openapi/graphql.d.ts +6 -0
- package/dist/types/openapi/model.d.ts +15 -4
- package/dist/types/openapi/parse.d.ts +3 -0
- package/dist/types/openapi/references.d.ts +16 -0
- package/dist/types/ratelimit/index.d.ts +1 -1
- package/dist/types/ratelimit/schema.d.ts +8 -0
- package/dist/types/ratelimit/upstash.d.ts +39 -14
- package/dist/types/reference/graphql.d.ts +76 -1
- package/dist/types/reference/index.d.ts +1 -1
- package/dist/types/reference/openapi.d.ts +2 -2
- package/dist/types/reference/options.d.ts +37 -1
- package/dist/types/reference/schema.d.ts +84 -0
- package/dist/types/search/adapters/registry.d.ts +3 -0
- package/dist/types/search/adapters/typesense.d.ts +14 -4
- package/dist/types/search/build.d.ts +3 -1
- package/dist/types/search/orama-index.d.ts +9 -9
- package/dist/types/search/sync/algolia.d.ts +5 -2
- package/dist/types/search/sync/index.d.ts +3 -1
- package/dist/types/search/sync/typesense.d.ts +11 -7
- package/dist/types/sources/contentful.d.ts +16 -0
- package/dist/types/sources/github-releases.d.ts +8 -1
- package/dist/types/sources/payload.d.ts +2 -0
- package/dist/types/sources/registry.d.ts +54 -0
- package/dist/types/sources/shared.d.ts +3 -0
- package/dist/types/sources/strapi.d.ts +2 -0
- package/docs/01-quickstart.mdx +4 -2
- package/docs/02-deployment.mdx +31 -13
- package/docs/advanced/changelog.mdx +11 -0
- package/docs/advanced/skills.mdx +1 -1
- package/docs/cli/audit.mdx +30 -7
- package/docs/cli/doctor.mdx +5 -1
- package/docs/cli/evals.mdx +4 -4
- package/docs/cli/translate.mdx +1 -0
- package/docs/cli/validate.mdx +4 -3
- package/docs/configuration/analytics.mdx +1 -1
- package/docs/configuration/assistant.mdx +6 -2
- package/docs/configuration/customization.mdx +4 -2
- package/docs/configuration/export.mdx +5 -3
- package/docs/configuration/index.mdx +3 -2
- package/docs/configuration/rate-limiting.mdx +23 -2
- package/docs/configuration/search.mdx +37 -14
- package/docs/content/components.mdx +2 -2
- package/docs/content/frontmatter.mdx +5 -3
- package/docs/content/i18n.mdx +2 -2
- package/docs/content/includes.mdx +1 -1
- package/docs/content/navigation.mdx +4 -4
- package/docs/content/sources/contentful.mdx +7 -4
- package/docs/content/sources/custom.mdx +7 -3
- package/docs/content/sources/github-releases.mdx +10 -2
- package/docs/content/sources/index.mdx +3 -3
- package/docs/content/sources/notion.mdx +1 -1
- package/docs/content/sources/obsidian.mdx +1 -1
- package/docs/content/sources/payload.mdx +3 -2
- package/docs/content/sources/remote-mdx.mdx +4 -0
- package/docs/content/sources/sanity.mdx +2 -2
- package/docs/content/sources/strapi.mdx +3 -2
- package/docs/content/syntax.mdx +2 -2
- package/docs/discoverability/agent-discovery.mdx +1 -1
- package/docs/discoverability/index.mdx +1 -1
- package/docs/discoverability/json-api.mdx +2 -0
- package/docs/discoverability/llms-txt.mdx +1 -1
- package/docs/discoverability/markdown.mdx +3 -1
- package/docs/discoverability/mcp.mdx +4 -0
- package/docs/discoverability/sitemap-and-robots.mdx +1 -1
- package/docs/references/api-pages.mdx +1 -1
- package/docs/references/asyncapi.mdx +3 -1
- package/docs/references/graphql.mdx +13 -1
- package/docs/references/openapi.mdx +50 -8
- package/package.json +9 -9
- package/skills/blume-migrate/SKILL.md +1 -1
- package/skills/blume-migrate/references/docusaurus.md +4 -4
- package/skills/blume-migrate/references/fumadocs.md +17 -13
- package/skills/blume-migrate/references/nextra.md +8 -5
- package/skills/blume-migrate/references/starlight.md +9 -7
- package/src/ai/ask-context.ts +33 -3
- package/src/ai/ask-limits.ts +13 -0
- package/src/ai/changelog-markdown.ts +11 -6
- package/src/ai/component-markdown.ts +1 -1
- package/src/ai/cors.ts +12 -3
- package/src/ai/mcp/query.ts +14 -6
- package/src/ai/openapi-components.ts +28 -4
- package/src/ai/relative-links.ts +4 -38
- package/src/astro/generate.ts +78 -48
- package/src/astro/integration.ts +16 -13
- package/src/astro/markdown-negotiation.ts +3 -0
- package/src/astro/module-types.ts +3 -1
- package/src/astro/pages.ts +23 -0
- package/src/astro/templates.ts +125 -80
- package/src/audit/catalog.ts +19 -3
- package/src/audit/checks/links.ts +14 -1
- package/src/audit/checks/network.ts +110 -6
- package/src/audit/checks/redirects.ts +6 -4
- package/src/audit/checks/robots.ts +58 -2
- package/src/audit/run.ts +3 -0
- package/src/audit/types.ts +2 -0
- package/src/audit/url.ts +14 -5
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/args.ts +51 -0
- package/src/cli/build-failure.ts +23 -0
- package/src/cli/commands/audit.ts +10 -1
- package/src/cli/commands/dev.ts +18 -2
- package/src/cli/commands/doctor.ts +45 -12
- package/src/cli/commands/eval.ts +36 -4
- package/src/cli/commands/init.ts +6 -4
- package/src/cli/commands/validate.ts +10 -1
- package/src/cli/init/scaffold.ts +3 -3
- package/src/cli/prepare.ts +6 -3
- package/src/cli/required-secrets.ts +1 -1
- package/src/components/content/ApiPlayground.astro +6 -2
- package/src/components/content/api-field.ts +4 -1
- package/src/components/content/api-page.ts +31 -7
- package/src/components/content/mermaid-element.ts +15 -1
- package/src/components/islands/hooks.ts +27 -1
- package/src/components/layout/PageActions.astro +139 -24
- package/src/components/layout/RootLayout.astro +10 -2
- package/src/components/layout/Search.astro +17 -3
- package/src/components/layout/TableOfContents.astro +24 -17
- package/src/components/layout/head-scripts.ts +6 -1
- package/src/components/layout/search/algolia.ts +10 -1
- package/src/components/layout/search/analytics.ts +42 -14
- package/src/components/layout/search/endpoint.ts +48 -5
- package/src/components/layout/search/pagefind.ts +114 -5
- package/src/components/layout/search/types.ts +12 -0
- package/src/components/layout/search/typesense.ts +18 -6
- package/src/components/openapi/AsyncApiOperation.astro +14 -7
- package/src/components/openapi/Authorization.astro +10 -0
- package/src/components/openapi/GraphqlOperation.astro +5 -0
- package/src/components/openapi/Operation.astro +24 -1
- package/src/components/openapi/ParametersTable.astro +8 -4
- package/src/components/openapi/Playground.astro +7 -1
- package/src/components/openapi/RequestBody.astro +19 -2
- package/src/components/openapi/Responses.astro +1 -0
- package/src/components/openapi/SchemaTable.astro +15 -3
- package/src/components/openapi/async-snippets.ts +61 -5
- package/src/components/openapi/async.ts +123 -29
- package/src/components/openapi/graphql-helpers.ts +35 -12
- package/src/components/openapi/helpers.ts +36 -2
- package/src/components/openapi/message-model.ts +54 -11
- package/src/components/openapi/message.ts +14 -4
- package/src/components/openapi/operation-model.ts +16 -7
- package/src/components/openapi/playground-client.ts +24 -5
- package/src/components/openapi/playground-schema.ts +1 -1
- package/src/components/openapi/schema-tree.ts +44 -14
- package/src/components/openapi/security.ts +10 -0
- package/src/components/openapi/snippets.ts +5 -3
- package/src/components/openapi/validate-json.ts +4 -3
- package/src/core/changelog-index.ts +31 -1
- package/src/core/component-diagnostics.ts +53 -0
- package/src/core/component-overrides.ts +32 -0
- package/src/core/config-input.ts +31 -3
- package/src/core/data.ts +2 -0
- package/src/core/diagnostics.ts +5 -0
- package/src/core/i18n-ui.ts +2 -0
- package/src/core/includes.ts +1 -1
- package/src/core/links.ts +47 -17
- package/src/core/probe.ts +26 -6
- package/src/core/project-graph.ts +59 -18
- package/src/core/redirect-patterns.ts +36 -7
- package/src/core/schema.ts +21 -2
- package/src/core/sources/cache.ts +6 -0
- package/src/core/sources/contentful-rich-text.ts +29 -6
- package/src/core/sources/contentful.ts +135 -25
- package/src/core/sources/filesystem.ts +26 -4
- package/src/core/sources/github-releases.ts +12 -1
- package/src/core/sources/lexical.ts +15 -10
- package/src/core/sources/mdx-remote.ts +62 -10
- package/src/core/sources/normalize.ts +227 -41
- package/src/core/sources/notion.ts +7 -1
- package/src/core/sources/obsidian.ts +16 -3
- package/src/core/sources/payload.ts +38 -15
- package/src/core/sources/remote.ts +72 -15
- package/src/core/sources/resolve.ts +11 -1
- package/src/core/sources/sanity.ts +43 -25
- package/src/core/sources/strapi.ts +2 -2
- package/src/core/types.ts +18 -0
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +1 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +1 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +1 -0
- package/src/core/ui-packs/el.ts +1 -0
- package/src/core/ui-packs/es.ts +1 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +1 -0
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +1 -0
- package/src/core/ui-packs/hu.ts +1 -0
- package/src/core/ui-packs/id.ts +1 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +1 -0
- package/src/core/ui-packs/ko.ts +1 -0
- package/src/core/ui-packs/nl.ts +1 -0
- package/src/core/ui-packs/no.ts +1 -0
- package/src/core/ui-packs/pl.ts +1 -0
- package/src/core/ui-packs/pt-br.ts +1 -0
- package/src/core/ui-packs/pt.ts +1 -0
- package/src/core/ui-packs/ro.ts +1 -0
- package/src/core/ui-packs/ru.ts +1 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +1 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +1 -0
- package/src/core/ui-packs/uk.ts +1 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/deploy/adapter-output.ts +7 -2
- package/src/deploy/adapters/cloudflare.ts +7 -5
- package/src/deploy/adapters/node.ts +47 -9
- package/src/deploy/adapters/types.ts +7 -3
- package/src/deploy/artifacts.ts +2 -1
- package/src/deploy/platforms/cloudflare.ts +13 -9
- package/src/deploy/platforms/netlify.ts +2 -1
- package/src/deploy/platforms/node.ts +12 -2
- package/src/deploy/platforms/types.ts +21 -7
- package/src/deploy/platforms/vercel.ts +113 -16
- package/src/deploy/redirects.ts +66 -6
- package/src/deploy/rss.ts +3 -1
- package/src/deploy/sitemap.ts +39 -5
- package/src/deploy/vercel-negotiation.ts +290 -102
- package/src/eval/agents.ts +18 -1
- package/src/eval/prompts.ts +6 -3
- package/src/eval/report.ts +34 -13
- package/src/eval/run.ts +5 -0
- package/src/markdown/relative-links.ts +3 -2
- package/src/openapi/checks.ts +273 -0
- package/src/openapi/graphql-build.ts +21 -0
- package/src/openapi/graphql.ts +11 -0
- package/src/openapi/model.ts +15 -3
- package/src/openapi/parse.ts +127 -2
- package/src/openapi/references.ts +43 -3
- package/src/openapi/source.ts +39 -14
- package/src/ratelimit/index.ts +1 -1
- package/src/ratelimit/runtime.ts +37 -19
- package/src/ratelimit/upstash.ts +40 -10
- package/src/reference/graphql.ts +15 -1
- package/src/reference/index.ts +1 -0
- package/src/reference/openapi.ts +2 -2
- package/src/reference/options.ts +29 -1
- package/src/registry/eject.ts +3 -1
- package/src/search/adapters/locale-scope.ts +30 -0
- package/src/search/adapters/typesense.ts +14 -4
- package/src/search/build.ts +21 -3
- package/src/search/orama-index.ts +163 -31
- package/src/search/source-pages.ts +29 -0
- package/src/search/sync/algolia.ts +99 -9
- package/src/search/sync/index.ts +52 -13
- package/src/search/sync/typesense.ts +72 -27
- package/src/sources/contentful.ts +12 -0
- package/src/sources/github-releases.ts +7 -1
- package/src/sources/shared.ts +3 -0
- package/src/theme/entry.ts +29 -3
- package/src/translate/validate.ts +23 -1
- package/dist/cli/chunk-0gz36akr.js.map +0 -12
- package/dist/cli/chunk-2n4ps1b3.js.map +0 -15
- package/dist/cli/chunk-43zcf4gz.js.map +0 -36
- package/dist/cli/chunk-4ejxbzee.js +0 -39
- package/dist/cli/chunk-4ejxbzee.js.map +0 -10
- package/dist/cli/chunk-4h8z9vv5.js.map +0 -16
- package/dist/cli/chunk-713mngdq.js.map +0 -14
- package/dist/cli/chunk-9xr4dgee.js.map +0 -10
- package/dist/cli/chunk-amrb6hgh.js +0 -145
- package/dist/cli/chunk-amrb6hgh.js.map +0 -11
- package/dist/cli/chunk-nd1821sk.js.map +0 -10
- package/dist/cli/chunk-rekqz4qg.js.map +0 -10
- package/dist/cli/chunk-ta27feev.js.map +0 -10
- package/dist/cli/chunk-xkz5xbtv.js.map +0 -213
- package/dist/cli/chunk-y5h4r2y0.js.map +0 -10
- /package/dist/cli/{chunk-gp84za0h.js.map → chunk-3v3ffbrj.js.map} +0 -0
- /package/dist/cli/{chunk-p804f3pe.js.map → chunk-8y2kmjv8.js.map} +0 -0
- /package/dist/cli/{chunk-2f7n4qzt.js.map → chunk-c1y538kd.js.map} +0 -0
- /package/dist/cli/{chunk-1n228xqb.js.map → chunk-c887fj0s.js.map} +0 -0
- /package/dist/cli/{chunk-cmtcnh9f.js.map → chunk-dzwehpy7.js.map} +0 -0
- /package/dist/cli/{chunk-ntv4qrt7.js.map → chunk-e9q7a2ak.js.map} +0 -0
- /package/dist/cli/{chunk-5wj2qwn2.js.map → chunk-ft9vfys6.js.map} +0 -0
- /package/dist/cli/{chunk-zhvd6adm.js.map → chunk-hx3xqnk9.js.map} +0 -0
- /package/dist/cli/{chunk-y4ykbark.js.map → chunk-kma9f6sf.js.map} +0 -0
- /package/dist/cli/{chunk-py62fewh.js.map → chunk-mrcccgnh.js.map} +0 -0
- /package/dist/cli/{chunk-2f3ndkpw.js.map → chunk-n0v7m8ry.js.map} +0 -0
- /package/dist/cli/{chunk-9geymqns.js.map → chunk-nq1d20w2.js.map} +0 -0
- /package/dist/cli/{chunk-g2z9x01v.js.map → chunk-nrns10mj.js.map} +0 -0
- /package/dist/cli/{chunk-e7vvk5ya.js.map → chunk-sds923fa.js.map} +0 -0
- /package/dist/cli/{chunk-yj17vs39.js.map → chunk-tdqhyxfz.js.map} +0 -0
- /package/dist/cli/{chunk-q3wxxq39.js.map → chunk-tphac74f.js.map} +0 -0
- /package/dist/cli/{chunk-6bm0xpgd.js.map → chunk-w1r4vase.js.map} +0 -0
|
@@ -868,13 +868,20 @@ const localeRedirect = localeTargets
|
|
|
868
868
|
/>
|
|
869
869
|
)
|
|
870
870
|
}
|
|
871
|
+
{/* Below `xl`, where the rail is hidden, the page actions ride in
|
|
872
|
+
the "On this page" dropdown, so it renders wherever the rail
|
|
873
|
+
would, with or without headings. They render once, in the rail:
|
|
874
|
+
the dropdown gets an empty mount the actions script moves them
|
|
875
|
+
into (see PageActions.astro). */}
|
|
871
876
|
{
|
|
872
|
-
|
|
877
|
+
showToc && (
|
|
873
878
|
<TableOfContentsSlot
|
|
874
879
|
headings={tocHeadings}
|
|
875
880
|
title={strings.toc.title}
|
|
876
881
|
variant="mobile"
|
|
877
|
-
|
|
882
|
+
>
|
|
883
|
+
<div data-blume-page-actions-mount />
|
|
884
|
+
</TableOfContentsSlot>
|
|
878
885
|
)
|
|
879
886
|
}
|
|
880
887
|
<PageHeaderSlot headings={tocHeadings} page={page} route={page.route} />
|
|
@@ -911,6 +918,7 @@ const localeRedirect = localeTargets
|
|
|
911
918
|
(apiRail || modeLayout.width === "full") && "[&>*]:max-w-none",
|
|
912
919
|
modeLayout.width === "center" && "[&>*]:max-w-[54rem]",
|
|
913
920
|
]}
|
|
921
|
+
data-blume-page-end
|
|
914
922
|
>
|
|
915
923
|
{directoryGroup && <Directory group={directoryGroup} />}
|
|
916
924
|
<RelatedPages links={relatedItems} title={strings.page.related} />
|
|
@@ -4,6 +4,7 @@ import data from "blume:data";
|
|
|
4
4
|
import { EN_UI } from "../../core/i18n-ui.ts";
|
|
5
5
|
import type { UIStrings } from "../../core/i18n-ui.ts";
|
|
6
6
|
import type { Navigation } from "../../core/types.ts";
|
|
7
|
+
import { scopesLocales } from "../../search/adapters/locale-scope.ts";
|
|
7
8
|
import { scopesVersions } from "../../search/adapters/version-scope.ts";
|
|
8
9
|
import { resolvePopularIconMarkup } from "../../search/popular-icon.ts";
|
|
9
10
|
import Icon from "../Icon.astro";
|
|
@@ -36,9 +37,15 @@ const {
|
|
|
36
37
|
navigation,
|
|
37
38
|
popularPages,
|
|
38
39
|
strings,
|
|
39
|
-
locale,
|
|
40
|
+
locale: viewedLocale,
|
|
40
41
|
version: viewedVersion = null,
|
|
41
42
|
} = Astro.props;
|
|
43
|
+
// Mixedbread searches every language whatever the dialog asks, so on it an
|
|
44
|
+
// "All languages" toggle would change nothing: treat the site as
|
|
45
|
+
// single-language for search instead.
|
|
46
|
+
const locale = scopesLocales(data.config.search.provider)
|
|
47
|
+
? viewedLocale
|
|
48
|
+
: undefined;
|
|
42
49
|
// Pagefind, Orama Cloud, and Mixedbread search every version whatever the
|
|
43
50
|
// dialog asks, so on those an "All versions" toggle would change nothing:
|
|
44
51
|
// treat the site as unversioned for search instead.
|
|
@@ -90,6 +97,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
90
97
|
data-i18n-popular={s.popular}
|
|
91
98
|
data-i18n-results={s.results}
|
|
92
99
|
data-locale={locale || undefined}
|
|
100
|
+
data-redact-queries={data.config.search.analytics.queries ? undefined : ""}
|
|
93
101
|
data-version={version ?? undefined}
|
|
94
102
|
data-versioned={version === null ? undefined : ""}
|
|
95
103
|
>
|
|
@@ -240,6 +248,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
240
248
|
|
|
241
249
|
import { track } from "./analytics-client.ts";
|
|
242
250
|
import { createSearchTracker } from "./search/analytics.ts";
|
|
251
|
+
import type { SearchTracker } from "./search/analytics.ts";
|
|
243
252
|
import { highlight, matchSnippet } from "./search/types.ts";
|
|
244
253
|
import type { SearchFn, SearchHit } from "./search/types.ts";
|
|
245
254
|
|
|
@@ -306,7 +315,8 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
306
315
|
searchClient ??= (async () => {
|
|
307
316
|
try {
|
|
308
317
|
const { createSearch } = await import("blume:search-client");
|
|
309
|
-
|
|
318
|
+
// The reader types each query, so each supersedes the last.
|
|
319
|
+
return await createSearch({ typing: true });
|
|
310
320
|
} catch (error) {
|
|
311
321
|
searchClient = undefined;
|
|
312
322
|
throw error;
|
|
@@ -325,7 +335,7 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
325
335
|
preview!: HTMLElement;
|
|
326
336
|
searchFn: SearchFn | null = null;
|
|
327
337
|
/** Records settled queries and picked results (search/analytics.ts). */
|
|
328
|
-
tracker
|
|
338
|
+
tracker!: SearchTracker;
|
|
329
339
|
loaded = false;
|
|
330
340
|
loadFailed = false;
|
|
331
341
|
/** First-open client/index load still in flight. */
|
|
@@ -419,6 +429,10 @@ const kbd = "rounded border border-border bg-muted px-1 py-0.5 font-mono";
|
|
|
419
429
|
this.message = this.querySelector("[data-blume-search-message]")!;
|
|
420
430
|
this.preview = this.querySelector("[data-blume-search-preview]")!;
|
|
421
431
|
this.assistantEnabled = this.hasAttribute("data-ask");
|
|
432
|
+
// `search.analytics.queries: false` keeps query text off providers.
|
|
433
|
+
this.tracker = createSearchTracker(track, {
|
|
434
|
+
queries: !this.hasAttribute("data-redact-queries"),
|
|
435
|
+
});
|
|
422
436
|
|
|
423
437
|
const popularScript = this.querySelector(
|
|
424
438
|
"[data-blume-search-popular]"
|
|
@@ -15,10 +15,14 @@ interface Props {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const { headings, title, variant } = Astro.props;
|
|
18
|
+
// The mobile dropdown also holds what the layout passes as children (the
|
|
19
|
+
// mount the page actions move into below `xl`, where the rail that renders
|
|
20
|
+
// them is hidden), so a page with actions but no headings still gets it.
|
|
21
|
+
const hasChildren = Astro.slots.has("default");
|
|
18
22
|
---
|
|
19
23
|
|
|
20
24
|
{
|
|
21
|
-
headings.length > 0 && variant === "mobile" && (
|
|
25
|
+
(headings.length > 0 || hasChildren) && variant === "mobile" && (
|
|
22
26
|
<details class="group mx-auto mb-6 max-w-content rounded-blume border border-border xl:hidden">
|
|
23
27
|
<summary class="flex cursor-pointer list-none items-center justify-between gap-2 px-4 py-3 font-medium text-foreground text-sm [&::-webkit-details-marker]:hidden">
|
|
24
28
|
<span>{title}</span>
|
|
@@ -28,23 +32,26 @@ const { headings, title, variant } = Astro.props;
|
|
|
28
32
|
size={16}
|
|
29
33
|
/>
|
|
30
34
|
</summary>
|
|
31
|
-
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<a
|
|
39
|
-
class="block rounded-md px-2 py-1.5 text-muted-foreground text-sm transition-colors hover:text-foreground aria-[current=location]:font-medium aria-[current=location]:text-foreground"
|
|
40
|
-
href={`#${heading.slug}`}
|
|
35
|
+
{headings.length > 0 && (
|
|
36
|
+
<blume-toc class="block">
|
|
37
|
+
<ul class="m-0 list-none border-border border-t p-2">
|
|
38
|
+
{headings.map((heading) => (
|
|
39
|
+
<li
|
|
40
|
+
data-depth={heading.depth}
|
|
41
|
+
style={`padding-inline-start:${(heading.depth - 2) * 0.75}rem`}
|
|
41
42
|
>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
<a
|
|
44
|
+
class="block rounded-md px-2 py-1.5 text-muted-foreground text-sm transition-colors hover:text-foreground aria-[current=location]:font-medium aria-[current=location]:text-foreground"
|
|
45
|
+
href={`#${heading.slug}`}
|
|
46
|
+
>
|
|
47
|
+
{heading.text}
|
|
48
|
+
</a>
|
|
49
|
+
</li>
|
|
50
|
+
))}
|
|
51
|
+
</ul>
|
|
52
|
+
</blume-toc>
|
|
53
|
+
)}
|
|
54
|
+
<slot />
|
|
48
55
|
</details>
|
|
49
56
|
)
|
|
50
57
|
}
|
|
@@ -44,8 +44,13 @@
|
|
|
44
44
|
* stored preference instead of aborting before the swap listeners exist, and
|
|
45
45
|
* after a swap the theme the `before-swap` stamp carried over stands, so a
|
|
46
46
|
* toggle the reader made still holds for the rest of their visit.
|
|
47
|
+
*
|
|
48
|
+
* Paper is white, so a dark page prints light: `beforeprint` switches the root
|
|
49
|
+
* to the light theme (every token, Shiki color, and `dark:` utility follows
|
|
50
|
+
* it) and `afterprint` puts the dark theme back. The listeners sit on
|
|
51
|
+
* `window`, so the one registration outlives client-router swaps.
|
|
47
52
|
*/
|
|
48
|
-
export const THEME_INIT_SCRIPT = `(()=>{const m=document.currentScript?.dataset.mode??"system";const apply=()=>{const r=document.documentElement;let s=null;let blocked=false;try{s=localStorage.getItem("blume-theme");}catch{blocked=true;}const sys=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";r.dataset.theme=s??(blocked&&r.dataset.theme?r.dataset.theme:m==="system"?sys:m);};apply();document.addEventListener("astro:before-swap",(e)=>{const t=document.documentElement.dataset.theme;if(t){e.newDocument.documentElement.dataset.theme=t;}});document.addEventListener("astro:after-swap",apply);})();`;
|
|
53
|
+
export const THEME_INIT_SCRIPT = `(()=>{const m=document.currentScript?.dataset.mode??"system";const apply=()=>{const r=document.documentElement;let s=null;let blocked=false;try{s=localStorage.getItem("blume-theme");}catch{blocked=true;}const sys=matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light";r.dataset.theme=s??(blocked&&r.dataset.theme?r.dataset.theme:m==="system"?sys:m);};apply();document.addEventListener("astro:before-swap",(e)=>{const t=document.documentElement.dataset.theme;if(t){e.newDocument.documentElement.dataset.theme=t;}});document.addEventListener("astro:after-swap",apply);let printed=null;addEventListener("beforeprint",()=>{const r=document.documentElement;if(r.dataset.theme==="dark"){printed="dark";r.dataset.theme="light";}});addEventListener("afterprint",()=>{if(printed){document.documentElement.dataset.theme=printed;printed=null;}});})();`;
|
|
49
54
|
|
|
50
55
|
/**
|
|
51
56
|
* Keep an embedded Scalar reference on Blume's theme. Scalar picks its own
|
|
@@ -48,7 +48,16 @@ export const createSearch = (opts: AlgoliaOptions): SearchFn => {
|
|
|
48
48
|
// shape, so hits returned by that index carry those fields.
|
|
49
49
|
const records =
|
|
50
50
|
first && "hits" in first ? (first.hits as AlgoliaRecord[]) : [];
|
|
51
|
-
|
|
51
|
+
// The sync splits a long page across records that share its `url`. The
|
|
52
|
+
// index's `distinct` returns one of them, unless the site set its own
|
|
53
|
+
// `attributeForDistinct`; either way a page is listed once.
|
|
54
|
+
const pages = new Set<string>();
|
|
55
|
+
const unique = records.filter((record) => {
|
|
56
|
+
const seen = pages.has(record.url);
|
|
57
|
+
pages.add(record.url);
|
|
58
|
+
return !seen;
|
|
59
|
+
});
|
|
60
|
+
const hits = unique.map((record) => ({
|
|
52
61
|
content: record.content ?? "",
|
|
53
62
|
excerpt: highlight(
|
|
54
63
|
excerptFor(record.description ?? "", record.content ?? "", query),
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* result is a `search_select` event with its position, for click-through. The
|
|
10
10
|
* same query settling twice in a row (a filter toggled back and forth) counts
|
|
11
11
|
* once.
|
|
12
|
+
*
|
|
13
|
+
* With `search.analytics.queries: false`, providers get each query's length
|
|
14
|
+
* (`queryChars`) instead of its text, and the text rides only the
|
|
15
|
+
* `blume:track` DOM event, the way the assistant reports questions.
|
|
12
16
|
*/
|
|
13
17
|
import type { TrackProps } from "../analytics-client.ts";
|
|
14
18
|
|
|
@@ -18,8 +22,15 @@ export const MAX_QUERY_CHARS = 100;
|
|
|
18
22
|
/** How long a query sits untouched before it counts as settled. */
|
|
19
23
|
export const SETTLE_MS = 1000;
|
|
20
24
|
|
|
21
|
-
/**
|
|
22
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Sends one analytics event; `local` rides only the `blume:track` DOM event
|
|
27
|
+
* (see `track`).
|
|
28
|
+
*/
|
|
29
|
+
export type SendEvent = (
|
|
30
|
+
event: string,
|
|
31
|
+
props: TrackProps,
|
|
32
|
+
local?: TrackProps
|
|
33
|
+
) => void;
|
|
23
34
|
|
|
24
35
|
/** The timer a tracker waits on; injectable for tests. */
|
|
25
36
|
export interface SettleTimer {
|
|
@@ -27,6 +38,14 @@ export interface SettleTimer {
|
|
|
27
38
|
start: (callback: () => void, ms: number) => number;
|
|
28
39
|
}
|
|
29
40
|
|
|
41
|
+
/** How a tracker reports. */
|
|
42
|
+
export interface SearchTrackerOptions {
|
|
43
|
+
/** Whether providers receive each query's text (`search.analytics.queries`). */
|
|
44
|
+
queries?: boolean;
|
|
45
|
+
/** The timer a query waits on to settle; injectable for tests. */
|
|
46
|
+
timer?: SettleTimer;
|
|
47
|
+
}
|
|
48
|
+
|
|
30
49
|
/** What the dialog tells the tracker. */
|
|
31
50
|
export interface SearchTracker {
|
|
32
51
|
/** The dialog closed; record the query it was showing. */
|
|
@@ -51,23 +70,33 @@ const browserTimer: SettleTimer = {
|
|
|
51
70
|
/** A tracker for one search dialog. */
|
|
52
71
|
export const createSearchTracker = (
|
|
53
72
|
send: SendEvent,
|
|
54
|
-
timer:
|
|
73
|
+
{ queries = true, timer = browserTimer }: SearchTrackerOptions = {}
|
|
55
74
|
): SearchTracker => {
|
|
56
75
|
let pending: PendingSearch | undefined;
|
|
57
76
|
let handle: number | undefined;
|
|
58
77
|
let last: string | undefined;
|
|
59
78
|
|
|
79
|
+
// Providers get the query's text, or only its length, in which case the
|
|
80
|
+
// text goes to the `blume:track` DOM event alone.
|
|
81
|
+
const report = (event: string, props: TrackProps, query: string): void => {
|
|
82
|
+
if (queries) {
|
|
83
|
+
send(event, { ...props, query });
|
|
84
|
+
} else {
|
|
85
|
+
send(event, { ...props, queryChars: query.length }, { query });
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
60
89
|
const flush = (): void => {
|
|
61
90
|
if (handle !== undefined) {
|
|
62
91
|
timer.cancel(handle);
|
|
63
92
|
handle = undefined;
|
|
64
93
|
}
|
|
65
94
|
if (pending && pending.query !== last) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
95
|
+
report(
|
|
96
|
+
"search",
|
|
97
|
+
{ path: location.pathname, results: pending.results },
|
|
98
|
+
pending.query
|
|
99
|
+
);
|
|
71
100
|
last = pending.query;
|
|
72
101
|
}
|
|
73
102
|
pending = undefined;
|
|
@@ -80,12 +109,11 @@ export const createSearchTracker = (
|
|
|
80
109
|
},
|
|
81
110
|
selected: (query, position, url) => {
|
|
82
111
|
flush();
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
position,
|
|
86
|
-
query
|
|
87
|
-
|
|
88
|
-
});
|
|
112
|
+
report(
|
|
113
|
+
"search_select",
|
|
114
|
+
{ path: location.pathname, position, url },
|
|
115
|
+
query.slice(0, MAX_QUERY_CHARS)
|
|
116
|
+
);
|
|
89
117
|
},
|
|
90
118
|
settled: (query, results) => {
|
|
91
119
|
if (handle !== undefined) {
|
|
@@ -1,22 +1,64 @@
|
|
|
1
1
|
import { highlight, SEARCH_LIMIT } from "./types.ts";
|
|
2
|
-
import type { SearchFn, SearchHit } from "./types.ts";
|
|
2
|
+
import type { SearchClientOptions, SearchFn, SearchHit } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How long a typed query waits for the reader to stop typing before it's
|
|
6
|
+
* sent, in milliseconds. The dialog searches on every keystroke, and each
|
|
7
|
+
* request counts toward the endpoint's `rateLimit`.
|
|
8
|
+
*/
|
|
9
|
+
export const SEARCH_DEBOUNCE_MS = 250;
|
|
10
|
+
|
|
11
|
+
/** Resolve after `ms`, or reject with the abort reason once `signal` aborts. */
|
|
12
|
+
const pause = (ms: number, signal: AbortSignal): Promise<void> =>
|
|
13
|
+
// oxlint-disable-next-line promise/avoid-new -- adapt setTimeout and the abort event
|
|
14
|
+
new Promise((resolve, reject) => {
|
|
15
|
+
const timer = setTimeout(resolve, ms);
|
|
16
|
+
signal.addEventListener(
|
|
17
|
+
"abort",
|
|
18
|
+
() => {
|
|
19
|
+
clearTimeout(timer);
|
|
20
|
+
reject(signal.reason);
|
|
21
|
+
},
|
|
22
|
+
{ once: true }
|
|
23
|
+
);
|
|
24
|
+
});
|
|
3
25
|
|
|
4
26
|
/**
|
|
5
27
|
* Server-proxied search (Mixedbread): POST the query to a generated endpoint
|
|
6
28
|
* that holds the secret key and talks to the service. The returned hits carry
|
|
7
29
|
* service-derived text and the dialog injects title/excerpt as HTML, so both
|
|
8
30
|
* are escaped (and query matches marked) here, like every other provider.
|
|
31
|
+
* A failed request rejects, as a hosted provider's does, so the dialog shows
|
|
32
|
+
* its error rather than an empty "no results".
|
|
33
|
+
*
|
|
34
|
+
* With `typing` (the search dialog), a query waits {@link SEARCH_DEBOUNCE_MS}
|
|
35
|
+
* before it's sent, and a newer one supersedes it: the earlier call's wait or
|
|
36
|
+
* request is aborted and it rejects with an `AbortError`. Typing a word then
|
|
37
|
+
* costs one request rather than one per letter, and a slow earlier response
|
|
38
|
+
* can't land after a later one. Without it, every call is sent at once.
|
|
9
39
|
*/
|
|
10
|
-
export const createSearch =
|
|
11
|
-
|
|
12
|
-
|
|
40
|
+
export const createSearch = (
|
|
41
|
+
opts: { api: string } & SearchClientOptions
|
|
42
|
+
): SearchFn => {
|
|
43
|
+
let pending: AbortController | undefined;
|
|
44
|
+
return async (query) => {
|
|
45
|
+
let signal: AbortSignal | undefined;
|
|
46
|
+
if (opts.typing) {
|
|
47
|
+
pending?.abort();
|
|
48
|
+
pending = new AbortController();
|
|
49
|
+
({ signal } = pending);
|
|
50
|
+
await pause(SEARCH_DEBOUNCE_MS, signal);
|
|
51
|
+
}
|
|
13
52
|
const response = await fetch(opts.api, {
|
|
14
53
|
body: JSON.stringify({ query }),
|
|
15
54
|
headers: { "Content-Type": "application/json" },
|
|
16
55
|
method: "POST",
|
|
56
|
+
signal,
|
|
17
57
|
});
|
|
18
58
|
if (!response.ok) {
|
|
19
|
-
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Search failed: ${opts.api} answered ${response.status}.`
|
|
61
|
+
);
|
|
20
62
|
}
|
|
21
63
|
// SAFETY: the endpoint is Blume-generated (`search-endpoint` template) and
|
|
22
64
|
// responds with the SearchHit list it built; title/excerpt are still
|
|
@@ -29,3 +71,4 @@ export const createSearch =
|
|
|
29
71
|
}));
|
|
30
72
|
return { hits, sections: [] };
|
|
31
73
|
};
|
|
74
|
+
};
|
|
@@ -9,14 +9,63 @@ interface PagefindResult {
|
|
|
9
9
|
}>;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/** One Pagefind instance: a page language's index, plus any merged into it. */
|
|
13
|
+
interface PagefindInstance {
|
|
14
|
+
mergeIndex: (
|
|
15
|
+
url: string,
|
|
16
|
+
options: { baseUrl: string; language: string }
|
|
17
|
+
) => Promise<void>;
|
|
13
18
|
search: (query: string) => Promise<{ results: PagefindResult[] }>;
|
|
14
19
|
}
|
|
15
20
|
|
|
21
|
+
interface PagefindModule {
|
|
22
|
+
createInstance: (options: {
|
|
23
|
+
basePath: string;
|
|
24
|
+
baseUrl: string;
|
|
25
|
+
}) => PagefindInstance;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** `pagefind-entry.json`: the bundle's index for each language. */
|
|
29
|
+
interface PagefindEntry {
|
|
30
|
+
languages: Record<string, { page_count: number }>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Pagefind names each page after its built file (`quickstart/index.html`), so
|
|
34
|
+
// its URLs end in a slash. Blume serves pages without one (`trailingSlash:
|
|
35
|
+
// "never"`): hosts redirect the slashed URL, and `blume preview` 404s it.
|
|
36
|
+
const TRAILING_SLASH = /(?<=.)\/(?=[#?]|$)/u;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The language whose index Pagefind loads for a page, picked the way its
|
|
40
|
+
* `findIndex` does: the page's own, else its base language (`pt` for
|
|
41
|
+
* `pt-br`), else the language with the most pages (a locale with no
|
|
42
|
+
* translated pages has no index of its own).
|
|
43
|
+
*/
|
|
44
|
+
const loadedLanguage = (
|
|
45
|
+
languages: PagefindEntry["languages"],
|
|
46
|
+
language: string
|
|
47
|
+
): string | undefined => {
|
|
48
|
+
const [base = language] = language.split("-");
|
|
49
|
+
if (languages[language]) {
|
|
50
|
+
return language;
|
|
51
|
+
}
|
|
52
|
+
if (languages[base]) {
|
|
53
|
+
return base;
|
|
54
|
+
}
|
|
55
|
+
return Object.entries(languages).toSorted(
|
|
56
|
+
([, a], [, b]) => b.page_count - a.page_count
|
|
57
|
+
)[0]?.[0];
|
|
58
|
+
};
|
|
59
|
+
|
|
16
60
|
/**
|
|
17
61
|
* Pagefind: load the index emitted into the built site and query it. The bundle
|
|
18
62
|
* lives in the output (not `node_modules`), so it is imported at runtime by URL
|
|
19
63
|
* — which is why this only works in the production build, not `dev`.
|
|
64
|
+
*
|
|
65
|
+
* Pagefind keeps an index per language and searches the one for the page's
|
|
66
|
+
* `<html lang>`, which Blume sets to the locale the dialog scopes to. A
|
|
67
|
+
* search scoped to a locale is that index alone; a search across every
|
|
68
|
+
* language (no `locale`) merges every other language's index into it.
|
|
20
69
|
*/
|
|
21
70
|
export const createSearch = async (opts: {
|
|
22
71
|
url: string;
|
|
@@ -24,25 +73,85 @@ export const createSearch = async (opts: {
|
|
|
24
73
|
// The pagefind bundle lives in the built site (not node_modules) and is
|
|
25
74
|
// resolved at runtime by URL — it can't be a static, code-splittable path.
|
|
26
75
|
// SAFETY: the URL points at the `pagefind.js` module our own build emitted,
|
|
27
|
-
// whose export contract (`
|
|
76
|
+
// whose export contract (`createInstance()`) is fixed by pagefind.
|
|
28
77
|
// oxlint-disable-next-line react-doctor/no-dynamic-import-path
|
|
29
78
|
const pagefind = (await import(
|
|
30
79
|
/* @vite-ignore */
|
|
31
80
|
opts.url
|
|
32
81
|
)) as PagefindModule;
|
|
82
|
+
// The bundle's folder, which holds the entry file and every language's index.
|
|
83
|
+
const bundle = new URL("./", new URL(opts.url, document.baseURI));
|
|
84
|
+
// `baseUrl` "/" keeps result URLs base-less routes, like every other
|
|
85
|
+
// provider's: by default Pagefind prefixes the folder the bundle is served
|
|
86
|
+
// under, and the dialog adds the deployment base itself.
|
|
87
|
+
const create = (): PagefindInstance =>
|
|
88
|
+
pagefind.createInstance({ basePath: bundle.pathname, baseUrl: "/" });
|
|
89
|
+
|
|
90
|
+
// Every language's index: the page's own, and every other one merged in.
|
|
91
|
+
const everyLanguage = async (language: string): Promise<PagefindInstance> => {
|
|
92
|
+
const instance = create();
|
|
93
|
+
const response = await fetch(new URL("pagefind-entry.json", bundle));
|
|
94
|
+
// SAFETY: the entry file is the one Pagefind wrote into this bundle.
|
|
95
|
+
const { languages } = (await response.json()) as PagefindEntry;
|
|
96
|
+
const own = loadedLanguage(languages, language);
|
|
97
|
+
// Pagefind skips a merge whose path its own `basePath` starts with, as
|
|
98
|
+
// the same index twice, so the bundle is merged by its full URL.
|
|
99
|
+
await Promise.all(
|
|
100
|
+
Object.keys(languages)
|
|
101
|
+
.filter((code) => code !== own)
|
|
102
|
+
.map((code) =>
|
|
103
|
+
instance.mergeIndex(bundle.href, { baseUrl: "/", language: code })
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
return instance;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Pagefind reads the page's language once, when an instance is created. A
|
|
110
|
+
// client-side language switch changes the page's language without a
|
|
111
|
+
// reload, so each language gets its own instance, created on its first
|
|
112
|
+
// search: after the switch, search moves to the new page's index. The
|
|
113
|
+
// every-language instance is one for the whole page load.
|
|
114
|
+
const instances = new Map<string, Promise<PagefindInstance>>();
|
|
115
|
+
const cached = (
|
|
116
|
+
key: string,
|
|
117
|
+
load: () => Promise<PagefindInstance>
|
|
118
|
+
): Promise<PagefindInstance> => {
|
|
119
|
+
const existing = instances.get(key);
|
|
120
|
+
if (existing) {
|
|
121
|
+
return existing;
|
|
122
|
+
}
|
|
123
|
+
const loading = (async () => {
|
|
124
|
+
try {
|
|
125
|
+
return await load();
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// A failed load (the entry file's fetch, say) is forgotten, so the
|
|
128
|
+
// next search retries it.
|
|
129
|
+
instances.delete(key);
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
})();
|
|
133
|
+
instances.set(key, loading);
|
|
134
|
+
return loading;
|
|
135
|
+
};
|
|
136
|
+
|
|
33
137
|
// Pagefind builds its own marked-up excerpt; we keep its `<mark>` highlights
|
|
34
138
|
// (dropping any other markup — the excerpt is rendered via innerHTML) and
|
|
35
139
|
// only highlight the title ourselves. It carries no section/breadcrumb data,
|
|
36
140
|
// so pills stay hidden and the preview pane falls back to the excerpt.
|
|
37
|
-
return async (query) => {
|
|
38
|
-
|
|
141
|
+
return async (query, options) => {
|
|
142
|
+
// Lowercased like Pagefind's own reading, which names the entry's indexes.
|
|
143
|
+
const language = document.documentElement.lang.toLowerCase() || "unknown";
|
|
144
|
+
const instance = await (options?.locale
|
|
145
|
+
? cached(language, () => Promise.resolve(create()))
|
|
146
|
+
: cached("*", () => everyLanguage(language)));
|
|
147
|
+
const response = await instance.search(query);
|
|
39
148
|
const docs = await Promise.all(
|
|
40
149
|
response.results.slice(0, SEARCH_LIMIT).map((result) => result.data())
|
|
41
150
|
);
|
|
42
151
|
const hits = docs.map((doc) => ({
|
|
43
152
|
excerpt: sanitizeExcerpt(doc.excerpt),
|
|
44
153
|
title: highlight(doc.meta?.title ?? doc.url, query),
|
|
45
|
-
url: doc.url,
|
|
154
|
+
url: doc.url.replace(TRAILING_SLASH, ""),
|
|
46
155
|
}));
|
|
47
156
|
return { hits, sections: [] };
|
|
48
157
|
};
|
|
@@ -43,6 +43,18 @@ export type SearchFn = (
|
|
|
43
43
|
}
|
|
44
44
|
) => Promise<SearchResult>;
|
|
45
45
|
|
|
46
|
+
/** What a caller tells `createSearch` (`blume:search-client`) about its queries. */
|
|
47
|
+
export interface SearchClientOptions {
|
|
48
|
+
/**
|
|
49
|
+
* The queries come from a reader typing, as in the search dialog, so each
|
|
50
|
+
* one supersedes the last. The server-proxied client (Mixedbread), whose
|
|
51
|
+
* endpoint is rate-limited, then waits for a pause in typing before it
|
|
52
|
+
* sends a query and aborts the one a newer query supersedes. Other
|
|
53
|
+
* providers ignore it.
|
|
54
|
+
*/
|
|
55
|
+
typing?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
/** A document in the client-loaded `blume-search.json` index. */
|
|
47
59
|
export interface IndexedDocument {
|
|
48
60
|
route: string;
|
|
@@ -17,10 +17,19 @@ interface TypesenseRecord extends Record<string, unknown> {
|
|
|
17
17
|
* key. Documents are imported at build time by the sync step. Every adapter
|
|
18
18
|
* option Blume doesn't read (`connectionTimeoutSeconds`, `numRetries`, …) is
|
|
19
19
|
* the site's own client option and goes to the `Client` untouched; the named
|
|
20
|
-
* connection options still decide `apiKey` and `nodes`.
|
|
20
|
+
* connection options still decide `apiKey` and `nodes`. `locale` is the
|
|
21
|
+
* sync's: Typesense tokenizes a query for the fields it searches.
|
|
21
22
|
*/
|
|
22
23
|
export const createSearch = (opts: TypesenseOptions): SearchFn => {
|
|
23
|
-
const {
|
|
24
|
+
const {
|
|
25
|
+
apiKey,
|
|
26
|
+
collection,
|
|
27
|
+
host,
|
|
28
|
+
locale: _locale,
|
|
29
|
+
port,
|
|
30
|
+
protocol,
|
|
31
|
+
...clientOptions
|
|
32
|
+
} = opts;
|
|
24
33
|
const client = new Client({
|
|
25
34
|
...clientOptions,
|
|
26
35
|
apiKey,
|
|
@@ -35,10 +44,13 @@ export const createSearch = (opts: TypesenseOptions): SearchFn => {
|
|
|
35
44
|
per_page: SEARCH_LIMIT,
|
|
36
45
|
q: query,
|
|
37
46
|
query_by: "title,keywords,description,content",
|
|
38
|
-
// Relevance first,
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
47
|
+
// Relevance first, then each page's `search.boost` among pages that
|
|
48
|
+
// match equally well: a boost settles a tie without floating a page
|
|
49
|
+
// over a better match. Bucketing the relevance instead
|
|
50
|
+
// (`_text_match(buckets: 10)`) ranks by boost alone inside each
|
|
51
|
+
// bucket, so at equal boosts a bucket falls back to indexing order
|
|
52
|
+
// and the best match can land behind weaker ones.
|
|
53
|
+
sort_by: "_text_match:desc,boost:desc",
|
|
42
54
|
// The sync marks `locale` and `version` as facets so hosted results
|
|
43
55
|
// scope to the active language and the viewed docs version (the
|
|
44
56
|
// current docs upload as "current").
|
|
@@ -7,11 +7,12 @@ import { asyncApiOperationObject } from "../../openapi/asyncapi.ts";
|
|
|
7
7
|
import { operationOf, specOf } from "../../openapi/model.ts";
|
|
8
8
|
import { highlightCode } from "../../markdown/index.ts";
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
asyncApiSecurity,
|
|
11
11
|
bindingGroups,
|
|
12
12
|
channelParameters,
|
|
13
|
-
|
|
13
|
+
kafkaTopic,
|
|
14
14
|
messageLabel,
|
|
15
|
+
namedChannelServers,
|
|
15
16
|
operationMessages,
|
|
16
17
|
payloadSchema,
|
|
17
18
|
protocolOf,
|
|
@@ -33,7 +34,6 @@ import PanelTabs from "./PanelTabs.astro";
|
|
|
33
34
|
import ParametersTable from "./ParametersTable.astro";
|
|
34
35
|
import SchemaTable from "./SchemaTable.astro";
|
|
35
36
|
import type { SecuritySchemeLike } from "./security.ts";
|
|
36
|
-
import { resolveAsyncApiSecurity } from "./security.ts";
|
|
37
37
|
import OperationPanel from "./OperationPanel.astro";
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -61,9 +61,11 @@ const schemas = (document.components?.schemas ?? {}) as Record<
|
|
|
61
61
|
string,
|
|
62
62
|
SchemaLike
|
|
63
63
|
>;
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
64
|
+
const namedServers = namedChannelServers(channel, document);
|
|
65
|
+
const servers = namedServers.map(({ server }) => server);
|
|
66
|
+
const security = asyncApiSecurity(
|
|
67
|
+
operation,
|
|
68
|
+
namedServers,
|
|
67
69
|
document.components?.securitySchemes as
|
|
68
70
|
| Record<string, SecuritySchemeLike>
|
|
69
71
|
| undefined
|
|
@@ -96,10 +98,15 @@ const messagePanels = await Promise.all(
|
|
|
96
98
|
|
|
97
99
|
// One derived model feeds the static default sample, the composer form, and
|
|
98
100
|
// (client-side) the live samples + WebSocket frame — they can never drift.
|
|
101
|
+
// A Kafka channel binding's `topic` names the real topic when the address
|
|
102
|
+
// doesn't, so the samples use it.
|
|
103
|
+
const topic =
|
|
104
|
+
protocol === "kafka" ? kafkaTopic(channel, document.components) : undefined;
|
|
99
105
|
const model = ref
|
|
100
106
|
? messageModel({
|
|
101
107
|
action: ref.method === "send" ? "send" : "receive",
|
|
102
|
-
address: ref.path,
|
|
108
|
+
address: topic ?? ref.path,
|
|
109
|
+
components: document.components,
|
|
103
110
|
messages,
|
|
104
111
|
parameters,
|
|
105
112
|
protocol,
|
|
@@ -28,6 +28,13 @@ const { security } = Astro.props;
|
|
|
28
28
|
Optional — this operation also accepts unauthenticated requests.
|
|
29
29
|
</p>
|
|
30
30
|
)}
|
|
31
|
+
{security.unauthenticatedServers &&
|
|
32
|
+
security.unauthenticatedServers.length > 0 && (
|
|
33
|
+
<p class="mb-2 text-muted-foreground text-sm">
|
|
34
|
+
No authorization needed on{" "}
|
|
35
|
+
{security.unauthenticatedServers.join(", ")}.
|
|
36
|
+
</p>
|
|
37
|
+
)}
|
|
31
38
|
{security.alternatives.map((alternative, index) => (
|
|
32
39
|
<>
|
|
33
40
|
{index > 0 && (
|
|
@@ -47,6 +54,9 @@ const { security } = Astro.props;
|
|
|
47
54
|
<span class="text-muted-foreground text-xs">
|
|
48
55
|
{schemeLabel(resolved)}
|
|
49
56
|
{carrier ? ` · ${carrier.in}` : ""}
|
|
57
|
+
{resolved.servers
|
|
58
|
+
? ` · on ${resolved.servers.join(", ")}`
|
|
59
|
+
: ""}
|
|
50
60
|
</span>
|
|
51
61
|
{!security.optional && (
|
|
52
62
|
<span class="font-medium text-[0.625rem] text-red-600 uppercase tracking-wide dark:text-red-400">
|