blume 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +110 -0
- package/README.md +2 -2
- package/dist/cli/{chunk-f2972sbt.js → chunk-00gs3wqs.js} +1 -1
- package/dist/cli/{chunk-fa25z98p.js → chunk-273ygyr4.js} +4 -3
- package/dist/cli/chunk-273ygyr4.js.map +11 -0
- package/dist/cli/{chunk-pat2zzwc.js → chunk-3yce002v.js} +21 -8
- package/dist/cli/{chunk-pat2zzwc.js.map → chunk-3yce002v.js.map} +4 -4
- package/dist/cli/{chunk-d1tadaw7.js → chunk-4e9b9ra6.js} +17 -7
- package/dist/cli/chunk-4e9b9ra6.js.map +10 -0
- package/dist/cli/{chunk-zg2gtj10.js → chunk-5r8g91qn.js} +352 -676
- package/dist/cli/chunk-5r8g91qn.js.map +34 -0
- package/dist/cli/{chunk-11j0384y.js → chunk-6dtt0zfn.js} +14 -14
- package/dist/cli/{chunk-11j0384y.js.map → chunk-6dtt0zfn.js.map} +3 -3
- package/dist/cli/{chunk-bw22s759.js → chunk-6k4ftwze.js} +38 -19
- package/dist/cli/{chunk-bw22s759.js.map → chunk-6k4ftwze.js.map} +4 -4
- package/dist/cli/{chunk-sqn5t4q0.js → chunk-7mbqtmgb.js} +16 -7
- package/dist/cli/chunk-7mbqtmgb.js.map +10 -0
- package/dist/cli/{chunk-79njf86q.js → chunk-7vtckvaw.js} +21 -19
- package/dist/cli/chunk-7vtckvaw.js.map +11 -0
- package/dist/cli/{chunk-fh5hj5jt.js → chunk-8g8ytmgx.js} +17 -18
- package/dist/cli/{chunk-fh5hj5jt.js.map → chunk-8g8ytmgx.js.map} +2 -2
- package/dist/cli/{chunk-1w8dp3qb.js → chunk-91ws1n6j.js} +18 -15
- package/dist/cli/chunk-91ws1n6j.js.map +10 -0
- package/dist/cli/{chunk-7ez8ny0t.js → chunk-bbnwccaz.js} +2 -2
- package/dist/cli/{chunk-5a2z0198.js → chunk-bfwp9vp6.js} +18 -10
- package/dist/cli/chunk-bfwp9vp6.js.map +10 -0
- package/dist/cli/{chunk-ernrthtr.js → chunk-d1v5rhy0.js} +14 -15
- package/dist/cli/{chunk-ernrthtr.js.map → chunk-d1v5rhy0.js.map} +2 -2
- package/dist/cli/{chunk-a9kptbw5.js → chunk-ddndchfr.js} +21 -6
- package/dist/cli/chunk-ddndchfr.js.map +14 -0
- package/dist/cli/{chunk-zxccj738.js → chunk-esh98wmb.js} +1 -1
- package/dist/cli/{chunk-88cpgt6h.js → chunk-fsmrqk8a.js} +1 -1
- package/dist/cli/{chunk-b5aj94ah.js → chunk-g698a744.js} +5 -5
- package/dist/cli/{chunk-jts8mvcz.js → chunk-gs7r695n.js} +9 -3
- package/dist/cli/{chunk-jts8mvcz.js.map → chunk-gs7r695n.js.map} +3 -3
- package/dist/cli/{chunk-41za066z.js → chunk-h2ez8dzb.js} +4 -4
- package/dist/cli/{chunk-by2290sx.js → chunk-h7k3nq3v.js} +2 -2
- package/dist/cli/{chunk-j8mw0za6.js → chunk-hqp2ajnh.js} +252 -123
- package/dist/cli/chunk-hqp2ajnh.js.map +35 -0
- package/dist/cli/{chunk-mnqj32sj.js → chunk-hr8ne106.js} +119 -50
- package/dist/cli/chunk-hr8ne106.js.map +13 -0
- package/dist/cli/{chunk-mwt1k8n7.js → chunk-j85scx15.js} +74 -30
- package/dist/cli/chunk-j85scx15.js.map +10 -0
- package/dist/cli/{chunk-z01ze5c1.js → chunk-k7pj68a8.js} +63 -22
- package/dist/cli/chunk-k7pj68a8.js.map +11 -0
- package/dist/cli/{chunk-2q1dwty4.js → chunk-mqc662a6.js} +8 -8
- package/dist/cli/{chunk-2q1dwty4.js.map → chunk-mqc662a6.js.map} +4 -4
- package/dist/cli/{chunk-y3e45rc8.js → chunk-n1yg3tj3.js} +4 -4
- package/dist/cli/{chunk-bctazmbk.js → chunk-nfcyttvj.js} +17 -6
- package/dist/cli/chunk-nfcyttvj.js.map +10 -0
- package/dist/cli/{chunk-6k8vp3ta.js → chunk-pbg5a4s3.js} +60 -28
- package/dist/cli/chunk-pbg5a4s3.js.map +19 -0
- package/dist/cli/{chunk-d80hr03s.js → chunk-ppzjqwx2.js} +24 -17
- package/dist/cli/{chunk-d80hr03s.js.map → chunk-ppzjqwx2.js.map} +4 -4
- package/dist/cli/{chunk-beat36xx.js → chunk-qkb5a8sa.js} +25 -10
- package/dist/cli/chunk-qkb5a8sa.js.map +10 -0
- package/dist/cli/{chunk-bnbmcwfb.js → chunk-sqw4ekg1.js} +5 -5
- package/dist/cli/{chunk-bnbmcwfb.js.map → chunk-sqw4ekg1.js.map} +3 -3
- package/dist/cli/{chunk-xaz13gwg.js → chunk-v6ya5kcb.js} +3268 -1150
- package/dist/cli/chunk-v6ya5kcb.js.map +189 -0
- package/dist/cli/{chunk-tzne8qfq.js → chunk-w4bxdvsa.js} +18 -15
- package/dist/cli/chunk-w4bxdvsa.js.map +10 -0
- package/dist/cli/{chunk-f7t03s3g.js → chunk-wdrt2k2v.js} +2 -2
- package/dist/cli/{chunk-z1f5arsg.js → chunk-xh43dwgw.js} +170 -55
- package/dist/cli/chunk-xh43dwgw.js.map +36 -0
- package/dist/cli/{chunk-pnnvybbk.js → chunk-zp79m0ts.js} +5 -5
- package/dist/cli/{chunk-pnnvybbk.js.map → chunk-zp79m0ts.js.map} +2 -2
- package/dist/cli/index.js +160 -35
- package/dist/cli/index.js.map +4 -4
- package/dist/types/ai/agent-readability.d.ts +1 -1
- package/dist/types/ai/agent-surface.d.ts +32 -0
- package/dist/types/ai/api/paths.d.ts +1 -1
- package/dist/types/ai/api-catalog.d.ts +7 -1
- package/dist/types/ai/ask-context.d.ts +14 -7
- package/dist/types/ai/ask.d.ts +43 -43
- package/dist/types/ai/component-markdown.d.ts +4 -4
- package/dist/types/ai/index.d.ts +3 -3
- package/dist/types/ai/openapi-components.d.ts +1 -1
- package/dist/types/ai/relative-links.d.ts +9 -4
- package/dist/types/ai/serializers.d.ts +1 -1
- package/dist/types/ai/static-expression.d.ts +28 -0
- package/dist/types/ai/visibility.d.ts +1 -1
- package/dist/types/analytics/databuddy.d.ts +43 -0
- package/dist/types/analytics/index.d.ts +2 -0
- package/dist/types/analytics/schema.d.ts +14 -0
- package/dist/types/cli/env.d.ts +5 -0
- package/dist/types/cli/init/scaffold.d.ts +19 -3
- package/dist/types/cli/init/starter-spec.d.ts +11 -0
- package/dist/types/core/base-path.d.ts +9 -0
- package/dist/types/core/config-input.d.ts +21 -21
- package/dist/types/core/config.d.ts +5 -5
- package/dist/types/core/data.d.ts +3 -3
- package/dist/types/core/graph.d.ts +2 -0
- package/dist/types/core/i18n-ui.d.ts +37 -8
- package/dist/types/core/i18n.d.ts +7 -1
- package/dist/types/core/links.d.ts +3 -1
- package/dist/types/core/load-module.d.ts +10 -0
- package/dist/types/core/locale-links.d.ts +12 -2
- package/dist/types/core/meta.d.ts +5 -1
- package/dist/types/core/nav-diagnostics.d.ts +10 -0
- package/dist/types/core/navigation.d.ts +38 -0
- package/dist/types/core/ordering-prefix.d.ts +4 -0
- package/dist/types/core/safe-links.d.ts +3 -1
- package/dist/types/core/schema.d.ts +57 -18
- package/dist/types/core/sources/github-releases.d.ts +5 -0
- package/dist/types/core/sources/lower.d.ts +14 -2
- package/dist/types/core/sources/normalize.d.ts +10 -1
- package/dist/types/core/sources/remote.d.ts +11 -1
- package/dist/types/core/sources/resolve.d.ts +12 -0
- package/dist/types/core/sources/types.d.ts +31 -0
- package/dist/types/core/types.d.ts +9 -0
- package/dist/types/core/unrecognized-keys.d.ts +1 -1
- package/dist/types/deploy/adapters/node.d.ts +5 -2
- package/dist/types/deploy/adapters/types.d.ts +7 -0
- package/dist/types/deploy/cloudflare-negotiation.d.ts +3 -2
- package/dist/types/deploy/headers.d.ts +31 -7
- package/dist/types/deploy/node-headers.d.ts +43 -8
- package/dist/types/deploy/platforms/netlify.d.ts +27 -2
- package/dist/types/deploy/platforms/node.d.ts +6 -5
- package/dist/types/deploy/platforms/types.d.ts +7 -0
- package/dist/types/deploy/platforms/vercel.d.ts +3 -2
- package/dist/types/deploy/redirects.d.ts +7 -2
- package/dist/types/deploy/vercel-negotiation.d.ts +3 -2
- package/dist/types/openapi/model.d.ts +20 -6
- package/dist/types/search/documents.d.ts +1 -1
- package/dist/types/search/orama-index.d.ts +1 -1
- package/dist/types/search/sync/algolia.d.ts +3 -1
- package/docs/01-quickstart.mdx +3 -2
- package/docs/02-deployment.mdx +21 -12
- package/docs/03-upgrading.mdx +22 -9
- package/docs/04-migrating.mdx +4 -4
- package/docs/08-faq.mdx +12 -5
- package/docs/advanced/changelog.mdx +1 -1
- package/docs/advanced/custom-pages.mdx +5 -3
- package/docs/advanced/skills.mdx +1 -1
- package/docs/cli/audit.mdx +5 -5
- package/docs/cli/doctor.mdx +4 -4
- package/docs/cli/evals.mdx +9 -9
- package/docs/cli/index.mdx +6 -3
- package/docs/cli/translate.mdx +8 -8
- package/docs/configuration/analytics.mdx +23 -2
- package/docs/configuration/{ask-ai.mdx → assistant.mdx} +20 -20
- package/docs/configuration/customization.mdx +5 -4
- package/docs/configuration/index.mdx +7 -5
- package/docs/configuration/meta.ts +1 -1
- package/docs/configuration/search.mdx +2 -2
- package/docs/content/components.mdx +1 -1
- package/docs/content/frontmatter.mdx +5 -1
- package/docs/content/i18n.mdx +3 -3
- package/docs/content/index.mdx +4 -2
- package/docs/content/islands.mdx +1 -1
- package/docs/content/meta.mdx +5 -3
- package/docs/content/navigation.mdx +29 -4
- package/docs/content/sources.mdx +18 -12
- package/docs/content/versioning.mdx +1 -0
- package/docs/discoverability/agent-discovery.mdx +22 -8
- package/docs/discoverability/index.mdx +3 -3
- package/docs/discoverability/llms-txt.mdx +2 -5
- package/docs/discoverability/markdown.mdx +5 -3
- package/docs/discoverability/mcp.mdx +4 -0
- package/docs/discoverability/metadata.mdx +3 -2
- package/docs/discoverability/open-graph.mdx +6 -4
- package/docs/discoverability/rss.mdx +3 -1
- package/docs/index.mdx +2 -2
- package/docs/references/asyncapi.mdx +1 -1
- package/docs/references/graphql.mdx +2 -2
- package/docs/references/openapi.mdx +3 -3
- package/package.json +1 -1
- package/skills/blume/SKILL.md +5 -5
- package/skills/blume-migrate/SKILL.md +6 -6
- package/skills/blume-migrate/references/docusaurus.md +6 -6
- package/skills/blume-migrate/references/mintlify.md +3 -3
- package/skills/blume-migrate/references/monorepo.md +1 -1
- package/skills/blume-migrate/references/nextra.md +1 -1
- package/skills/blume-migrate/references/starlight.md +5 -5
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +2 -2
- package/skills/blume-update-docs/references/audit-checklist.md +1 -1
- package/src/ai/agent-readability.ts +4 -4
- package/src/ai/agent-surface.ts +56 -0
- package/src/ai/api/paths.ts +1 -1
- package/src/ai/api/spec.ts +15 -2
- package/src/ai/api-catalog.ts +7 -1
- package/src/ai/ask-context.ts +21 -9
- package/src/ai/ask-data.ts +28 -14
- package/src/ai/ask.ts +84 -71
- package/src/ai/component-markdown.ts +43 -37
- package/src/ai/cors.ts +3 -3
- package/src/ai/index.ts +16 -16
- package/src/ai/link-headers.ts +8 -3
- package/src/ai/llms.ts +4 -3
- package/src/ai/markdown.ts +23 -11
- package/src/ai/mcp/server.ts +78 -7
- package/src/ai/openapi-components.ts +1 -1
- package/src/ai/relative-links.ts +78 -10
- package/src/ai/serializers.ts +1 -1
- package/src/ai/static-expression.ts +416 -0
- package/src/ai/visibility.ts +46 -15
- package/src/analytics/databuddy.ts +67 -0
- package/src/analytics/head.ts +4 -0
- package/src/analytics/index.ts +2 -0
- package/src/analytics/posthog.ts +21 -3
- package/src/analytics/schema.ts +2 -0
- package/src/astro/generate.ts +90 -45
- package/src/astro/module-types.ts +1 -1
- package/src/astro/runtime-deps.ts +6 -6
- package/src/astro/templates.ts +87 -49
- package/src/audit/catalog.ts +2 -2
- package/src/audit/checks/assets.ts +25 -5
- package/src/audit/checks/content.ts +20 -2
- package/src/audit/checks/i18n.ts +46 -8
- package/src/audit/checks/indexability.ts +39 -19
- package/src/audit/checks/links.ts +14 -0
- package/src/audit/checks/llms.ts +6 -3
- package/src/audit/checks/network.ts +3 -1
- package/src/audit/checks/og-image.ts +10 -0
- package/src/audit/checks/robots.ts +6 -1
- package/src/audit/checks/sitemap.ts +56 -31
- package/src/audit/checks/social.ts +21 -2
- package/src/audit/crawl.ts +88 -15
- package/src/audit/report.ts +54 -17
- package/src/audit/run.ts +1 -0
- package/src/audit/types.ts +18 -0
- package/src/audit/url.ts +37 -6
- package/src/blume-modules.d.ts +2 -2
- package/src/cli/build-failure.ts +50 -0
- package/src/cli/commands/audit.ts +8 -1
- package/src/cli/commands/build.ts +19 -5
- package/src/cli/commands/check.ts +2 -0
- package/src/cli/commands/doctor.ts +7 -5
- package/src/cli/commands/eval.ts +4 -10
- package/src/cli/commands/init.ts +15 -5
- package/src/cli/commands/migrate.ts +2 -2
- package/src/cli/commands/preview.ts +15 -0
- package/src/cli/commands/sync.ts +2 -0
- package/src/cli/commands/translate.ts +12 -9
- package/src/cli/commands/upgrade.ts +13 -2
- package/src/cli/eject-scripts.ts +32 -7
- package/src/cli/env.ts +12 -1
- package/src/cli/init/scaffold.ts +117 -11
- package/src/cli/init/starter-spec.ts +235 -0
- package/src/cli/required-secrets.ts +3 -3
- package/src/components/content/AccordionItem.astro +26 -22
- package/src/components/content/Badge.astro +2 -9
- package/src/components/content/Card.astro +2 -2
- package/src/components/content/Component.astro +9 -3
- package/src/components/content/Expandable.astro +5 -1
- package/src/components/content/Frame.astro +2 -7
- package/src/components/content/GithubInfo.astro +12 -2
- package/src/components/content/Prompt.astro +2 -7
- package/src/components/content/Tabs.astro +54 -5
- package/src/components/content/Tile.astro +1 -1
- package/src/components/content/Tooltip.astro +69 -7
- package/src/components/content/Tree.astro +7 -2
- package/src/components/content/TypeTable.astro +10 -5
- package/src/components/content/Update.astro +8 -2
- package/src/components/content/auto-type-table.ts +4 -1
- package/src/components/content/badge-color.ts +17 -0
- package/src/components/content/base-href.ts +18 -3
- package/src/components/content/inline-markdown.ts +27 -7
- package/src/components/copy-feedback.ts +36 -9
- package/src/components/islands/{AskAI.astro → Assistant.astro} +10 -10
- package/src/components/islands/{ask-ai.tsx → assistant.tsx} +39 -25
- package/src/components/islands/hooks.ts +21 -14
- package/src/components/islands/webmcp.ts +12 -8
- package/src/components/layout/Banner.astro +23 -4
- package/src/components/layout/Header.astro +32 -19
- package/src/components/layout/Logo.astro +5 -0
- package/src/components/layout/NavSelector.astro +6 -1
- package/src/components/layout/NavTabMenu.astro +133 -0
- package/src/components/layout/NavTree.astro +15 -6
- package/src/components/layout/NavTreeCache.astro +5 -2
- package/src/components/layout/NavTreeScript.astro +45 -5
- package/src/components/layout/PageLayout.astro +28 -14
- package/src/components/layout/Pagination.astro +7 -7
- package/src/components/layout/ReferenceLayout.astro +5 -2
- package/src/components/layout/RootLayout.astro +27 -14
- package/src/components/layout/Search.astro +18 -14
- package/src/components/layout/analytics-client.ts +3 -1
- package/src/components/layout/drawer-inert.ts +1 -1
- package/src/components/openapi/GraphqlType.astro +11 -3
- package/src/components/openapi/MessageComposer.astro +1 -1
- package/src/components/openapi/Operation.astro +21 -5
- package/src/components/openapi/PanelTabs.astro +4 -1
- package/src/components/openapi/Playground.astro +8 -2
- package/src/components/openapi/RequestPanel.astro +9 -5
- package/src/components/openapi/SchemaProperty.astro +11 -33
- package/src/components/openapi/SchemaTable.astro +27 -52
- package/src/components/openapi/description.ts +2 -2
- package/src/components/openapi/helpers.ts +90 -12
- package/src/components/openapi/message-composer.ts +8 -0
- package/src/components/openapi/message-model.ts +12 -2
- package/src/components/openapi/message.ts +4 -1
- package/src/components/openapi/operation-model.ts +40 -8
- package/src/components/openapi/panel.ts +29 -5
- package/src/components/openapi/playground-client.ts +37 -7
- package/src/components/openapi/playground-schema.ts +25 -6
- package/src/components/openapi/request.ts +2 -0
- package/src/components/openapi/schema-tree.ts +203 -0
- package/src/components/openapi/snippets.ts +30 -15
- package/src/components/openapi/validate-json.ts +1 -1
- package/src/core/base-path.ts +15 -0
- package/src/core/code-fences.ts +1 -1
- package/src/core/config-input.ts +23 -23
- package/src/core/config.ts +21 -7
- package/src/core/data.ts +3 -3
- package/src/core/diagnostics.ts +217 -30
- package/src/core/graph.ts +120 -11
- package/src/core/i18n-ui.ts +74 -11
- package/src/core/i18n.ts +13 -2
- package/src/core/links.ts +7 -4
- package/src/core/load-module.ts +20 -0
- package/src/core/locale-links.ts +17 -17
- package/src/core/manifest.ts +3 -2
- package/src/core/meta.ts +69 -6
- package/src/core/nav-diagnostics.ts +56 -1
- package/src/core/navigation.ts +245 -76
- package/src/core/ordering-prefix.ts +27 -0
- package/src/core/project-graph.ts +23 -1
- package/src/core/request-body.ts +1 -1
- package/src/core/safe-href.ts +53 -1
- package/src/core/safe-links.ts +11 -2
- package/src/core/schema.ts +95 -34
- package/src/core/server-features.ts +2 -2
- package/src/core/sources/assets.ts +83 -41
- package/src/core/sources/contentful-rich-text.ts +3 -2
- package/src/core/sources/contentful.ts +25 -12
- package/src/core/sources/filesystem.ts +5 -1
- package/src/core/sources/github-releases.ts +106 -5
- package/src/core/sources/lexical.ts +9 -4
- package/src/core/sources/lower.ts +79 -26
- package/src/core/sources/mdx-remote.ts +1 -0
- package/src/core/sources/normalize.ts +132 -30
- package/src/core/sources/notion.ts +26 -10
- package/src/core/sources/obsidian.ts +17 -3
- package/src/core/sources/payload.ts +1 -0
- package/src/core/sources/portable-text.ts +63 -44
- package/src/core/sources/remote.ts +18 -2
- package/src/core/sources/resolve.ts +52 -33
- package/src/core/sources/sanity.ts +1 -0
- package/src/core/sources/strapi-blocks.ts +9 -4
- package/src/core/sources/strapi.ts +1 -0
- package/src/core/sources/types.ts +31 -0
- package/src/core/types.ts +9 -0
- package/src/core/ui-packs/ar.ts +17 -5
- package/src/core/ui-packs/bg.ts +17 -5
- package/src/core/ui-packs/bn.ts +17 -5
- package/src/core/ui-packs/ca.ts +17 -5
- package/src/core/ui-packs/cs.ts +17 -5
- package/src/core/ui-packs/da.ts +17 -5
- package/src/core/ui-packs/de.ts +17 -5
- package/src/core/ui-packs/el.ts +17 -5
- package/src/core/ui-packs/es.ts +17 -5
- package/src/core/ui-packs/fa.ts +17 -5
- package/src/core/ui-packs/fi.ts +17 -5
- package/src/core/ui-packs/fr.ts +17 -5
- package/src/core/ui-packs/he.ts +17 -5
- package/src/core/ui-packs/hi.ts +17 -5
- package/src/core/ui-packs/hr.ts +17 -5
- package/src/core/ui-packs/hu.ts +17 -5
- package/src/core/ui-packs/id.ts +17 -5
- package/src/core/ui-packs/it.ts +17 -5
- package/src/core/ui-packs/ja.ts +17 -5
- package/src/core/ui-packs/ko.ts +17 -5
- package/src/core/ui-packs/nl.ts +17 -5
- package/src/core/ui-packs/no.ts +17 -5
- package/src/core/ui-packs/pl.ts +17 -5
- package/src/core/ui-packs/pt-br.ts +17 -5
- package/src/core/ui-packs/pt.ts +17 -5
- package/src/core/ui-packs/ro.ts +17 -5
- package/src/core/ui-packs/ru.ts +17 -5
- package/src/core/ui-packs/sk.ts +17 -5
- package/src/core/ui-packs/sr.ts +17 -5
- package/src/core/ui-packs/sv.ts +17 -5
- package/src/core/ui-packs/th.ts +17 -5
- package/src/core/ui-packs/tr.ts +17 -5
- package/src/core/ui-packs/uk.ts +17 -5
- package/src/core/ui-packs/vi.ts +17 -5
- package/src/core/ui-packs/zh-tw.ts +17 -5
- package/src/core/ui-packs/zh.ts +17 -5
- package/src/core/unrecognized-keys.ts +1 -1
- package/src/core/version-cut.ts +17 -2
- package/src/core/versions.ts +4 -1
- package/src/deploy/adapters/node.ts +5 -2
- package/src/deploy/adapters/registry.ts +2 -1
- package/src/deploy/adapters/types.ts +13 -1
- package/src/deploy/artifacts.ts +25 -5
- package/src/deploy/cloudflare-negotiation.ts +23 -4
- package/src/deploy/headers.ts +67 -51
- package/src/deploy/node-headers.ts +148 -27
- package/src/deploy/platforms/cloudflare.ts +1 -0
- package/src/deploy/platforms/netlify.ts +82 -5
- package/src/deploy/platforms/node.ts +7 -5
- package/src/deploy/platforms/static.ts +1 -0
- package/src/deploy/platforms/types.ts +7 -0
- package/src/deploy/platforms/vercel.ts +9 -3
- package/src/deploy/redirects.ts +14 -3
- package/src/deploy/vercel-negotiation.ts +35 -3
- package/src/eval/agents.ts +10 -2
- package/src/eval/run.ts +25 -0
- package/src/markdown/base-links.ts +55 -8
- package/src/markdown/index.ts +6 -3
- package/src/markdown/relative-links.ts +3 -23
- package/src/markdown/route-snapshot.ts +37 -0
- package/src/og/card.ts +20 -4
- package/src/og/derive.ts +145 -4
- package/src/openapi/graphql-build.ts +28 -2
- package/src/openapi/model.ts +77 -13
- package/src/openapi/render-mdx.ts +10 -1
- package/src/registry/eject.ts +135 -37
- package/src/search/documents.ts +24 -3
- package/src/search/orama-index.ts +1 -1
- package/src/search/sync/algolia.ts +36 -2
- package/src/sources/registry.ts +5 -0
- package/src/theme/entry.ts +11 -4
- package/src/translate/agents.ts +6 -1
- package/src/translate/ledger.ts +26 -3
- package/src/translate/meta.ts +11 -3
- package/src/translate/report.ts +1 -1
- package/src/translate/run.ts +11 -5
- package/src/translate/validate.ts +10 -1
- package/src/translate/work-list.ts +36 -3
- package/src/upgrade/upgrade.ts +37 -5
- package/dist/cli/chunk-1w8dp3qb.js.map +0 -10
- package/dist/cli/chunk-5a2z0198.js.map +0 -10
- package/dist/cli/chunk-6k8vp3ta.js.map +0 -19
- package/dist/cli/chunk-79njf86q.js.map +0 -11
- package/dist/cli/chunk-a9kptbw5.js.map +0 -14
- package/dist/cli/chunk-abh8yjkn.js +0 -31
- package/dist/cli/chunk-abh8yjkn.js.map +0 -10
- package/dist/cli/chunk-bctazmbk.js.map +0 -10
- package/dist/cli/chunk-beat36xx.js.map +0 -10
- package/dist/cli/chunk-d1tadaw7.js.map +0 -10
- package/dist/cli/chunk-fa25z98p.js.map +0 -11
- package/dist/cli/chunk-j8mw0za6.js.map +0 -35
- package/dist/cli/chunk-mnqj32sj.js.map +0 -12
- package/dist/cli/chunk-mwt1k8n7.js.map +0 -10
- package/dist/cli/chunk-nk3ts2xk.js +0 -51
- package/dist/cli/chunk-nk3ts2xk.js.map +0 -10
- package/dist/cli/chunk-sqn5t4q0.js.map +0 -10
- package/dist/cli/chunk-tzne8qfq.js.map +0 -10
- package/dist/cli/chunk-xaz13gwg.js.map +0 -182
- package/dist/cli/chunk-z01ze5c1.js.map +0 -10
- package/dist/cli/chunk-z1f5arsg.js.map +0 -36
- package/dist/cli/chunk-zg2gtj10.js.map +0 -35
- /package/dist/cli/{chunk-f2972sbt.js.map → chunk-00gs3wqs.js.map} +0 -0
- /package/dist/cli/{chunk-7ez8ny0t.js.map → chunk-bbnwccaz.js.map} +0 -0
- /package/dist/cli/{chunk-zxccj738.js.map → chunk-esh98wmb.js.map} +0 -0
- /package/dist/cli/{chunk-88cpgt6h.js.map → chunk-fsmrqk8a.js.map} +0 -0
- /package/dist/cli/{chunk-b5aj94ah.js.map → chunk-g698a744.js.map} +0 -0
- /package/dist/cli/{chunk-41za066z.js.map → chunk-h2ez8dzb.js.map} +0 -0
- /package/dist/cli/{chunk-by2290sx.js.map → chunk-h7k3nq3v.js.map} +0 -0
- /package/dist/cli/{chunk-y3e45rc8.js.map → chunk-n1yg3tj3.js.map} +0 -0
- /package/dist/cli/{chunk-f7t03s3g.js.map → chunk-wdrt2k2v.js.map} +0 -0
|
@@ -180,8 +180,10 @@ export const networkChecks: CheckModule = {
|
|
|
180
180
|
found.push(...servedPageChecks(context, page, result, origin));
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// With `seo.robots: false` Blume writes no robots.txt, so its absence on
|
|
184
|
+
// the live site is the configured outcome, not a defect.
|
|
183
185
|
const robots = results.get(robotsUrl);
|
|
184
|
-
if (robots && !robots.ok) {
|
|
186
|
+
if (context.project.config.seo.robots && robots && !robots.ok) {
|
|
185
187
|
found.push(
|
|
186
188
|
finding(
|
|
187
189
|
"BLUME_AUDIT_ROBOTS_NOT_ACCESSIBLE",
|
|
@@ -71,6 +71,16 @@ export const ogImageChecks: CheckModule = {
|
|
|
71
71
|
if (resolved.kind === "external" || resolved.kind === "ignored") {
|
|
72
72
|
continue;
|
|
73
73
|
}
|
|
74
|
+
if (resolved.kind === "outside-base") {
|
|
75
|
+
found.push(
|
|
76
|
+
finding(
|
|
77
|
+
"BLUME_AUDIT_OG_IMAGE_BROKEN",
|
|
78
|
+
pageSite(context, page, ["seo", "image"]),
|
|
79
|
+
`og:image points at ${resolved.path}, which is missing deployment.base (${deployBase}), so the deployed site does not serve it.`
|
|
80
|
+
)
|
|
81
|
+
);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
74
84
|
|
|
75
85
|
const { path } = resolved;
|
|
76
86
|
if (
|
|
@@ -47,7 +47,12 @@ export const robotsChecks: CheckModule = {
|
|
|
47
47
|
)
|
|
48
48
|
);
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
// Blume only writes the Sitemap line when there is a sitemap to point at.
|
|
51
|
+
if (
|
|
52
|
+
site &&
|
|
53
|
+
context.project.config.seo.sitemap &&
|
|
54
|
+
robots.sitemaps.length === 0
|
|
55
|
+
) {
|
|
51
56
|
found.push(
|
|
52
57
|
finding(
|
|
53
58
|
"BLUME_AUDIT_ROBOTS_SITEMAP_MISSING",
|
|
@@ -138,6 +138,40 @@ const checkListedUrl = (
|
|
|
138
138
|
return found;
|
|
139
139
|
};
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* The other direction: a page that was built, is indexable, and should be
|
|
143
|
+
* findable — but never made it into the sitemap. A page that names another
|
|
144
|
+
* URL as its canonical (an archived-version page pointing at the latest docs)
|
|
145
|
+
* and an i18n fallback copy are left out of the sitemap on purpose: it lists
|
|
146
|
+
* canonical URLs only (see NON_CANONICAL_IN_SITEMAP).
|
|
147
|
+
*/
|
|
148
|
+
const unlistedPages = (
|
|
149
|
+
context: AuditContext,
|
|
150
|
+
listed: Map<string, string>,
|
|
151
|
+
deployBase: string
|
|
152
|
+
): Diagnostic[] =>
|
|
153
|
+
context.pages.flatMap((page) => {
|
|
154
|
+
const canonical = page.canonical
|
|
155
|
+
? canonicalPath(page.canonical, deployBase)
|
|
156
|
+
: null;
|
|
157
|
+
if (
|
|
158
|
+
!page.indexable ||
|
|
159
|
+
page.route?.fallback ||
|
|
160
|
+
(canonical && canonical !== normalizePath(page.url)) ||
|
|
161
|
+
ERROR_ROUTES.has(page.url) ||
|
|
162
|
+
listed.has(normalizePath(page.url))
|
|
163
|
+
) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
return [
|
|
167
|
+
finding(
|
|
168
|
+
"BLUME_AUDIT_INDEXABLE_PAGE_NOT_IN_SITEMAP",
|
|
169
|
+
pageSite(context, page),
|
|
170
|
+
`${page.url} is built and indexable but is not listed in sitemap.xml.`
|
|
171
|
+
),
|
|
172
|
+
];
|
|
173
|
+
});
|
|
174
|
+
|
|
141
175
|
/**
|
|
142
176
|
* The sitemap, cross-checked against what was actually built.
|
|
143
177
|
*
|
|
@@ -175,23 +209,32 @@ export const sitemapChecks: CheckModule = {
|
|
|
175
209
|
finding(
|
|
176
210
|
"BLUME_AUDIT_SITEMAP_INVALID",
|
|
177
211
|
{ file: sitemap.file, url: "/sitemap.xml" },
|
|
178
|
-
`sitemap.xml is not a valid urlset: ${sitemap.error}`
|
|
212
|
+
`sitemap.xml is not a valid urlset or sitemap index: ${sitemap.error}`
|
|
179
213
|
)
|
|
180
214
|
);
|
|
181
215
|
return found;
|
|
182
216
|
}
|
|
183
217
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
218
|
+
// The limits are per file: a sitemap index splits a large site across
|
|
219
|
+
// urlsets, each of which must fit on its own.
|
|
220
|
+
const parts = sitemap.parts ?? [
|
|
221
|
+
{
|
|
222
|
+
bytes: sitemap.bytes,
|
|
223
|
+
file: sitemap.file,
|
|
224
|
+
url: "/sitemap.xml",
|
|
225
|
+
urls: sitemap.urls.length,
|
|
226
|
+
},
|
|
227
|
+
];
|
|
228
|
+
for (const part of parts) {
|
|
229
|
+
if (part.bytes > MAX_SITEMAP_BYTES || part.urls > MAX_SITEMAP_URLS) {
|
|
230
|
+
found.push(
|
|
231
|
+
finding(
|
|
232
|
+
"BLUME_AUDIT_SITEMAP_TOO_LARGE",
|
|
233
|
+
{ file: part.file, url: part.url },
|
|
234
|
+
`${part.url.slice(1)} holds ${part.urls} URLs in ${Math.round(part.bytes / 1024 / 1024)} MB.`
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
}
|
|
195
238
|
}
|
|
196
239
|
|
|
197
240
|
// A `<lastmod>` that lies — malformed, or claiming the future — teaches
|
|
@@ -230,25 +273,7 @@ export const sitemapChecks: CheckModule = {
|
|
|
230
273
|
);
|
|
231
274
|
}
|
|
232
275
|
|
|
233
|
-
|
|
234
|
-
// findable — but never made it into the sitemap.
|
|
235
|
-
for (const page of context.pages) {
|
|
236
|
-
if (
|
|
237
|
-
!page.indexable ||
|
|
238
|
-
ERROR_ROUTES.has(page.url) ||
|
|
239
|
-
listed.has(normalizePath(page.url))
|
|
240
|
-
) {
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
found.push(
|
|
244
|
-
finding(
|
|
245
|
-
"BLUME_AUDIT_INDEXABLE_PAGE_NOT_IN_SITEMAP",
|
|
246
|
-
pageSite(context, page),
|
|
247
|
-
`${page.url} is built and indexable but is not listed in sitemap.xml.`
|
|
248
|
-
)
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
|
|
276
|
+
found.push(...unlistedPages(context, listed, deployBase));
|
|
252
277
|
return found;
|
|
253
278
|
},
|
|
254
279
|
tier: "static",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { normalizeBasePath, stripBasePath } from "../../core/base-path.ts";
|
|
1
2
|
import type { Diagnostic } from "../../core/types.ts";
|
|
2
3
|
import { finding } from "../catalog.ts";
|
|
3
4
|
import { pageSite } from "../locate.ts";
|
|
4
|
-
import type { CheckModule, PageSnapshot } from "../types.ts";
|
|
5
|
+
import type { AuditContext, CheckModule, PageSnapshot } from "../types.ts";
|
|
5
6
|
import { normalizePath } from "../url.ts";
|
|
6
7
|
|
|
7
8
|
/** The Open Graph properties a share card is unusable without. */
|
|
@@ -175,6 +176,23 @@ const URL_STYLE: { name: string; test: RegExp }[] = [
|
|
|
175
176
|
{ name: "spaces", test: /%20| /u },
|
|
176
177
|
];
|
|
177
178
|
|
|
179
|
+
/**
|
|
180
|
+
* The part of a page URL its author named: the URL minus a leading i18n locale
|
|
181
|
+
* segment. A locale code like `pt-BR` is BCP 47 casing set once in config,
|
|
182
|
+
* not an untidy slug, so it must not trip the uppercase check on every page
|
|
183
|
+
* of that locale. The segment follows `basePath`, which page URLs carry.
|
|
184
|
+
*/
|
|
185
|
+
const authoredPath = (context: AuditContext, url: string): string => {
|
|
186
|
+
const { basePath, i18n } = context.project.config;
|
|
187
|
+
if (!i18n) {
|
|
188
|
+
return url;
|
|
189
|
+
}
|
|
190
|
+
const base = normalizeBasePath(basePath);
|
|
191
|
+
const [, first = "", ...rest] = stripBasePath(base, url).split("/");
|
|
192
|
+
const isLocale = i18n.locales.some((locale) => locale.code === first);
|
|
193
|
+
return isLocale ? `${base}/${rest.join("/")}` : url;
|
|
194
|
+
};
|
|
195
|
+
|
|
178
196
|
/** A protocol-relative URL names a dotted host (`//cdn.example.com/x`). */
|
|
179
197
|
const DOTTED_HOST = /^\/\/[^/]*\./u;
|
|
180
198
|
|
|
@@ -220,7 +238,8 @@ export const urlChecks: CheckModule = {
|
|
|
220
238
|
}
|
|
221
239
|
}
|
|
222
240
|
|
|
223
|
-
const
|
|
241
|
+
const authored = authoredPath(context, page.url);
|
|
242
|
+
const untidy = URL_STYLE.filter((style) => style.test.test(authored));
|
|
224
243
|
if (untidy.length > 0) {
|
|
225
244
|
found.push(
|
|
226
245
|
finding(
|
package/src/audit/crawl.ts
CHANGED
|
@@ -125,6 +125,11 @@ type XmlValue =
|
|
|
125
125
|
const isUrlsetElement = (value: XmlValue): value is { url?: XmlValue } =>
|
|
126
126
|
typeof value === "object" && value !== null;
|
|
127
127
|
|
|
128
|
+
const isSitemapIndexElement = (
|
|
129
|
+
value: XmlValue
|
|
130
|
+
): value is { sitemap?: XmlValue } =>
|
|
131
|
+
typeof value === "object" && value !== null;
|
|
132
|
+
|
|
128
133
|
const isUrlEntry = (
|
|
129
134
|
value: XmlValue
|
|
130
135
|
): value is { lastmod?: XmlValue; loc?: XmlValue } =>
|
|
@@ -134,7 +139,9 @@ const isText = (value: XmlValue): value is string => typeof value === "string";
|
|
|
134
139
|
|
|
135
140
|
/**
|
|
136
141
|
* Parse `sitemap.xml`. Deliberately shallow: the checks only need the `<loc>`
|
|
137
|
-
* list, each loc's `<lastmod>`, and whether the document is a urlset at all
|
|
142
|
+
* list, each loc's `<lastmod>`, and whether the document is a urlset at all —
|
|
143
|
+
* or, for a sitemap index, the child sitemaps it lists (`sitemaps`), which
|
|
144
|
+
* the crawler reads from the build.
|
|
138
145
|
*/
|
|
139
146
|
export const parseSitemap = (
|
|
140
147
|
file: string,
|
|
@@ -149,10 +156,20 @@ export const parseSitemap = (
|
|
|
149
156
|
doc.error = "no <urlset> element";
|
|
150
157
|
return doc;
|
|
151
158
|
}
|
|
159
|
+
if (Object.hasOwn(parsed, "sitemapindex")) {
|
|
160
|
+
const { sitemapindex } = parsed;
|
|
161
|
+
const children = isSitemapIndexElement(sitemapindex)
|
|
162
|
+
? [sitemapindex.sitemap].flat()
|
|
163
|
+
: [];
|
|
164
|
+
doc.sitemaps = children.flatMap((child) =>
|
|
165
|
+
isUrlEntry(child) && isText(child.loc) && child.loc.trim()
|
|
166
|
+
? [child.loc.trim()]
|
|
167
|
+
: []
|
|
168
|
+
);
|
|
169
|
+
return doc;
|
|
170
|
+
}
|
|
152
171
|
if (!Object.hasOwn(parsed, "urlset")) {
|
|
153
|
-
doc.error =
|
|
154
|
-
? "sitemap is an index, not a urlset"
|
|
155
|
-
: "no <urlset> element";
|
|
172
|
+
doc.error = "no <urlset> element";
|
|
156
173
|
return doc;
|
|
157
174
|
}
|
|
158
175
|
const { urlset } = parsed;
|
|
@@ -252,6 +269,69 @@ const readIfPresent = async (file: string): Promise<string | null> => {
|
|
|
252
269
|
}
|
|
253
270
|
};
|
|
254
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Read `sitemap.xml`, following it through a sitemap index: each child
|
|
274
|
+
* sitemap is read from the build (its `<loc>` carries the site origin and the
|
|
275
|
+
* deployment base, the file tree neither) and its URLs merged into the one
|
|
276
|
+
* document the checks see. A child that can't be resolved, is missing, or
|
|
277
|
+
* isn't a urlset makes the whole document invalid.
|
|
278
|
+
*/
|
|
279
|
+
const readSitemap = async (
|
|
280
|
+
staticDir: string,
|
|
281
|
+
files: Map<string, number>,
|
|
282
|
+
deployBase: string
|
|
283
|
+
): Promise<SitemapDoc | null> => {
|
|
284
|
+
const file = join(staticDir, "sitemap.xml");
|
|
285
|
+
const xml = await readIfPresent(file);
|
|
286
|
+
if (xml === null) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
const doc = parseSitemap(
|
|
290
|
+
file,
|
|
291
|
+
xml,
|
|
292
|
+
files.get("/sitemap.xml") ?? Buffer.byteLength(xml, "utf-8")
|
|
293
|
+
);
|
|
294
|
+
if (!doc.sitemaps) {
|
|
295
|
+
return doc;
|
|
296
|
+
}
|
|
297
|
+
doc.parts = [];
|
|
298
|
+
for (const loc of doc.sitemaps) {
|
|
299
|
+
let url: string;
|
|
300
|
+
try {
|
|
301
|
+
url = stripBasePath(deployBase, decodeURI(new URL(loc).pathname));
|
|
302
|
+
} catch {
|
|
303
|
+
doc.error = `the sitemap index lists "${loc}", which is not a valid absolute URL`;
|
|
304
|
+
return doc;
|
|
305
|
+
}
|
|
306
|
+
const childFile = join(staticDir, url);
|
|
307
|
+
// oxlint-disable-next-line no-await-in-loop -- one read per child, in index order
|
|
308
|
+
const childXml = await readIfPresent(childFile);
|
|
309
|
+
const child =
|
|
310
|
+
childXml === null
|
|
311
|
+
? null
|
|
312
|
+
: parseSitemap(
|
|
313
|
+
childFile,
|
|
314
|
+
childXml,
|
|
315
|
+
files.get(url) ?? Buffer.byteLength(childXml, "utf-8")
|
|
316
|
+
);
|
|
317
|
+
if (!child || child.error || child.sitemaps) {
|
|
318
|
+
doc.error = `the sitemap index lists ${loc}, which is not a urlset in the build`;
|
|
319
|
+
return doc;
|
|
320
|
+
}
|
|
321
|
+
doc.urls.push(...child.urls);
|
|
322
|
+
for (const [key, value] of child.lastmod ?? []) {
|
|
323
|
+
doc.lastmod?.set(key, value);
|
|
324
|
+
}
|
|
325
|
+
doc.parts.push({
|
|
326
|
+
bytes: child.bytes,
|
|
327
|
+
file: childFile,
|
|
328
|
+
url,
|
|
329
|
+
urls: child.urls.length,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return doc;
|
|
333
|
+
};
|
|
334
|
+
|
|
255
335
|
/**
|
|
256
336
|
* Read the built site: every HTML page reduced to a snapshot, the full file
|
|
257
337
|
* index (for resolving subresource references), plus sitemap.xml and robots.txt.
|
|
@@ -260,8 +340,10 @@ export const crawlStaticDir = async (options: {
|
|
|
260
340
|
staticDir: string;
|
|
261
341
|
manifest: BlumeManifest;
|
|
262
342
|
basePath: string;
|
|
343
|
+
/** The normalized `deployment.base`, which sitemap-index `<loc>`s carry. */
|
|
344
|
+
deployBase?: string;
|
|
263
345
|
}): Promise<CrawlResult> => {
|
|
264
|
-
const { staticDir, manifest, basePath } = options;
|
|
346
|
+
const { staticDir, manifest, basePath, deployBase = "" } = options;
|
|
265
347
|
const routes = routeIndex(manifest, basePath);
|
|
266
348
|
|
|
267
349
|
const htmlFiles = await glob("**/*.html", { absolute: true, cwd: staticDir });
|
|
@@ -287,8 +369,6 @@ export const crawlStaticDir = async (options: {
|
|
|
287
369
|
);
|
|
288
370
|
const pages = snapshots.filter((page) => page !== null);
|
|
289
371
|
|
|
290
|
-
const sitemapFile = join(staticDir, "sitemap.xml");
|
|
291
|
-
const sitemapXml = await readIfPresent(sitemapFile);
|
|
292
372
|
const robotsFile = join(staticDir, "robots.txt");
|
|
293
373
|
const robotsTxt = await readIfPresent(robotsFile);
|
|
294
374
|
const llmsFile = join(staticDir, "llms.txt");
|
|
@@ -300,13 +380,6 @@ export const crawlStaticDir = async (options: {
|
|
|
300
380
|
llms: llmsText === null ? null : parseLlms(llmsFile, llmsText),
|
|
301
381
|
pages,
|
|
302
382
|
robots: robotsTxt === null ? null : parseRobots(robotsFile, robotsTxt),
|
|
303
|
-
sitemap:
|
|
304
|
-
sitemapXml === null
|
|
305
|
-
? null
|
|
306
|
-
: parseSitemap(
|
|
307
|
-
sitemapFile,
|
|
308
|
-
sitemapXml,
|
|
309
|
-
files.get("/sitemap.xml") ?? Buffer.byteLength(sitemapXml, "utf-8")
|
|
310
|
-
),
|
|
383
|
+
sitemap: await readSitemap(staticDir, files, deployBase),
|
|
311
384
|
};
|
|
312
385
|
};
|
package/src/audit/report.ts
CHANGED
|
@@ -33,12 +33,47 @@ const TIER_FLAGS: { tier: AuditTier; flag: string }[] = [
|
|
|
33
33
|
interface CheckRollup {
|
|
34
34
|
id: CheckId;
|
|
35
35
|
count: number;
|
|
36
|
+
/** Distinct affected URLs — one page can carry several findings. */
|
|
37
|
+
pages: number;
|
|
36
38
|
severity: DiagnosticSeverity;
|
|
37
39
|
category: AuditCategory;
|
|
38
40
|
title: string;
|
|
39
41
|
findings: Diagnostic[];
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
const SEVERITY_ORDER = {
|
|
45
|
+
error: 0,
|
|
46
|
+
info: 2,
|
|
47
|
+
warning: 1,
|
|
48
|
+
} satisfies Record<DiagnosticSeverity, number>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The group's severity: the worst of its findings'. A check can downgrade a
|
|
52
|
+
* finding at runtime (an external link answering 503 is a warning, not the
|
|
53
|
+
* catalog's error), and the group's glyph must agree with the summary counts.
|
|
54
|
+
*/
|
|
55
|
+
const worstSeverity = (findings: Diagnostic[]): DiagnosticSeverity => {
|
|
56
|
+
let worst: DiagnosticSeverity = "info";
|
|
57
|
+
for (const finding of findings) {
|
|
58
|
+
if (SEVERITY_ORDER[finding.severity] < SEVERITY_ORDER[worst]) {
|
|
59
|
+
worst = finding.severity;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return worst;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** The first finding per affected URL, in report order. */
|
|
66
|
+
const firstPerPage = (findings: Diagnostic[]): Diagnostic[] => {
|
|
67
|
+
const seen = new Set<string | undefined>();
|
|
68
|
+
return findings.filter((finding) => {
|
|
69
|
+
if (seen.has(finding.url)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
seen.add(finding.url);
|
|
73
|
+
return true;
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
42
77
|
/**
|
|
43
78
|
* Group findings by check. This is the difference between a report people read
|
|
44
79
|
* and one they close: 214 pages × 6 findings is an unreadable wall, but "Meta
|
|
@@ -55,22 +90,18 @@ export const rollup = (diagnostics: Diagnostic[]): CheckRollup[] => {
|
|
|
55
90
|
}
|
|
56
91
|
}
|
|
57
92
|
|
|
58
|
-
const order = {
|
|
59
|
-
error: 0,
|
|
60
|
-
info: 2,
|
|
61
|
-
warning: 1,
|
|
62
|
-
} satisfies Record<DiagnosticSeverity, number>;
|
|
63
93
|
const checks = [...groups.entries()].map(([id, findings]) => {
|
|
64
94
|
// SAFETY: audit findings are only ever created through `finding()`, whose
|
|
65
95
|
// codes are the check catalog's ids.
|
|
66
96
|
const checkId = id as CheckId;
|
|
67
|
-
const { category,
|
|
97
|
+
const { category, title } = checkMeta(checkId);
|
|
68
98
|
return {
|
|
69
99
|
category,
|
|
70
100
|
count: findings.length,
|
|
71
101
|
findings,
|
|
72
102
|
id: checkId,
|
|
73
|
-
|
|
103
|
+
pages: firstPerPage(findings).length,
|
|
104
|
+
severity: worstSeverity(findings),
|
|
74
105
|
title,
|
|
75
106
|
};
|
|
76
107
|
});
|
|
@@ -80,7 +111,7 @@ export const rollup = (diagnostics: Diagnostic[]): CheckRollup[] => {
|
|
|
80
111
|
// and print "content" three separate times.
|
|
81
112
|
const worst = new Map<AuditCategory, number>();
|
|
82
113
|
for (const check of checks) {
|
|
83
|
-
const rank =
|
|
114
|
+
const rank = SEVERITY_ORDER[check.severity];
|
|
84
115
|
worst.set(
|
|
85
116
|
check.category,
|
|
86
117
|
Math.min(worst.get(check.category) ?? rank, rank)
|
|
@@ -91,7 +122,7 @@ export const rollup = (diagnostics: Diagnostic[]): CheckRollup[] => {
|
|
|
91
122
|
(a, b) =>
|
|
92
123
|
(worst.get(a.category) ?? 0) - (worst.get(b.category) ?? 0) ||
|
|
93
124
|
a.category.localeCompare(b.category) ||
|
|
94
|
-
|
|
125
|
+
SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity] ||
|
|
95
126
|
b.count - a.count
|
|
96
127
|
);
|
|
97
128
|
};
|
|
@@ -176,14 +207,16 @@ export const formatReport = (
|
|
|
176
207
|
}
|
|
177
208
|
|
|
178
209
|
const color = SEVERITY_COLOR[group.severity];
|
|
179
|
-
const pages = `${group.
|
|
210
|
+
const pages = `${group.pages} page${group.pages === 1 ? "" : "s"}`;
|
|
180
211
|
lines.push(
|
|
181
212
|
` ${color(`${GLYPH[group.severity]} ${group.title}`)} ${colors.dim(pages)}`
|
|
182
213
|
);
|
|
183
214
|
|
|
215
|
+
// The preview lists affected pages, each once; --verbose lists every
|
|
216
|
+
// finding, since each one's message names a different specific.
|
|
184
217
|
const shown = options.verbose
|
|
185
218
|
? group.findings
|
|
186
|
-
: group.findings.slice(0, PREVIEW);
|
|
219
|
+
: firstPerPage(group.findings).slice(0, PREVIEW);
|
|
187
220
|
for (const diagnostic of shown) {
|
|
188
221
|
lines.push(findingLine(diagnostic, root));
|
|
189
222
|
// The message names the specifics the rolled-up line can't — which target
|
|
@@ -192,16 +225,20 @@ export const formatReport = (
|
|
|
192
225
|
lines.push(` ${colors.dim(diagnostic.message)}`);
|
|
193
226
|
}
|
|
194
227
|
}
|
|
195
|
-
const hidden = group.
|
|
228
|
+
const hidden = options.verbose ? 0 : group.pages - shown.length;
|
|
196
229
|
if (hidden > 0) {
|
|
197
230
|
lines.push(` ${colors.dim(`… and ${hidden} more (--verbose)`)}`);
|
|
198
231
|
}
|
|
199
232
|
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
233
|
+
// Findings share the catalog's fix unless one overrode it (a navigation
|
|
234
|
+
// entry, a page generated from an API spec), so each distinct fix is
|
|
235
|
+
// printed once.
|
|
236
|
+
const fixes = new Set(
|
|
237
|
+
group.findings.flatMap((finding) =>
|
|
238
|
+
finding.suggestion ? [finding.suggestion] : []
|
|
239
|
+
)
|
|
240
|
+
);
|
|
241
|
+
for (const fix of fixes) {
|
|
205
242
|
lines.push(` ${colors.cyan(`fix: ${fix}`)}`);
|
|
206
243
|
}
|
|
207
244
|
lines.push("");
|
package/src/audit/run.ts
CHANGED
|
@@ -137,6 +137,7 @@ export const runAudit = async (options: AuditOptions): Promise<AuditResult> => {
|
|
|
137
137
|
|
|
138
138
|
const crawl = await crawlStaticDir({
|
|
139
139
|
basePath,
|
|
140
|
+
deployBase: normalizeBasePath(project.config.deployment.options.base),
|
|
140
141
|
manifest: project.manifest,
|
|
141
142
|
staticDir,
|
|
142
143
|
});
|
package/src/audit/types.ts
CHANGED
|
@@ -113,6 +113,16 @@ export interface RedirectResolution {
|
|
|
113
113
|
outcome: "ok" | "loop" | "broken" | "chain";
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/** One urlset file behind a sitemap, for the per-file size limits. */
|
|
117
|
+
export interface SitemapPart {
|
|
118
|
+
file: string;
|
|
119
|
+
/** Site path the file is served at, e.g. `/sitemap-1.xml`. */
|
|
120
|
+
url: string;
|
|
121
|
+
bytes: number;
|
|
122
|
+
/** How many `<url>` entries the file lists. */
|
|
123
|
+
urls: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
116
126
|
/** A parsed `sitemap.xml`. */
|
|
117
127
|
export interface SitemapDoc {
|
|
118
128
|
file: string;
|
|
@@ -121,6 +131,14 @@ export interface SitemapDoc {
|
|
|
121
131
|
urls: string[];
|
|
122
132
|
/** Each `<url>` block's `<lastmod>`, keyed by its `<loc>`. */
|
|
123
133
|
lastmod?: Map<string, string>;
|
|
134
|
+
/**
|
|
135
|
+
* The child sitemaps' `<loc>`s, when `sitemap.xml` is a sitemap index —
|
|
136
|
+
* Blume writes one past 50,000 URLs. The crawler reads each child from the
|
|
137
|
+
* build and merges its URLs into `urls` and `lastmod`.
|
|
138
|
+
*/
|
|
139
|
+
sitemaps?: string[];
|
|
140
|
+
/** The index's child urlset files, once the crawler has read them. */
|
|
141
|
+
parts?: SitemapPart[];
|
|
124
142
|
/** Parse failure, when the document isn't usable. */
|
|
125
143
|
error?: string;
|
|
126
144
|
}
|
package/src/audit/url.ts
CHANGED
|
@@ -43,11 +43,21 @@ export type ResolvedHref =
|
|
|
43
43
|
| { kind: "internal"; path: string; hash: string }
|
|
44
44
|
/** An absolute URL that resolves back to this site — should have been a path. */
|
|
45
45
|
| { kind: "self-origin"; path: string; hash: string }
|
|
46
|
+
/**
|
|
47
|
+
* A path on this origin that lacks `deployment.base`. The host serves the
|
|
48
|
+
* whole build under the base, so nothing answers here — `path` is the path
|
|
49
|
+
* as requested, base missing.
|
|
50
|
+
*/
|
|
51
|
+
| { kind: "outside-base"; path: string }
|
|
46
52
|
/** An absolute URL on another origin. */
|
|
47
53
|
| { kind: "external"; url: string }
|
|
48
54
|
/** In-page anchor, `mailto:`, `tel:`, `javascript:`, data URI — not a page link. */
|
|
49
55
|
| { kind: "ignored" };
|
|
50
56
|
|
|
57
|
+
/** Whether a served path sits under the deployment base (always, without one). */
|
|
58
|
+
const underBase = (deployBase: string, path: string): boolean =>
|
|
59
|
+
!deployBase || path === deployBase || path.startsWith(`${deployBase}/`);
|
|
60
|
+
|
|
51
61
|
const NON_HTTP_SCHEME = /^(?!https?:)[a-z][a-z0-9+.-]*:/iu;
|
|
52
62
|
|
|
53
63
|
/**
|
|
@@ -65,6 +75,20 @@ export const decodePath = (path: string): string => {
|
|
|
65
75
|
}
|
|
66
76
|
};
|
|
67
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Whether a `--url` value is an absolute http(s) URL the network tier can
|
|
80
|
+
* probe. A bare host (`example.com`) has no scheme, and `localhost:4321`
|
|
81
|
+
* parses with `localhost:` as its scheme — both are rejected.
|
|
82
|
+
*/
|
|
83
|
+
export const isHttpUrl = (input: string): boolean => {
|
|
84
|
+
try {
|
|
85
|
+
const { protocol } = new URL(input);
|
|
86
|
+
return protocol === "http:" || protocol === "https:";
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
68
92
|
/** The origin of `deployment.site`, or null when no site is configured. */
|
|
69
93
|
export const siteOrigin = (site?: string): string | null => {
|
|
70
94
|
if (!site) {
|
|
@@ -113,12 +137,14 @@ export const resolveHref = (
|
|
|
113
137
|
return { kind: "ignored" };
|
|
114
138
|
}
|
|
115
139
|
if (origin && parsed.origin === origin) {
|
|
140
|
+
const served = decodePath(parsed.pathname);
|
|
141
|
+
if (!underBase(deployBase, served)) {
|
|
142
|
+
return { kind: "outside-base", path: normalizePath(served) };
|
|
143
|
+
}
|
|
116
144
|
return {
|
|
117
145
|
hash: parsed.hash.slice(1),
|
|
118
146
|
kind: "self-origin",
|
|
119
|
-
path: normalizePath(
|
|
120
|
-
stripBasePath(deployBase, decodePath(parsed.pathname))
|
|
121
|
-
),
|
|
147
|
+
path: normalizePath(stripBasePath(deployBase, served)),
|
|
122
148
|
};
|
|
123
149
|
}
|
|
124
150
|
return { kind: "external", url: parsed.toString() };
|
|
@@ -140,11 +166,16 @@ export const resolveHref = (
|
|
|
140
166
|
} catch {
|
|
141
167
|
return { kind: "ignored" };
|
|
142
168
|
}
|
|
169
|
+
const served = decodePath(resolved.pathname);
|
|
170
|
+
// Only a root-relative href names the served path outright; a relative one
|
|
171
|
+
// (`./auth`) was resolved against the base-less page URL above, so it
|
|
172
|
+
// inherits the page's base by construction.
|
|
173
|
+
if (target.startsWith("/") && !underBase(deployBase, served)) {
|
|
174
|
+
return { kind: "outside-base", path: normalizePath(served) };
|
|
175
|
+
}
|
|
143
176
|
return {
|
|
144
177
|
hash: resolved.hash.slice(1),
|
|
145
178
|
kind: "internal",
|
|
146
|
-
path: normalizePath(
|
|
147
|
-
stripBasePath(deployBase, decodePath(resolved.pathname))
|
|
148
|
-
),
|
|
179
|
+
path: normalizePath(stripBasePath(deployBase, served)),
|
|
149
180
|
};
|
|
150
181
|
};
|
package/src/blume-modules.d.ts
CHANGED
|
@@ -18,9 +18,9 @@ declare module "blume:search-client" {
|
|
|
18
18
|
declare module "blume:ask" {
|
|
19
19
|
/** The generated Ask trigger (see `askComponentTemplate`); empty when Ask is off. */
|
|
20
20
|
// Mirrors the generated `.blume/src/env.d.ts` declaration: the trigger's type
|
|
21
|
-
// is the
|
|
21
|
+
// is the Assistant island the enabled template re-exports.
|
|
22
22
|
// oxlint-disable-next-line typescript/consistent-type-imports
|
|
23
|
-
const Ask: typeof import("./components/islands/
|
|
23
|
+
const Ask: typeof import("./components/islands/Assistant.astro").default;
|
|
24
24
|
export default Ask;
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import type { Diagnostic } from "../core/types.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The fields of an `astro build` rejection the report reads. Rolldown bundles
|
|
7
|
+
* every plugin failure (an MDX compile error, say) into one error whose
|
|
8
|
+
* `errors` holds each; a failure carries `loc` where the compiler pinned it,
|
|
9
|
+
* or only `id`, the module that failed.
|
|
10
|
+
*/
|
|
11
|
+
const buildErrorSchema = z.looseObject({
|
|
12
|
+
errors: z.array(z.instanceof(Error)).optional(),
|
|
13
|
+
id: z.string().optional(),
|
|
14
|
+
loc: z
|
|
15
|
+
.looseObject({
|
|
16
|
+
column: z.number().optional(),
|
|
17
|
+
file: z.string().optional(),
|
|
18
|
+
line: z.number().optional(),
|
|
19
|
+
})
|
|
20
|
+
.optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** One failure as a diagnostic at the file (and position) it names. */
|
|
24
|
+
const failureDiagnostic = (error: Error): Diagnostic => {
|
|
25
|
+
const parsed = buildErrorSchema.safeParse(error);
|
|
26
|
+
const { id, loc } = parsed.success ? parsed.data : {};
|
|
27
|
+
// A named error (`MDXError`, `CompilerError`) says which step failed.
|
|
28
|
+
const name = error.name === "Error" ? "" : `${error.name}: `;
|
|
29
|
+
return {
|
|
30
|
+
code: "BLUME_BUILD_FAILED",
|
|
31
|
+
column: loc?.column,
|
|
32
|
+
file: loc?.file ?? id,
|
|
33
|
+
line: loc?.line,
|
|
34
|
+
message: `${name}${error.message}`,
|
|
35
|
+
severity: "error",
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What `astro build` rejected with, as build diagnostics: one per failure
|
|
41
|
+
* Rolldown collected, else the error itself. A page Blume's own checks pass
|
|
42
|
+
* can still fail Astro's compile (MDX that doesn't parse) or render, and that
|
|
43
|
+
* is a problem in the site, reported at its file, rather than an internal
|
|
44
|
+
* error blaming Blume.
|
|
45
|
+
*/
|
|
46
|
+
export const astroBuildDiagnostics = (error: Error): Diagnostic[] => {
|
|
47
|
+
const parsed = buildErrorSchema.safeParse(error);
|
|
48
|
+
const failures = parsed.success ? (parsed.data.errors ?? []) : [];
|
|
49
|
+
return (failures.length > 0 ? failures : [error]).map(failureDiagnostic);
|
|
50
|
+
};
|