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,31 @@
|
|
|
1
|
+
import { substituteVariables } from "../core/variables.ts";
|
|
2
|
+
import type { ContentVariables } from "../core/variables.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The MDX half of content variables (see `core/variables.ts`): a Vite plugin
|
|
6
|
+
* that replaces `{{name}}` in an `.mdx` module's source before the MDX
|
|
7
|
+
* compiler reads it. It has to run first: the compiler parses `{{version}}`
|
|
8
|
+
* as a JavaScript expression (a `ReferenceError` at render) and rejects
|
|
9
|
+
* `{{api-url}}` outright, so no plugin after parsing could see either.
|
|
10
|
+
*/
|
|
11
|
+
/** The Vite plugin slice this needs (structurally typed, like Blume's other plugins). */
|
|
12
|
+
export interface VariablesVitePlugin {
|
|
13
|
+
enforce: "pre";
|
|
14
|
+
name: string;
|
|
15
|
+
transform: (code: string, id: string) => { code: string; map: null } | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const variablesVitePlugin = (
|
|
19
|
+
variables: ContentVariables
|
|
20
|
+
): VariablesVitePlugin => ({
|
|
21
|
+
enforce: "pre",
|
|
22
|
+
name: "blume:variables",
|
|
23
|
+
transform(code, id) {
|
|
24
|
+
const [path = ""] = id.split("?");
|
|
25
|
+
if (!path.endsWith(".mdx")) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const next = substituteVariables(code, variables);
|
|
29
|
+
return next === code ? null : { code: next, map: null };
|
|
30
|
+
},
|
|
31
|
+
});
|
package/src/audit/catalog.ts
CHANGED
|
@@ -161,7 +161,7 @@ export const CHECKS = [
|
|
|
161
161
|
},
|
|
162
162
|
{
|
|
163
163
|
category: "duplicates",
|
|
164
|
-
fix: "Merge the pages, or
|
|
164
|
+
fix: "Merge the pages, or point `seo.canonical` on all but one at the page to index.",
|
|
165
165
|
id: "BLUME_AUDIT_DUPLICATE_CONTENT",
|
|
166
166
|
severity: "warning",
|
|
167
167
|
tier: "static",
|
|
@@ -359,7 +359,7 @@ export const CHECKS = [
|
|
|
359
359
|
},
|
|
360
360
|
{
|
|
361
361
|
category: "redirects",
|
|
362
|
-
fix: "Remove the redirect, or delete the page it shadows —
|
|
362
|
+
fix: "Remove the redirect, or delete the page it shadows — a path can't be both, and which one readers get depends on the host.",
|
|
363
363
|
id: "BLUME_AUDIT_REDIRECT_SOURCE_IS_PAGE",
|
|
364
364
|
severity: "error",
|
|
365
365
|
tier: "static",
|
|
@@ -373,6 +373,14 @@ export const CHECKS = [
|
|
|
373
373
|
tier: "network",
|
|
374
374
|
title: "HTTPS to HTTP redirect",
|
|
375
375
|
},
|
|
376
|
+
{
|
|
377
|
+
category: "redirects",
|
|
378
|
+
fix: "Deploy the redirect file your host reads (see Redirects in the deployment docs), or name the host in `deployment`.",
|
|
379
|
+
id: "BLUME_AUDIT_REDIRECT_NOT_SERVED",
|
|
380
|
+
severity: "error",
|
|
381
|
+
tier: "network",
|
|
382
|
+
title: "Live site doesn't redirect as configured",
|
|
383
|
+
},
|
|
376
384
|
|
|
377
385
|
// Social
|
|
378
386
|
{
|
|
@@ -498,6 +506,25 @@ export const CHECKS = [
|
|
|
498
506
|
title: "Hreflang group has a language conflict",
|
|
499
507
|
},
|
|
500
508
|
|
|
509
|
+
// Accessibility. Theme colors are one per site, not per page: these run
|
|
510
|
+
// on the config, the way the build resolves it.
|
|
511
|
+
{
|
|
512
|
+
category: "accessibility",
|
|
513
|
+
fix: "Pick a darker shade for light mode or a lighter one for dark mode, per mode with `theme.accent: { light, dark }`.",
|
|
514
|
+
id: "BLUME_AUDIT_THEME_CONTRAST_LOW",
|
|
515
|
+
severity: "warning",
|
|
516
|
+
tier: "static",
|
|
517
|
+
title: "Theme colors miss WCAG AA contrast",
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
category: "accessibility",
|
|
521
|
+
fix: "Write the color as hex, `rgb()`, `hsl()`, `hwb()`, `lab()`, `lch()`, `oklab()`, `oklch()`, or a named color so its contrast can be checked.",
|
|
522
|
+
id: "BLUME_AUDIT_THEME_COLOR_UNCHECKED",
|
|
523
|
+
severity: "info",
|
|
524
|
+
tier: "static",
|
|
525
|
+
title: "Theme color can't be checked for contrast",
|
|
526
|
+
},
|
|
527
|
+
|
|
501
528
|
// Assets
|
|
502
529
|
{
|
|
503
530
|
category: "assets",
|
|
@@ -551,7 +578,7 @@ export const CHECKS = [
|
|
|
551
578
|
// Sitemap
|
|
552
579
|
{
|
|
553
580
|
category: "sitemap",
|
|
554
|
-
fix: "Remove `draft`/`hidden`/`noindex` from the page's frontmatter if it should be indexed.",
|
|
581
|
+
fix: "Remove `draft`/`hidden`/`noindex` from the page's frontmatter if it should be indexed. A `public/sitemap.xml` replaces the generated sitemap, so add the page there or delete that file.",
|
|
555
582
|
id: "BLUME_AUDIT_INDEXABLE_PAGE_NOT_IN_SITEMAP",
|
|
556
583
|
severity: "warning",
|
|
557
584
|
tier: "static",
|
|
@@ -647,6 +674,14 @@ export const CHECKS = [
|
|
|
647
674
|
tier: "static",
|
|
648
675
|
title: "robots.txt disallows a page that is in the sitemap",
|
|
649
676
|
},
|
|
677
|
+
{
|
|
678
|
+
category: "robots",
|
|
679
|
+
fix: "Narrow or remove the rule in that crawler's `User-agent` group if it should read these pages. Blocking an AI crawler on purpose is fine; skip this check.",
|
|
680
|
+
id: "BLUME_AUDIT_ROBOTS_BLOCKS_CRAWLER",
|
|
681
|
+
severity: "warning",
|
|
682
|
+
tier: "static",
|
|
683
|
+
title: "robots.txt blocks one crawler from pages in the sitemap",
|
|
684
|
+
},
|
|
650
685
|
{
|
|
651
686
|
category: "robots",
|
|
652
687
|
fix: "Set `deployment.site` so robots.txt can reference the sitemap.",
|
|
@@ -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
|
*
|
|
@@ -108,6 +101,12 @@ export const linkChecks: CheckModule = {
|
|
|
108
101
|
return;
|
|
109
102
|
}
|
|
110
103
|
if (resolved.kind === "outside-base") {
|
|
104
|
+
// A full URL on this host outside the base names another app there
|
|
105
|
+
// (the navigation docs say to link one that way), not a page of this
|
|
106
|
+
// build that lost its base.
|
|
107
|
+
if (resolved.absolute) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
111
110
|
if (link.content) {
|
|
112
111
|
found.push(
|
|
113
112
|
finding(
|
|
@@ -150,7 +149,7 @@ export const linkChecks: CheckModule = {
|
|
|
150
149
|
}
|
|
151
150
|
|
|
152
151
|
const { path } = resolved;
|
|
153
|
-
const redirect =
|
|
152
|
+
const redirect = redirectAt(context.redirects, path);
|
|
154
153
|
if (redirect) {
|
|
155
154
|
if (link.content) {
|
|
156
155
|
found.push(
|
|
@@ -200,7 +199,7 @@ export const linkChecks: CheckModule = {
|
|
|
200
199
|
);
|
|
201
200
|
}
|
|
202
201
|
for (const [path, page] of redirectedChrome) {
|
|
203
|
-
const redirect =
|
|
202
|
+
const redirect = redirectAt(context.redirects, path);
|
|
204
203
|
found.push(
|
|
205
204
|
finding(
|
|
206
205
|
"BLUME_AUDIT_LINK_TO_REDIRECT",
|
|
@@ -222,11 +221,18 @@ export const linkChecks: CheckModule = {
|
|
|
222
221
|
|
|
223
222
|
const homeUrl = normalizeBasePath(context.project.config.basePath) || "/";
|
|
224
223
|
for (const page of orphanPages(context.pages, context.graph, homeUrl)) {
|
|
224
|
+
// A `sidebar.hidden` page is in no other page's navigation either, so
|
|
225
|
+
// it isn't reachable from the sidebar at all.
|
|
226
|
+
const fromNavigation = [
|
|
227
|
+
...(context.graph.chromeIn.get(page.url) ?? []),
|
|
228
|
+
].some((from) => from !== page.url);
|
|
225
229
|
found.push(
|
|
226
230
|
finding(
|
|
227
231
|
"BLUME_AUDIT_ORPHAN_PAGE",
|
|
228
232
|
pageSite(context, page),
|
|
229
|
-
|
|
233
|
+
fromNavigation
|
|
234
|
+
? "No other page's body links here — it is reachable only from the sidebar."
|
|
235
|
+
: "No page links here, from its body or from navigation, so readers and crawlers can't reach it."
|
|
230
236
|
)
|
|
231
237
|
);
|
|
232
238
|
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import { normalizeBasePath } from "../../core/base-path.ts";
|
|
1
|
+
import { normalizeBasePath, stripBasePath } from "../../core/base-path.ts";
|
|
2
2
|
import { gradeExternal, probeAll } from "../../core/probe.ts";
|
|
3
3
|
import type { ProbeResult } from "../../core/probe.ts";
|
|
4
4
|
import type { Diagnostic } from "../../core/types.ts";
|
|
5
5
|
import { finding } from "../catalog.ts";
|
|
6
6
|
import { pageSite } from "../locate.ts";
|
|
7
|
-
import type {
|
|
8
|
-
|
|
7
|
+
import type {
|
|
8
|
+
AuditContext,
|
|
9
|
+
CheckModule,
|
|
10
|
+
PageSnapshot,
|
|
11
|
+
RedirectResolution,
|
|
12
|
+
} from "../types.ts";
|
|
13
|
+
import { decodePath, normalizePath, resolveHref, siteOrigin } from "../url.ts";
|
|
9
14
|
|
|
10
15
|
const CLIENT_ERROR = 400;
|
|
11
16
|
const SERVER_ERROR = 500;
|
|
@@ -135,6 +140,79 @@ export const servedPageChecks = (
|
|
|
135
140
|
return found;
|
|
136
141
|
};
|
|
137
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The configured redirects the live site should answer: only those the static
|
|
145
|
+
* checks passed. A pattern (`/beta/:slug*`) names no single URL to request, a
|
|
146
|
+
* loop never lands, and a redirect whose source is also a page never fires.
|
|
147
|
+
*/
|
|
148
|
+
const liveRedirects = (context: AuditContext): RedirectResolution[] =>
|
|
149
|
+
context.redirects.filter(
|
|
150
|
+
(redirect) =>
|
|
151
|
+
(redirect.outcome === "ok" || redirect.outcome === "chain") &&
|
|
152
|
+
!context.byUrl.has(normalizePath(redirect.from))
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* What requesting a redirect's old URL on the live site got wrong, or null
|
|
157
|
+
* when it redirected to the configured destination.
|
|
158
|
+
*
|
|
159
|
+
* The build writes the host's redirect file, but a host that doesn't read it
|
|
160
|
+
* answers with the meta-refresh page Astro writes at the old URL (a 200:
|
|
161
|
+
* readers still get there, so it's a warning), and a missing redirect 404s (an
|
|
162
|
+
* error). An external destination isn't compared: its site may redirect again.
|
|
163
|
+
*/
|
|
164
|
+
export const liveRedirectCheck = (
|
|
165
|
+
context: AuditContext,
|
|
166
|
+
redirect: RedirectResolution,
|
|
167
|
+
result: ProbeResult,
|
|
168
|
+
deployBase: string
|
|
169
|
+
): Diagnostic | null => {
|
|
170
|
+
const from = normalizePath(redirect.from);
|
|
171
|
+
const site = {
|
|
172
|
+
file: context.project.context.configFile ?? undefined,
|
|
173
|
+
url: from,
|
|
174
|
+
};
|
|
175
|
+
const landed =
|
|
176
|
+
result.redirected && result.finalUrl
|
|
177
|
+
? normalizePath(
|
|
178
|
+
stripBasePath(
|
|
179
|
+
deployBase,
|
|
180
|
+
decodePath(new URL(result.finalUrl).pathname)
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
: from;
|
|
184
|
+
|
|
185
|
+
// A host that only adds a trailing slash (`/old` → `/old/`) still didn't
|
|
186
|
+
// redirect to the destination.
|
|
187
|
+
if (landed === from) {
|
|
188
|
+
let answer = `could not be reached (${result.error ?? "no response"})`;
|
|
189
|
+
if (result.timedOut) {
|
|
190
|
+
answer = "did not respond in time";
|
|
191
|
+
} else if (result.status !== undefined) {
|
|
192
|
+
answer = `answered it with HTTP ${result.status} instead`;
|
|
193
|
+
}
|
|
194
|
+
const found = finding(
|
|
195
|
+
"BLUME_AUDIT_REDIRECT_NOT_SERVED",
|
|
196
|
+
site,
|
|
197
|
+
`${from} should redirect to ${redirect.to}, but the live site ${answer}.`
|
|
198
|
+
);
|
|
199
|
+
return result.ok ? { ...found, severity: "warning" } : found;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const destination = redirect.chain.at(-1) ?? from;
|
|
203
|
+
if (/^https?:\/\//iu.test(destination) || landed === destination) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
...finding(
|
|
208
|
+
"BLUME_AUDIT_REDIRECT_NOT_SERVED",
|
|
209
|
+
site,
|
|
210
|
+
`${from} redirects to ${landed} on the live site, not to ${destination}.`
|
|
211
|
+
),
|
|
212
|
+
severity: "warning",
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
|
|
138
216
|
/**
|
|
139
217
|
* The built site, checked against a live deployment.
|
|
140
218
|
*
|
|
@@ -164,8 +242,23 @@ export const networkChecks: CheckModule = {
|
|
|
164
242
|
// build output, which the host serves under the deployment base.
|
|
165
243
|
const robotsUrl = new URL(`${deployBase}/robots.txt`, origin).toString();
|
|
166
244
|
const sitemapUrl = new URL(`${deployBase}/sitemap.xml`, origin).toString();
|
|
245
|
+
// Each configured redirect is requested at its old URL, the way a reader
|
|
246
|
+
// following an old link arrives. Its `from` carries `basePath`; the host
|
|
247
|
+
// serves it under the deployment base too.
|
|
248
|
+
const redirects = liveRedirects(context).map(
|
|
249
|
+
(redirect) =>
|
|
250
|
+
[
|
|
251
|
+
redirect,
|
|
252
|
+
new URL(`${deployBase}${redirect.from}`, origin).toString(),
|
|
253
|
+
] as const
|
|
254
|
+
);
|
|
167
255
|
|
|
168
|
-
const results = await probeAll([
|
|
256
|
+
const results = await probeAll([
|
|
257
|
+
...targets,
|
|
258
|
+
robotsUrl,
|
|
259
|
+
sitemapUrl,
|
|
260
|
+
...redirects.map(([, url]) => url),
|
|
261
|
+
]);
|
|
169
262
|
|
|
170
263
|
for (const page of context.pages) {
|
|
171
264
|
const result = results.get(liveUrl(origin, page, deployBase));
|
|
@@ -180,6 +273,16 @@ export const networkChecks: CheckModule = {
|
|
|
180
273
|
found.push(...servedPageChecks(context, page, result, origin));
|
|
181
274
|
}
|
|
182
275
|
|
|
276
|
+
for (const [redirect, url] of redirects) {
|
|
277
|
+
// SAFETY: every redirect URL was in the probed list, and `probeAll`
|
|
278
|
+
// returns a result for each URL it was given.
|
|
279
|
+
const result = results.get(url) as ProbeResult;
|
|
280
|
+
const wrong = liveRedirectCheck(context, redirect, result, deployBase);
|
|
281
|
+
if (wrong) {
|
|
282
|
+
found.push(wrong);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
183
286
|
// With `seo.robots: false` Blume writes no robots.txt, so its absence on
|
|
184
287
|
// the live site is the configured outcome, not a defect.
|
|
185
288
|
const robots = results.get(robotsUrl);
|
|
@@ -210,7 +313,8 @@ export const networkChecks: CheckModule = {
|
|
|
210
313
|
};
|
|
211
314
|
|
|
212
315
|
/**
|
|
213
|
-
* Outbound links, probed over the network (`--external`)
|
|
316
|
+
* Outbound links, probed over the network (`--external`), except those an
|
|
317
|
+
* `--ignore` glob matches.
|
|
214
318
|
*
|
|
215
319
|
* Severity is graded rather than flat: a 404 is the author's bug, but a 403 or a
|
|
216
320
|
* 5xx is usually rate limiting or someone else's outage, and failing a build on
|
|
@@ -229,7 +333,7 @@ export const externalChecks: CheckModule = {
|
|
|
229
333
|
for (const page of context.pages) {
|
|
230
334
|
for (const link of page.links) {
|
|
231
335
|
const resolved = resolveHref(page.url, link.href, origin, deployBase);
|
|
232
|
-
if (resolved.kind !== "external") {
|
|
336
|
+
if (resolved.kind !== "external" || context.ignore(resolved.url)) {
|
|
233
337
|
continue;
|
|
234
338
|
}
|
|
235
339
|
const pages = linkers.get(resolved.url);
|
|
@@ -23,15 +23,17 @@ export const redirectChecks: CheckModule = {
|
|
|
23
23
|
for (const redirect of context.redirects) {
|
|
24
24
|
const from = normalizePath(redirect.from);
|
|
25
25
|
|
|
26
|
-
// A redirect whose source is also a built page
|
|
27
|
-
//
|
|
28
|
-
//
|
|
26
|
+
// A redirect whose source is also a built page: one of the two never
|
|
27
|
+
// serves, and which depends on the host (a static layer that answers
|
|
28
|
+
// files first keeps the page; a router that runs redirects first keeps
|
|
29
|
+
// the redirect). Ahrefs can't see this: it only observes the served
|
|
30
|
+
// response, which looks perfectly healthy.
|
|
29
31
|
if (context.byUrl.has(from)) {
|
|
30
32
|
found.push(
|
|
31
33
|
finding(
|
|
32
34
|
"BLUME_AUDIT_REDIRECT_SOURCE_IS_PAGE",
|
|
33
35
|
{ ...site, url: from },
|
|
34
|
-
`A redirect is configured from ${from}, but ${from} is also a real page — the
|
|
36
|
+
`A redirect is configured from ${from}, but ${from} is also a real page — readers get one or the other, depending on the host.`
|
|
35
37
|
)
|
|
36
38
|
);
|
|
37
39
|
continue;
|
|
@@ -12,9 +12,30 @@ import { normalizePath } from "../url.ts";
|
|
|
12
12
|
*/
|
|
13
13
|
const MATCH_ORIGIN = "https://robots-audit.invalid";
|
|
14
14
|
|
|
15
|
+
const USER_AGENT_LINE = /^\s*user-agent\s*:\s*(?<agent>[^#]*)/iu;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The crawlers robots.txt names in a `User-agent` line, other than `*`, as
|
|
19
|
+
* written (first spelling wins). robots-parser matches a group by the token
|
|
20
|
+
* before any `/version`, case-insensitively, so that's what dedupes them.
|
|
21
|
+
*/
|
|
22
|
+
const namedAgents = (raw: string): string[] => {
|
|
23
|
+
const agents = new Map<string, string>();
|
|
24
|
+
for (const line of raw.split(/\r?\n/u)) {
|
|
25
|
+
const agent = USER_AGENT_LINE.exec(line)?.groups?.agent?.trim() ?? "";
|
|
26
|
+
const key = agent.toLowerCase().split("/")[0]?.trim() ?? "";
|
|
27
|
+
if (key && key !== "*" && !agents.has(key)) {
|
|
28
|
+
agents.set(key, agent);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return [...agents.values()];
|
|
32
|
+
};
|
|
33
|
+
|
|
15
34
|
/**
|
|
16
35
|
* robots.txt: is it there, is it well-formed, does it point at the sitemap, and
|
|
17
36
|
* — the one that matters — does it block a page the sitemap is advertising?
|
|
37
|
+
* Blocking every crawler (`User-agent: *`) is an error; a group aimed at one
|
|
38
|
+
* crawler is a warning, since shutting out an AI crawler is often on purpose.
|
|
18
39
|
*
|
|
19
40
|
* Ahrefs also tracks "robots.txt has too many redirects". A static host serves
|
|
20
41
|
* the file directly, so that is effectively unreachable here and isn't checked.
|
|
@@ -70,6 +91,14 @@ export const robotsChecks: CheckModule = {
|
|
|
70
91
|
// consecutive User-agent lines form one group as the spec requires.
|
|
71
92
|
const parser = robotsParser(`${MATCH_ORIGIN}/robots.txt`, robots.raw);
|
|
72
93
|
const lines = robots.raw.split(/\r?\n/u);
|
|
94
|
+
const ruleAt = (url: string, agent: string) => {
|
|
95
|
+
const line = parser.getMatchingLineNumber(url, agent);
|
|
96
|
+
return { line, rule: line > 0 ? lines[line - 1]?.trim() : undefined };
|
|
97
|
+
};
|
|
98
|
+
// A crawler named in its own group follows only that group, so a rule
|
|
99
|
+
// there can block it from a page every other crawler may read.
|
|
100
|
+
const agents = namedAgents(robots.raw);
|
|
101
|
+
const blockedFor = new Map<string, string[]>();
|
|
73
102
|
for (const loc of context.sitemap?.urls ?? []) {
|
|
74
103
|
let pathname: string;
|
|
75
104
|
try {
|
|
@@ -82,8 +111,7 @@ export const robotsChecks: CheckModule = {
|
|
|
82
111
|
const path = normalizePath(pathname);
|
|
83
112
|
const url = `${MATCH_ORIGIN}${pathname}`;
|
|
84
113
|
if (parser.isDisallowed(url, "*")) {
|
|
85
|
-
const
|
|
86
|
-
const rule = line > 0 ? lines[line - 1]?.trim() : undefined;
|
|
114
|
+
const { rule } = ruleAt(url, "*");
|
|
87
115
|
found.push(
|
|
88
116
|
finding(
|
|
89
117
|
"BLUME_AUDIT_ROBOTS_DISALLOWS_INDEXABLE",
|
|
@@ -91,7 +119,35 @@ export const robotsChecks: CheckModule = {
|
|
|
91
119
|
`robots.txt "${rule ?? "Disallow"}" blocks ${path}, which sitemap.xml advertises.`
|
|
92
120
|
)
|
|
93
121
|
);
|
|
122
|
+
continue;
|
|
94
123
|
}
|
|
124
|
+
for (const agent of agents) {
|
|
125
|
+
if (!parser.isDisallowed(url, agent)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const blocked = blockedFor.get(agent);
|
|
129
|
+
if (blocked) {
|
|
130
|
+
blocked.push(url);
|
|
131
|
+
} else {
|
|
132
|
+
blockedFor.set(agent, [url]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// One finding per crawler, at the rule that blocks its first page: a
|
|
138
|
+
// `Disallow: /` aimed at one bot would otherwise list every page.
|
|
139
|
+
for (const [agent, urls] of blockedFor) {
|
|
140
|
+
const [first = ""] = urls;
|
|
141
|
+
const { line, rule } = ruleAt(first, agent);
|
|
142
|
+
const path = normalizePath(new URL(first).pathname);
|
|
143
|
+
const more = urls.length > 1 ? ` and ${urls.length - 1} more` : "";
|
|
144
|
+
found.push(
|
|
145
|
+
finding(
|
|
146
|
+
"BLUME_AUDIT_ROBOTS_BLOCKS_CRAWLER",
|
|
147
|
+
{ file: robots.file, line, url: "/robots.txt" },
|
|
148
|
+
`robots.txt "${rule ?? "Disallow"}" blocks ${agent} from ${path}${more}, which sitemap.xml advertises.`
|
|
149
|
+
)
|
|
150
|
+
);
|
|
95
151
|
}
|
|
96
152
|
|
|
97
153
|
return found;
|