blume 2.1.0 → 2.1.2
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 +110 -0
- 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-43zcf4gz.js → chunk-0pmb4tr4.js} +541 -368
- package/dist/cli/chunk-0pmb4tr4.js.map +35 -0
- package/dist/cli/{chunk-9geymqns.js → chunk-0w6hs479.js} +15 -15
- package/dist/cli/{chunk-xkz5xbtv.js → chunk-124fvmzq.js} +2802 -1422
- package/dist/cli/chunk-124fvmzq.js.map +220 -0
- package/dist/cli/chunk-150jk7km.js +52 -0
- package/dist/cli/chunk-150jk7km.js.map +10 -0
- package/dist/cli/{chunk-y4ykbark.js → chunk-1f882900.js} +2 -2
- package/dist/cli/{chunk-5ffywsmb.js → chunk-297ajdcv.js} +8 -6
- package/dist/cli/{chunk-5ffywsmb.js.map → chunk-297ajdcv.js.map} +3 -3
- package/dist/cli/{chunk-h2a48yhd.js → chunk-32khwcb6.js} +120 -16
- package/dist/cli/{chunk-h2a48yhd.js.map → chunk-32khwcb6.js.map} +10 -10
- package/dist/cli/{chunk-5cg2rzgf.js → chunk-36bxs9c7.js} +20 -49
- package/dist/cli/{chunk-5cg2rzgf.js.map → chunk-36bxs9c7.js.map} +4 -5
- package/dist/cli/{chunk-py62fewh.js → chunk-53jqw8z0.js} +3 -3
- package/dist/cli/{chunk-5wj2qwn2.js → chunk-58bt3hag.js} +4 -4
- package/dist/cli/{chunk-e7vvk5ya.js → chunk-5cmts6dv.js} +3 -3
- package/dist/cli/{chunk-y5h4r2y0.js → chunk-60hz265r.js} +15 -5
- package/dist/cli/chunk-60hz265r.js.map +10 -0
- package/dist/cli/chunk-74wfhv79.js +65 -0
- package/dist/cli/chunk-74wfhv79.js.map +10 -0
- package/dist/cli/{chunk-g2z9x01v.js → chunk-9qzgg6mj.js} +3 -3
- package/dist/cli/{chunk-q3wxxq39.js → chunk-a57yh0h9.js} +5 -5
- package/dist/cli/{chunk-713mngdq.js → chunk-ave1wcbr.js} +76 -10
- package/dist/cli/chunk-ave1wcbr.js.map +14 -0
- package/dist/cli/{chunk-2n4ps1b3.js → chunk-bqxaqn5p.js} +40 -22
- package/dist/cli/chunk-bqxaqn5p.js.map +15 -0
- package/dist/cli/{chunk-jnwj5kjj.js → chunk-bxwhtvw8.js} +20 -7
- package/dist/cli/{chunk-jnwj5kjj.js.map → chunk-bxwhtvw8.js.map} +3 -3
- package/dist/cli/chunk-ckrcm08h.js +324 -0
- package/dist/cli/chunk-ckrcm08h.js.map +14 -0
- package/dist/cli/{chunk-2f3ndkpw.js → chunk-desvm72z.js} +3 -3
- package/dist/cli/{chunk-gp84za0h.js → chunk-dhwtqnnd.js} +2 -2
- package/dist/cli/{chunk-4h8z9vv5.js → chunk-dpy8qdjf.js} +93 -47
- package/dist/cli/chunk-dpy8qdjf.js.map +17 -0
- package/dist/cli/{chunk-2f7n4qzt.js → chunk-dqhg54mr.js} +1 -1
- package/dist/cli/{chunk-0gz36akr.js → chunk-dv2z0rzf.js} +21 -5
- package/dist/cli/chunk-dv2z0rzf.js.map +12 -0
- package/dist/cli/{chunk-cm89scdv.js → chunk-es8h0v3j.js} +9 -11
- package/dist/cli/{chunk-cm89scdv.js.map → chunk-es8h0v3j.js.map} +3 -3
- package/dist/cli/{chunk-zhvd6adm.js → chunk-gfc5fqvf.js} +14 -14
- package/dist/cli/{chunk-nd1821sk.js → chunk-gxtg6ed2.js} +33 -19
- package/dist/cli/chunk-gxtg6ed2.js.map +10 -0
- package/dist/cli/{chunk-p804f3pe.js → chunk-hvcxng5f.js} +6 -6
- package/dist/cli/{chunk-ta27feev.js → chunk-j1x92mx9.js} +21 -2
- package/dist/cli/chunk-j1x92mx9.js.map +10 -0
- package/dist/cli/{chunk-rekqz4qg.js → chunk-k3gr1nm9.js} +19 -16
- package/dist/cli/chunk-k3gr1nm9.js.map +10 -0
- package/dist/cli/{chunk-1n228xqb.js → chunk-kdpzw38a.js} +2 -2
- package/dist/cli/{chunk-cy5bm0j0.js → chunk-ms9z66nz.js} +2 -2
- package/dist/cli/{chunk-cy5bm0j0.js.map → chunk-ms9z66nz.js.map} +2 -2
- package/dist/cli/{chunk-zf748avv.js → chunk-nbyvgnnm.js} +193 -286
- package/dist/cli/chunk-nbyvgnnm.js.map +36 -0
- package/dist/cli/{chunk-6bm0xpgd.js → chunk-s0bdnc7v.js} +2 -2
- package/dist/cli/{chunk-yxmk4pec.js → chunk-sc93apfs.js} +17 -16
- package/dist/cli/{chunk-yxmk4pec.js.map → chunk-sc93apfs.js.map} +3 -3
- package/dist/cli/{chunk-cmtcnh9f.js → chunk-tfycddbt.js} +2 -2
- package/dist/cli/{chunk-9xr4dgee.js → chunk-twy7psdz.js} +29 -16
- package/dist/cli/chunk-twy7psdz.js.map +10 -0
- package/dist/cli/{chunk-ntv4qrt7.js → chunk-y4msrwm9.js} +15 -15
- package/dist/cli/{chunk-yj17vs39.js → chunk-ydp3065p.js} +5 -5
- 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/ask.d.ts +24 -5
- package/dist/types/ai/component-markdown.d.ts +2 -0
- package/dist/types/ai/openapi-components.d.ts +4 -1
- package/dist/types/analytics/clics.d.ts +37 -0
- package/dist/types/analytics/index.d.ts +2 -0
- package/dist/types/analytics/schema.d.ts +18 -0
- package/dist/types/astro/integration.d.ts +11 -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 +37 -6
- 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/powered-by.d.ts +12 -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 +228 -3
- 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/lower.d.ts +10 -2
- package/dist/types/core/sources/normalize.d.ts +20 -1
- package/dist/types/core/sources/notion.d.ts +8 -0
- 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/cloudflare-negotiation.d.ts +5 -2
- package/dist/types/deploy/node-headers.d.ts +12 -4
- 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 +21 -2
- 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 +44 -21
- package/dist/types/narration/build.d.ts +34 -12
- package/dist/types/narration/provider.d.ts +96 -8
- 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/openapi/sentence.d.ts +11 -1
- 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/seo/jsonld.d.ts +11 -6
- package/dist/types/sources/contentful.d.ts +16 -0
- package/dist/types/sources/github-releases.d.ts +8 -1
- package/dist/types/sources/notion.d.ts +8 -0
- package/dist/types/sources/payload.d.ts +2 -0
- package/dist/types/sources/registry.d.ts +63 -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 +59 -13
- package/docs/03-upgrading.mdx +2 -1
- package/docs/04-migrating.mdx +9 -9
- package/docs/08-faq.mdx +12 -12
- package/docs/advanced/changelog.mdx +30 -2
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/advanced/skills.mdx +1 -1
- package/docs/cli/audit.mdx +30 -7
- package/docs/cli/doctor.mdx +6 -2
- 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 +19 -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 +4 -2
- package/docs/configuration/narration.mdx +43 -6
- 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 +8 -6
- package/docs/content/sources/contentful.mdx +9 -4
- package/docs/content/sources/custom.mdx +7 -3
- package/docs/content/sources/github-releases.mdx +12 -2
- package/docs/content/sources/index.mdx +3 -3
- package/docs/content/sources/notion.mdx +17 -2
- package/docs/content/sources/obsidian.mdx +3 -1
- package/docs/content/sources/payload.mdx +5 -2
- package/docs/content/sources/remote-mdx.mdx +6 -0
- package/docs/content/sources/sanity.mdx +4 -2
- package/docs/content/sources/strapi.mdx +5 -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 +6 -0
- package/docs/discoverability/sitemap-and-robots.mdx +1 -1
- package/docs/discoverability/structured-data.mdx +8 -6
- package/docs/references/api-pages.mdx +1 -1
- package/docs/references/asyncapi.mdx +5 -1
- package/docs/references/graphql.mdx +15 -1
- package/docs/references/openapi.mdx +64 -8
- package/package.json +9 -9
- package/skills/blume/SKILL.md +1 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/assets/{oxfmt@0.67.0.patch → oxfmt@0.71.0.patch} +7 -7
- package/skills/blume-migrate/references/docusaurus.md +4 -4
- package/skills/blume-migrate/references/fumadocs.md +17 -13
- package/skills/blume-migrate/references/monorepo.md +5 -5
- 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/ask.ts +24 -6
- 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/analytics/clics.ts +58 -0
- package/src/analytics/head.ts +5 -0
- package/src/analytics/index.ts +2 -0
- package/src/analytics/schema.ts +2 -0
- package/src/astro/generate.ts +91 -56
- package/src/astro/integration.ts +72 -22
- 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/render-deps.ts +10 -5
- package/src/astro/runtime-deps.ts +23 -2
- package/src/astro/templates.ts +129 -81
- 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 +9 -6
- package/src/cli/prepare.ts +6 -3
- package/src/cli/required-secrets.ts +3 -2
- 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/content/view-switcher.ts +7 -4
- package/src/components/islands/hooks.ts +27 -1
- package/src/components/layout/NavTree.astro +49 -5
- package/src/components/layout/NavTreeScript.astro +18 -0
- package/src/components/layout/PageActions.astro +139 -24
- package/src/components/layout/PageLayout.astro +13 -3
- package/src/components/layout/RootLayout.astro +27 -14
- package/src/components/layout/Search.astro +17 -3
- package/src/components/layout/TableOfContents.astro +24 -17
- package/src/components/layout/analytics-client.ts +8 -8
- 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 +77 -58
- 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/components/powered-by-middleware.ts +33 -0
- 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 +42 -6
- 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/powered-by.ts +12 -0
- 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 +26 -6
- 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/lower.ts +19 -3
- package/src/core/sources/mdx-remote.ts +62 -10
- package/src/core/sources/normalize.ts +227 -41
- package/src/core/sources/notion.ts +34 -10
- 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/cloudflare-negotiation.ts +36 -7
- package/src/deploy/headers.ts +15 -1
- package/src/deploy/node-headers.ts +32 -20
- package/src/deploy/platforms/cloudflare.ts +14 -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 +115 -17
- 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 +325 -103
- 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/base-links.ts +15 -14
- package/src/markdown/package-commands.ts +9 -8
- package/src/markdown/relative-links.ts +3 -2
- package/src/narration/build.ts +218 -40
- package/src/narration/provider.ts +89 -9
- 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/render-mdx.ts +68 -8
- package/src/openapi/sentence.ts +29 -2
- 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 +8 -2
- 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/seo/jsonld.ts +38 -20
- package/src/sources/contentful.ts +12 -0
- package/src/sources/github-releases.ts +7 -1
- package/src/sources/notion.ts +7 -0
- package/src/sources/shared.ts +3 -0
- package/src/theme/color.ts +9 -10
- package/src/theme/entry.ts +43 -6
- 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-zf748avv.js.map +0 -37
- /package/dist/cli/{chunk-9geymqns.js.map → chunk-0w6hs479.js.map} +0 -0
- /package/dist/cli/{chunk-y4ykbark.js.map → chunk-1f882900.js.map} +0 -0
- /package/dist/cli/{chunk-py62fewh.js.map → chunk-53jqw8z0.js.map} +0 -0
- /package/dist/cli/{chunk-5wj2qwn2.js.map → chunk-58bt3hag.js.map} +0 -0
- /package/dist/cli/{chunk-e7vvk5ya.js.map → chunk-5cmts6dv.js.map} +0 -0
- /package/dist/cli/{chunk-g2z9x01v.js.map → chunk-9qzgg6mj.js.map} +0 -0
- /package/dist/cli/{chunk-q3wxxq39.js.map → chunk-a57yh0h9.js.map} +0 -0
- /package/dist/cli/{chunk-2f3ndkpw.js.map → chunk-desvm72z.js.map} +0 -0
- /package/dist/cli/{chunk-gp84za0h.js.map → chunk-dhwtqnnd.js.map} +0 -0
- /package/dist/cli/{chunk-2f7n4qzt.js.map → chunk-dqhg54mr.js.map} +0 -0
- /package/dist/cli/{chunk-zhvd6adm.js.map → chunk-gfc5fqvf.js.map} +0 -0
- /package/dist/cli/{chunk-p804f3pe.js.map → chunk-hvcxng5f.js.map} +0 -0
- /package/dist/cli/{chunk-1n228xqb.js.map → chunk-kdpzw38a.js.map} +0 -0
- /package/dist/cli/{chunk-6bm0xpgd.js.map → chunk-s0bdnc7v.js.map} +0 -0
- /package/dist/cli/{chunk-cmtcnh9f.js.map → chunk-tfycddbt.js.map} +0 -0
- /package/dist/cli/{chunk-ntv4qrt7.js.map → chunk-y4msrwm9.js.map} +0 -0
- /package/dist/cli/{chunk-yj17vs39.js.map → chunk-ydp3065p.js.map} +0 -0
package/src/reference/options.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
import { AUTH_METHODS } from "../components/content/api-page.ts";
|
|
4
|
+
import type { AuthMethod } from "../components/content/api-page.ts";
|
|
3
5
|
import { unrecognizedKeysMessage } from "../core/unrecognized-keys.ts";
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -99,18 +101,44 @@ export interface OpenApiSourceOptions extends ReferenceSourceOptions {
|
|
|
99
101
|
overlays?: string[];
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
/**
|
|
105
|
+
* How a GraphQL endpoint authenticates, in the shape the site's `api.auth`
|
|
106
|
+
* takes for hand-written endpoints: `bearer`, `basic`, or `key` (sent in the
|
|
107
|
+
* `name` header, `x-api-key` by default). A schema, unlike an OpenAPI
|
|
108
|
+
* document, declares no security schemes, so this is what gives the Try it
|
|
109
|
+
* panel its credential field.
|
|
110
|
+
*/
|
|
111
|
+
export const graphqlAuthSchema = z.strictObject({
|
|
112
|
+
method: z.enum(AUTH_METHODS),
|
|
113
|
+
/** The header an API key goes in. Defaults to `x-api-key`. */
|
|
114
|
+
name: z.string().optional(),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/** The `auth` option of `graphql()` and its sources. */
|
|
118
|
+
export interface GraphqlAuthOptions {
|
|
119
|
+
/** `bearer`, `basic`, `key`, or `none`. */
|
|
120
|
+
method: AuthMethod;
|
|
121
|
+
/** The header an API key goes in. Defaults to `x-api-key`. */
|
|
122
|
+
name?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
102
125
|
/**
|
|
103
126
|
* A single GraphQL schema: the shared source shape plus `endpoint`, the live
|
|
104
127
|
* GraphQL API URL the playground and code samples target (a schema, unlike an
|
|
105
|
-
* OpenAPI document, names no server)
|
|
128
|
+
* OpenAPI document, names no server), and `auth`, how that endpoint
|
|
129
|
+
* authenticates.
|
|
106
130
|
*/
|
|
107
131
|
export const graphqlSourceSchema = referenceSourceSchema.extend({
|
|
132
|
+
/** How the endpoint authenticates (playground + code samples). */
|
|
133
|
+
auth: graphqlAuthSchema.optional(),
|
|
108
134
|
/** URL of the live GraphQL endpoint (playground + code samples). */
|
|
109
135
|
endpoint: z.string().optional(),
|
|
110
136
|
});
|
|
111
137
|
|
|
112
138
|
/** One schema source, as `graphql()` accepts it. */
|
|
113
139
|
export interface GraphqlSourceOptions extends ReferenceSourceOptions {
|
|
140
|
+
/** How the endpoint authenticates; wins over the adapter's `auth`. */
|
|
141
|
+
auth?: GraphqlAuthOptions;
|
|
114
142
|
/** URL of the live GraphQL endpoint (playground + code samples); wins over the adapter's `endpoint`. */
|
|
115
143
|
endpoint?: string;
|
|
116
144
|
}
|
package/src/registry/eject.ts
CHANGED
|
@@ -88,6 +88,7 @@ import {
|
|
|
88
88
|
import { buildReferenceFiles } from "../openapi/scalar.ts";
|
|
89
89
|
import { isOpenApiSource } from "../openapi/source.ts";
|
|
90
90
|
import { buildSearchDocuments } from "../search/documents.ts";
|
|
91
|
+
import { sourcePages } from "../search/source-pages.ts";
|
|
91
92
|
import {
|
|
92
93
|
examplesEntryTemplate,
|
|
93
94
|
tailwindEntryTemplate,
|
|
@@ -528,12 +529,15 @@ const examplesPreviewFiles = (
|
|
|
528
529
|
* eject declares the ones its generated files import directly: the AI SDK the
|
|
529
530
|
* assistant route streams through (unless `ai.assistant.endpoint` points
|
|
530
531
|
* elsewhere, when no route is written) and the EPUB generator's browser bundle
|
|
531
|
-
* `features.ts` loads (only named there when `export.epub` is on).
|
|
532
|
+
* `features.ts` loads (only named there when `export.epub` is on). The
|
|
533
|
+
* narration provider's SDK (`openai()`'s `@ai-sdk/openai`) is no runtime
|
|
534
|
+
* import, but the ejected app's `astro build` loads it to generate the clips.
|
|
532
535
|
*/
|
|
533
536
|
const ejectDependencies = (
|
|
534
537
|
options: Parameters<typeof runtimeDependencies>[0]
|
|
535
538
|
): string[] => {
|
|
536
539
|
const { assistant } = options.config.ai;
|
|
540
|
+
const { narration } = options.config;
|
|
537
541
|
return [
|
|
538
542
|
...new Set([
|
|
539
543
|
"astro",
|
|
@@ -544,6 +548,7 @@ const ejectDependencies = (
|
|
|
544
548
|
...runtimeDependencies(options),
|
|
545
549
|
...(options.needsReact ? ["react", "react-dom"] : []),
|
|
546
550
|
...(assistant?.enabled && !assistant.endpoint ? ["ai"] : []),
|
|
551
|
+
...(narration.enabled ? (narration.provider?.runtimeDeps ?? []) : []),
|
|
547
552
|
...(options.config.export.epub ? ["epub-gen-memory"] : []),
|
|
548
553
|
]),
|
|
549
554
|
];
|
|
@@ -755,7 +760,7 @@ export const eject = async (
|
|
|
755
760
|
// the package is hoisted out of the project's own node_modules.
|
|
756
761
|
sources: [
|
|
757
762
|
blumeSourceGlob(root, genDir),
|
|
758
|
-
"../../**/*.{astro,mdx,ts,tsx}",
|
|
763
|
+
"../../**/*.{astro,jsx,mdx,ts,tsx}",
|
|
759
764
|
],
|
|
760
765
|
twoslashCss: twoslashCss(),
|
|
761
766
|
userTheme,
|
|
@@ -914,6 +919,7 @@ export const eject = async (
|
|
|
914
919
|
files.push({
|
|
915
920
|
content: mixedbreadSearchEndpointTemplate(
|
|
916
921
|
searchAdapter.options,
|
|
922
|
+
sourcePages(project),
|
|
917
923
|
config.rateLimit
|
|
918
924
|
),
|
|
919
925
|
path: join(srcDir, "pages", "api", "search.ts"),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SearchAdapterKind } from "./registry.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether each adapter's client can limit results to one language. The
|
|
5
|
+
* source-built indexes (Orama, FlexSearch) store every document's `locale`,
|
|
6
|
+
* the Algolia, Orama Cloud, and Typesense syncs upload it for their clients
|
|
7
|
+
* to filter on, and Pagefind keeps an index per language, merging the others
|
|
8
|
+
* in to search them all. The Mixedbread endpoint takes no filter, so it
|
|
9
|
+
* always searches every language. Keyed by every kind so a new adapter has
|
|
10
|
+
* to decide.
|
|
11
|
+
*/
|
|
12
|
+
const SCOPES_LOCALES = {
|
|
13
|
+
algolia: true,
|
|
14
|
+
flexsearch: true,
|
|
15
|
+
mixedbread: false,
|
|
16
|
+
none: false,
|
|
17
|
+
orama: true,
|
|
18
|
+
"orama-cloud": true,
|
|
19
|
+
pagefind: true,
|
|
20
|
+
typesense: true,
|
|
21
|
+
} satisfies Record<SearchAdapterKind, boolean>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Whether the configured search adapter scopes results to the page's
|
|
25
|
+
* language. The search dialog offers its "All languages" toggle only when it
|
|
26
|
+
* does: on the others the toggle would change nothing, and every query
|
|
27
|
+
* already spans all languages.
|
|
28
|
+
*/
|
|
29
|
+
export const scopesLocales = (kind: SearchAdapterKind): boolean =>
|
|
30
|
+
SCOPES_LOCALES[kind];
|
|
@@ -6,14 +6,22 @@ import type { SearchAdapter } from "./types.ts";
|
|
|
6
6
|
/**
|
|
7
7
|
* Connection details for a self-hosted or cloud Typesense node. The browser
|
|
8
8
|
* queries the collection with the search-only `apiKey`; the build-time sync
|
|
9
|
-
*
|
|
10
|
-
* the config.
|
|
9
|
+
* rebuilds it with `TYPESENSE_ADMIN_API_KEY`, which never enters the config.
|
|
11
10
|
*/
|
|
12
11
|
export interface TypesenseNamedOptions {
|
|
13
12
|
/** The search-only API key (public — it ships to the browser). */
|
|
14
13
|
apiKey: string;
|
|
14
|
+
/** The alias the sync points at each new collection, and searches read. */
|
|
15
15
|
collection: string;
|
|
16
16
|
host: string;
|
|
17
|
+
/**
|
|
18
|
+
* The language Typesense tokenizes the text fields for, as a two-letter
|
|
19
|
+
* code (`ja`, `zh`, `ko`, `th`, …). Typesense's default tokenizer splits on
|
|
20
|
+
* spaces, so text in a script written without them — Chinese, Japanese,
|
|
21
|
+
* Thai — only matches from the start of a run. Omitted, the fields keep
|
|
22
|
+
* that default.
|
|
23
|
+
*/
|
|
24
|
+
locale?: string;
|
|
17
25
|
/** Defaults to 443. */
|
|
18
26
|
port?: number;
|
|
19
27
|
/** Defaults to `https`. */
|
|
@@ -39,14 +47,16 @@ export const typesenseOptionsSchema = z
|
|
|
39
47
|
apiKey: z.string(),
|
|
40
48
|
collection: z.string(),
|
|
41
49
|
host: z.string(),
|
|
50
|
+
locale: z.string().optional(),
|
|
42
51
|
port: z.number().int().positive().optional(),
|
|
43
52
|
protocol: z.enum(["http", "https"]).optional(),
|
|
44
53
|
})
|
|
45
54
|
.catchall(z.json());
|
|
46
55
|
|
|
47
56
|
/**
|
|
48
|
-
* Hosted search on Typesense. Each `blume build`
|
|
49
|
-
* collection
|
|
57
|
+
* Hosted search on Typesense. Each `blume build` imports into a new
|
|
58
|
+
* collection and swaps the `collection` alias onto it, so deleted or renamed
|
|
59
|
+
* pages don't linger as stale hits and searches never read a partial index.
|
|
50
60
|
*/
|
|
51
61
|
export const typesense = (options: TypesenseOptions): TypesenseAdapter => ({
|
|
52
62
|
kind: "typesense",
|
package/src/search/build.ts
CHANGED
|
@@ -3,6 +3,11 @@ import { mkdir, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { close, createIndex } from "pagefind";
|
|
4
4
|
import { dirname, join } from "pathe";
|
|
5
5
|
|
|
6
|
+
/** The part of `pagefind-entry.json` that counts the index's pages. */
|
|
7
|
+
interface PagefindEntry {
|
|
8
|
+
languages: Record<string, { page_count: number }>;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
/**
|
|
7
12
|
* Build a local Pagefind search index over the built site. Pagefind indexes
|
|
8
13
|
* every rendered page except those whose `<html>` carries
|
|
@@ -23,7 +28,9 @@ import { dirname, join } from "pathe";
|
|
|
23
28
|
* Pagefind/pagefind#1272; switch back to `writeFiles()` once a release ships
|
|
24
29
|
* that fix.
|
|
25
30
|
*
|
|
26
|
-
* Returns the number of pages indexed
|
|
31
|
+
* Returns the number of pages indexed: the index's own count, summed over its
|
|
32
|
+
* languages from `pagefind-entry.json`. `addDirectory`'s `page_count` counts
|
|
33
|
+
* every HTML file it read, including the ones kept out of the index.
|
|
27
34
|
*/
|
|
28
35
|
export const buildSearchIndex = async (outDir: string): Promise<number> => {
|
|
29
36
|
const { index } = await createIndex({});
|
|
@@ -34,7 +41,7 @@ export const buildSearchIndex = async (outDir: string): Promise<number> => {
|
|
|
34
41
|
// These awaits are strictly ordered, not independent: the directory must be
|
|
35
42
|
// indexed before its files are read, and the index closed only after.
|
|
36
43
|
// oxlint-disable-next-line react-doctor/async-parallel
|
|
37
|
-
|
|
44
|
+
await index.addDirectory({ path: outDir });
|
|
38
45
|
const { files } = await index.getFiles();
|
|
39
46
|
await close();
|
|
40
47
|
|
|
@@ -49,5 +56,16 @@ export const buildSearchIndex = async (outDir: string): Promise<number> => {
|
|
|
49
56
|
files.map((file) => writeFile(join(searchDir, file.path), file.content))
|
|
50
57
|
);
|
|
51
58
|
|
|
52
|
-
|
|
59
|
+
// Pagefind always writes the entry file, listing no languages when nothing
|
|
60
|
+
// was indexed.
|
|
61
|
+
const entry = files.find((file) => file.path === "pagefind-entry.json");
|
|
62
|
+
// SAFETY: the entry file is Pagefind's JSON, which counts each language's
|
|
63
|
+
// pages in `page_count`.
|
|
64
|
+
const { languages } = JSON.parse(
|
|
65
|
+
new TextDecoder().decode(entry?.content)
|
|
66
|
+
) as PagefindEntry;
|
|
67
|
+
return Object.values(languages).reduce(
|
|
68
|
+
(sum, language) => sum + language.page_count,
|
|
69
|
+
0
|
|
70
|
+
);
|
|
53
71
|
};
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
components,
|
|
3
|
+
create,
|
|
4
|
+
insert,
|
|
5
|
+
insertMultiple,
|
|
6
|
+
search,
|
|
7
|
+
} from "@orama/orama";
|
|
2
8
|
import type {
|
|
3
9
|
AnyOrama,
|
|
4
10
|
EnumArrComparisonOperator,
|
|
5
11
|
EnumComparisonOperator,
|
|
12
|
+
Result,
|
|
13
|
+
SearchParamsFullText,
|
|
6
14
|
Tokenizer,
|
|
7
15
|
} from "@orama/orama";
|
|
8
16
|
|
|
@@ -314,32 +322,89 @@ const segmentingTokenizer = (locale?: string): Tokenizer | undefined => {
|
|
|
314
322
|
};
|
|
315
323
|
};
|
|
316
324
|
|
|
325
|
+
/**
|
|
326
|
+
* On a Latin-default index holding pages in non-Latin locales, the tokenizer
|
|
327
|
+
* each of those locales' pages was indexed with, keyed by locale code. A query
|
|
328
|
+
* reads it to tokenize its term the way the pages it searches were.
|
|
329
|
+
*/
|
|
330
|
+
const translationTokenizers = new WeakMap<AnyOrama, Map<string, Tokenizer>>();
|
|
331
|
+
|
|
332
|
+
/** A segmenting tokenizer for each document locale Orama's can't serve. */
|
|
333
|
+
const localeTokenizers = (documents: OramaDoc[]): Map<string, Tokenizer> => {
|
|
334
|
+
const tokenizers = new Map<string, Tokenizer>();
|
|
335
|
+
for (const code of new Set(documents.map((doc) => doc.locale ?? ""))) {
|
|
336
|
+
const tokenizer = segmentingTokenizer(code);
|
|
337
|
+
if (tokenizer) {
|
|
338
|
+
tokenizers.set(code, tokenizer);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return tokenizers;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* One tokenizer that hands each text to its locale's: Orama passes the
|
|
346
|
+
* language given to `insert` and `search` through to `tokenize`, so a page
|
|
347
|
+
* inserted under `ja` and a query searched under `ja` both reach the Japanese
|
|
348
|
+
* tokenizer, and everything else reaches Orama's own, exactly as on an index
|
|
349
|
+
* with no translations.
|
|
350
|
+
*/
|
|
351
|
+
const dispatchingTokenizer = (
|
|
352
|
+
tokenizers: Map<string, Tokenizer>
|
|
353
|
+
): Tokenizer => {
|
|
354
|
+
const standard = components.tokenizer.createTokenizer();
|
|
355
|
+
return {
|
|
356
|
+
language: standard.language,
|
|
357
|
+
normalizationCache: standard.normalizationCache,
|
|
358
|
+
tokenize: (raw, language, prop, withCache) =>
|
|
359
|
+
// Orama's tokenizer rejects a language other than its own, so the
|
|
360
|
+
// locale that picked it is not passed on.
|
|
361
|
+
(tokenizers.get(language ?? "") ?? standard).tokenize(
|
|
362
|
+
raw,
|
|
363
|
+
undefined,
|
|
364
|
+
prop,
|
|
365
|
+
withCache
|
|
366
|
+
),
|
|
367
|
+
};
|
|
368
|
+
};
|
|
369
|
+
|
|
317
370
|
/**
|
|
318
371
|
* Build an in-memory Orama full-text index from search documents. Shared by the
|
|
319
372
|
* Orama client loader (browser), the MCP server, and assistant grounding (Node),
|
|
320
373
|
* so ranking is identical wherever docs are queried. `locale` — the site's
|
|
321
374
|
* `i18n.defaultLocale` — swaps in a word-segmenting tokenizer for every
|
|
322
375
|
* non-Latin script, all of which Orama's default tokenizer reduces to zero
|
|
323
|
-
* tokens
|
|
324
|
-
* applies to every document. That is safe in one direction only: Latin words
|
|
376
|
+
* tokens. A non-Latin default's tokenizer serves every page: Latin words
|
|
325
377
|
* survive segmentation intact, so English pages on a segmented index stay
|
|
326
|
-
* searchable
|
|
327
|
-
*
|
|
378
|
+
* searchable. A Latin default keeps Orama's tokenizer for its Latin-script
|
|
379
|
+
* pages, and each page in a non-Latin locale (the Japanese and Hindi
|
|
380
|
+
* translations of an English site) is indexed with its own locale's tokenizer.
|
|
328
381
|
*/
|
|
329
382
|
export const buildOramaIndex = async (
|
|
330
383
|
documents: OramaDoc[],
|
|
331
384
|
locale?: string
|
|
332
385
|
): Promise<AnyOrama> => {
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
? create({ components: { tokenizer }, schema: SCHEMA })
|
|
336
|
-
: create({ schema: SCHEMA });
|
|
337
|
-
await insertMultiple(
|
|
338
|
-
db,
|
|
339
|
-
documents.map((doc) =>
|
|
340
|
-
doc.facets ? { ...doc, facetTerms: toFacetTerms(doc.facets) } : doc
|
|
341
|
-
)
|
|
386
|
+
const rows = documents.map((doc) =>
|
|
387
|
+
doc.facets ? { ...doc, facetTerms: toFacetTerms(doc.facets) } : doc
|
|
342
388
|
);
|
|
389
|
+
const tokenizer = segmentingTokenizer(locale);
|
|
390
|
+
const translations = tokenizer ? undefined : localeTokenizers(documents);
|
|
391
|
+
let db: AnyOrama;
|
|
392
|
+
if (translations && translations.size > 0) {
|
|
393
|
+
db = create({
|
|
394
|
+
components: { tokenizer: dispatchingTokenizer(translations) },
|
|
395
|
+
schema: SCHEMA,
|
|
396
|
+
});
|
|
397
|
+
for (const row of rows) {
|
|
398
|
+
// oxlint-disable-next-line no-await-in-loop -- one at a time, in order, so each page is tokenized under its own locale and ties still rank in document order.
|
|
399
|
+
await insert(db, row, row.locale);
|
|
400
|
+
}
|
|
401
|
+
translationTokenizers.set(db, translations);
|
|
402
|
+
} else {
|
|
403
|
+
db = tokenizer
|
|
404
|
+
? create({ components: { tokenizer }, schema: SCHEMA })
|
|
405
|
+
: create({ schema: SCHEMA });
|
|
406
|
+
await insertMultiple(db, rows);
|
|
407
|
+
}
|
|
343
408
|
if (documents.some((doc) => (doc.boost ?? 1) !== 1)) {
|
|
344
409
|
boostedDatabases.add(db);
|
|
345
410
|
}
|
|
@@ -379,17 +444,56 @@ interface OramaWhereClause {
|
|
|
379
444
|
| undefined;
|
|
380
445
|
}
|
|
381
446
|
|
|
447
|
+
/** A ranked match, scored and carrying its document. */
|
|
448
|
+
type Hit = Result<OramaDoc>;
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* One search, with the term tokenized under `language`. On a bigrammed
|
|
452
|
+
* tokenizer the strict pass runs first: a term is only meant to match where
|
|
453
|
+
* its bigrams sit together, and scoring them independently lets a page
|
|
454
|
+
* sharing a couple of windows outrank the page the term is about. Terms
|
|
455
|
+
* spanning several words rarely appear in full on one page, so an empty
|
|
456
|
+
* strict result falls back to the default pass rather than reporting no
|
|
457
|
+
* matches.
|
|
458
|
+
*/
|
|
459
|
+
const rankedHits = async (
|
|
460
|
+
db: AnyOrama,
|
|
461
|
+
params: SearchParamsFullText<AnyOrama, OramaDoc>,
|
|
462
|
+
language: string | undefined,
|
|
463
|
+
bigrammed: boolean
|
|
464
|
+
): Promise<Hit[]> => {
|
|
465
|
+
// The result-document generic is OramaDoc because `buildOramaIndex` is the
|
|
466
|
+
// only writer to this database and inserts OramaDoc records (plus the
|
|
467
|
+
// derived `facetTerms`).
|
|
468
|
+
const strict = bigrammed
|
|
469
|
+
? await search<AnyOrama, OramaDoc>(
|
|
470
|
+
db,
|
|
471
|
+
{ ...params, threshold: ALL_TOKENS },
|
|
472
|
+
language
|
|
473
|
+
)
|
|
474
|
+
: undefined;
|
|
475
|
+
const found =
|
|
476
|
+
strict && strict.hits.length > 0
|
|
477
|
+
? strict
|
|
478
|
+
: await search<AnyOrama, OramaDoc>(db, params, language);
|
|
479
|
+
return found.hits;
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
/** A hit's rank across searches: its relevance times the page's boost. */
|
|
483
|
+
const boostedScore = (hit: Hit): number =>
|
|
484
|
+
hit.score * (hit.document.boost ?? 1);
|
|
485
|
+
|
|
382
486
|
/**
|
|
383
487
|
* Query the index, returning the matching documents (highest-ranked first).
|
|
384
488
|
* `filters` narrows results by exact `where` matches on the enum fields:
|
|
385
489
|
* `locale` to one language, `contentTypes` to a set of page types, `facets`
|
|
386
490
|
* to documents carrying every requested `key:value` term.
|
|
387
491
|
*
|
|
388
|
-
* On
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
492
|
+
* On an index whose translations have tokenizers of their own, a query scoped
|
|
493
|
+
* to a locale is tokenized with that locale's tokenizer. An unscoped query
|
|
494
|
+
* searches each locale's pages with its own tokenizer, and the Latin-script
|
|
495
|
+
* pages with Orama's, then merges the matches by score: every search runs over
|
|
496
|
+
* the one index, so their scores share its term statistics.
|
|
393
497
|
*/
|
|
394
498
|
export const queryOramaIndex = async (
|
|
395
499
|
db: AnyOrama,
|
|
@@ -424,16 +528,44 @@ export const queryOramaIndex = async (
|
|
|
424
528
|
};
|
|
425
529
|
const params =
|
|
426
530
|
Object.keys(where).length > 0 ? { ...unfiltered, where } : unfiltered;
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
531
|
+
const translations = translationTokenizers.get(db);
|
|
532
|
+
if (!translations) {
|
|
533
|
+
const bigrammed = isBigramLanguage(db.tokenizer?.language ?? "");
|
|
534
|
+
const hits = await rankedHits(db, params, undefined, bigrammed);
|
|
535
|
+
return hits.map((hit) => hit.document);
|
|
536
|
+
}
|
|
537
|
+
if (filters?.locale) {
|
|
538
|
+
const { language = "" } = translations.get(filters.locale) ?? {};
|
|
539
|
+
const bigrammed = isBigramLanguage(language);
|
|
540
|
+
const hits = await rankedHits(db, params, filters.locale, bigrammed);
|
|
541
|
+
return hits.map((hit) => hit.document);
|
|
542
|
+
}
|
|
543
|
+
// `nin` matches only pages with a locale, which every Blume-built index
|
|
544
|
+
// gives each page. (Orama's `not` would admit locale-less pages, but it
|
|
545
|
+
// drops the sibling filters beside it.)
|
|
546
|
+
const searches = [
|
|
547
|
+
rankedHits(
|
|
548
|
+
db,
|
|
549
|
+
{
|
|
550
|
+
...params,
|
|
551
|
+
where: { ...where, locale: { nin: [...translations.keys()] } },
|
|
552
|
+
},
|
|
553
|
+
undefined,
|
|
554
|
+
false
|
|
555
|
+
),
|
|
556
|
+
...[...translations].map(([code, tokenizer]) =>
|
|
557
|
+
rankedHits(
|
|
558
|
+
db,
|
|
559
|
+
{ ...params, where: { ...where, locale: { eq: code } } },
|
|
560
|
+
code,
|
|
561
|
+
isBigramLanguage(tokenizer.language)
|
|
562
|
+
)
|
|
563
|
+
),
|
|
564
|
+
];
|
|
565
|
+
const found = await Promise.all(searches);
|
|
566
|
+
return found
|
|
567
|
+
.flat()
|
|
568
|
+
.toSorted((a, b) => boostedScore(b) - boostedScore(a))
|
|
569
|
+
.slice(0, limit)
|
|
570
|
+
.map((hit) => hit.document);
|
|
439
571
|
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { relative } from "pathe";
|
|
2
|
+
|
|
3
|
+
import type { BlumeProject } from "../core/project-graph.ts";
|
|
4
|
+
|
|
5
|
+
/** Where a page is served, and its title: what a search hit links to. */
|
|
6
|
+
export interface SourcePage {
|
|
7
|
+
title: string;
|
|
8
|
+
url: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Each search-indexable page's source file, relative to the project root,
|
|
13
|
+
* paired with the page it renders. The Mixedbread endpoint links a hit to its
|
|
14
|
+
* page through the path `mxbai store sync` recorded for the chunk's file,
|
|
15
|
+
* which is relative to wherever the CLI ran: the project root, or a directory
|
|
16
|
+
* above it in a monorepo. Pages from a source without local files have no
|
|
17
|
+
* path to match and are left out.
|
|
18
|
+
*/
|
|
19
|
+
export const sourcePages = (project: BlumeProject): [string, SourcePage][] =>
|
|
20
|
+
project.manifest.routes.flatMap((route): [string, SourcePage][] =>
|
|
21
|
+
route.indexable && route.sourcePath
|
|
22
|
+
? [
|
|
23
|
+
[
|
|
24
|
+
relative(project.context.root, route.sourcePath),
|
|
25
|
+
{ title: route.title, url: route.path },
|
|
26
|
+
],
|
|
27
|
+
]
|
|
28
|
+
: []
|
|
29
|
+
);
|
|
@@ -23,6 +23,87 @@ const BOOST_RANKING = "desc(boost)";
|
|
|
23
23
|
const facetAttribute = (declaration: string): string =>
|
|
24
24
|
declaration.replace(/^\w+\((?<name>.*)\)$/u, "$<name>");
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* The largest record Algolia accepts on its Build and Grow plans, in bytes of
|
|
28
|
+
* the record's JSON. One larger record fails the whole upload.
|
|
29
|
+
*/
|
|
30
|
+
const MAX_RECORD_BYTES = 10_000;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A record as uploaded: the search record's fields plus Algolia's `objectID`.
|
|
34
|
+
* (Mapped rather than the interface itself, which the SDK's
|
|
35
|
+
* `Record<string, unknown>` objects don't accept.)
|
|
36
|
+
*/
|
|
37
|
+
type AlgoliaObject = Pick<SearchRecord, keyof SearchRecord> & {
|
|
38
|
+
objectID: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** A string's or record's size as Algolia measures it: UTF-8 bytes of its JSON. */
|
|
42
|
+
const jsonBytes = (value: string | AlgoliaObject): number =>
|
|
43
|
+
Buffer.byteLength(JSON.stringify(value), "utf-8");
|
|
44
|
+
|
|
45
|
+
/** The JSON of the widest single character: a control character's `\u` escape. */
|
|
46
|
+
const WIDEST_CHARACTER = jsonBytes("\u0000");
|
|
47
|
+
|
|
48
|
+
/** The length of `text`'s longest prefix whose JSON string fits `budget`. */
|
|
49
|
+
const fittingLength = (text: string, budget: number): number => {
|
|
50
|
+
let size = jsonBytes("");
|
|
51
|
+
let end = 0;
|
|
52
|
+
[...text].some((character) => {
|
|
53
|
+
size += jsonBytes(character) - jsonBytes("");
|
|
54
|
+
if (size > budget) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
end += character.length;
|
|
58
|
+
return false;
|
|
59
|
+
});
|
|
60
|
+
return end;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Cut `text` into pieces whose JSON strings fit `budget` bytes each, at the
|
|
65
|
+
* last space that fits so no word is split; a run with no space in reach (an
|
|
66
|
+
* unspaced script, a long URL) is cut between characters.
|
|
67
|
+
*/
|
|
68
|
+
const splitText = (text: string, budget: number): string[] => {
|
|
69
|
+
const pieces: string[] = [];
|
|
70
|
+
let rest = text;
|
|
71
|
+
while (jsonBytes(rest) > budget) {
|
|
72
|
+
const end = fittingLength(rest, budget);
|
|
73
|
+
const space = rest.lastIndexOf(" ", end);
|
|
74
|
+
const cut = space > 0 ? space : end;
|
|
75
|
+
pieces.push(rest.slice(0, cut));
|
|
76
|
+
rest = rest.slice(cut).trimStart();
|
|
77
|
+
}
|
|
78
|
+
pieces.push(rest);
|
|
79
|
+
return pieces;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A page's record, split to fit Algolia's cap the way Algolia recommends for
|
|
84
|
+
* long documents: every piece carries the page's title, description, URL and
|
|
85
|
+
* facets with a stretch of its body, and the index's `distinct` on `url`
|
|
86
|
+
* shows the page once, at its best-matching piece. The first piece keeps the
|
|
87
|
+
* page's own `objectID`; the rest add `#1`, `#2`… A record whose other fields
|
|
88
|
+
* leave no room for a single character of body can't be split to fit, and
|
|
89
|
+
* uploads whole for Algolia to reject by name.
|
|
90
|
+
*/
|
|
91
|
+
const toObjects = (record: SearchRecord): AlgoliaObject[] => {
|
|
92
|
+
const whole = { ...record, objectID: record._id };
|
|
93
|
+
const budget =
|
|
94
|
+
MAX_RECORD_BYTES -
|
|
95
|
+
jsonBytes({ ...whole, content: "", objectID: `${record._id}#000` }) +
|
|
96
|
+
jsonBytes("");
|
|
97
|
+
if (jsonBytes(whole) <= MAX_RECORD_BYTES || budget < WIDEST_CHARACTER) {
|
|
98
|
+
return [whole];
|
|
99
|
+
}
|
|
100
|
+
return splitText(record.content, budget).map((content, index) => ({
|
|
101
|
+
...record,
|
|
102
|
+
content,
|
|
103
|
+
objectID: index === 0 ? record._id : `${record._id}#${index}`,
|
|
104
|
+
}));
|
|
105
|
+
};
|
|
106
|
+
|
|
26
107
|
/**
|
|
27
108
|
* Upload the search records to Algolia. Uses the admin key from
|
|
28
109
|
* `ALGOLIA_ADMIN_API_KEY` (never the adapter options, which hold only the
|
|
@@ -30,9 +111,12 @@ const facetAttribute = (declaration: string): string =>
|
|
|
30
111
|
*
|
|
31
112
|
* Uses `replaceAllObjects`, which atomically replaces the index contents, so
|
|
32
113
|
* pages deleted or renamed since the last sync don't linger as stale search
|
|
33
|
-
* hits that 404 when clicked.
|
|
114
|
+
* hits that 404 when clicked. A page too long for one record is split across
|
|
115
|
+
* several (see {@link toObjects}). Then adds `filterOnly(locale)` and
|
|
34
116
|
* `filterOnly(version)` to the index's `attributesForFaceting`, keeping any
|
|
35
|
-
* the site declared itself, so the dialog's locale and version filters match
|
|
117
|
+
* the site declared itself, so the dialog's locale and version filters match,
|
|
118
|
+
* and, once a page is split, `url` as the index's `attributeForDistinct`
|
|
119
|
+
* unless the site set its own.
|
|
36
120
|
*/
|
|
37
121
|
export const syncAlgolia = async (
|
|
38
122
|
records: SearchRecord[],
|
|
@@ -47,12 +131,13 @@ export const syncAlgolia = async (
|
|
|
47
131
|
const { algoliasearch }: typeof AlgoliaSdk = nodeRequire("algoliasearch");
|
|
48
132
|
const client = algoliasearch(config.appId, adminKey);
|
|
49
133
|
const { indexName } = config;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
134
|
+
const objects = records.flatMap(toObjects);
|
|
135
|
+
await client.replaceAllObjects({ indexName, objects });
|
|
136
|
+
const {
|
|
137
|
+
attributeForDistinct,
|
|
138
|
+
attributesForFaceting = [],
|
|
139
|
+
customRanking = [],
|
|
140
|
+
} = await client.getSettings({ indexName });
|
|
56
141
|
const declared = new Set(attributesForFaceting.map(facetAttribute));
|
|
57
142
|
const missing = FILTER_ATTRIBUTES.filter((name) => !declared.has(name));
|
|
58
143
|
// Every record carries `boost` (search.boost, 1 by default): ranking by it
|
|
@@ -61,7 +146,11 @@ export const syncAlgolia = async (
|
|
|
61
146
|
const ranksByBoost = customRanking.some((rule) =>
|
|
62
147
|
/^(?:asc|desc)\(boost\)$/u.test(rule)
|
|
63
148
|
);
|
|
64
|
-
|
|
149
|
+
// A split page's records share its `url`: distinct on it lists the page
|
|
150
|
+
// once, at its best-matching record.
|
|
151
|
+
const setsDistinct =
|
|
152
|
+
objects.length > records.length && attributeForDistinct === undefined;
|
|
153
|
+
if (missing.length === 0 && ranksByBoost && !setsDistinct) {
|
|
65
154
|
return;
|
|
66
155
|
}
|
|
67
156
|
const { taskID } = await client.setSettings({
|
|
@@ -74,6 +163,7 @@ export const syncAlgolia = async (
|
|
|
74
163
|
customRanking: ranksByBoost
|
|
75
164
|
? customRanking
|
|
76
165
|
: [...customRanking, BOOST_RANKING],
|
|
166
|
+
...(setsDistinct && { attributeForDistinct: "url", distinct: true }),
|
|
77
167
|
},
|
|
78
168
|
});
|
|
79
169
|
await client.waitForTask({ indexName, taskID });
|