blume 2.0.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +48 -0
- package/README.md +1 -1
- package/dist/cli/{chunk-1d7ve1dm.js → chunk-0gz36akr.js} +18 -2
- package/dist/cli/{chunk-1d7ve1dm.js.map → chunk-0gz36akr.js.map} +3 -3
- package/dist/cli/{chunk-2eytanqx.js → chunk-1n228xqb.js} +2 -2
- package/dist/cli/{chunk-yfyb25rh.js → chunk-2f3ndkpw.js} +24 -8
- package/dist/cli/chunk-2f3ndkpw.js.map +14 -0
- package/dist/cli/{chunk-r20tn01b.js → chunk-2f7n4qzt.js} +8 -2
- package/dist/cli/{chunk-r20tn01b.js.map → chunk-2f7n4qzt.js.map} +3 -3
- package/dist/cli/{chunk-35d4wj9f.js → chunk-2n4ps1b3.js} +7 -7
- package/dist/cli/{chunk-j85vccga.js → chunk-43zcf4gz.js} +97 -57
- package/dist/cli/{chunk-j85vccga.js.map → chunk-43zcf4gz.js.map} +6 -6
- package/dist/cli/{chunk-e04dxsz1.js → chunk-4ejxbzee.js} +1 -1
- package/dist/cli/{chunk-5m5nmvyq.js → chunk-4h8z9vv5.js} +233 -39
- package/dist/cli/chunk-4h8z9vv5.js.map +16 -0
- package/dist/cli/{chunk-5f86nr5m.js → chunk-5cg2rzgf.js} +18 -16
- package/dist/cli/{chunk-5f86nr5m.js.map → chunk-5cg2rzgf.js.map} +2 -2
- package/dist/cli/{chunk-tzmab476.js → chunk-5ffywsmb.js} +4 -4
- package/dist/cli/{chunk-2z928egk.js → chunk-5wj2qwn2.js} +5 -5
- package/dist/cli/chunk-6bm0xpgd.js +18 -0
- package/dist/cli/chunk-6bm0xpgd.js.map +10 -0
- package/dist/cli/{chunk-vg9r4eb9.js → chunk-713mngdq.js} +12 -5
- package/dist/cli/{chunk-vg9r4eb9.js.map → chunk-713mngdq.js.map} +3 -3
- package/dist/cli/{chunk-5xvm6tfj.js → chunk-9geymqns.js} +17 -15
- package/dist/cli/{chunk-5xvm6tfj.js.map → chunk-9geymqns.js.map} +2 -2
- package/dist/cli/{chunk-q58y5e6a.js → chunk-9xr4dgee.js} +26 -17
- package/dist/cli/chunk-9xr4dgee.js.map +10 -0
- package/dist/cli/{chunk-b07cmahc.js → chunk-amrb6hgh.js} +2 -2
- package/dist/cli/chunk-cm89scdv.js +443 -0
- package/dist/cli/chunk-cm89scdv.js.map +11 -0
- package/dist/cli/{chunk-akbpwfxc.js → chunk-cmtcnh9f.js} +2 -2
- package/dist/cli/chunk-cy5bm0j0.js +104 -0
- package/dist/cli/chunk-cy5bm0j0.js.map +11 -0
- package/dist/cli/{chunk-crgn1q09.js → chunk-e7vvk5ya.js} +4 -4
- package/dist/cli/{chunk-pehfxfta.js → chunk-g2z9x01v.js} +6 -98
- package/dist/cli/chunk-g2z9x01v.js.map +10 -0
- package/dist/cli/{chunk-tkacnehg.js → chunk-gp84za0h.js} +2 -2
- package/dist/cli/{chunk-wrr3j9w9.js → chunk-h2a48yhd.js} +186 -63
- package/dist/cli/chunk-h2a48yhd.js.map +36 -0
- package/dist/cli/{chunk-hm3vjy5s.js → chunk-jnwj5kjj.js} +14 -9
- package/dist/cli/{chunk-hm3vjy5s.js.map → chunk-jnwj5kjj.js.map} +4 -4
- package/dist/cli/chunk-kwrg63tm.js +31 -0
- package/dist/cli/chunk-kwrg63tm.js.map +10 -0
- package/dist/cli/{chunk-2hsdwb9n.js → chunk-nd1821sk.js} +18 -16
- package/dist/cli/{chunk-2hsdwb9n.js.map → chunk-nd1821sk.js.map} +2 -2
- package/dist/cli/{chunk-6dsbexzp.js → chunk-ntv4qrt7.js} +17 -15
- package/dist/cli/{chunk-6dsbexzp.js.map → chunk-ntv4qrt7.js.map} +2 -2
- package/dist/cli/{chunk-r9rcc4w7.js → chunk-p804f3pe.js} +7 -7
- package/dist/cli/{chunk-ey84smr6.js → chunk-py62fewh.js} +6 -6
- package/dist/cli/{chunk-ey84smr6.js.map → chunk-py62fewh.js.map} +1 -1
- package/dist/cli/chunk-q3wxxq39.js +126 -0
- package/dist/cli/chunk-q3wxxq39.js.map +11 -0
- package/dist/cli/{chunk-9t7a85s3.js → chunk-rekqz4qg.js} +23 -13
- package/dist/cli/chunk-rekqz4qg.js.map +10 -0
- package/dist/cli/{chunk-8ktnccpt.js → chunk-ta27feev.js} +1 -1
- package/dist/cli/{chunk-pv29h0wf.js → chunk-xkz5xbtv.js} +3476 -822
- package/dist/cli/chunk-xkz5xbtv.js.map +213 -0
- package/dist/cli/{chunk-a58773jm.js → chunk-y4ykbark.js} +2 -2
- package/dist/cli/{chunk-p3v96n38.js → chunk-y5h4r2y0.js} +4 -4
- package/dist/cli/{chunk-acanzt5p.js → chunk-yj17vs39.js} +6 -6
- package/dist/cli/{chunk-p73c0m7w.js → chunk-yxmk4pec.js} +16 -14
- package/dist/cli/{chunk-p73c0m7w.js.map → chunk-yxmk4pec.js.map} +2 -2
- 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/{chunk-c8chx29p.js → chunk-zf748avv.js} +384 -207
- package/dist/cli/chunk-zf748avv.js.map +37 -0
- package/dist/cli/{chunk-g4hq16wv.js → chunk-zhvd6adm.js} +16 -14
- package/dist/cli/{chunk-g4hq16wv.js.map → chunk-zhvd6adm.js.map} +2 -2
- package/dist/cli/index.js +26 -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 +22 -0
- package/dist/types/ai/ask.d.ts +271 -58
- package/dist/types/ai/index.d.ts +2 -2
- 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 +8 -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/components/content/api-field.d.ts +59 -0
- package/dist/types/components/content/api-page.d.ts +95 -0
- package/dist/types/components/layout/nav-utils.d.ts +26 -0
- package/dist/types/components/openapi/helpers.d.ts +137 -0
- package/dist/types/components/openapi/security.d.ts +85 -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/config-input.d.ts +187 -20
- package/dist/types/core/data.d.ts +49 -4
- package/dist/types/core/footer.d.ts +8 -0
- package/dist/types/core/i18n-ui.d.ts +86 -0
- package/dist/types/core/includes.d.ts +8 -2
- package/dist/types/core/localizable.d.ts +7 -0
- package/dist/types/core/page-modes.d.ts +29 -0
- package/dist/types/core/redirect-patterns.d.ts +144 -0
- package/dist/types/core/related.d.ts +40 -0
- package/dist/types/core/schema.d.ts +551 -56
- package/dist/types/core/sources/read.d.ts +5 -0
- package/dist/types/core/types.d.ts +1 -1
- package/dist/types/core/variables.d.ts +42 -0
- package/dist/types/deploy/node-headers.d.ts +13 -2
- package/dist/types/deploy/platforms/cloudflare.d.ts +5 -0
- package/dist/types/deploy/platforms/netlify.d.ts +7 -0
- package/dist/types/deploy/platforms/vercel.d.ts +6 -0
- package/dist/types/deploy/redirects.d.ts +39 -11
- package/dist/types/deploy/vercel-negotiation.d.ts +9 -0
- 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/model.d.ts +12 -6
- package/dist/types/openapi/overlay.d.ts +54 -0
- package/dist/types/openapi/parse.d.ts +13 -0
- package/dist/types/openapi/references.d.ts +14 -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 +111 -0
- package/dist/types/ratelimit/upstash.d.ts +21 -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 +13 -10
- package/dist/types/reference/openapi.d.ts +33 -12
- package/dist/types/reference/options.d.ts +45 -5
- package/dist/types/reference/scalar.d.ts +13 -0
- package/dist/types/reference/schema.d.ts +60 -36
- package/dist/types/search/documents.d.ts +12 -1
- package/dist/types/search/orama-index.d.ts +4 -0
- package/dist/types/seo/metatags.d.ts +20 -0
- package/docs/02-deployment.mdx +41 -5
- package/docs/03-upgrading.mdx +4 -4
- package/docs/04-migrating.mdx +1 -1
- package/docs/advanced/changelog.mdx +2 -2
- package/docs/advanced/skills.mdx +14 -0
- package/docs/cli/audit.mdx +21 -1
- package/docs/cli/index.mdx +1 -0
- package/docs/configuration/analytics.mdx +3 -1
- package/docs/configuration/assistant.mdx +188 -36
- package/docs/configuration/consent.mdx +100 -0
- package/docs/configuration/customization.mdx +4 -4
- package/docs/configuration/index.mdx +63 -5
- package/docs/configuration/meta.ts +3 -0
- package/docs/configuration/narration.mdx +119 -0
- package/docs/configuration/rate-limiting.mdx +69 -0
- package/docs/configuration/search.mdx +29 -1
- package/docs/configuration/theming.mdx +3 -1
- package/docs/content/components.mdx +106 -0
- package/docs/content/frontmatter.mdx +76 -0
- package/docs/content/i18n.mdx +38 -3
- 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 +28 -6
- package/docs/content/sources/contentful.mdx +32 -0
- package/docs/content/sources/custom.mdx +39 -0
- package/docs/content/sources/github-releases.mdx +31 -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 +30 -0
- package/docs/content/sources/remote-mdx.mdx +40 -0
- package/docs/content/sources/sanity.mdx +29 -0
- package/docs/content/sources/strapi.mdx +31 -0
- package/docs/content/syntax.mdx +67 -2
- package/docs/content/variables.mdx +41 -0
- package/docs/discoverability/agent-discovery.mdx +29 -2
- package/docs/discoverability/index.mdx +1 -1
- package/docs/discoverability/llms-txt.mdx +1 -1
- package/docs/discoverability/metadata.mdx +17 -0
- package/docs/references/api-pages.mdx +74 -0
- package/docs/references/asyncapi.mdx +1 -1
- package/docs/references/graphql.mdx +2 -2
- package/docs/references/meta.ts +1 -1
- package/docs/references/openapi.mdx +102 -3
- package/docs/references/scalar.mdx +3 -1
- package/package.json +36 -1
- package/skills/blume/SKILL.md +3 -0
- package/skills/blume-migrate/SKILL.md +5 -5
- package/skills/blume-migrate/references/docusaurus.md +3 -3
- package/skills/blume-migrate/references/mintlify.md +37 -19
- package/skills/blume-migrate/references/nextra.md +2 -2
- package/skills/blume-migrate/references/starlight.md +2 -1
- 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 +73 -20
- package/src/ai/ask-data.ts +6 -0
- package/src/ai/ask-tools.ts +142 -0
- package/src/ai/ask.ts +354 -76
- package/src/ai/component-markdown.ts +75 -0
- package/src/ai/cors.ts +2 -0
- 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/openapi-components.ts +9 -3
- 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 +132 -18
- package/src/astro/index.ts +1 -0
- package/src/astro/integration.ts +35 -0
- package/src/astro/module-types.ts +6 -0
- package/src/astro/templates.ts +283 -30
- package/src/astro/variables.ts +31 -0
- package/src/audit/catalog.ts +19 -0
- 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 +5 -12
- package/src/audit/checks/sitemap.ts +2 -3
- package/src/audit/redirects.ts +46 -15
- package/src/audit/run.ts +2 -0
- package/src/audit/types.ts +2 -1
- package/src/blume-modules.d.ts +7 -0
- 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/command-meta.ts +5 -0
- package/src/cli/commands/doctor.ts +22 -16
- package/src/cli/commands/skill.ts +96 -0
- package/src/cli/index.ts +5 -0
- package/src/cli/required-secrets.ts +71 -29
- 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 +62 -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 +100 -0
- package/src/components/content/api-page.ts +224 -0
- 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 +110 -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/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 +308 -45
- package/src/components/layout/Search.astro +31 -17
- package/src/components/layout/SiteFooter.astro +124 -0
- package/src/components/layout/SiteMetatags.astro +16 -0
- 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/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/analytics.ts +98 -0
- package/src/components/layout/search/flexsearch.ts +17 -5
- package/src/components/layout/search/orama-cloud.ts +26 -14
- package/src/components/layout/search/types.ts +20 -0
- package/src/components/layout/search/typesense.ts +5 -1
- package/src/components/openapi/Operation.astro +161 -13
- package/src/components/openapi/RequestBody.astro +6 -4
- package/src/components/openapi/RequestPanel.astro +59 -7
- package/src/components/openapi/Responses.astro +6 -4
- package/src/components/openapi/async-snippets.ts +4 -4
- package/src/components/openapi/code-samples.ts +97 -0
- package/src/components/openapi/security.ts +6 -3
- package/src/components/openapi/snippets.ts +869 -8
- 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/client-address.ts +19 -0
- package/src/core/config-input.ts +194 -20
- package/src/core/data.ts +44 -4
- package/src/core/diagnostics.ts +3 -2
- package/src/core/footer.ts +26 -0
- package/src/core/graph.ts +39 -36
- package/src/core/i18n-ui.ts +82 -2
- package/src/core/includes.ts +20 -3
- package/src/core/links.ts +16 -2
- 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/project-graph.ts +111 -5
- package/src/core/redirect-patterns.ts +491 -0
- package/src/core/related.ts +131 -0
- package/src/core/schema.ts +318 -64
- package/src/core/sources/normalize.ts +2 -1
- package/src/core/sources/read.ts +8 -3
- package/src/core/types.ts +1 -1
- package/src/core/ui-packs/ar.ts +43 -0
- package/src/core/ui-packs/bg.ts +45 -0
- package/src/core/ui-packs/bn.ts +43 -0
- package/src/core/ui-packs/ca.ts +46 -0
- package/src/core/ui-packs/cs.ts +44 -0
- package/src/core/ui-packs/da.ts +45 -0
- package/src/core/ui-packs/de.ts +46 -0
- package/src/core/ui-packs/el.ts +45 -0
- package/src/core/ui-packs/es.ts +46 -0
- package/src/core/ui-packs/fa.ts +45 -0
- package/src/core/ui-packs/fi.ts +45 -0
- package/src/core/ui-packs/fr.ts +46 -0
- package/src/core/ui-packs/he.ts +43 -0
- package/src/core/ui-packs/hi.ts +44 -0
- package/src/core/ui-packs/hr.ts +45 -0
- package/src/core/ui-packs/hu.ts +44 -0
- package/src/core/ui-packs/id.ts +45 -0
- package/src/core/ui-packs/it.ts +45 -0
- package/src/core/ui-packs/ja.ts +45 -0
- package/src/core/ui-packs/ko.ts +44 -0
- package/src/core/ui-packs/nl.ts +46 -0
- package/src/core/ui-packs/no.ts +44 -0
- package/src/core/ui-packs/pl.ts +45 -0
- package/src/core/ui-packs/pt-br.ts +46 -0
- package/src/core/ui-packs/pt.ts +45 -0
- package/src/core/ui-packs/ro.ts +45 -0
- package/src/core/ui-packs/ru.ts +45 -0
- package/src/core/ui-packs/sk.ts +44 -0
- package/src/core/ui-packs/sr.ts +45 -0
- package/src/core/ui-packs/sv.ts +44 -0
- package/src/core/ui-packs/th.ts +43 -0
- package/src/core/ui-packs/tr.ts +45 -0
- package/src/core/ui-packs/uk.ts +45 -0
- package/src/core/ui-packs/vi.ts +45 -0
- package/src/core/ui-packs/zh-tw.ts +43 -0
- package/src/core/ui-packs/zh.ts +43 -0
- package/src/core/variables.ts +103 -0
- package/src/deploy/artifacts.ts +77 -8
- 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 +32 -0
- package/src/deploy/platforms/netlify.ts +33 -1
- package/src/deploy/platforms/vercel.ts +37 -2
- package/src/deploy/redirects.ts +91 -24
- package/src/deploy/vercel-negotiation.ts +36 -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/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/model.ts +57 -24
- package/src/openapi/overlay.ts +256 -0
- package/src/openapi/parse.ts +55 -2
- package/src/openapi/references.ts +21 -5
- package/src/openapi/render-mdx.ts +5 -1
- package/src/openapi/scalar.ts +40 -6
- package/src/openapi/source.ts +1 -1
- 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 +229 -0
- package/src/ratelimit/schema.ts +28 -0
- package/src/ratelimit/upstash.ts +30 -0
- package/src/ratelimit/wrangler.ts +70 -0
- package/src/reference/asyncapi.ts +2 -2
- package/src/reference/graphql.ts +5 -2
- package/src/reference/openapi.ts +21 -7
- package/src/reference/options.ts +51 -7
- package/src/reference/scalar.ts +33 -8
- package/src/registry/eject.ts +20 -4
- package/src/registry/registry.ts +7 -0
- package/src/search/documents.ts +26 -2
- package/src/search/orama-index.ts +28 -3
- package/src/search/pagefind-ranking.ts +39 -0
- package/src/search/sync/algolia.ts +15 -4
- package/src/search/sync/orama-cloud.ts +3 -0
- package/src/search/sync/typesense.ts +5 -0
- package/src/seo/metatags.ts +67 -0
- package/src/theme/chrome-icons.ts +1 -0
- package/src/theme/color.ts +500 -0
- package/src/theme/entry.ts +56 -3
- package/src/theme/palette.ts +121 -45
- package/src/translate/prompts.ts +1 -0
- package/src/translate/validate.ts +8 -2
- package/src/upgrade/upgrade.ts +4 -171
- 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-9t7a85s3.js.map +0 -10
- package/dist/cli/chunk-c8chx29p.js.map +0 -36
- 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-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-2eytanqx.js.map → chunk-1n228xqb.js.map} +0 -0
- /package/dist/cli/{chunk-35d4wj9f.js.map → chunk-2n4ps1b3.js.map} +0 -0
- /package/dist/cli/{chunk-e04dxsz1.js.map → chunk-4ejxbzee.js.map} +0 -0
- /package/dist/cli/{chunk-tzmab476.js.map → chunk-5ffywsmb.js.map} +0 -0
- /package/dist/cli/{chunk-2z928egk.js.map → chunk-5wj2qwn2.js.map} +0 -0
- /package/dist/cli/{chunk-b07cmahc.js.map → chunk-amrb6hgh.js.map} +0 -0
- /package/dist/cli/{chunk-akbpwfxc.js.map → chunk-cmtcnh9f.js.map} +0 -0
- /package/dist/cli/{chunk-crgn1q09.js.map → chunk-e7vvk5ya.js.map} +0 -0
- /package/dist/cli/{chunk-tkacnehg.js.map → chunk-gp84za0h.js.map} +0 -0
- /package/dist/cli/{chunk-r9rcc4w7.js.map → chunk-p804f3pe.js.map} +0 -0
- /package/dist/cli/{chunk-8ktnccpt.js.map → chunk-ta27feev.js.map} +0 -0
- /package/dist/cli/{chunk-a58773jm.js.map → chunk-y4ykbark.js.map} +0 -0
- /package/dist/cli/{chunk-p3v96n38.js.map → chunk-y5h4r2y0.js.map} +0 -0
- /package/dist/cli/{chunk-acanzt5p.js.map → chunk-yj17vs39.js.map} +0 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { ResolvedConfig } from "../../core/schema.ts";
|
|
2
|
+
import type { Diagnostic } from "../../core/types.ts";
|
|
3
|
+
import { composite, contrastRatio, parseColor } from "../../theme/color.ts";
|
|
4
|
+
import type { Rgba } from "../../theme/color.ts";
|
|
5
|
+
import {
|
|
6
|
+
MIN_TEXT_CONTRAST,
|
|
7
|
+
labelOn,
|
|
8
|
+
resolveAccent,
|
|
9
|
+
resolveAction,
|
|
10
|
+
themeInk,
|
|
11
|
+
} from "../../theme/palette.ts";
|
|
12
|
+
import type { ColorMode } from "../../theme/palette.ts";
|
|
13
|
+
import { finding } from "../catalog.ts";
|
|
14
|
+
import type { CheckModule } from "../types.ts";
|
|
15
|
+
|
|
16
|
+
const MODES = ["light", "dark"] as const satisfies readonly ColorMode[];
|
|
17
|
+
|
|
18
|
+
/** The theme config key a color comes from. */
|
|
19
|
+
type ThemeKey = "theme.accent" | "theme.action" | "theme.background";
|
|
20
|
+
|
|
21
|
+
/** A color the theme draws text in, over the surface it sits on. */
|
|
22
|
+
interface TextPair {
|
|
23
|
+
fix: string;
|
|
24
|
+
key: ThemeKey;
|
|
25
|
+
/** What the pair is, for the message: "Accent text on the page". */
|
|
26
|
+
label: string;
|
|
27
|
+
surface: Rgba;
|
|
28
|
+
text: Rgba;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A theme color, resolved for one mode, and the config key it came from. */
|
|
32
|
+
interface ThemeColor {
|
|
33
|
+
key: ThemeKey;
|
|
34
|
+
value: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What one mode's check found: pairs to measure, colors it couldn't read. */
|
|
38
|
+
interface ModeColors {
|
|
39
|
+
pairs: TextPair[];
|
|
40
|
+
unread: ThemeColor[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A fill and the label the build puts on it (`labelOn`, as `readableOn`). */
|
|
44
|
+
const fillPair = (
|
|
45
|
+
key: "theme.accent" | "theme.action",
|
|
46
|
+
fill: Rgba,
|
|
47
|
+
page: Rgba,
|
|
48
|
+
mode: ColorMode
|
|
49
|
+
): TextPair => ({
|
|
50
|
+
fix: `Neither white nor dark text reaches ${MIN_TEXT_CONTRAST}:1 on this shade. Move \`${key}\`'s ${mode} shade lighter or darker.`,
|
|
51
|
+
key,
|
|
52
|
+
label:
|
|
53
|
+
key === "theme.action"
|
|
54
|
+
? "The call-to-action label"
|
|
55
|
+
: "Button labels on the accent",
|
|
56
|
+
surface: composite(fill, page),
|
|
57
|
+
text: parseColor(labelOn(fill, mode)) ?? fill,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** The accent's pairs: accent text on the page, and labels on accent fills. */
|
|
61
|
+
const accentPairs = (fill: Rgba, page: Rgba, mode: ColorMode): TextPair[] => [
|
|
62
|
+
{
|
|
63
|
+
fix: `${mode === "light" ? "Darken" : "Lighten"} \`theme.accent\`'s ${mode} shade (\`theme.accent: { light, dark }\`).`,
|
|
64
|
+
key: "theme.accent",
|
|
65
|
+
label: "Accent text on the page",
|
|
66
|
+
surface: page,
|
|
67
|
+
text: fill,
|
|
68
|
+
},
|
|
69
|
+
fillPair("theme.accent", fill, page, mode),
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/** The pairs a custom page background sets: body and secondary text on it. */
|
|
73
|
+
const backgroundPairs = (page: Rgba, mode: ColorMode): TextPair[] => {
|
|
74
|
+
const fix = `Pick a \`theme.background\` ${mode} color further from the text color.`;
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
fix,
|
|
78
|
+
key: "theme.background",
|
|
79
|
+
label: "Body text on the page",
|
|
80
|
+
surface: page,
|
|
81
|
+
text: themeInk(mode, "foreground"),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
fix,
|
|
85
|
+
key: "theme.background",
|
|
86
|
+
label: "Secondary text on the page",
|
|
87
|
+
surface: page,
|
|
88
|
+
text: themeInk(mode, "mutedForeground"),
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** Everything one mode draws text in, from the theme config. */
|
|
94
|
+
const modeColors = (
|
|
95
|
+
theme: ResolvedConfig["theme"],
|
|
96
|
+
mode: ColorMode
|
|
97
|
+
): ModeColors => {
|
|
98
|
+
const unread: ThemeColor[] = [];
|
|
99
|
+
const read = (color: ThemeColor): Rgba | null => {
|
|
100
|
+
const parsed = parseColor(color.value);
|
|
101
|
+
if (!parsed) {
|
|
102
|
+
unread.push(color);
|
|
103
|
+
}
|
|
104
|
+
return parsed;
|
|
105
|
+
};
|
|
106
|
+
const custom = theme.background?.[mode];
|
|
107
|
+
const customPage =
|
|
108
|
+
custom === undefined
|
|
109
|
+
? null
|
|
110
|
+
: read({ key: "theme.background", value: custom });
|
|
111
|
+
// Text on a page color the audit can't read can't be measured.
|
|
112
|
+
if (custom !== undefined && !customPage) {
|
|
113
|
+
return { pairs: [], unread };
|
|
114
|
+
}
|
|
115
|
+
const page = customPage ?? themeInk(mode, "background");
|
|
116
|
+
const pairs = customPage ? backgroundPairs(customPage, mode) : [];
|
|
117
|
+
const accent = read({
|
|
118
|
+
key: "theme.accent",
|
|
119
|
+
value: resolveAccent(theme)[mode],
|
|
120
|
+
});
|
|
121
|
+
if (accent) {
|
|
122
|
+
pairs.push(...accentPairs(accent, page, mode));
|
|
123
|
+
}
|
|
124
|
+
const action = resolveAction(theme);
|
|
125
|
+
const actionFill =
|
|
126
|
+
action && read({ key: "theme.action", value: action[mode] });
|
|
127
|
+
if (actionFill) {
|
|
128
|
+
pairs.push(fillPair("theme.action", actionFill, page, mode));
|
|
129
|
+
}
|
|
130
|
+
return { pairs, unread };
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Theme contrast: the text colors a configured theme draws — accent text,
|
|
135
|
+
* labels on accent and call-to-action fills, and body text on a custom
|
|
136
|
+
* background — measured against WCAG AA's 4.5:1 in both color modes, resolved
|
|
137
|
+
* the way the build resolves them (presets per mode, labels by `labelOn`).
|
|
138
|
+
* One finding per failing pair, pointed at blume.config.ts, since a theme
|
|
139
|
+
* color is a site-wide choice, not a page's.
|
|
140
|
+
*/
|
|
141
|
+
export const contrastChecks: CheckModule = {
|
|
142
|
+
category: "accessibility",
|
|
143
|
+
run(context) {
|
|
144
|
+
const { theme } = context.project.config;
|
|
145
|
+
const site = {
|
|
146
|
+
file: context.project.context.configFile ?? undefined,
|
|
147
|
+
url: "/",
|
|
148
|
+
};
|
|
149
|
+
const found: Diagnostic[] = [];
|
|
150
|
+
const reported = new Set<string>();
|
|
151
|
+
for (const mode of MODES) {
|
|
152
|
+
const { pairs, unread } = modeColors(theme, mode);
|
|
153
|
+
for (const color of unread) {
|
|
154
|
+
const id = `${color.key}:${color.value}`;
|
|
155
|
+
if (!reported.has(id)) {
|
|
156
|
+
reported.add(id);
|
|
157
|
+
found.push(
|
|
158
|
+
finding(
|
|
159
|
+
"BLUME_AUDIT_THEME_COLOR_UNCHECKED",
|
|
160
|
+
site,
|
|
161
|
+
`\`${color.key}\` is "${color.value}", which the audit can't read as a color, so its contrast went unchecked.`
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (const pair of pairs) {
|
|
167
|
+
const ratio = contrastRatio(pair.text, pair.surface);
|
|
168
|
+
if (ratio < MIN_TEXT_CONTRAST) {
|
|
169
|
+
found.push(
|
|
170
|
+
finding(
|
|
171
|
+
"BLUME_AUDIT_THEME_CONTRAST_LOW",
|
|
172
|
+
site,
|
|
173
|
+
`${pair.label} in ${mode} mode is ${ratio.toFixed(2)}:1, under the ${MIN_TEXT_CONTRAST}:1 WCAG AA asks of body text (\`${pair.key}\`).`,
|
|
174
|
+
pair.fix
|
|
175
|
+
)
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return found;
|
|
181
|
+
},
|
|
182
|
+
tier: "static",
|
|
183
|
+
};
|
package/src/audit/checks/i18n.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { normalizeBasePath, stripBasePath } from "../../core/base-path.ts";
|
|
|
2
2
|
import type { Diagnostic } from "../../core/types.ts";
|
|
3
3
|
import { finding } from "../catalog.ts";
|
|
4
4
|
import { pageSite } from "../locate.ts";
|
|
5
|
+
import { redirectAt } from "../redirects.ts";
|
|
5
6
|
import type { AuditContext, CheckModule, PageSnapshot } from "../types.ts";
|
|
6
7
|
import { decodePath, normalizePath } from "../url.ts";
|
|
7
8
|
import { isArchivedLatestCanonical } from "./indexability.ts";
|
|
@@ -134,9 +135,7 @@ const hreflangChecks = (
|
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
const target = context.byUrl.get(path);
|
|
137
|
-
const redirect = context.redirects
|
|
138
|
-
(entry) => normalizePath(entry.from) === path
|
|
139
|
-
);
|
|
138
|
+
const redirect = redirectAt(context.redirects, path);
|
|
140
139
|
if (redirect) {
|
|
141
140
|
found.push(
|
|
142
141
|
finding(
|
|
@@ -3,6 +3,7 @@ import type { Diagnostic } from "../../core/types.ts";
|
|
|
3
3
|
import { deployPlatform } from "../../deploy/platforms/index.ts";
|
|
4
4
|
import { finding } from "../catalog.ts";
|
|
5
5
|
import { pageSite } from "../locate.ts";
|
|
6
|
+
import { redirectAt } from "../redirects.ts";
|
|
6
7
|
import { ERROR_ROUTES } from "../types.ts";
|
|
7
8
|
import type { AuditContext, CheckModule, PageSnapshot } from "../types.ts";
|
|
8
9
|
import { decodePath, normalizePath, siteOrigin } from "../url.ts";
|
|
@@ -118,9 +119,7 @@ const canonicalChecks = (
|
|
|
118
119
|
|
|
119
120
|
// The canonical points at another page on this site. It must exist, and it
|
|
120
121
|
// must not itself redirect — a canonical to a redirect is a dead end.
|
|
121
|
-
const redirect = context.redirects
|
|
122
|
-
(entry) => normalizePath(entry.from) === target
|
|
123
|
-
);
|
|
122
|
+
const redirect = redirectAt(context.redirects, target);
|
|
124
123
|
if (redirect) {
|
|
125
124
|
found.push(
|
|
126
125
|
finding(
|
|
@@ -3,13 +3,9 @@ import type { Diagnostic } from "../../core/types.ts";
|
|
|
3
3
|
import { finding } from "../catalog.ts";
|
|
4
4
|
import { orphanPages } from "../graph.ts";
|
|
5
5
|
import { pageSite } from "../locate.ts";
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
PageSnapshot,
|
|
10
|
-
SnapshotLink,
|
|
11
|
-
} from "../types.ts";
|
|
12
|
-
import { isServed, normalizePath, resolveHref, siteOrigin } from "../url.ts";
|
|
6
|
+
import { redirectAt } from "../redirects.ts";
|
|
7
|
+
import type { CheckModule, PageSnapshot, SnapshotLink } from "../types.ts";
|
|
8
|
+
import { isServed, resolveHref, siteOrigin } from "../url.ts";
|
|
13
9
|
|
|
14
10
|
/** Browser-magic fragments that scroll without needing a matching id. */
|
|
15
11
|
const MAGIC_FRAGMENTS = new Set(["", "top"]);
|
|
@@ -33,9 +29,6 @@ const anchorResolves = (target: PageSnapshot, fragment: string): boolean => {
|
|
|
33
29
|
}
|
|
34
30
|
};
|
|
35
31
|
|
|
36
|
-
const redirectFrom = (context: AuditContext, path: string) =>
|
|
37
|
-
context.redirects.find((entry) => normalizePath(entry.from) === path);
|
|
38
|
-
|
|
39
32
|
/**
|
|
40
33
|
* Internal links, and what they land on.
|
|
41
34
|
*
|
|
@@ -150,7 +143,7 @@ export const linkChecks: CheckModule = {
|
|
|
150
143
|
}
|
|
151
144
|
|
|
152
145
|
const { path } = resolved;
|
|
153
|
-
const redirect =
|
|
146
|
+
const redirect = redirectAt(context.redirects, path);
|
|
154
147
|
if (redirect) {
|
|
155
148
|
if (link.content) {
|
|
156
149
|
found.push(
|
|
@@ -200,7 +193,7 @@ export const linkChecks: CheckModule = {
|
|
|
200
193
|
);
|
|
201
194
|
}
|
|
202
195
|
for (const [path, page] of redirectedChrome) {
|
|
203
|
-
const redirect =
|
|
196
|
+
const redirect = redirectAt(context.redirects, path);
|
|
204
197
|
found.push(
|
|
205
198
|
finding(
|
|
206
199
|
"BLUME_AUDIT_LINK_TO_REDIRECT",
|
|
@@ -2,6 +2,7 @@ import { normalizeBasePath, stripBasePath } from "../../core/base-path.ts";
|
|
|
2
2
|
import type { Diagnostic } from "../../core/types.ts";
|
|
3
3
|
import { finding } from "../catalog.ts";
|
|
4
4
|
import { pageSite } from "../locate.ts";
|
|
5
|
+
import { redirectAt } from "../redirects.ts";
|
|
5
6
|
import type { AuditContext, CheckModule } from "../types.ts";
|
|
6
7
|
import { decodePath, normalizePath, siteOrigin } from "../url.ts";
|
|
7
8
|
|
|
@@ -100,9 +101,7 @@ const checkListedUrl = (
|
|
|
100
101
|
);
|
|
101
102
|
const page = context.byUrl.get(path);
|
|
102
103
|
if (!page) {
|
|
103
|
-
const redirect = context.redirects
|
|
104
|
-
(entry) => normalizePath(entry.from) === path
|
|
105
|
-
);
|
|
104
|
+
const redirect = redirectAt(context.redirects, path);
|
|
106
105
|
return [
|
|
107
106
|
finding(
|
|
108
107
|
"BLUME_AUDIT_SITEMAP_BAD_URL",
|
package/src/audit/redirects.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isPatternPath,
|
|
3
|
+
patternDestination,
|
|
4
|
+
} from "../core/redirect-patterns.ts";
|
|
1
5
|
import type { RedirectResolution } from "./types.ts";
|
|
2
6
|
import { normalizePath } from "./url.ts";
|
|
3
7
|
|
|
@@ -7,18 +11,6 @@ interface ConfiguredRedirect {
|
|
|
7
11
|
status: number;
|
|
8
12
|
}
|
|
9
13
|
|
|
10
|
-
/**
|
|
11
|
-
* Follow every configured redirect through to its destination, classifying what
|
|
12
|
-
* it lands on.
|
|
13
|
-
*
|
|
14
|
-
* - `loop` — the chain revisits a hop it has already been to. Never resolves.
|
|
15
|
-
* - `broken` — the chain ends somewhere the build does not serve.
|
|
16
|
-
* - `chain` — it resolves, but through at least one intermediate redirect.
|
|
17
|
-
* - `ok` — one hop, straight to a real page.
|
|
18
|
-
*
|
|
19
|
-
* An external destination (`https://…`) is always `ok`: it's outside the site,
|
|
20
|
-
* so there's no local page to check it against.
|
|
21
|
-
*/
|
|
22
14
|
/**
|
|
23
15
|
* A destination's page path: the part before any query string or fragment. A
|
|
24
16
|
* redirect to `/guide#setup` or `/search?q=x` lands on the `/guide` / `/search`
|
|
@@ -30,6 +22,22 @@ const pathOnly = (value: string): string => {
|
|
|
30
22
|
return cut === -1 ? value : value.slice(0, cut);
|
|
31
23
|
};
|
|
32
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Follow every configured redirect through to its destination, classifying what
|
|
27
|
+
* it lands on.
|
|
28
|
+
*
|
|
29
|
+
* - `loop` — the chain revisits a hop it has already been to. Never resolves.
|
|
30
|
+
* - `broken` — the chain ends somewhere the build does not serve.
|
|
31
|
+
* - `chain` — it resolves, but through at least one intermediate redirect.
|
|
32
|
+
* - `ok` — one hop, straight to a real page.
|
|
33
|
+
* - `pattern` — a pattern (`/beta/:slug*`), which covers paths rather than
|
|
34
|
+
* naming one, so there is no single chain to walk; {@link redirectAt}
|
|
35
|
+
* resolves a path it covers.
|
|
36
|
+
*
|
|
37
|
+
* An external destination (`https://…`) is always `ok`: it's outside the site,
|
|
38
|
+
* so there's no local page to check it against. A hop onto a path a pattern
|
|
39
|
+
* covers continues through that pattern.
|
|
40
|
+
*/
|
|
33
41
|
export const resolveRedirects = (
|
|
34
42
|
redirects: readonly ConfiguredRedirect[],
|
|
35
43
|
/** Whether the build serves a normalized path — see `isServed`. */
|
|
@@ -39,9 +47,14 @@ export const resolveRedirects = (
|
|
|
39
47
|
for (const redirect of redirects) {
|
|
40
48
|
byFrom.set(normalizePath(redirect.from), redirect);
|
|
41
49
|
}
|
|
50
|
+
const hopFrom = (path: string): string | undefined =>
|
|
51
|
+
byFrom.get(path)?.to ?? patternDestination(redirects, path);
|
|
42
52
|
|
|
43
53
|
return redirects.map((redirect) => {
|
|
44
54
|
const from = normalizePath(redirect.from);
|
|
55
|
+
if (isPatternPath(redirect.from)) {
|
|
56
|
+
return { ...redirect, chain: [from], outcome: "pattern" as const };
|
|
57
|
+
}
|
|
45
58
|
const chain: string[] = [from];
|
|
46
59
|
const seen = new Set<string>([from]);
|
|
47
60
|
let current = redirect.to;
|
|
@@ -63,11 +76,11 @@ export const resolveRedirects = (
|
|
|
63
76
|
}
|
|
64
77
|
chain.push(next);
|
|
65
78
|
seen.add(next);
|
|
66
|
-
const hop =
|
|
67
|
-
if (
|
|
79
|
+
const hop = hopFrom(next);
|
|
80
|
+
if (hop === undefined) {
|
|
68
81
|
break;
|
|
69
82
|
}
|
|
70
|
-
current = hop
|
|
83
|
+
current = hop;
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
const destination = chain.at(-1) ?? from;
|
|
@@ -84,3 +97,21 @@ export const resolveRedirects = (
|
|
|
84
97
|
};
|
|
85
98
|
});
|
|
86
99
|
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The configured redirect a path takes, for the checks that report a link,
|
|
103
|
+
* canonical, sitemap entry, or `hreflang` pointing through one: the exact
|
|
104
|
+
* redirect from that path, else the pattern covering it, its `to` filled in
|
|
105
|
+
* for this path.
|
|
106
|
+
*/
|
|
107
|
+
export const redirectAt = (
|
|
108
|
+
redirects: readonly RedirectResolution[],
|
|
109
|
+
path: string
|
|
110
|
+
): { to: string } | undefined => {
|
|
111
|
+
const exact = redirects.find((entry) => normalizePath(entry.from) === path);
|
|
112
|
+
if (exact) {
|
|
113
|
+
return exact;
|
|
114
|
+
}
|
|
115
|
+
const to = patternDestination(redirects, path);
|
|
116
|
+
return to === undefined ? undefined : { to };
|
|
117
|
+
};
|
package/src/audit/run.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { CHECKS } from "./catalog.ts";
|
|
|
12
12
|
import type { CheckId } from "./catalog.ts";
|
|
13
13
|
import { assetChecks } from "./checks/assets.ts";
|
|
14
14
|
import { contentChecks } from "./checks/content.ts";
|
|
15
|
+
import { contrastChecks } from "./checks/contrast.ts";
|
|
15
16
|
import { dnsAidChecks } from "./checks/dns-aid.ts";
|
|
16
17
|
import { duplicateChecks } from "./checks/duplicates.ts";
|
|
17
18
|
import { i18nChecks } from "./checks/i18n.ts";
|
|
@@ -51,6 +52,7 @@ const MODULES: CheckModule[] = [
|
|
|
51
52
|
ogImageChecks,
|
|
52
53
|
i18nChecks,
|
|
53
54
|
assetChecks,
|
|
55
|
+
contrastChecks,
|
|
54
56
|
sitemapChecks,
|
|
55
57
|
robotsChecks,
|
|
56
58
|
llmsChecks,
|
package/src/audit/types.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
export type AuditTier = "static" | "network" | "external";
|
|
14
14
|
|
|
15
15
|
export type AuditCategory =
|
|
16
|
+
| "accessibility"
|
|
16
17
|
| "content"
|
|
17
18
|
| "duplicates"
|
|
18
19
|
| "indexability"
|
|
@@ -110,7 +111,7 @@ export interface RedirectResolution {
|
|
|
110
111
|
status: number;
|
|
111
112
|
/** Every hop from `from` to the final target, inclusive. */
|
|
112
113
|
chain: string[];
|
|
113
|
-
outcome: "ok" | "loop" | "broken" | "chain";
|
|
114
|
+
outcome: "ok" | "loop" | "broken" | "chain" | "pattern";
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
/** One urlset file behind a sitemap, for the per-file size limits. */
|
package/src/blume-modules.d.ts
CHANGED
|
@@ -15,6 +15,13 @@ declare module "blume:search-client" {
|
|
|
15
15
|
export const createSearch: () => Fn | Promise<Fn>;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
declare module "blume:consent-client" {
|
|
19
|
+
/** Start the configured consent adapter's browser module (see `consentClientTemplate`); a no-op for one without. */
|
|
20
|
+
// oxlint-disable-next-line typescript/consistent-type-imports
|
|
21
|
+
type Consent = import("./components/layout/consent/types.ts").BlumeConsent;
|
|
22
|
+
export const startConsentClient: (consent: Consent) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
declare module "blume:ask" {
|
|
19
26
|
/** The generated Ask trigger (see `askComponentTemplate`); empty when Ask is off. */
|
|
20
27
|
// Mirrors the generated `.blume/src/env.d.ts` declaration: the trigger's type
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser half of the bot check: a fresh token for each question the
|
|
3
|
+
* assistant sends (both providers' tokens are single-use). The provider's
|
|
4
|
+
* script loads with the first question rather than with the page, and one
|
|
5
|
+
* widget serves every question after it: an invisible hCaptcha, or a
|
|
6
|
+
* Turnstile that only shows itself when it needs the reader to interact,
|
|
7
|
+
* in a small container at the foot of the viewport.
|
|
8
|
+
*
|
|
9
|
+
* The client router swaps `<body>` on every navigation, taking the container
|
|
10
|
+
* and its widget with it, so a question after a swap mounts a new one.
|
|
11
|
+
*/
|
|
12
|
+
import type { CaptchaSettings } from "./schema.ts";
|
|
13
|
+
|
|
14
|
+
/** Each provider's script, rendering only when asked to. */
|
|
15
|
+
export const CAPTCHA_SCRIPTS = {
|
|
16
|
+
hcaptcha: "https://js.hcaptcha.com/1/api.js?render=explicit",
|
|
17
|
+
turnstile:
|
|
18
|
+
"https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit",
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
/** What Blume asks a Turnstile widget for. */
|
|
22
|
+
interface TurnstileRenderOptions {
|
|
23
|
+
appearance: "interaction-only";
|
|
24
|
+
callback: (token: string) => void;
|
|
25
|
+
"error-callback": () => void;
|
|
26
|
+
execution: "execute";
|
|
27
|
+
sitekey: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The slice of `window.turnstile` the check uses. */
|
|
31
|
+
export interface TurnstileApi {
|
|
32
|
+
execute: (widget: string) => void;
|
|
33
|
+
render: (container: HTMLElement, options: TurnstileRenderOptions) => string;
|
|
34
|
+
reset: (widget: string) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What Blume asks an hCaptcha widget for. */
|
|
38
|
+
interface HcaptchaRenderOptions {
|
|
39
|
+
sitekey: string;
|
|
40
|
+
size: "invisible";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The slice of `window.hcaptcha` the check uses. */
|
|
44
|
+
export interface HcaptchaApi {
|
|
45
|
+
execute: (
|
|
46
|
+
widget: string,
|
|
47
|
+
options: { async: true }
|
|
48
|
+
) => Promise<{ response: string }>;
|
|
49
|
+
render: (container: HTMLElement, options: HcaptchaRenderOptions) => string;
|
|
50
|
+
reset: (widget: string) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The window, with the providers' globals once their scripts ran. */
|
|
54
|
+
type CaptchaWindow = Window & {
|
|
55
|
+
hcaptcha?: HcaptchaApi;
|
|
56
|
+
turnstile?: TurnstileApi;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** A Turnstile token on its way. */
|
|
60
|
+
interface PendingToken {
|
|
61
|
+
reject: (error: Error) => void;
|
|
62
|
+
resolve: (token: string) => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Get a token for the next question. */
|
|
66
|
+
export type CaptchaTokenGetter = (settings: CaptchaSettings) => Promise<string>;
|
|
67
|
+
|
|
68
|
+
/** A token getter with its own script, container, and widget. */
|
|
69
|
+
export const createCaptchaClient = (): CaptchaTokenGetter => {
|
|
70
|
+
let loaded: Promise<void> | undefined;
|
|
71
|
+
let host: HTMLElement | undefined;
|
|
72
|
+
let widget: string | undefined;
|
|
73
|
+
let pending: PendingToken | undefined;
|
|
74
|
+
|
|
75
|
+
const load = (kind: CaptchaSettings["kind"]): Promise<void> => {
|
|
76
|
+
// oxlint-disable-next-line promise/avoid-new -- adapt the script tag's load events
|
|
77
|
+
loaded ??= new Promise<void>((resolve, reject) => {
|
|
78
|
+
const tag = document.createElement("script");
|
|
79
|
+
tag.async = true;
|
|
80
|
+
tag.src = CAPTCHA_SCRIPTS[kind];
|
|
81
|
+
tag.addEventListener("load", () => resolve());
|
|
82
|
+
tag.addEventListener("error", () => {
|
|
83
|
+
// Let the next question try again.
|
|
84
|
+
loaded = undefined;
|
|
85
|
+
reject(new Error(`The ${kind} script didn't load.`));
|
|
86
|
+
});
|
|
87
|
+
document.head.append(tag);
|
|
88
|
+
});
|
|
89
|
+
return loaded;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// The widget's container, mounted again when a swap took the last one.
|
|
93
|
+
const mount = (): HTMLElement => {
|
|
94
|
+
if (host?.isConnected) {
|
|
95
|
+
return host;
|
|
96
|
+
}
|
|
97
|
+
host = document.createElement("div");
|
|
98
|
+
host.className = "fixed bottom-4 left-1/2 z-[60] -translate-x-1/2";
|
|
99
|
+
host.dataset.blumeCaptcha = "";
|
|
100
|
+
document.body.append(host);
|
|
101
|
+
widget = undefined;
|
|
102
|
+
return host;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const turnstileToken = (api: TurnstileApi, siteKey: string) =>
|
|
106
|
+
// oxlint-disable-next-line promise/avoid-new -- adapt Turnstile's token callbacks
|
|
107
|
+
new Promise<string>((resolve, reject) => {
|
|
108
|
+
pending = { reject, resolve };
|
|
109
|
+
const target = mount();
|
|
110
|
+
if (widget === undefined) {
|
|
111
|
+
widget = api.render(target, {
|
|
112
|
+
appearance: "interaction-only",
|
|
113
|
+
callback: (token) => pending?.resolve(token),
|
|
114
|
+
"error-callback": () =>
|
|
115
|
+
pending?.reject(new Error("The Turnstile check failed.")),
|
|
116
|
+
execution: "execute",
|
|
117
|
+
sitekey: siteKey,
|
|
118
|
+
});
|
|
119
|
+
} else {
|
|
120
|
+
api.reset(widget);
|
|
121
|
+
}
|
|
122
|
+
api.execute(widget);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const hcaptchaToken = async (api: HcaptchaApi, siteKey: string) => {
|
|
126
|
+
const target = mount();
|
|
127
|
+
if (widget === undefined) {
|
|
128
|
+
widget = api.render(target, { sitekey: siteKey, size: "invisible" });
|
|
129
|
+
} else {
|
|
130
|
+
api.reset(widget);
|
|
131
|
+
}
|
|
132
|
+
const { response } = await api.execute(widget, { async: true });
|
|
133
|
+
return response;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
return async (settings) => {
|
|
137
|
+
await load(settings.kind);
|
|
138
|
+
// SAFETY: CaptchaWindow only adds the providers' optional globals, each
|
|
139
|
+
// checked before use.
|
|
140
|
+
const { hcaptcha, turnstile } = window as CaptchaWindow;
|
|
141
|
+
if (settings.kind === "hcaptcha" && hcaptcha) {
|
|
142
|
+
return hcaptchaToken(hcaptcha, settings.siteKey);
|
|
143
|
+
}
|
|
144
|
+
if (settings.kind === "turnstile" && turnstile) {
|
|
145
|
+
return turnstileToken(turnstile, settings.siteKey);
|
|
146
|
+
}
|
|
147
|
+
throw new Error(`The ${settings.kind} script loaded without its API.`);
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/** The page's token getter: one script and one widget per page load. */
|
|
152
|
+
export const captchaToken = createCaptchaClient();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import type { AdapterDescriptor } from "../core/adapter.ts";
|
|
4
|
+
import { adapterDescriptorSchema } from "../core/adapter.ts";
|
|
5
|
+
|
|
6
|
+
/** Options for {@link hcaptcha}. */
|
|
7
|
+
export interface HcaptchaOptions {
|
|
8
|
+
/** The site key, from the hCaptcha dashboard. */
|
|
9
|
+
siteKey: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const hcaptchaOptionsSchema = z.strictObject({
|
|
13
|
+
siteKey: z.string().min(1),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type HcaptchaAdapter = AdapterDescriptor<"hcaptcha", HcaptchaOptions>;
|
|
17
|
+
|
|
18
|
+
export const hcaptchaAdapterSchema = adapterDescriptorSchema(
|
|
19
|
+
"hcaptcha",
|
|
20
|
+
hcaptchaOptionsSchema
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
/** Where the assistant's route checks an hCaptcha token. */
|
|
24
|
+
export const HCAPTCHA_VERIFY_URL = "https://api.hcaptcha.com/siteverify";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* hCaptcha, as an invisible check: most readers never see a challenge. The
|
|
28
|
+
* site key is public and goes to the browser; the secret the route checks
|
|
29
|
+
* tokens with comes from `HCAPTCHA_SECRET_KEY`.
|
|
30
|
+
*/
|
|
31
|
+
export const hcaptcha = (options: HcaptchaOptions): HcaptchaAdapter => ({
|
|
32
|
+
kind: "hcaptcha",
|
|
33
|
+
options,
|
|
34
|
+
requiredSecrets: ["HCAPTCHA_SECRET_KEY"],
|
|
35
|
+
runtimeDeps: [],
|
|
36
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bot checks for the assistant (`ai.assistant.captcha` in `blume.config.ts`).
|
|
3
|
+
* The assistant gets a token from the check before each question and the
|
|
4
|
+
* generated route verifies it before calling the model:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { defineConfig } from "blume";
|
|
8
|
+
* import { turnstile } from "blume/captcha";
|
|
9
|
+
*
|
|
10
|
+
* export default defineConfig({
|
|
11
|
+
* ai: {
|
|
12
|
+
* assistant: {
|
|
13
|
+
* enabled: true,
|
|
14
|
+
* captcha: turnstile({ siteKey: "0x4AAAAAAA…" }),
|
|
15
|
+
* },
|
|
16
|
+
* },
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Each factory returns a plain descriptor (see `core/adapter.ts`); the browser
|
|
21
|
+
* half is `client.ts`, the server half `verify.ts`.
|
|
22
|
+
*/
|
|
23
|
+
export type { AdapterDescriptor, JsonValue } from "../core/adapter.ts";
|
|
24
|
+
export { hcaptcha } from "./hcaptcha.ts";
|
|
25
|
+
export type { HcaptchaAdapter, HcaptchaOptions } from "./hcaptcha.ts";
|
|
26
|
+
export type { CaptchaAdapter, CaptchaSettings } from "./schema.ts";
|
|
27
|
+
export { turnstile } from "./turnstile.ts";
|
|
28
|
+
export type { TurnstileAdapter, TurnstileOptions } from "./turnstile.ts";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import { hcaptchaAdapterSchema } from "./hcaptcha.ts";
|
|
4
|
+
import { turnstileAdapterSchema } from "./turnstile.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The configured bot check, as its factory returned it. Anything that isn't
|
|
8
|
+
* one fails with the adapter hint; an adapter's own option errors keep their
|
|
9
|
+
* messages.
|
|
10
|
+
*/
|
|
11
|
+
export const captchaAdapterSchema = z.discriminatedUnion(
|
|
12
|
+
"kind",
|
|
13
|
+
[hcaptchaAdapterSchema, turnstileAdapterSchema],
|
|
14
|
+
{
|
|
15
|
+
error:
|
|
16
|
+
'captcha takes one adapter — e.g. `captcha: turnstile({ siteKey })` or `captcha: hcaptcha({ siteKey })`, imported from "blume/captcha".',
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export type CaptchaAdapter = z.output<typeof captchaAdapterSchema>;
|
|
21
|
+
|
|
22
|
+
/** What the browser needs to run the check: never the secret. */
|
|
23
|
+
export interface CaptchaSettings {
|
|
24
|
+
kind: CaptchaAdapter["kind"];
|
|
25
|
+
siteKey: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The public half of an adapter, for the data snapshot. */
|
|
29
|
+
export const captchaSettings = (adapter: CaptchaAdapter): CaptchaSettings => ({
|
|
30
|
+
kind: adapter.kind,
|
|
31
|
+
siteKey: adapter.options.siteKey,
|
|
32
|
+
});
|