blume 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +147 -0
- package/dist/cli/{chunk-qwsrynx5.js → chunk-1d7ve1dm.js} +38 -19
- package/dist/cli/{chunk-qwsrynx5.js.map → chunk-1d7ve1dm.js.map} +4 -4
- package/dist/cli/{chunk-s6jhgk0q.js → chunk-2eytanqx.js} +2 -2
- package/dist/cli/{chunk-kdp5q7ke.js → chunk-2hsdwb9n.js} +18 -19
- package/dist/cli/{chunk-kdp5q7ke.js.map → chunk-2hsdwb9n.js.map} +2 -2
- package/dist/cli/{chunk-hdpx1tax.js → chunk-2z928egk.js} +5 -5
- package/dist/cli/{chunk-jwyddg7y.js → chunk-35d4wj9f.js} +30 -24
- package/dist/cli/chunk-35d4wj9f.js.map +15 -0
- package/dist/cli/{chunk-ah61y8py.js → chunk-364znk6q.js} +2 -2
- package/dist/cli/{chunk-jts8mvcz.js → chunk-3em5wd2y.js} +25 -6
- package/dist/cli/{chunk-jts8mvcz.js.map → chunk-3em5wd2y.js.map} +3 -3
- package/dist/cli/{chunk-s1p84fyh.js → chunk-5f86nr5m.js} +63 -22
- package/dist/cli/chunk-5f86nr5m.js.map +11 -0
- package/dist/cli/{chunk-2hn4b8z7.js → chunk-5m5nmvyq.js} +170 -76
- package/dist/cli/chunk-5m5nmvyq.js.map +13 -0
- package/dist/cli/{chunk-epjnccmv.js → chunk-5xvm6tfj.js} +18 -15
- package/dist/cli/chunk-5xvm6tfj.js.map +10 -0
- package/dist/cli/{chunk-fz5wtpmh.js → chunk-6dsbexzp.js} +18 -15
- package/dist/cli/chunk-6dsbexzp.js.map +10 -0
- package/dist/cli/{chunk-vtk4a6dg.js → chunk-8ktnccpt.js} +1 -1
- package/dist/cli/{chunk-6hsn950k.js → chunk-9t7a85s3.js} +178 -48
- package/dist/cli/chunk-9t7a85s3.js.map +10 -0
- package/dist/cli/{chunk-fxypxtvm.js → chunk-a58773jm.js} +2 -2
- package/dist/cli/{chunk-mb2919y2.js → chunk-acanzt5p.js} +17 -6
- package/dist/cli/chunk-acanzt5p.js.map +10 -0
- package/dist/cli/{chunk-27g6wdth.js → chunk-akbpwfxc.js} +90 -26
- package/dist/cli/chunk-akbpwfxc.js.map +10 -0
- package/dist/cli/{chunk-wm7js3j9.js → chunk-b07cmahc.js} +2 -2
- package/dist/cli/{chunk-wgm7m9qk.js → chunk-c8chx29p.js} +179 -51
- package/dist/cli/chunk-c8chx29p.js.map +36 -0
- package/dist/cli/{chunk-qs4q5p4e.js → chunk-crgn1q09.js} +32 -14
- package/dist/cli/chunk-crgn1q09.js.map +10 -0
- package/dist/cli/chunk-e04dxsz1.js +39 -0
- package/dist/cli/chunk-e04dxsz1.js.map +10 -0
- package/dist/cli/{chunk-yt5n7ppj.js → chunk-ey84smr6.js} +17 -7
- package/dist/cli/chunk-ey84smr6.js.map +10 -0
- package/dist/cli/{chunk-f2z5v128.js → chunk-g4hq16wv.js} +14 -15
- package/dist/cli/{chunk-f2z5v128.js.map → chunk-g4hq16wv.js.map} +2 -2
- package/dist/cli/{chunk-fa25z98p.js → chunk-ga0pf4aj.js} +9 -8
- package/dist/cli/chunk-ga0pf4aj.js.map +11 -0
- package/dist/cli/{chunk-kpf8rrjc.js → chunk-hm3vjy5s.js} +109 -58
- package/dist/cli/chunk-hm3vjy5s.js.map +19 -0
- package/dist/cli/{chunk-fs23ddbb.js → chunk-j85vccga.js} +625 -750
- package/dist/cli/chunk-j85vccga.js.map +36 -0
- package/dist/cli/{chunk-ch6g3ar0.js → chunk-p3v96n38.js} +6 -6
- package/dist/cli/{chunk-ch6g3ar0.js.map → chunk-p3v96n38.js.map} +3 -3
- package/dist/cli/{chunk-q5163e60.js → chunk-p73c0m7w.js} +21 -19
- package/dist/cli/chunk-p73c0m7w.js.map +11 -0
- package/dist/cli/{chunk-dh8cwk36.js → chunk-pehfxfta.js} +24 -9
- package/dist/cli/chunk-pehfxfta.js.map +10 -0
- package/dist/cli/{chunk-qkqwkpte.js → chunk-pv29h0wf.js} +3688 -1313
- package/dist/cli/chunk-pv29h0wf.js.map +190 -0
- package/dist/cli/{chunk-6vm74dry.js → chunk-q58y5e6a.js} +10 -10
- package/dist/cli/{chunk-6vm74dry.js.map → chunk-q58y5e6a.js.map} +3 -3
- package/dist/cli/{chunk-zxcczpyx.js → chunk-r20tn01b.js} +1 -1
- package/dist/cli/{chunk-m3vmjgmq.js → chunk-r9rcc4w7.js} +16 -8
- package/dist/cli/chunk-r9rcc4w7.js.map +10 -0
- package/dist/cli/{chunk-5shv93fd.js → chunk-tkacnehg.js} +2 -2
- package/dist/cli/{chunk-zxh4d9vy.js → chunk-tzmab476.js} +4 -4
- package/dist/cli/{chunk-yw7dm696.js → chunk-vg9r4eb9.js} +24 -11
- package/dist/cli/chunk-vg9r4eb9.js.map +14 -0
- package/dist/cli/{chunk-79jhk4py.js → chunk-wrr3j9w9.js} +260 -132
- package/dist/cli/chunk-wrr3j9w9.js.map +35 -0
- package/dist/cli/{chunk-6crbhc3x.js → chunk-yfyb25rh.js} +54 -27
- package/dist/cli/chunk-yfyb25rh.js.map +14 -0
- package/dist/cli/index.js +162 -35
- package/dist/cli/index.js.map +4 -4
- package/dist/types/ai/agent-surface.d.ts +32 -0
- package/dist/types/ai/api-catalog.d.ts +7 -1
- package/dist/types/ai/ask-context.d.ts +7 -0
- package/dist/types/ai/component-markdown.d.ts +4 -4
- package/dist/types/ai/link-headers.d.ts +8 -1
- package/dist/types/ai/openapi-components.d.ts +5 -2
- package/dist/types/ai/relative-links.d.ts +11 -4
- package/dist/types/ai/skills.d.ts +4 -1
- package/dist/types/ai/static-expression.d.ts +28 -0
- package/dist/types/ai/tar.d.ts +1 -3
- package/dist/types/analytics/databuddy.d.ts +43 -0
- package/dist/types/analytics/index.d.ts +4 -0
- package/dist/types/analytics/one-dollar-stats.d.ts +48 -0
- package/dist/types/analytics/schema.d.ts +28 -0
- package/dist/types/astro/integration.d.ts +3 -2
- 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 +24 -1
- package/dist/types/core/config-input.d.ts +13 -11
- package/dist/types/core/config.d.ts +2 -2
- package/dist/types/core/directive-diagnostics.d.ts +12 -0
- package/dist/types/core/graph.d.ts +2 -0
- package/dist/types/core/heading-markers.d.ts +5 -7
- package/dist/types/core/i18n-ui.d.ts +31 -0
- package/dist/types/core/i18n.d.ts +9 -1
- package/dist/types/core/last-modified.d.ts +10 -0
- 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 +13 -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 +59 -13
- package/dist/types/core/sources/github-releases.d.ts +5 -0
- package/dist/types/core/sources/lower.d.ts +38 -13
- package/dist/types/core/sources/normalize.d.ts +23 -2
- 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/sources/watch.d.ts +12 -5
- package/dist/types/core/standard-schema.d.ts +5 -0
- package/dist/types/core/types.d.ts +9 -0
- package/dist/types/deploy/adapters/node.d.ts +5 -2
- package/dist/types/deploy/adapters/types.d.ts +7 -0
- package/dist/types/deploy/artifacts.d.ts +6 -4
- package/dist/types/deploy/cloudflare-negotiation.d.ts +3 -2
- package/dist/types/deploy/headers.d.ts +36 -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 +15 -0
- package/dist/types/deploy/platforms/vercel.d.ts +3 -2
- package/dist/types/deploy/redirects.d.ts +31 -15
- package/dist/types/deploy/vercel-negotiation.d.ts +3 -2
- package/dist/types/markdown/directives.d.ts +62 -0
- package/dist/types/markdown/features.d.ts +21 -0
- package/dist/types/markdown/mdast.d.ts +63 -0
- package/dist/types/openapi/asyncapi.d.ts +4 -2
- package/dist/types/openapi/model.d.ts +20 -6
- package/dist/types/search/sync/algolia.d.ts +3 -1
- package/docs/01-quickstart.mdx +3 -2
- package/docs/02-deployment.mdx +20 -9
- package/docs/08-faq.mdx +10 -3
- package/docs/advanced/changelog.mdx +1 -1
- package/docs/advanced/custom-pages.mdx +7 -5
- package/docs/cli/audit.mdx +19 -3
- package/docs/cli/doctor.mdx +2 -2
- package/docs/cli/evals.mdx +4 -4
- package/docs/cli/index.mdx +4 -1
- package/docs/cli/translate.mdx +4 -4
- package/docs/cli/version.mdx +1 -1
- package/docs/configuration/analytics.mdx +42 -2
- package/docs/configuration/assistant.mdx +1 -1
- package/docs/configuration/customization.mdx +6 -3
- package/docs/configuration/index.mdx +5 -3
- package/docs/configuration/search.mdx +2 -2
- package/docs/content/components.mdx +2 -2
- package/docs/content/frontmatter.mdx +5 -1
- package/docs/content/i18n.mdx +3 -1
- package/docs/content/index.mdx +8 -4
- package/docs/content/islands.mdx +1 -1
- package/docs/content/meta.mdx +7 -3
- package/docs/content/navigation.mdx +29 -4
- package/docs/content/sources.mdx +18 -16
- package/docs/content/syntax.mdx +14 -0
- package/docs/content/versioning.mdx +2 -1
- package/docs/discoverability/agent-discovery.mdx +22 -8
- package/docs/discoverability/index.mdx +4 -3
- package/docs/discoverability/llms-txt.mdx +2 -5
- package/docs/discoverability/markdown.mdx +6 -4
- package/docs/discoverability/mcp.mdx +4 -0
- package/docs/discoverability/metadata.mdx +3 -2
- package/docs/discoverability/open-graph.mdx +8 -4
- package/docs/discoverability/rss.mdx +4 -2
- package/docs/references/asyncapi.mdx +2 -2
- package/docs/references/graphql.mdx +2 -2
- package/docs/references/openapi.mdx +6 -4
- package/package.json +1 -1
- package/skills/blume-migrate/SKILL.md +6 -6
- package/skills/blume-migrate/references/docusaurus.md +6 -6
- package/skills/blume-migrate/references/fumadocs.md +1 -1
- 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 +2 -2
- package/skills/blume-migrate/references/starlight.md +5 -5
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +9 -4
- package/skills/blume-update-docs/references/audit-checklist.md +1 -1
- package/src/ai/agent-readability.ts +2 -2
- package/src/ai/agent-surface.ts +56 -0
- package/src/ai/ai-catalog.ts +6 -2
- package/src/ai/api/handlers.ts +2 -2
- package/src/ai/api/spec.ts +17 -4
- package/src/ai/api-catalog.ts +13 -3
- package/src/ai/ask-context.ts +14 -2
- package/src/ai/ask-data.ts +26 -12
- package/src/ai/changelog-markdown.ts +2 -2
- package/src/ai/component-markdown.ts +43 -37
- package/src/ai/link-headers.ts +22 -8
- package/src/ai/llms.ts +22 -10
- package/src/ai/markdown.ts +23 -11
- package/src/ai/mcp/discovery.ts +2 -2
- package/src/ai/mcp/query.ts +2 -2
- package/src/ai/mcp/server.ts +80 -9
- package/src/ai/openapi-components.ts +22 -6
- package/src/ai/relative-links.ts +99 -15
- package/src/ai/serializers.ts +2 -1
- package/src/ai/skills.ts +14 -1
- package/src/ai/static-expression.ts +416 -0
- package/src/ai/tar.ts +139 -12
- package/src/ai/visibility.ts +45 -14
- package/src/analytics/databuddy.ts +67 -0
- package/src/analytics/head.ts +8 -0
- package/src/analytics/index.ts +7 -0
- package/src/analytics/one-dollar-stats.ts +86 -0
- package/src/analytics/posthog.ts +21 -3
- package/src/analytics/schema.ts +4 -0
- package/src/astro/generate.ts +72 -28
- package/src/astro/include-hmr.ts +31 -12
- package/src/astro/include-refresh.ts +19 -8
- package/src/astro/integration.ts +108 -14
- package/src/astro/runtime-modules.ts +28 -13
- package/src/astro/templates.ts +139 -57
- 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 +22 -1
- 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/graph.ts +3 -1
- package/src/audit/report.ts +54 -17
- package/src/audit/run.ts +13 -9
- package/src/audit/snapshot.ts +5 -0
- package/src/audit/types.ts +18 -0
- package/src/audit/url.ts +37 -6
- package/src/cli/args.ts +32 -0
- package/src/cli/build-failure.ts +50 -0
- package/src/cli/commands/audit.ts +11 -1
- package/src/cli/commands/build.ts +19 -5
- package/src/cli/commands/check.ts +2 -0
- package/src/cli/commands/doctor.ts +3 -1
- package/src/cli/commands/eval.ts +20 -18
- package/src/cli/commands/init.ts +15 -5
- package/src/cli/commands/preview.ts +15 -0
- package/src/cli/commands/sync.ts +2 -0
- package/src/cli/commands/translate.ts +18 -18
- package/src/cli/commands/upgrade.ts +11 -0
- package/src/cli/commands/validate.ts +3 -1
- package/src/cli/dev-lock.ts +157 -37
- 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/report-format.ts +11 -6
- package/src/components/colors.ts +19 -0
- package/src/components/content/AccordionItem.astro +26 -22
- package/src/components/content/Badge.astro +7 -12
- package/src/components/content/Card.astro +2 -2
- package/src/components/content/Component.astro +11 -5
- 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/Tab.astro +0 -1
- package/src/components/content/Tabs.astro +58 -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 +20 -0
- package/src/components/content/base-href.ts +14 -22
- package/src/components/content/inline-markdown.ts +27 -7
- package/src/components/copy-feedback.ts +35 -8
- package/src/components/islands/assistant.tsx +37 -6
- package/src/components/islands/base-path.ts +47 -10
- package/src/components/islands/hooks.ts +42 -21
- package/src/components/islands/webmcp.ts +16 -10
- package/src/components/layout/Banner.astro +23 -4
- package/src/components/layout/Breadcrumbs.astro +5 -2
- package/src/components/layout/DiscoveryLinks.astro +9 -5
- package/src/components/layout/Header.astro +23 -10
- package/src/components/layout/LanguageSwitcher.astro +2 -2
- package/src/components/layout/Logo.astro +5 -0
- package/src/components/layout/NavSelector.astro +8 -3
- package/src/components/layout/NavTabMenu.astro +133 -0
- package/src/components/layout/NavTree.astro +31 -12
- package/src/components/layout/NavTreeCache.astro +5 -2
- package/src/components/layout/NavTreeScript.astro +45 -5
- package/src/components/layout/PageActions.astro +4 -3
- package/src/components/layout/PageLayout.astro +28 -13
- package/src/components/layout/Pagination.astro +3 -3
- package/src/components/layout/ReferenceLayout.astro +4 -1
- package/src/components/layout/RootLayout.astro +25 -12
- package/src/components/layout/Search.astro +38 -14
- package/src/components/layout/VersionBanner.astro +2 -2
- package/src/components/layout/analytics-client.ts +14 -0
- package/src/components/layout/toc-active.ts +41 -0
- package/src/components/layout/toc-element.ts +8 -14
- package/src/components/openapi/ApiTagOperations.astro +2 -2
- package/src/components/openapi/AsyncApiOperation.astro +7 -4
- package/src/components/openapi/GraphqlChip.astro +2 -2
- package/src/components/openapi/GraphqlType.astro +11 -3
- package/src/components/openapi/MessageComposer.astro +1 -1
- package/src/components/openapi/Operation.astro +23 -5
- package/src/components/openapi/PanelTabs.astro +4 -1
- package/src/components/openapi/Playground.astro +8 -2
- package/src/components/openapi/RequestPanel.astro +13 -6
- package/src/components/openapi/SchemaProperty.astro +11 -33
- package/src/components/openapi/SchemaTable.astro +34 -52
- package/src/components/openapi/async.ts +38 -6
- package/src/components/openapi/helpers.ts +100 -12
- package/src/components/openapi/message-composer.ts +14 -1
- package/src/components/openapi/message-model.ts +12 -2
- package/src/components/openapi/message.ts +21 -3
- package/src/components/openapi/operation-model.ts +119 -17
- package/src/components/openapi/panel.ts +31 -5
- package/src/components/openapi/param-style.ts +181 -0
- package/src/components/openapi/playground-client.ts +110 -23
- package/src/components/openapi/playground-schema.ts +25 -6
- package/src/components/openapi/request.ts +192 -26
- package/src/components/openapi/schema-tree.ts +209 -0
- package/src/components/openapi/snippets.ts +113 -19
- package/src/components/openapi/validate-json.ts +1 -1
- package/src/components/openapi/ws-client.ts +18 -2
- package/src/core/base-path.ts +69 -8
- package/src/core/config-input.ts +13 -11
- package/src/core/config.ts +18 -4
- package/src/core/diagnostics.ts +219 -30
- package/src/core/directive-diagnostics.ts +99 -0
- package/src/core/frontmatter.ts +21 -18
- package/src/core/graph.ts +120 -11
- package/src/core/heading-markers.ts +5 -18
- package/src/core/i18n-ui.ts +39 -2
- package/src/core/i18n.ts +44 -5
- package/src/core/last-modified.ts +25 -3
- package/src/core/links.ts +7 -4
- package/src/core/load-module.ts +20 -0
- package/src/core/locale-links.ts +22 -18
- package/src/core/manifest.ts +3 -2
- package/src/core/meta.ts +89 -12
- package/src/core/nav-diagnostics.ts +56 -1
- package/src/core/navigation.ts +272 -83
- package/src/core/ordering-prefix.ts +27 -0
- package/src/core/project-graph.ts +50 -6
- package/src/core/safe-href.ts +53 -1
- package/src/core/safe-links.ts +11 -2
- package/src/core/schema.ts +94 -18
- package/src/core/sources/assets.ts +83 -41
- package/src/core/sources/contentful-rich-text.ts +28 -20
- package/src/core/sources/contentful.ts +25 -12
- package/src/core/sources/filesystem.ts +25 -3
- package/src/core/sources/github-releases.ts +131 -11
- package/src/core/sources/lexical.ts +30 -20
- package/src/core/sources/lower.ts +276 -56
- package/src/core/sources/mdx-remote.ts +52 -16
- package/src/core/sources/normalize.ts +456 -119
- package/src/core/sources/notion.ts +77 -34
- package/src/core/sources/obsidian.ts +40 -9
- package/src/core/sources/payload.ts +1 -0
- package/src/core/sources/portable-text.ts +85 -49
- 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 +28 -19
- package/src/core/sources/strapi.ts +1 -0
- package/src/core/sources/types.ts +31 -0
- package/src/core/sources/watch.ts +20 -7
- package/src/core/standard-schema.ts +10 -6
- package/src/core/types.ts +9 -0
- package/src/core/ui-packs/ar.ts +13 -0
- package/src/core/ui-packs/bg.ts +13 -0
- package/src/core/ui-packs/bn.ts +13 -0
- package/src/core/ui-packs/ca.ts +13 -0
- package/src/core/ui-packs/cs.ts +13 -0
- package/src/core/ui-packs/da.ts +13 -0
- package/src/core/ui-packs/de.ts +13 -0
- package/src/core/ui-packs/el.ts +13 -0
- package/src/core/ui-packs/es.ts +13 -0
- package/src/core/ui-packs/fa.ts +13 -0
- package/src/core/ui-packs/fi.ts +13 -0
- package/src/core/ui-packs/fr.ts +13 -0
- package/src/core/ui-packs/he.ts +13 -0
- package/src/core/ui-packs/hi.ts +13 -0
- package/src/core/ui-packs/hr.ts +13 -0
- package/src/core/ui-packs/hu.ts +13 -0
- package/src/core/ui-packs/id.ts +13 -0
- package/src/core/ui-packs/it.ts +13 -0
- package/src/core/ui-packs/ja.ts +13 -0
- package/src/core/ui-packs/ko.ts +13 -0
- package/src/core/ui-packs/nl.ts +13 -0
- package/src/core/ui-packs/no.ts +13 -0
- package/src/core/ui-packs/pl.ts +13 -0
- package/src/core/ui-packs/pt-br.ts +13 -0
- package/src/core/ui-packs/pt.ts +13 -0
- package/src/core/ui-packs/ro.ts +13 -0
- package/src/core/ui-packs/ru.ts +13 -0
- package/src/core/ui-packs/sk.ts +13 -0
- package/src/core/ui-packs/sr.ts +13 -0
- package/src/core/ui-packs/sv.ts +13 -0
- package/src/core/ui-packs/th.ts +13 -0
- package/src/core/ui-packs/tr.ts +13 -0
- package/src/core/ui-packs/uk.ts +13 -0
- package/src/core/ui-packs/vi.ts +13 -0
- package/src/core/ui-packs/zh-tw.ts +13 -0
- package/src/core/ui-packs/zh.ts +13 -0
- package/src/core/version-cut.ts +69 -17
- 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 +52 -11
- package/src/deploy/cloudflare-negotiation.ts +27 -16
- package/src/deploy/headers.ts +75 -55
- package/src/deploy/node-headers.ts +148 -27
- package/src/deploy/platforms/cloudflare.ts +13 -3
- package/src/deploy/platforms/netlify.ts +83 -5
- package/src/deploy/platforms/node.ts +8 -5
- package/src/deploy/platforms/static.ts +2 -0
- package/src/deploy/platforms/types.ts +15 -0
- package/src/deploy/platforms/vercel.ts +10 -3
- package/src/deploy/redirects.ts +68 -21
- package/src/deploy/robots.ts +2 -2
- package/src/deploy/rss.ts +4 -3
- package/src/deploy/sitemap.ts +7 -5
- 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 +74 -29
- package/src/markdown/directives.ts +242 -36
- package/src/markdown/features.ts +17 -0
- package/src/markdown/index.ts +13 -9
- package/src/markdown/mdast.ts +5 -2
- package/src/markdown/relative-links.ts +15 -27
- package/src/markdown/route-snapshot.ts +37 -0
- package/src/og/card.ts +149 -9
- package/src/og/derive.ts +156 -5
- package/src/og/index.ts +1 -0
- package/src/openapi/asyncapi.ts +4 -2
- package/src/openapi/graphql-build.ts +28 -2
- package/src/openapi/model.ts +99 -27
- package/src/openapi/proxy.ts +63 -10
- package/src/openapi/render-mdx.ts +51 -3
- package/src/registry/eject.ts +297 -43
- package/src/search/adapters/version-scope.ts +30 -0
- package/src/search/documents.ts +89 -27
- package/src/search/popular.ts +2 -1
- package/src/search/sync/algolia.ts +36 -2
- package/src/seo/jsonld.ts +7 -3
- 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 +76 -24
- package/src/translate/run.ts +14 -8
- package/src/translate/validate.ts +14 -2
- package/src/translate/work-list.ts +91 -19
- package/src/upgrade/upgrade.ts +36 -4
- package/dist/cli/chunk-27g6wdth.js.map +0 -10
- package/dist/cli/chunk-2hn4b8z7.js.map +0 -12
- package/dist/cli/chunk-6crbhc3x.js.map +0 -14
- package/dist/cli/chunk-6hsn950k.js.map +0 -10
- package/dist/cli/chunk-79jhk4py.js.map +0 -35
- package/dist/cli/chunk-82bbrxdn.js +0 -51
- package/dist/cli/chunk-82bbrxdn.js.map +0 -10
- package/dist/cli/chunk-abh8yjkn.js +0 -31
- package/dist/cli/chunk-abh8yjkn.js.map +0 -10
- package/dist/cli/chunk-ce574jw2.js +0 -23
- package/dist/cli/chunk-ce574jw2.js.map +0 -10
- package/dist/cli/chunk-dh8cwk36.js.map +0 -10
- package/dist/cli/chunk-epjnccmv.js.map +0 -10
- package/dist/cli/chunk-fa25z98p.js.map +0 -11
- package/dist/cli/chunk-fs23ddbb.js.map +0 -35
- package/dist/cli/chunk-fz5wtpmh.js.map +0 -10
- package/dist/cli/chunk-jwyddg7y.js.map +0 -15
- package/dist/cli/chunk-kpf8rrjc.js.map +0 -19
- package/dist/cli/chunk-m3vmjgmq.js.map +0 -10
- package/dist/cli/chunk-mb2919y2.js.map +0 -10
- package/dist/cli/chunk-q5163e60.js.map +0 -11
- package/dist/cli/chunk-qkqwkpte.js.map +0 -182
- package/dist/cli/chunk-qs4q5p4e.js.map +0 -10
- package/dist/cli/chunk-s1p84fyh.js.map +0 -10
- package/dist/cli/chunk-wgm7m9qk.js.map +0 -36
- package/dist/cli/chunk-yt5n7ppj.js.map +0 -10
- package/dist/cli/chunk-yw7dm696.js.map +0 -14
- /package/dist/cli/{chunk-s6jhgk0q.js.map → chunk-2eytanqx.js.map} +0 -0
- /package/dist/cli/{chunk-hdpx1tax.js.map → chunk-2z928egk.js.map} +0 -0
- /package/dist/cli/{chunk-ah61y8py.js.map → chunk-364znk6q.js.map} +0 -0
- /package/dist/cli/{chunk-vtk4a6dg.js.map → chunk-8ktnccpt.js.map} +0 -0
- /package/dist/cli/{chunk-fxypxtvm.js.map → chunk-a58773jm.js.map} +0 -0
- /package/dist/cli/{chunk-wm7js3j9.js.map → chunk-b07cmahc.js.map} +0 -0
- /package/dist/cli/{chunk-zxcczpyx.js.map → chunk-r20tn01b.js.map} +0 -0
- /package/dist/cli/{chunk-5shv93fd.js.map → chunk-tkacnehg.js.map} +0 -0
- /package/dist/cli/{chunk-zxh4d9vy.js.map → chunk-tzmab476.js.map} +0 -0
package/src/audit/checks/i18n.ts
CHANGED
|
@@ -3,7 +3,8 @@ import type { Diagnostic } from "../../core/types.ts";
|
|
|
3
3
|
import { finding } from "../catalog.ts";
|
|
4
4
|
import { pageSite } from "../locate.ts";
|
|
5
5
|
import type { AuditContext, CheckModule, PageSnapshot } from "../types.ts";
|
|
6
|
-
import { normalizePath } from "../url.ts";
|
|
6
|
+
import { decodePath, normalizePath } from "../url.ts";
|
|
7
|
+
import { isArchivedLatestCanonical } from "./indexability.ts";
|
|
7
8
|
|
|
8
9
|
/** The normalized `deployment.base` — hreflang hrefs carry it, page URLs don't. */
|
|
9
10
|
const deployBaseOf = (context: AuditContext): string =>
|
|
@@ -21,15 +22,52 @@ const isValidBcp47 = (tag: string): boolean => {
|
|
|
21
22
|
}
|
|
22
23
|
};
|
|
23
24
|
|
|
24
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* The site path an hreflang `href` points at, or null when it isn't parseable.
|
|
27
|
+
* Percent-decoded like every other URL the audit compares, since page URLs
|
|
28
|
+
* come from raw on-disk names (`/ガイド`) and `URL#pathname` is encoded.
|
|
29
|
+
*/
|
|
25
30
|
const alternatePath = (href: string, deployBase: string): string | null => {
|
|
26
31
|
try {
|
|
27
|
-
return normalizePath(
|
|
32
|
+
return normalizePath(
|
|
33
|
+
stripBasePath(deployBase, decodePath(new URL(href).pathname))
|
|
34
|
+
);
|
|
28
35
|
} catch {
|
|
29
36
|
return null;
|
|
30
37
|
}
|
|
31
38
|
};
|
|
32
39
|
|
|
40
|
+
/**
|
|
41
|
+
* An i18n fallback copy renders another locale's page at this locale's URL.
|
|
42
|
+
* Blume gives it hreflang links to the real translations, which by design
|
|
43
|
+
* never link back to it (it isn't one), and leaves it out of the sitemap —
|
|
44
|
+
* so the hreflang cluster checks don't apply to it.
|
|
45
|
+
*/
|
|
46
|
+
const isFallbackCopy = (page: PageSnapshot): boolean =>
|
|
47
|
+
page.route?.fallback === true;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether `target` names a URL other than its own as canonical — unless that
|
|
51
|
+
* is Blume's archived-version default pointing at the latest equivalent,
|
|
52
|
+
* which the canonical checks already accept.
|
|
53
|
+
*/
|
|
54
|
+
const canonicalizesElsewhere = (
|
|
55
|
+
context: AuditContext,
|
|
56
|
+
target: PageSnapshot,
|
|
57
|
+
deployBase: string
|
|
58
|
+
): boolean => {
|
|
59
|
+
if (!target.canonical) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const canonical = alternatePath(target.canonical, deployBase);
|
|
63
|
+
if (canonical === normalizePath(target.url)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return !(
|
|
67
|
+
canonical !== null && isArchivedLatestCanonical(context, target, canonical)
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
33
71
|
const langChecks = (
|
|
34
72
|
context: AuditContext,
|
|
35
73
|
page: PageSnapshot
|
|
@@ -115,10 +153,7 @@ const hreflangChecks = (
|
|
|
115
153
|
`hreflang="${alternate.lang}" points at ${path}, which the build does not serve.`
|
|
116
154
|
)
|
|
117
155
|
);
|
|
118
|
-
} else if (
|
|
119
|
-
target.canonical &&
|
|
120
|
-
alternatePath(target.canonical, deployBase) !== normalizePath(target.url)
|
|
121
|
-
) {
|
|
156
|
+
} else if (canonicalizesElsewhere(context, target, deployBase)) {
|
|
122
157
|
found.push(
|
|
123
158
|
finding(
|
|
124
159
|
"BLUME_AUDIT_HREFLANG_BAD_TARGET",
|
|
@@ -190,6 +225,9 @@ const returnTagChecks = (context: AuditContext): Diagnostic[] => {
|
|
|
190
225
|
const found: Diagnostic[] = [];
|
|
191
226
|
const deployBase = deployBaseOf(context);
|
|
192
227
|
for (const page of context.pages) {
|
|
228
|
+
if (isFallbackCopy(page)) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
193
231
|
for (const alternate of page.hreflang) {
|
|
194
232
|
if (alternate.lang === X_DEFAULT) {
|
|
195
233
|
continue;
|
|
@@ -235,7 +273,7 @@ export const i18nChecks: CheckModule = {
|
|
|
235
273
|
const found: Diagnostic[] = [];
|
|
236
274
|
for (const page of context.pages) {
|
|
237
275
|
found.push(...langChecks(context, page));
|
|
238
|
-
if (page.hreflang.length > 0) {
|
|
276
|
+
if (page.hreflang.length > 0 && !isFallbackCopy(page)) {
|
|
239
277
|
found.push(...hreflangChecks(context, page));
|
|
240
278
|
}
|
|
241
279
|
}
|
|
@@ -16,6 +16,33 @@ const parseCanonical = (canonical: string): URL | null => {
|
|
|
16
16
|
}
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Whether `page` naming `target` as its canonical is Blume's own default for
|
|
21
|
+
* an archived-version page (`versions.archived[].canonical: "latest"`)
|
|
22
|
+
* pointing at its latest equivalent — the live page is the authoritative one
|
|
23
|
+
* by design, not a defect. `target` is a base-less, normalized site path.
|
|
24
|
+
*/
|
|
25
|
+
export const isArchivedLatestCanonical = (
|
|
26
|
+
context: AuditContext,
|
|
27
|
+
page: PageSnapshot,
|
|
28
|
+
target: string
|
|
29
|
+
): boolean => {
|
|
30
|
+
const version = page.route?.version;
|
|
31
|
+
const archived = version
|
|
32
|
+
? context.project.config.versions?.archived.find(
|
|
33
|
+
(entry) => entry.id === version
|
|
34
|
+
)
|
|
35
|
+
: undefined;
|
|
36
|
+
const latestAlternate = page.route?.versionAlternates.find(
|
|
37
|
+
(alternate) => alternate.version === ""
|
|
38
|
+
);
|
|
39
|
+
return (
|
|
40
|
+
archived?.canonical === "latest" &&
|
|
41
|
+
latestAlternate !== undefined &&
|
|
42
|
+
normalizePath(latestAlternate.path) === target
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
19
46
|
const canonicalChecks = (
|
|
20
47
|
context: AuditContext,
|
|
21
48
|
page: PageSnapshot
|
|
@@ -33,8 +60,10 @@ const canonicalChecks = (
|
|
|
33
60
|
if (!page.canonical) {
|
|
34
61
|
// Without `deployment.site` Blume has no absolute URL to canonicalize to, so
|
|
35
62
|
// report the root cause once (in the sitemap/robots checks) rather than
|
|
36
|
-
// flagging every page for a config field they can't fix individually.
|
|
37
|
-
|
|
63
|
+
// flagging every page for a config field they can't fix individually. A
|
|
64
|
+
// noindex page omits its canonical on purpose — the layouts drop it, since
|
|
65
|
+
// Google treats the pairing as contradictory (see CANONICAL_ON_NOINDEX).
|
|
66
|
+
return site && page.indexable
|
|
38
67
|
? [
|
|
39
68
|
finding(
|
|
40
69
|
"BLUME_AUDIT_CANONICAL_MISSING",
|
|
@@ -101,23 +130,14 @@ const canonicalChecks = (
|
|
|
101
130
|
)
|
|
102
131
|
);
|
|
103
132
|
} else if (context.byUrl.has(target)) {
|
|
104
|
-
// An
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
: undefined;
|
|
113
|
-
const latestAlternate = page.route?.versionAlternates.find(
|
|
114
|
-
(alternate) => alternate.version === ""
|
|
115
|
-
);
|
|
116
|
-
const intentional =
|
|
117
|
-
archived?.canonical === "latest" &&
|
|
118
|
-
latestAlternate !== undefined &&
|
|
119
|
-
normalizePath(latestAlternate.path) === target;
|
|
120
|
-
if (!intentional) {
|
|
133
|
+
// An i18n fallback copy canonicalizes to the page it copies on purpose,
|
|
134
|
+
// like an archived page to its latest equivalent.
|
|
135
|
+
if (
|
|
136
|
+
!(
|
|
137
|
+
page.route?.fallback === true ||
|
|
138
|
+
isArchivedLatestCanonical(context, page, target)
|
|
139
|
+
)
|
|
140
|
+
) {
|
|
121
141
|
found.push(
|
|
122
142
|
finding(
|
|
123
143
|
"BLUME_AUDIT_CANONICAL_NOT_SELF",
|
|
@@ -14,6 +14,9 @@ import { isServed, normalizePath, resolveHref, siteOrigin } from "../url.ts";
|
|
|
14
14
|
/** Browser-magic fragments that scroll without needing a matching id. */
|
|
15
15
|
const MAGIC_FRAGMENTS = new Set(["", "top"]);
|
|
16
16
|
|
|
17
|
+
/** Where a fragment's directives (text fragments) start. */
|
|
18
|
+
const FRAGMENT_DIRECTIVE = ":~:";
|
|
19
|
+
|
|
17
20
|
/** Whether a fragment lands on an id of the target page. */
|
|
18
21
|
const anchorResolves = (target: PageSnapshot, fragment: string): boolean => {
|
|
19
22
|
if (MAGIC_FRAGMENTS.has(fragment)) {
|
|
@@ -65,8 +68,12 @@ export const linkChecks: CheckModule = {
|
|
|
65
68
|
page: PageSnapshot,
|
|
66
69
|
link: SnapshotLink,
|
|
67
70
|
target: PageSnapshot,
|
|
68
|
-
|
|
71
|
+
hash: string
|
|
69
72
|
): void => {
|
|
73
|
+
// A text fragment (`#:~:text=…`) is a directive the browser strips
|
|
74
|
+
// before it looks for an id; only the part in front of `:~:` names an
|
|
75
|
+
// element, and an empty one scrolls to the text instead.
|
|
76
|
+
const [fragment = ""] = hash.split(FRAGMENT_DIRECTIVE);
|
|
70
77
|
if (anchorResolves(target, fragment)) {
|
|
71
78
|
return;
|
|
72
79
|
}
|
|
@@ -100,6 +107,20 @@ export const linkChecks: CheckModule = {
|
|
|
100
107
|
if (resolved.kind === "external" || resolved.kind === "ignored") {
|
|
101
108
|
return;
|
|
102
109
|
}
|
|
110
|
+
if (resolved.kind === "outside-base") {
|
|
111
|
+
if (link.content) {
|
|
112
|
+
found.push(
|
|
113
|
+
finding(
|
|
114
|
+
"BLUME_AUDIT_LINK_TO_BROKEN",
|
|
115
|
+
pageSite(context, page),
|
|
116
|
+
`Link to ${link.href} is missing deployment.base (${deployBase}), so the deployed site does not serve it.`
|
|
117
|
+
)
|
|
118
|
+
);
|
|
119
|
+
} else if (!brokenChrome.has(resolved.path)) {
|
|
120
|
+
brokenChrome.set(resolved.path, page);
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
103
124
|
|
|
104
125
|
const anchorTarget = resolved.hash
|
|
105
126
|
? context.byUrl.get(resolved.path)
|
package/src/audit/checks/llms.ts
CHANGED
|
@@ -113,15 +113,18 @@ export const llmsChecks: CheckModule = {
|
|
|
113
113
|
|
|
114
114
|
// The reverse direction, mirroring INDEXABLE_PAGE_NOT_IN_SITEMAP: a page
|
|
115
115
|
// that is built, indexable, and in the nav belongs in the index. Pages the
|
|
116
|
-
// generator deliberately skips — hidden, drafts,
|
|
117
|
-
//
|
|
118
|
-
//
|
|
116
|
+
// generator deliberately skips — hidden, drafts, `ai.exclude`, i18n
|
|
117
|
+
// fallback copies, error routes, API reference when
|
|
118
|
+
// `agents.llmsTxt.openapi` is off, custom pages with no manifest route —
|
|
119
|
+
// are skipped here for the same reasons.
|
|
119
120
|
for (const page of context.pages) {
|
|
120
121
|
const { route } = page;
|
|
121
122
|
if (
|
|
122
123
|
!route ||
|
|
123
124
|
route.hidden ||
|
|
124
125
|
route.draft ||
|
|
126
|
+
route.aiExclude ||
|
|
127
|
+
route.fallback ||
|
|
125
128
|
!page.indexable ||
|
|
126
129
|
ERROR_ROUTES.has(page.url) ||
|
|
127
130
|
(!openapi && route.source.name === "openapi") ||
|
|
@@ -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/graph.ts
CHANGED
|
@@ -58,7 +58,8 @@ export const buildGraph = (
|
|
|
58
58
|
* Non-indexable pages are excluded — a `noindex` page is *meant* to be
|
|
59
59
|
* unreachable — as is the home page, which is nobody's job to link to. Under a
|
|
60
60
|
* `basePath` the home page's built URL is the base itself, so the caller names
|
|
61
|
-
* it via `homeUrl`.
|
|
61
|
+
* it via `homeUrl`. An i18n fallback copy is excluded too: it stands in for a
|
|
62
|
+
* translation nobody has written yet, so the page to link to is the original.
|
|
62
63
|
*/
|
|
63
64
|
export const orphanPages = (
|
|
64
65
|
pages: PageSnapshot[],
|
|
@@ -68,6 +69,7 @@ export const orphanPages = (
|
|
|
68
69
|
pages.filter(
|
|
69
70
|
(page) =>
|
|
70
71
|
page.indexable &&
|
|
72
|
+
page.route?.fallback !== true &&
|
|
71
73
|
page.url !== "/" &&
|
|
72
74
|
page.url !== homeUrl &&
|
|
73
75
|
(graph.contentIn.get(page.url)?.size ?? 0) === 0
|