blume 2.0.3 → 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/AGENTS.md +1 -0
- package/CHANGELOG.md +142 -0
- package/README.md +1 -1
- package/dist/cli/{chunk-5f86nr5m.js → chunk-073rdvgb.js} +21 -48
- package/dist/cli/{chunk-5f86nr5m.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-9t7a85s3.js → chunk-313vnn40.js} +30 -19
- package/dist/cli/chunk-313vnn40.js.map +10 -0
- package/dist/cli/{chunk-tkacnehg.js → chunk-3v3ffbrj.js} +2 -2
- package/dist/cli/{chunk-q58y5e6a.js → chunk-3x6x3pph.js} +46 -24
- package/dist/cli/chunk-3x6x3pph.js.map +10 -0
- package/dist/cli/{chunk-1d7ve1dm.js → chunk-42t4jxdx.js} +37 -5
- package/dist/cli/chunk-42t4jxdx.js.map +12 -0
- package/dist/cli/{chunk-8ktnccpt.js → chunk-54ztw1nj.js} +21 -2
- package/dist/cli/chunk-54ztw1nj.js.map +10 -0
- package/dist/cli/{chunk-hm3vjy5s.js → chunk-5vjtgs6e.js} +28 -10
- package/dist/cli/{chunk-hm3vjy5s.js.map → chunk-5vjtgs6e.js.map} +4 -4
- package/dist/cli/chunk-66w9j7p6.js +322 -0
- package/dist/cli/chunk-66w9j7p6.js.map +14 -0
- package/dist/cli/{chunk-2hsdwb9n.js → chunk-7ppf1rva.js} +34 -18
- package/dist/cli/chunk-7ppf1rva.js.map +10 -0
- package/dist/cli/{chunk-r9rcc4w7.js → chunk-8y2kmjv8.js} +7 -7
- package/dist/cli/{chunk-r20tn01b.js → chunk-c1y538kd.js} +8 -2
- package/dist/cli/{chunk-r20tn01b.js.map → chunk-c1y538kd.js.map} +3 -3
- package/dist/cli/{chunk-2eytanqx.js → chunk-c887fj0s.js} +2 -2
- package/dist/cli/{chunk-p3v96n38.js → chunk-cfyg47zg.js} +16 -6
- package/dist/cli/chunk-cfyg47zg.js.map +10 -0
- package/dist/cli/chunk-cm89scdv.js +443 -0
- package/dist/cli/chunk-cm89scdv.js.map +11 -0
- package/dist/cli/{chunk-akbpwfxc.js → chunk-dzwehpy7.js} +2 -2
- package/dist/cli/{chunk-wrr3j9w9.js → chunk-e6atmn4h.js} +299 -72
- package/dist/cli/chunk-e6atmn4h.js.map +36 -0
- package/dist/cli/{chunk-tzmab476.js → chunk-e8xw9qkh.js} +8 -6
- package/dist/cli/{chunk-tzmab476.js.map → chunk-e8xw9qkh.js.map} +3 -3
- package/dist/cli/{chunk-6dsbexzp.js → chunk-e9q7a2ak.js} +17 -15
- package/dist/cli/{chunk-6dsbexzp.js.map → chunk-e9q7a2ak.js.map} +2 -2
- package/dist/cli/{chunk-pv29h0wf.js → chunk-ehweg4fr.js} +5155 -1249
- package/dist/cli/chunk-ehweg4fr.js.map +218 -0
- package/dist/cli/{chunk-c8chx29p.js → chunk-ep6nd0rn.js} +390 -318
- package/dist/cli/chunk-ep6nd0rn.js.map +36 -0
- package/dist/cli/{chunk-p73c0m7w.js → chunk-fdrkhpjy.js} +18 -15
- package/dist/cli/{chunk-p73c0m7w.js.map → chunk-fdrkhpjy.js.map} +3 -3
- package/dist/cli/{chunk-2z928egk.js → chunk-ft9vfys6.js} +5 -5
- package/dist/cli/{chunk-5m5nmvyq.js → chunk-gb420vvz.js} +316 -79
- package/dist/cli/chunk-gb420vvz.js.map +17 -0
- package/dist/cli/{chunk-vg9r4eb9.js → chunk-hebn0tpx.js} +86 -13
- package/dist/cli/chunk-hebn0tpx.js.map +14 -0
- package/dist/cli/{chunk-35d4wj9f.js → chunk-htrwgabr.js} +41 -23
- package/dist/cli/chunk-htrwgabr.js.map +15 -0
- package/dist/cli/{chunk-g4hq16wv.js → chunk-hx3xqnk9.js} +16 -14
- package/dist/cli/{chunk-g4hq16wv.js.map → chunk-hx3xqnk9.js.map} +2 -2
- package/dist/cli/chunk-j134vrp2.js +65 -0
- package/dist/cli/chunk-j134vrp2.js.map +10 -0
- package/dist/cli/{chunk-a58773jm.js → chunk-kma9f6sf.js} +2 -2
- package/dist/cli/chunk-kwrg63tm.js +31 -0
- package/dist/cli/chunk-kwrg63tm.js.map +10 -0
- package/dist/cli/{chunk-ey84smr6.js → chunk-mrcccgnh.js} +6 -6
- package/dist/cli/{chunk-ey84smr6.js.map → chunk-mrcccgnh.js.map} +1 -1
- package/dist/cli/{chunk-yfyb25rh.js → chunk-n0v7m8ry.js} +24 -8
- package/dist/cli/chunk-n0v7m8ry.js.map +14 -0
- package/dist/cli/{chunk-j85vccga.js → chunk-ncj7w596.js} +576 -367
- package/dist/cli/chunk-ncj7w596.js.map +35 -0
- package/dist/cli/{chunk-5xvm6tfj.js → chunk-nq1d20w2.js} +17 -15
- package/dist/cli/{chunk-5xvm6tfj.js.map → chunk-nq1d20w2.js.map} +2 -2
- package/dist/cli/{chunk-pehfxfta.js → chunk-nrns10mj.js} +6 -98
- package/dist/cli/chunk-nrns10mj.js.map +10 -0
- package/dist/cli/{chunk-crgn1q09.js → chunk-sds923fa.js} +4 -4
- package/dist/cli/chunk-t2m5j4pr.js +104 -0
- package/dist/cli/chunk-t2m5j4pr.js.map +11 -0
- package/dist/cli/{chunk-acanzt5p.js → chunk-tdqhyxfz.js} +6 -6
- package/dist/cli/chunk-tphac74f.js +126 -0
- package/dist/cli/chunk-tphac74f.js.map +11 -0
- package/dist/cli/chunk-w1r4vase.js +18 -0
- package/dist/cli/chunk-w1r4vase.js.map +10 -0
- package/dist/cli/{chunk-3em5wd2y.js → chunk-zccbjcj0.js} +4 -3
- package/dist/cli/{chunk-3em5wd2y.js.map → chunk-zccbjcj0.js.map} +2 -2
- package/dist/cli/index.js +31 -20
- package/dist/cli/index.js.map +5 -5
- package/dist/types/ai/agent-surface.d.ts +8 -7
- package/dist/types/ai/ai-catalog.d.ts +7 -0
- package/dist/types/ai/ask-context.d.ts +26 -0
- package/dist/types/ai/ask.d.ts +271 -58
- package/dist/types/ai/component-markdown.d.ts +2 -0
- package/dist/types/ai/index.d.ts +2 -2
- package/dist/types/ai/openapi-components.d.ts +4 -1
- package/dist/types/ai/site-skill.d.ts +26 -0
- package/dist/types/ai/skills.d.ts +16 -0
- package/dist/types/astro/integration.d.ts +9 -0
- package/dist/types/astro/markdown-negotiation.d.ts +3 -0
- package/dist/types/astro/pages.d.ts +10 -0
- package/dist/types/captcha/hcaptcha.d.ts +27 -0
- package/dist/types/captcha/index.d.ts +28 -0
- package/dist/types/captcha/schema.d.ts +29 -0
- package/dist/types/captcha/turnstile.d.ts +28 -0
- package/dist/types/cli/init/scaffold.d.ts +1 -1
- package/dist/types/components/content/api-field.d.ts +62 -0
- package/dist/types/components/content/api-page.d.ts +100 -0
- package/dist/types/components/layout/nav-utils.d.ts +26 -0
- package/dist/types/components/openapi/code-samples.d.ts +29 -0
- package/dist/types/components/openapi/helpers.d.ts +158 -0
- package/dist/types/components/openapi/security.d.ts +95 -0
- package/dist/types/components/openapi/snippets.d.ts +60 -0
- package/dist/types/consent/ethyca.d.ts +48 -0
- package/dist/types/consent/index.d.ts +37 -0
- package/dist/types/consent/native.d.ts +28 -0
- package/dist/types/consent/osano.d.ts +42 -0
- package/dist/types/consent/schema.d.ts +110 -0
- package/dist/types/core/changelog-index.d.ts +8 -0
- package/dist/types/core/config-input.d.ts +217 -23
- package/dist/types/core/data.d.ts +53 -4
- package/dist/types/core/footer.d.ts +8 -0
- package/dist/types/core/i18n-ui.d.ts +88 -0
- package/dist/types/core/includes.d.ts +8 -2
- package/dist/types/core/links.d.ts +8 -0
- package/dist/types/core/localizable.d.ts +7 -0
- package/dist/types/core/page-modes.d.ts +29 -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 +152 -0
- package/dist/types/core/related.d.ts +40 -0
- package/dist/types/core/schema.d.ts +670 -56
- 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/read.d.ts +5 -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 +18 -1
- package/dist/types/core/variables.d.ts +42 -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/node-headers.d.ts +13 -2
- package/dist/types/deploy/platforms/cloudflare.d.ts +15 -7
- package/dist/types/deploy/platforms/netlify.d.ts +7 -0
- 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 +26 -1
- package/dist/types/deploy/redirects.d.ts +55 -11
- package/dist/types/deploy/sitemap.d.ts +2 -1
- package/dist/types/deploy/vercel-negotiation.d.ts +51 -21
- package/dist/types/markdown/code-title.d.ts +17 -0
- package/dist/types/markdown/features.d.ts +3 -2
- package/dist/types/narration/build.d.ts +70 -0
- package/dist/types/narration/provider.d.ts +26 -0
- package/dist/types/narration/script.d.ts +154 -0
- 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 +27 -10
- package/dist/types/openapi/overlay.d.ts +54 -0
- package/dist/types/openapi/parse.d.ts +16 -0
- package/dist/types/openapi/references.d.ts +30 -4
- package/dist/types/ratelimit/cloudflare.d.ts +39 -0
- package/dist/types/ratelimit/index.d.ts +27 -0
- package/dist/types/ratelimit/memory.d.ts +36 -0
- package/dist/types/ratelimit/schema.d.ts +119 -0
- package/dist/types/ratelimit/upstash.d.ts +46 -0
- package/dist/types/ratelimit/wrangler.d.ts +23 -0
- package/dist/types/reference/asyncapi.d.ts +10 -10
- package/dist/types/reference/graphql.d.ts +89 -11
- package/dist/types/reference/index.d.ts +1 -1
- package/dist/types/reference/openapi.d.ts +33 -12
- package/dist/types/reference/options.d.ts +82 -6
- package/dist/types/reference/scalar.d.ts +13 -0
- package/dist/types/reference/schema.d.ts +144 -36
- 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/documents.d.ts +12 -1
- package/dist/types/search/orama-index.d.ts +13 -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/metatags.d.ts +20 -0
- 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 +71 -17
- package/docs/03-upgrading.mdx +4 -4
- package/docs/04-migrating.mdx +1 -1
- package/docs/advanced/changelog.mdx +13 -2
- package/docs/advanced/skills.mdx +15 -1
- package/docs/cli/audit.mdx +51 -8
- package/docs/cli/doctor.mdx +5 -1
- package/docs/cli/evals.mdx +4 -4
- package/docs/cli/index.mdx +1 -0
- package/docs/cli/translate.mdx +1 -0
- package/docs/cli/validate.mdx +4 -3
- package/docs/configuration/analytics.mdx +3 -1
- package/docs/configuration/assistant.mdx +194 -38
- package/docs/configuration/consent.mdx +100 -0
- package/docs/configuration/customization.mdx +8 -6
- package/docs/configuration/export.mdx +5 -3
- package/docs/configuration/index.mdx +64 -5
- package/docs/configuration/meta.ts +3 -0
- package/docs/configuration/narration.mdx +119 -0
- package/docs/configuration/rate-limiting.mdx +90 -0
- package/docs/configuration/search.mdx +63 -12
- package/docs/configuration/theming.mdx +3 -1
- package/docs/content/components.mdx +108 -2
- package/docs/content/frontmatter.mdx +80 -2
- package/docs/content/i18n.mdx +39 -4
- package/docs/content/includes.mdx +16 -0
- package/docs/content/meta.mdx +1 -0
- package/docs/content/meta.ts +1 -0
- package/docs/content/navigation.mdx +32 -10
- package/docs/content/sources/contentful.mdx +35 -0
- package/docs/content/sources/custom.mdx +43 -0
- package/docs/content/sources/github-releases.mdx +39 -0
- package/docs/content/sources/index.mdx +91 -0
- package/docs/content/sources/meta.ts +16 -0
- package/docs/content/sources/notion.mdx +32 -0
- package/docs/content/sources/obsidian.mdx +37 -0
- package/docs/content/sources/payload.mdx +31 -0
- package/docs/content/sources/remote-mdx.mdx +44 -0
- package/docs/content/sources/sanity.mdx +29 -0
- package/docs/content/sources/strapi.mdx +32 -0
- package/docs/content/syntax.mdx +69 -4
- package/docs/content/variables.mdx +41 -0
- package/docs/discoverability/agent-discovery.mdx +29 -2
- package/docs/discoverability/index.mdx +2 -2
- package/docs/discoverability/json-api.mdx +2 -0
- package/docs/discoverability/llms-txt.mdx +2 -2
- package/docs/discoverability/markdown.mdx +3 -1
- package/docs/discoverability/mcp.mdx +4 -0
- package/docs/discoverability/metadata.mdx +17 -0
- package/docs/discoverability/sitemap-and-robots.mdx +1 -1
- package/docs/references/api-pages.mdx +74 -0
- package/docs/references/asyncapi.mdx +4 -2
- package/docs/references/graphql.mdx +14 -2
- package/docs/references/meta.ts +1 -1
- package/docs/references/openapi.mdx +148 -7
- package/docs/references/scalar.mdx +3 -1
- package/package.json +43 -8
- package/skills/blume/SKILL.md +3 -0
- package/skills/blume-migrate/SKILL.md +6 -6
- package/skills/blume-migrate/references/docusaurus.md +7 -7
- package/skills/blume-migrate/references/fumadocs.md +17 -13
- package/skills/blume-migrate/references/mintlify.md +37 -19
- package/skills/blume-migrate/references/nextra.md +10 -7
- package/skills/blume-migrate/references/starlight.md +11 -8
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +60 -14
- package/skills/blume-write-skill/SKILL.md +82 -0
- package/src/ai/agent-readability.ts +2 -1
- package/src/ai/agent-surface.ts +9 -7
- package/src/ai/ai-catalog.ts +6 -3
- package/src/ai/ask-context.ts +103 -20
- package/src/ai/ask-data.ts +6 -0
- package/src/ai/ask-limits.ts +13 -0
- package/src/ai/ask-tools.ts +142 -0
- package/src/ai/ask.ts +354 -76
- package/src/ai/changelog-markdown.ts +11 -6
- package/src/ai/component-markdown.ts +76 -1
- package/src/ai/cors.ts +14 -3
- package/src/ai/index.ts +12 -0
- package/src/ai/llms.ts +6 -2
- package/src/ai/markdown.ts +2 -1
- package/src/ai/mcp/data.ts +6 -0
- package/src/ai/mcp/query.ts +14 -6
- package/src/ai/openapi-components.ts +36 -6
- package/src/ai/relative-links.ts +4 -38
- package/src/ai/site-skill.ts +274 -0
- package/src/ai/skills.ts +35 -2
- package/src/ai/write-skill.ts +97 -0
- package/src/astro/generate.ts +210 -66
- package/src/astro/index.ts +1 -0
- package/src/astro/integration.ts +42 -4
- package/src/astro/markdown-negotiation.ts +3 -0
- package/src/astro/module-types.ts +9 -1
- package/src/astro/pages.ts +23 -0
- package/src/astro/templates.ts +406 -108
- package/src/astro/variables.ts +31 -0
- package/src/audit/catalog.ts +38 -3
- package/src/audit/checks/contrast.ts +183 -0
- package/src/audit/checks/i18n.ts +2 -3
- package/src/audit/checks/indexability.ts +2 -3
- package/src/audit/checks/links.ts +19 -13
- 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/checks/sitemap.ts +2 -3
- package/src/audit/redirects.ts +46 -15
- package/src/audit/run.ts +5 -0
- package/src/audit/types.ts +4 -1
- package/src/audit/url.ts +14 -5
- package/src/blume-modules.d.ts +12 -1
- package/src/captcha/client.ts +152 -0
- package/src/captcha/hcaptcha.ts +36 -0
- package/src/captcha/index.ts +28 -0
- package/src/captcha/schema.ts +32 -0
- package/src/captcha/turnstile.ts +38 -0
- package/src/captcha/verify.ts +71 -0
- package/src/cli/args.ts +51 -0
- package/src/cli/build-failure.ts +23 -0
- package/src/cli/command-meta.ts +5 -0
- package/src/cli/commands/audit.ts +10 -1
- package/src/cli/commands/dev.ts +18 -2
- package/src/cli/commands/doctor.ts +67 -28
- package/src/cli/commands/eval.ts +36 -4
- package/src/cli/commands/init.ts +6 -4
- package/src/cli/commands/skill.ts +96 -0
- package/src/cli/commands/validate.ts +10 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/init/scaffold.ts +3 -3
- package/src/cli/prepare.ts +6 -3
- package/src/cli/required-secrets.ts +72 -30
- package/src/components/BlumePage.astro +2 -0
- package/src/components/content/ApiEndpoint.astro +18 -0
- package/src/components/content/ApiField.astro +79 -0
- package/src/components/content/ApiPlayground.astro +66 -0
- package/src/components/content/ApiRail.astro +19 -0
- package/src/components/content/Color.astro +1 -0
- package/src/components/content/FileTree.astro +1 -0
- package/src/components/content/GithubInfo.astro +1 -0
- package/src/components/content/Panel.astro +1 -0
- package/src/components/content/ParamField.astro +25 -0
- package/src/components/content/RequestExample.astro +17 -0
- package/src/components/content/ResponseExample.astro +17 -0
- package/src/components/content/ResponseField.astro +32 -0
- package/src/components/content/Step.astro +1 -0
- package/src/components/content/Tile.astro +1 -0
- package/src/components/content/Tooltip.astro +1 -0
- package/src/components/content/Tree.astro +1 -0
- package/src/components/content/TypeTable.astro +4 -1
- package/src/components/content/View.astro +21 -0
- package/src/components/content/ViewSwitcher.astro +168 -0
- package/src/components/content/api-field.ts +103 -0
- package/src/components/content/api-page.ts +248 -0
- package/src/components/content/mermaid-element.ts +15 -1
- package/src/components/content/view-switcher.ts +71 -0
- package/src/components/index.ts +2 -0
- package/src/components/islands/Assistant.astro +10 -1
- package/src/components/islands/assistant.tsx +104 -14
- package/src/components/islands/code-question.ts +62 -0
- package/src/components/islands/hooks.ts +136 -21
- package/src/components/islands/support-link.ts +68 -0
- package/src/components/layout/Analytics.astro +11 -2
- package/src/components/layout/Banner.astro +14 -10
- package/src/components/layout/Breadcrumbs.astro +6 -2
- package/src/components/layout/ConsentBanner.astro +62 -0
- package/src/components/layout/ConsentHead.astro +38 -0
- package/src/components/layout/Directory.astro +94 -0
- package/src/components/layout/DrawerSwitchers.astro +51 -0
- package/src/components/layout/Empty.astro +2 -2
- package/src/components/layout/Header.astro +122 -80
- package/src/components/layout/LanguageSwitcher.astro +48 -15
- package/src/components/layout/NarrationPlayer.astro +1085 -0
- package/src/components/layout/NavSelector.astro +33 -7
- package/src/components/layout/PageActions.astro +139 -24
- package/src/components/layout/PageFeedback.astro +54 -49
- package/src/components/layout/PageLayout.astro +71 -3
- package/src/components/layout/ReferenceLayout.astro +17 -6
- package/src/components/layout/RelatedPages.astro +42 -0
- package/src/components/layout/RootLayout.astro +318 -47
- package/src/components/layout/Search.astro +47 -19
- package/src/components/layout/SiteFooter.astro +124 -0
- package/src/components/layout/SiteMetatags.astro +16 -0
- package/src/components/layout/TableOfContents.astro +24 -17
- package/src/components/layout/consent/native.ts +79 -0
- package/src/components/layout/consent/types.ts +37 -0
- package/src/components/layout/footer-links.ts +53 -0
- package/src/components/layout/footer-socials.ts +87 -0
- package/src/components/layout/head-scripts.ts +6 -1
- package/src/components/layout/locale-redirect.ts +107 -0
- package/src/components/layout/nav-utils.ts +83 -0
- package/src/components/layout/page-feedback.ts +96 -0
- package/src/components/layout/search/algolia.ts +10 -1
- package/src/components/layout/search/analytics.ts +126 -0
- package/src/components/layout/search/endpoint.ts +48 -5
- package/src/components/layout/search/flexsearch.ts +17 -5
- package/src/components/layout/search/orama-cloud.ts +26 -14
- package/src/components/layout/search/pagefind.ts +114 -5
- package/src/components/layout/search/types.ts +32 -0
- package/src/components/layout/search/typesense.ts +19 -3
- 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 +184 -13
- package/src/components/openapi/ParametersTable.astro +8 -4
- package/src/components/openapi/Playground.astro +7 -1
- package/src/components/openapi/RequestBody.astro +24 -5
- package/src/components/openapi/RequestPanel.astro +59 -7
- package/src/components/openapi/Responses.astro +7 -4
- package/src/components/openapi/SchemaTable.astro +15 -3
- package/src/components/openapi/async-snippets.ts +65 -9
- package/src/components/openapi/async.ts +123 -29
- package/src/components/openapi/code-samples.ts +97 -0
- 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 +16 -3
- package/src/components/openapi/snippets.ts +873 -10
- package/src/components/openapi/validate-json.ts +4 -3
- package/src/components/props.ts +6 -0
- package/src/consent/client.ts +95 -0
- package/src/consent/clients.ts +19 -0
- package/src/consent/ethyca.ts +71 -0
- package/src/consent/head.ts +24 -0
- package/src/consent/index.ts +37 -0
- package/src/consent/init.ts +29 -0
- package/src/consent/native.ts +36 -0
- package/src/consent/osano.ts +59 -0
- package/src/consent/schema.ts +30 -0
- package/src/core/builtin-tags.ts +5 -0
- package/src/core/changelog-index.ts +31 -1
- package/src/core/client-address.ts +19 -0
- package/src/core/component-diagnostics.ts +53 -0
- package/src/core/component-overrides.ts +32 -0
- package/src/core/config-input.ts +225 -23
- package/src/core/data.ts +46 -4
- package/src/core/diagnostics.ts +8 -2
- package/src/core/footer.ts +26 -0
- package/src/core/graph.ts +39 -36
- package/src/core/i18n-ui.ts +84 -2
- package/src/core/includes.ts +21 -4
- package/src/core/links.ts +63 -19
- package/src/core/localizable.ts +26 -0
- package/src/core/navigation.ts +43 -12
- package/src/core/page-modes.ts +56 -0
- package/src/core/probe.ts +26 -6
- package/src/core/project-graph.ts +163 -16
- package/src/core/redirect-patterns.ts +520 -0
- package/src/core/related.ts +131 -0
- package/src/core/schema.ts +339 -66
- 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 +228 -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/read.ts +8 -3
- 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 +19 -1
- package/src/core/ui-packs/ar.ts +44 -0
- package/src/core/ui-packs/bg.ts +46 -0
- package/src/core/ui-packs/bn.ts +44 -0
- package/src/core/ui-packs/ca.ts +47 -0
- package/src/core/ui-packs/cs.ts +45 -0
- package/src/core/ui-packs/da.ts +46 -0
- package/src/core/ui-packs/de.ts +47 -0
- package/src/core/ui-packs/el.ts +46 -0
- package/src/core/ui-packs/es.ts +47 -0
- package/src/core/ui-packs/fa.ts +46 -0
- package/src/core/ui-packs/fi.ts +46 -0
- package/src/core/ui-packs/fr.ts +47 -0
- package/src/core/ui-packs/he.ts +44 -0
- package/src/core/ui-packs/hi.ts +45 -0
- package/src/core/ui-packs/hr.ts +46 -0
- package/src/core/ui-packs/hu.ts +45 -0
- package/src/core/ui-packs/id.ts +46 -0
- package/src/core/ui-packs/it.ts +46 -0
- package/src/core/ui-packs/ja.ts +46 -0
- package/src/core/ui-packs/ko.ts +45 -0
- package/src/core/ui-packs/nl.ts +47 -0
- package/src/core/ui-packs/no.ts +45 -0
- package/src/core/ui-packs/pl.ts +46 -0
- package/src/core/ui-packs/pt-br.ts +47 -0
- package/src/core/ui-packs/pt.ts +46 -0
- package/src/core/ui-packs/ro.ts +46 -0
- package/src/core/ui-packs/ru.ts +46 -0
- package/src/core/ui-packs/sk.ts +45 -0
- package/src/core/ui-packs/sr.ts +46 -0
- package/src/core/ui-packs/sv.ts +45 -0
- package/src/core/ui-packs/th.ts +44 -0
- package/src/core/ui-packs/tr.ts +46 -0
- package/src/core/ui-packs/uk.ts +46 -0
- package/src/core/ui-packs/vi.ts +46 -0
- package/src/core/ui-packs/zh-tw.ts +44 -0
- package/src/core/ui-packs/zh.ts +44 -0
- package/src/core/variables.ts +103 -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 +79 -9
- package/src/deploy/cloudflare-negotiation.ts +35 -5
- package/src/deploy/headers.ts +2 -1
- package/src/deploy/node-headers.ts +63 -9
- package/src/deploy/platforms/cloudflare.ts +45 -9
- package/src/deploy/platforms/netlify.ts +35 -2
- package/src/deploy/platforms/node.ts +12 -2
- package/src/deploy/platforms/types.ts +21 -7
- package/src/deploy/platforms/vercel.ts +149 -17
- package/src/deploy/redirects.ts +151 -24
- package/src/deploy/rss.ts +3 -1
- package/src/deploy/sitemap.ts +39 -5
- package/src/deploy/vercel-negotiation.ts +326 -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/api-rail.ts +259 -0
- package/src/markdown/code-title.ts +35 -9
- package/src/markdown/features.ts +3 -2
- package/src/markdown/fence-meta.ts +2 -0
- package/src/markdown/include.ts +11 -1
- package/src/markdown/index.ts +28 -5
- package/src/markdown/math.ts +4 -4
- package/src/markdown/relative-links.ts +3 -2
- package/src/markdown/variables.ts +56 -0
- package/src/markdown/views.ts +82 -0
- package/src/narration/build.ts +412 -0
- package/src/narration/player.ts +249 -0
- package/src/narration/provider.ts +29 -0
- package/src/narration/script.ts +592 -0
- package/src/og/card.ts +7 -6
- 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 +72 -27
- package/src/openapi/overlay.ts +256 -0
- package/src/openapi/parse.ts +182 -4
- package/src/openapi/references.ts +64 -8
- package/src/openapi/render-mdx.ts +5 -1
- package/src/openapi/scalar.ts +40 -6
- package/src/openapi/source.ts +40 -15
- package/src/ratelimit/cloudflare.ts +54 -0
- package/src/ratelimit/index.ts +30 -0
- package/src/ratelimit/memory.ts +45 -0
- package/src/ratelimit/runtime.ts +247 -0
- package/src/ratelimit/schema.ts +28 -0
- package/src/ratelimit/upstash.ts +60 -0
- package/src/ratelimit/wrangler.ts +70 -0
- package/src/reference/asyncapi.ts +2 -2
- package/src/reference/graphql.ts +20 -3
- package/src/reference/index.ts +1 -0
- package/src/reference/openapi.ts +21 -7
- package/src/reference/options.ts +80 -8
- package/src/reference/scalar.ts +33 -8
- package/src/registry/eject.ts +23 -5
- package/src/registry/registry.ts +7 -0
- 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/documents.ts +26 -2
- package/src/search/orama-index.ts +191 -34
- package/src/search/pagefind-ranking.ts +39 -0
- package/src/search/source-pages.ts +29 -0
- package/src/search/sync/algolia.ts +111 -10
- package/src/search/sync/index.ts +52 -13
- package/src/search/sync/orama-cloud.ts +3 -0
- package/src/search/sync/typesense.ts +76 -26
- package/src/seo/metatags.ts +67 -0
- 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/chrome-icons.ts +1 -0
- package/src/theme/color.ts +500 -0
- package/src/theme/entry.ts +85 -6
- package/src/theme/palette.ts +121 -45
- package/src/translate/prompts.ts +1 -0
- package/src/translate/validate.ts +31 -3
- package/src/upgrade/upgrade.ts +4 -171
- package/dist/cli/chunk-1d7ve1dm.js.map +0 -12
- package/dist/cli/chunk-2hsdwb9n.js.map +0 -10
- package/dist/cli/chunk-35d4wj9f.js.map +0 -15
- package/dist/cli/chunk-364znk6q.js +0 -75
- package/dist/cli/chunk-364znk6q.js.map +0 -11
- package/dist/cli/chunk-5m5nmvyq.js.map +0 -13
- package/dist/cli/chunk-8ktnccpt.js.map +0 -10
- package/dist/cli/chunk-9t7a85s3.js.map +0 -10
- package/dist/cli/chunk-b07cmahc.js +0 -145
- package/dist/cli/chunk-b07cmahc.js.map +0 -11
- package/dist/cli/chunk-c8chx29p.js.map +0 -36
- package/dist/cli/chunk-e04dxsz1.js +0 -39
- package/dist/cli/chunk-e04dxsz1.js.map +0 -10
- package/dist/cli/chunk-j85vccga.js.map +0 -36
- package/dist/cli/chunk-p3v96n38.js.map +0 -10
- package/dist/cli/chunk-pehfxfta.js.map +0 -10
- package/dist/cli/chunk-pv29h0wf.js.map +0 -190
- package/dist/cli/chunk-q58y5e6a.js.map +0 -10
- package/dist/cli/chunk-vg9r4eb9.js.map +0 -14
- package/dist/cli/chunk-wrr3j9w9.js.map +0 -35
- package/dist/cli/chunk-yfyb25rh.js.map +0 -14
- package/docs/content/sources.mdx +0 -334
- /package/dist/cli/{chunk-tkacnehg.js.map → chunk-3v3ffbrj.js.map} +0 -0
- /package/dist/cli/{chunk-r9rcc4w7.js.map → chunk-8y2kmjv8.js.map} +0 -0
- /package/dist/cli/{chunk-2eytanqx.js.map → chunk-c887fj0s.js.map} +0 -0
- /package/dist/cli/{chunk-akbpwfxc.js.map → chunk-dzwehpy7.js.map} +0 -0
- /package/dist/cli/{chunk-2z928egk.js.map → chunk-ft9vfys6.js.map} +0 -0
- /package/dist/cli/{chunk-a58773jm.js.map → chunk-kma9f6sf.js.map} +0 -0
- /package/dist/cli/{chunk-crgn1q09.js.map → chunk-sds923fa.js.map} +0 -0
- /package/dist/cli/{chunk-acanzt5p.js.map → chunk-tdqhyxfz.js.map} +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { withBasePath } from "../../core/base-path.ts";
|
|
2
|
+
import type { BlumeDataI18n } from "../../core/data.ts";
|
|
3
|
+
import { localizeRoute } from "../../core/i18n.ts";
|
|
4
|
+
import type { RouteSet } from "../../core/locale-links.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Routing by browser language (`i18n.routeByBrowserLanguage`): a visitor who
|
|
8
|
+
* lands on the default language's home page from outside the site goes to
|
|
9
|
+
* the home page in the first language their browser prefers that the site
|
|
10
|
+
* has, until they pick a language with the switcher. It runs in the browser,
|
|
11
|
+
* from the home page's head, so it works on a static host, and the redirect
|
|
12
|
+
* happens before anything paints.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Where a language picked with the switcher is remembered. The inline script
|
|
17
|
+
* below spells it out.
|
|
18
|
+
*/
|
|
19
|
+
export const LOCALE_STORAGE_KEY = "blume-locale";
|
|
20
|
+
|
|
21
|
+
/** What the home page's routing needs from the site. */
|
|
22
|
+
export interface LocaleRedirectContext {
|
|
23
|
+
basePath: string;
|
|
24
|
+
i18n: BlumeDataI18n | null;
|
|
25
|
+
/** The page being rendered. */
|
|
26
|
+
route: string;
|
|
27
|
+
routes: RouteSet;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The other languages' home pages a visitor could be sent to from `route`,
|
|
32
|
+
* by locale code, or `null` when this page doesn't route: the option is off,
|
|
33
|
+
* this isn't the default language's home, or no other home is served.
|
|
34
|
+
*/
|
|
35
|
+
export const browserLanguageTargets = (
|
|
36
|
+
context: LocaleRedirectContext
|
|
37
|
+
): Record<string, string> | null => {
|
|
38
|
+
const { i18n } = context;
|
|
39
|
+
if (!i18n?.routeByBrowserLanguage) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const home = (code: string): string =>
|
|
43
|
+
withBasePath(context.basePath, localizeRoute("/", code, i18n));
|
|
44
|
+
if (context.route !== home(i18n.defaultLocale)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const targets = i18n.locales
|
|
48
|
+
.filter(({ code }) => code !== i18n.defaultLocale)
|
|
49
|
+
.map(({ code }) => [code, home(code)] as const)
|
|
50
|
+
.filter(([, path]) => context.routes.has(path));
|
|
51
|
+
return targets.length > 0 ? Object.fromEntries(targets) : null;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The inline script, given the default locale (`data-default`) and the other
|
|
56
|
+
* homes (`data-targets`). For each language the browser prefers, in order, it
|
|
57
|
+
* matches a locale by exact code, then by base language (`fr-CA` finds `fr`,
|
|
58
|
+
* `pt` finds `pt-br`): the default language stays put, another one's home is
|
|
59
|
+
* where the visitor goes. A visitor who picked a language, arrived from
|
|
60
|
+
* another page of the site, or whose storage can't be read stays too.
|
|
61
|
+
*/
|
|
62
|
+
export const LOCALE_REDIRECT_SCRIPT = `(()=>{const s=document.currentScript;if(!s)return;try{if(localStorage.getItem("blume-locale")!==null)return}catch{return}if(document.referrer.startsWith(location.origin+"/"))return;let t;try{t=JSON.parse(s.dataset.targets||"")}catch{return}const d=s.dataset.default||"";const c=[d,...Object.keys(t)];const low=(v)=>v.toLowerCase();const base=(v)=>low(v).split("-")[0];for(const l of navigator.languages||[]){const m=c.find((v)=>low(v)===low(l))||c.find((v)=>low(v)===base(l))||c.find((v)=>base(v)===base(l));if(m===d)return;if(m){location.replace(t[m]+location.search+location.hash);return}}})();`;
|
|
63
|
+
|
|
64
|
+
/** A click target that can look up its ancestors: an element. */
|
|
65
|
+
interface ClosestTarget {
|
|
66
|
+
closest: (selector: string) => {
|
|
67
|
+
getAttribute: (name: string) => string | null;
|
|
68
|
+
} | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A click, as the listener reads it. */
|
|
72
|
+
interface Click {
|
|
73
|
+
target: EventTarget | ClosestTarget | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** What {@link rememberLocaleChoice} listens on: the document. */
|
|
77
|
+
export interface ChoiceTarget {
|
|
78
|
+
addEventListener: (type: "click", listener: (event: Click) => void) => void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const hasClosest = (target: Click["target"]): target is ClosestTarget =>
|
|
82
|
+
target !== null && "closest" in target;
|
|
83
|
+
|
|
84
|
+
/** The code of the switcher link a click landed on, if any. */
|
|
85
|
+
const chosenCode = (target: Click["target"]): string | null | undefined =>
|
|
86
|
+
hasClosest(target)
|
|
87
|
+
? target.closest("a[data-blume-locale-choice]")?.getAttribute("hreflang")
|
|
88
|
+
: null;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Remember a language picked with the switcher, so routing by browser
|
|
92
|
+
* language stops sending the reader elsewhere. One listener on the document
|
|
93
|
+
* covers every switcher, on every page the client router swaps in.
|
|
94
|
+
*/
|
|
95
|
+
export const rememberLocaleChoice = (target: ChoiceTarget): void => {
|
|
96
|
+
target.addEventListener("click", (event) => {
|
|
97
|
+
const code = chosenCode(event.target);
|
|
98
|
+
if (!code) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
localStorage.setItem(LOCALE_STORAGE_KEY, code);
|
|
103
|
+
} catch {
|
|
104
|
+
// Storage can be off; routing then stays off too (see the script).
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
};
|
|
@@ -270,6 +270,89 @@ export const getPagination = (flat: FlatPage[], route: string) => {
|
|
|
270
270
|
};
|
|
271
271
|
};
|
|
272
272
|
|
|
273
|
+
/** A group node in the sidebar tree. */
|
|
274
|
+
export type NavGroup = Extract<NavNode, { kind: "group" }>;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The group whose own page `route` is, when that page lists the group's
|
|
278
|
+
* pages: its `directory` (inherited when the group sets none) is `card` or
|
|
279
|
+
* `accordion`. `null` on every other page.
|
|
280
|
+
*/
|
|
281
|
+
export const directoryGroupForRoute = (
|
|
282
|
+
nodes: NavNode[],
|
|
283
|
+
route: string
|
|
284
|
+
): NavGroup | null => {
|
|
285
|
+
for (const node of nodes) {
|
|
286
|
+
if (node.kind !== "group") {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (
|
|
290
|
+
node.route === route &&
|
|
291
|
+
(node.directory === "card" || node.directory === "accordion")
|
|
292
|
+
) {
|
|
293
|
+
return node;
|
|
294
|
+
}
|
|
295
|
+
const nested = directoryGroupForRoute(node.children, route);
|
|
296
|
+
if (nested) {
|
|
297
|
+
return nested;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
/** One entry in a group page's directory: a page, or a subgroup and its pages. */
|
|
304
|
+
export interface DirectoryEntry {
|
|
305
|
+
description?: string;
|
|
306
|
+
/** The page, or the subgroup's own page (its first page when it has none). */
|
|
307
|
+
href: string;
|
|
308
|
+
icon?: string;
|
|
309
|
+
label: string;
|
|
310
|
+
/** A subgroup's pages in sidebar order, its own page first; absent for a page. */
|
|
311
|
+
pages?: FlatPage[];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* What a group's page lists: the group's pages and subgroups, in sidebar
|
|
316
|
+
* order, without the page itself or external links. A subgroup is described
|
|
317
|
+
* by its own page, and one with no page to link to is left out.
|
|
318
|
+
*/
|
|
319
|
+
export const directoryEntries = (group: NavGroup): DirectoryEntry[] =>
|
|
320
|
+
group.children.flatMap((child): DirectoryEntry[] => {
|
|
321
|
+
if (child.kind === "page") {
|
|
322
|
+
return child.pageId && child.route !== group.route
|
|
323
|
+
? [
|
|
324
|
+
{
|
|
325
|
+
description: child.description,
|
|
326
|
+
href: child.route,
|
|
327
|
+
icon: child.icon,
|
|
328
|
+
label: child.label,
|
|
329
|
+
},
|
|
330
|
+
]
|
|
331
|
+
: [];
|
|
332
|
+
}
|
|
333
|
+
const listed = flattenPages(child.children);
|
|
334
|
+
const pages =
|
|
335
|
+
child.route && !listed.some((page) => page.route === child.route)
|
|
336
|
+
? [{ label: child.label, route: child.route }, ...listed]
|
|
337
|
+
: listed;
|
|
338
|
+
const [first] = pages;
|
|
339
|
+
if (!first) {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
const own = child.children.find(
|
|
343
|
+
(node) => node.kind === "page" && node.route === child.route
|
|
344
|
+
);
|
|
345
|
+
return [
|
|
346
|
+
{
|
|
347
|
+
description: own?.kind === "page" ? own.description : undefined,
|
|
348
|
+
href: child.route ?? first.route,
|
|
349
|
+
icon: child.icon,
|
|
350
|
+
label: child.label,
|
|
351
|
+
pages,
|
|
352
|
+
},
|
|
353
|
+
];
|
|
354
|
+
});
|
|
355
|
+
|
|
273
356
|
/**
|
|
274
357
|
* A stable id for every group in a sidebar — `g<n>` by pre-order position in
|
|
275
358
|
* the full tree. The layout hands `NavTree` a scoped view of that tree (a
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { track } from "./analytics-client.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The "Was this page helpful?" rating (`PageFeedback.astro`). A rating is
|
|
4
|
+
* sent as a `feedback` analytics event, the buttons give way to a thank-you,
|
|
5
|
+
* and, with `feedback.comments` on, a box below asks the reader to say more.
|
|
6
|
+
* A comment they send goes out as a `feedback_comment` event with the same
|
|
7
|
+
* rating, path, and title, so the two line up in a dashboard.
|
|
8
|
+
*
|
|
9
|
+
* Both go through analytics, so under a consent layer (`consent`) the widget
|
|
10
|
+
* renders hidden and only shows once the reader allows analytics: from
|
|
11
|
+
* anyone else, an answer would go nowhere.
|
|
12
|
+
*
|
|
13
|
+
* Runs on the initial load and again after every client-router swap, which
|
|
14
|
+
* rebuilds the widget from server-rendered markup, so the handlers always
|
|
15
|
+
* bind to the freshly swapped-in elements.
|
|
16
|
+
*/
|
|
17
|
+
import type { BlumeConsent } from "./consent/types.ts";
|
|
18
|
+
|
|
19
|
+
/** The longest comment the box takes. */
|
|
20
|
+
export const COMMENT_MAX_LENGTH = 1000;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether an answer would reach analytics: the site has no consent layer, or
|
|
24
|
+
* the reader has allowed analytics.
|
|
25
|
+
*/
|
|
26
|
+
const answersReachAnalytics = (): boolean => {
|
|
27
|
+
// SAFETY: the consent init script (`consent/init.ts`) is the only writer of
|
|
28
|
+
// `window.blumeConsent`; without a consent layer it's absent.
|
|
29
|
+
const { blumeConsent } = window as Window & { blumeConsent?: BlumeConsent };
|
|
30
|
+
return blumeConsent === undefined || blumeConsent.analytics === true;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Show the page's rating only while its answers would reach analytics. Runs
|
|
35
|
+
* when the widget is wired and again on every `blume:consent` change, so a
|
|
36
|
+
* reader who accepts the banner gets the rating right away.
|
|
37
|
+
*/
|
|
38
|
+
export const syncFeedbackVisibility = (): void => {
|
|
39
|
+
const root = document.querySelector<HTMLElement>(
|
|
40
|
+
"[data-blume-page-feedback]"
|
|
41
|
+
);
|
|
42
|
+
if (root) {
|
|
43
|
+
root.hidden = !answersReachAnalytics();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Wire the page's rating, when it has one. */
|
|
48
|
+
export const initFeedback = (): void => {
|
|
49
|
+
const root = document.querySelector("[data-blume-page-feedback]");
|
|
50
|
+
if (!root) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
syncFeedbackVisibility();
|
|
54
|
+
const actions = root.querySelector("[data-feedback-actions]");
|
|
55
|
+
const thanks = root.querySelector<HTMLElement>("[data-feedback-thanks]");
|
|
56
|
+
const form = root.querySelector<HTMLFormElement>("[data-feedback-form]");
|
|
57
|
+
let helpful = "";
|
|
58
|
+
const rate = (value: string) => {
|
|
59
|
+
helpful = value;
|
|
60
|
+
track("feedback", {
|
|
61
|
+
helpful,
|
|
62
|
+
path: location.pathname,
|
|
63
|
+
title: document.title,
|
|
64
|
+
});
|
|
65
|
+
actions?.classList.add("hidden");
|
|
66
|
+
thanks?.classList.remove("hidden");
|
|
67
|
+
if (form) {
|
|
68
|
+
form.hidden = false;
|
|
69
|
+
}
|
|
70
|
+
// The clicked button just disappeared under focus; hand focus to the
|
|
71
|
+
// (live) thanks line so the reader isn't dropped at the top of the page.
|
|
72
|
+
// The comment box is the next stop from there.
|
|
73
|
+
thanks?.focus();
|
|
74
|
+
};
|
|
75
|
+
for (const button of root.querySelectorAll<HTMLElement>(
|
|
76
|
+
"[data-feedback-value]"
|
|
77
|
+
)) {
|
|
78
|
+
button.addEventListener("click", () =>
|
|
79
|
+
rate(button.dataset.feedbackValue ?? "")
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
form?.addEventListener("submit", (event) => {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
const comment = form.querySelector("textarea")?.value.trim() ?? "";
|
|
85
|
+
if (comment) {
|
|
86
|
+
track("feedback_comment", {
|
|
87
|
+
comment: comment.slice(0, COMMENT_MAX_LENGTH),
|
|
88
|
+
helpful,
|
|
89
|
+
path: location.pathname,
|
|
90
|
+
title: document.title,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
form.hidden = true;
|
|
94
|
+
thanks?.focus();
|
|
95
|
+
});
|
|
96
|
+
};
|
|
@@ -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),
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search analytics: what readers search for, what they find, and what they
|
|
3
|
+
* open, sent through the configured analytics adapters (`track`).
|
|
4
|
+
*
|
|
5
|
+
* The dialog searches on every keystroke, so a query is only recorded once it
|
|
6
|
+
* settles: a second without typing, or the reader picking a result or closing
|
|
7
|
+
* the dialog. Each settled query is one `search` event with the number of
|
|
8
|
+
* results, so a dashboard can list the queries that found nothing. Picking a
|
|
9
|
+
* result is a `search_select` event with its position, for click-through. The
|
|
10
|
+
* same query settling twice in a row (a filter toggled back and forth) counts
|
|
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.
|
|
16
|
+
*/
|
|
17
|
+
import type { TrackProps } from "../analytics-client.ts";
|
|
18
|
+
|
|
19
|
+
/** The longest query sent: past this it's rarely a search term. */
|
|
20
|
+
export const MAX_QUERY_CHARS = 100;
|
|
21
|
+
|
|
22
|
+
/** How long a query sits untouched before it counts as settled. */
|
|
23
|
+
export const SETTLE_MS = 1000;
|
|
24
|
+
|
|
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;
|
|
34
|
+
|
|
35
|
+
/** The timer a tracker waits on; injectable for tests. */
|
|
36
|
+
export interface SettleTimer {
|
|
37
|
+
cancel: (handle: number) => void;
|
|
38
|
+
start: (callback: () => void, ms: number) => number;
|
|
39
|
+
}
|
|
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
|
+
|
|
49
|
+
/** What the dialog tells the tracker. */
|
|
50
|
+
export interface SearchTracker {
|
|
51
|
+
/** The dialog closed; record the query it was showing. */
|
|
52
|
+
closed: () => void;
|
|
53
|
+
/** The reader picked result `position` (1-based) for `query`. */
|
|
54
|
+
selected: (query: string, position: number, url: string) => void;
|
|
55
|
+
/** Results for `query` are on screen. */
|
|
56
|
+
settled: (query: string, results: number) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A query and how many results it found. */
|
|
60
|
+
interface PendingSearch {
|
|
61
|
+
query: string;
|
|
62
|
+
results: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const browserTimer: SettleTimer = {
|
|
66
|
+
cancel: (handle) => clearTimeout(handle),
|
|
67
|
+
start: (callback, ms) => Number(setTimeout(callback, ms)),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** A tracker for one search dialog. */
|
|
71
|
+
export const createSearchTracker = (
|
|
72
|
+
send: SendEvent,
|
|
73
|
+
{ queries = true, timer = browserTimer }: SearchTrackerOptions = {}
|
|
74
|
+
): SearchTracker => {
|
|
75
|
+
let pending: PendingSearch | undefined;
|
|
76
|
+
let handle: number | undefined;
|
|
77
|
+
let last: string | undefined;
|
|
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
|
+
|
|
89
|
+
const flush = (): void => {
|
|
90
|
+
if (handle !== undefined) {
|
|
91
|
+
timer.cancel(handle);
|
|
92
|
+
handle = undefined;
|
|
93
|
+
}
|
|
94
|
+
if (pending && pending.query !== last) {
|
|
95
|
+
report(
|
|
96
|
+
"search",
|
|
97
|
+
{ path: location.pathname, results: pending.results },
|
|
98
|
+
pending.query
|
|
99
|
+
);
|
|
100
|
+
last = pending.query;
|
|
101
|
+
}
|
|
102
|
+
pending = undefined;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
closed: () => {
|
|
107
|
+
flush();
|
|
108
|
+
last = undefined;
|
|
109
|
+
},
|
|
110
|
+
selected: (query, position, url) => {
|
|
111
|
+
flush();
|
|
112
|
+
report(
|
|
113
|
+
"search_select",
|
|
114
|
+
{ path: location.pathname, position, url },
|
|
115
|
+
query.slice(0, MAX_QUERY_CHARS)
|
|
116
|
+
);
|
|
117
|
+
},
|
|
118
|
+
settled: (query, results) => {
|
|
119
|
+
if (handle !== undefined) {
|
|
120
|
+
timer.cancel(handle);
|
|
121
|
+
}
|
|
122
|
+
pending = { query: query.slice(0, MAX_QUERY_CHARS), results };
|
|
123
|
+
handle = timer.start(flush, SETTLE_MS);
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Document } from "flexsearch";
|
|
2
2
|
|
|
3
|
-
import { buildResult, RESULT_POOL } from "./types.ts";
|
|
3
|
+
import { buildResult, byBoost, RESULT_POOL } from "./types.ts";
|
|
4
4
|
import type { IndexedDocument, SearchFn } from "./types.ts";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -11,7 +11,7 @@ import type { IndexedDocument, SearchFn } from "./types.ts";
|
|
|
11
11
|
type SearchDocument = Pick<
|
|
12
12
|
IndexedDocument,
|
|
13
13
|
"content" | "description" | "route" | "title"
|
|
14
|
-
|
|
14
|
+
> & { keywords: string };
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* FlexSearch: reuse the same static `blume-search.json` Orama ships, but build
|
|
@@ -29,11 +29,14 @@ export const createSearch = async (opts: {
|
|
|
29
29
|
const byRoute = new Map(documents.map((doc) => [doc.route, doc]));
|
|
30
30
|
|
|
31
31
|
const index = new Document<SearchDocument>({
|
|
32
|
-
document: {
|
|
32
|
+
document: {
|
|
33
|
+
id: "route",
|
|
34
|
+
index: ["title", "keywords", "description", "content"],
|
|
35
|
+
},
|
|
33
36
|
tokenize: "forward",
|
|
34
37
|
});
|
|
35
38
|
for (const doc of documents) {
|
|
36
|
-
index.add(doc);
|
|
39
|
+
index.add({ ...doc, keywords: doc.keywords?.join(" ") ?? "" });
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
// FlexSearch's in-memory search is synchronous, so the SearchFn resolves
|
|
@@ -63,6 +66,15 @@ export const createSearch = async (opts: {
|
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
|
-
|
|
69
|
+
// FlexSearch returns an order, not scores: rank by position, then apply
|
|
70
|
+
// each page's `search.boost` to that.
|
|
71
|
+
const ranked = byBoost(
|
|
72
|
+
matched.map((doc, position) => ({
|
|
73
|
+
boost: doc.boost,
|
|
74
|
+
match: doc,
|
|
75
|
+
score: 1 / (position + 1),
|
|
76
|
+
}))
|
|
77
|
+
);
|
|
78
|
+
return Promise.resolve(buildResult(ranked, query, options?.section));
|
|
67
79
|
};
|
|
68
80
|
};
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { OramaClient } from "@oramacloud/client";
|
|
2
2
|
|
|
3
3
|
import type { OramaCloudOptions } from "../../../search/adapters/orama-cloud.ts";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
byBoost,
|
|
6
|
+
excerptFor,
|
|
7
|
+
highlight,
|
|
8
|
+
RESULT_POOL,
|
|
9
|
+
SEARCH_LIMIT,
|
|
10
|
+
} from "./types.ts";
|
|
5
11
|
import type { SearchFn } from "./types.ts";
|
|
6
12
|
|
|
7
13
|
interface OramaCloudRecord {
|
|
@@ -9,6 +15,7 @@ interface OramaCloudRecord {
|
|
|
9
15
|
title: string;
|
|
10
16
|
description?: string;
|
|
11
17
|
content?: string;
|
|
18
|
+
boost?: number;
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
/**
|
|
@@ -25,25 +32,30 @@ export const createSearch = (opts: OramaCloudOptions): SearchFn => {
|
|
|
25
32
|
endpoint,
|
|
26
33
|
});
|
|
27
34
|
return async (query, options) => {
|
|
35
|
+
// A pool beyond the visible limit, so each page's `search.boost` can
|
|
36
|
+
// lift a hit into view before the list is cut.
|
|
28
37
|
const results = await client.search({
|
|
29
|
-
limit:
|
|
38
|
+
limit: RESULT_POOL,
|
|
30
39
|
term: query,
|
|
31
40
|
// The sync carries `locale` on every record so an i18n site can scope
|
|
32
41
|
// hosted results to the active language.
|
|
33
42
|
...(options?.locale && { where: { locale: options.locale } }),
|
|
34
43
|
});
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const ranked = byBoost(
|
|
45
|
+
(results?.hits ?? []).map((hit) => {
|
|
46
|
+
const doc: OramaCloudRecord = hit.document;
|
|
47
|
+
return { boost: doc.boost, match: doc, score: hit.score };
|
|
48
|
+
})
|
|
49
|
+
);
|
|
50
|
+
const hits = ranked.slice(0, SEARCH_LIMIT).map((doc) => ({
|
|
51
|
+
content: doc.content ?? "",
|
|
52
|
+
excerpt: highlight(
|
|
53
|
+
excerptFor(doc.description ?? "", doc.content ?? "", query),
|
|
54
|
+
query
|
|
55
|
+
),
|
|
56
|
+
title: highlight(doc.title, query),
|
|
57
|
+
url: doc.url,
|
|
58
|
+
}));
|
|
47
59
|
return { hits, sections: [] };
|
|
48
60
|
};
|
|
49
61
|
};
|