blume 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/dist/cli/index.js +13255 -10232
- package/dist/cli/index.js.map +91 -60
- package/dist/types/core/config-input.d.ts +61 -1
- package/dist/types/core/data.d.ts +9 -0
- package/dist/types/core/deployment-env.d.ts +6 -0
- package/dist/types/core/diagnostics.d.ts +23 -0
- package/dist/types/core/i18n-ui.d.ts +8 -8
- package/dist/types/core/schema.d.ts +131 -22
- package/dist/types/core/sources/types.d.ts +3 -1
- package/dist/types/core/standard-schema.d.ts +41 -0
- package/dist/types/core/types.d.ts +13 -0
- package/dist/types/og/card.d.ts +63 -0
- package/dist/types/og/dimensions.d.ts +12 -0
- package/docs/01-quickstart.mdx +1 -1
- package/docs/02-deployment.mdx +9 -1
- package/docs/advanced/api-reference.mdx +11 -0
- package/docs/advanced/changelog.mdx +1 -1
- package/docs/advanced/skills.mdx +1 -1
- package/docs/configuration/ai.mdx +1 -1
- package/docs/configuration/customization.mdx +1 -1
- package/docs/configuration/export.mdx +1 -1
- package/docs/configuration/index.mdx +21 -1
- package/docs/configuration/search.mdx +28 -1
- package/docs/configuration/seo.mdx +21 -2
- package/docs/configuration/theming.mdx +1 -1
- package/docs/content/components.mdx +14 -0
- package/docs/content/index.mdx +1 -1
- package/docs/content/meta.mdx +1 -1
- package/docs/content/navigation.mdx +1 -1
- package/docs/content/sources.mdx +1 -1
- package/docs/reference/cli.mdx +79 -1
- package/docs/reference/frontmatter.mdx +29 -1
- package/package.json +3 -3
- package/skills/blume-migrate/SKILL.md +1 -1
- package/skills/blume-migrate/references/mintlify.md +3 -2
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +16 -4
- package/src/ai/llms.ts +15 -0
- package/src/astro/adapter-root.ts +70 -0
- package/src/astro/generate.ts +50 -19
- package/src/astro/index.ts +1 -0
- package/src/astro/pages.ts +18 -3
- package/src/astro/templates.ts +65 -22
- package/src/audit/agent.ts +114 -0
- package/src/audit/catalog.ts +826 -0
- package/src/audit/checks/assets.ts +177 -0
- package/src/audit/checks/content.ts +231 -0
- package/src/audit/checks/duplicates.ts +131 -0
- package/src/audit/checks/i18n.ts +246 -0
- package/src/audit/checks/indexability.ts +213 -0
- package/src/audit/checks/links.ts +223 -0
- package/src/audit/checks/llms.ts +135 -0
- package/src/audit/checks/network.ts +272 -0
- package/src/audit/checks/og-image.ts +113 -0
- package/src/audit/checks/redirects.ts +87 -0
- package/src/audit/checks/robots.ts +114 -0
- package/src/audit/checks/sitemap.ts +229 -0
- package/src/audit/checks/social.ts +238 -0
- package/src/audit/crawl.ts +259 -0
- package/src/audit/graph.ts +74 -0
- package/src/audit/html.ts +54 -0
- package/src/audit/image-size.ts +63 -0
- package/src/audit/locate.ts +33 -0
- package/src/audit/redirects.ts +74 -0
- package/src/audit/report.ts +278 -0
- package/src/audit/run.ts +198 -0
- package/src/audit/snapshot.ts +189 -0
- package/src/audit/types.ts +214 -0
- package/src/audit/url.ts +103 -0
- package/src/cli/commands/audit.ts +205 -0
- package/src/cli/commands/build.ts +51 -12
- package/src/cli/index.ts +2 -0
- package/src/components/content/Tabs.astro +98 -15
- package/src/components/layout/Breadcrumbs.astro +1 -1
- package/src/components/layout/Header.astro +1 -0
- package/src/components/layout/PageFeedback.astro +1 -1
- package/src/components/layout/PageLayout.astro +5 -1
- package/src/components/layout/Pagination.astro +1 -1
- package/src/components/layout/RootLayout.astro +5 -3
- package/src/components/layout/Search.astro +35 -6
- package/src/components/layout/TableOfContents.astro +1 -1
- package/src/components/openapi/Authorization.astro +80 -0
- package/src/components/openapi/Operation.astro +19 -1
- package/src/components/openapi/ParametersTable.astro +1 -1
- package/src/components/openapi/security.ts +201 -0
- package/src/components/openapi/snippets.ts +42 -13
- package/src/core/config-input.ts +66 -1
- package/src/core/data.ts +9 -1
- package/src/core/deployment-env.ts +9 -0
- package/src/core/diagnostics.ts +59 -12
- package/src/core/links.ts +2 -91
- package/src/core/nav-diagnostics.ts +48 -4
- package/src/core/probe.ts +136 -0
- package/src/core/project-graph.ts +8 -0
- package/src/core/schema.ts +86 -3
- package/src/core/sources/normalize.ts +198 -25
- package/src/core/sources/types.ts +3 -1
- package/src/core/standard-schema.ts +54 -0
- package/src/core/types.ts +13 -0
- package/src/deploy/adapter-output.ts +27 -15
- package/src/deploy/headers.ts +66 -0
- package/src/deploy/redirects.ts +49 -9
- package/src/og/card.ts +98 -33
- package/src/og/index.ts +1 -1
- package/src/search/popular.ts +33 -0
- package/src/theme/entry.ts +6 -1
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
import type { Diagnostic } from "../core/types.ts";
|
|
2
|
+
import type { CheckMeta } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Every check `blume audit` can report.
|
|
6
|
+
*
|
|
7
|
+
* This is deliberately ~76 checks, not the ~173 rows an Ahrefs site audit
|
|
8
|
+
* lists. Ahrefs crawls the open web, so its list is padded with rows that are
|
|
9
|
+
* structurally impossible for an Astro-built Blume site, and shipping those as
|
|
10
|
+
* permanent zeroes teaches people to ignore the report. What was dropped, and
|
|
11
|
+
* why:
|
|
12
|
+
*
|
|
13
|
+
* - **Every `nofollow` check** (~6 rows, including all four "nofollow incoming
|
|
14
|
+
* internal links" variants). `RootLayout.astro` only ever emits `<meta
|
|
15
|
+
* name="robots" content="noindex">`; Blume has no way to emit `nofollow`, so
|
|
16
|
+
* they could never fire. Hand-written `rel="nofollow"` in MDX is still caught
|
|
17
|
+
* by `INTERNAL_LINK_NOFOLLOW`.
|
|
18
|
+
* - **The whole JavaScript + CSS section** (~11 rows: "JS redirects", "CSS
|
|
19
|
+
* redirects", "page has redirected JS/CSS", "HTTPS page links to HTTP
|
|
20
|
+
* JS/CSS"…). Vite emits content-hashed, existent, non-redirecting bundles.
|
|
21
|
+
* Collapsed into one `SUBRESOURCE_MISSING`.
|
|
22
|
+
* - **Ahrefs' indexable/non-indexable duplicates** (~20 rows). Carried instead
|
|
23
|
+
* by `PageSnapshot.indexable` on a single finding.
|
|
24
|
+
* - **"Only one dofollow incoming internal link"** and **"page has no outgoing
|
|
25
|
+
* links"**. Blume's sidebar links every page from every page, so these fire on
|
|
26
|
+
* ~100% of pages. Pure noise.
|
|
27
|
+
* - **"3XX redirect"** and **"302 redirect"**: having a redirect is inventory,
|
|
28
|
+
* not a finding. **"HTTP to HTTPS redirect"** is correct behavior.
|
|
29
|
+
* - **"Font size too small" / "tap targets too close" / "content not sized
|
|
30
|
+
* correctly"**: properties of the theme, identical on every page. A regression
|
|
31
|
+
* there is a Blume bug, not a user finding.
|
|
32
|
+
* - **"Document uses plugins"** and **"poor FID"** (FID is deprecated; lab
|
|
33
|
+
* Lighthouse cannot measure INP, so we report TBT and say so).
|
|
34
|
+
* - **"More than three parameters in URL"**: a static docs site emits no query
|
|
35
|
+
* strings in its own links.
|
|
36
|
+
* - **"Page in multiple sitemaps"**: Blume emits exactly one sitemap.
|
|
37
|
+
* - **Core Web Vitals** (LCP/CLS/INP): these need a real browser. Rather than
|
|
38
|
+
* ship a `--lighthouse` flag that silently reports nothing, they are deferred
|
|
39
|
+
* until Lighthouse is actually wired in. A tier the audit cannot run is worse
|
|
40
|
+
* than one it openly doesn't have.
|
|
41
|
+
*
|
|
42
|
+
* Severity discipline: `error` means "this is definitely broken". Anything
|
|
43
|
+
* advisory ships as `warning`/`info`. One noisy check and the whole command gets
|
|
44
|
+
* switched off.
|
|
45
|
+
*/
|
|
46
|
+
export const CHECKS = [
|
|
47
|
+
// Content
|
|
48
|
+
{
|
|
49
|
+
category: "content",
|
|
50
|
+
fix: "Add a `title` to the page's frontmatter.",
|
|
51
|
+
id: "BLUME_AUDIT_TITLE_MISSING",
|
|
52
|
+
severity: "error",
|
|
53
|
+
tier: "static",
|
|
54
|
+
title: "Title tag missing or empty",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
category: "content",
|
|
58
|
+
fix: "Remove the extra <title> from the page's layout or MDX.",
|
|
59
|
+
id: "BLUME_AUDIT_TITLE_MULTIPLE",
|
|
60
|
+
severity: "error",
|
|
61
|
+
tier: "static",
|
|
62
|
+
title: "Multiple title tags",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
category: "content",
|
|
66
|
+
fix: "Rewrite `title` in the frontmatter to fit the length range.",
|
|
67
|
+
id: "BLUME_AUDIT_TITLE_LENGTH",
|
|
68
|
+
severity: "warning",
|
|
69
|
+
tier: "static",
|
|
70
|
+
title: "Title too long or too short",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
category: "content",
|
|
74
|
+
fix: "Add a `description` to the page's frontmatter.",
|
|
75
|
+
id: "BLUME_AUDIT_DESCRIPTION_MISSING",
|
|
76
|
+
severity: "warning",
|
|
77
|
+
tier: "static",
|
|
78
|
+
title: "Meta description missing or empty",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
category: "content",
|
|
82
|
+
fix: "Remove the extra description <meta> from the page's layout or MDX.",
|
|
83
|
+
id: "BLUME_AUDIT_DESCRIPTION_MULTIPLE",
|
|
84
|
+
severity: "error",
|
|
85
|
+
tier: "static",
|
|
86
|
+
title: "Multiple meta description tags",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
category: "content",
|
|
90
|
+
fix: "Rewrite `description` in the frontmatter to fit the length range.",
|
|
91
|
+
id: "BLUME_AUDIT_DESCRIPTION_LENGTH",
|
|
92
|
+
severity: "warning",
|
|
93
|
+
tier: "static",
|
|
94
|
+
title: "Meta description too long or too short",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
category: "content",
|
|
98
|
+
fix: "Give the page a `title` — Blume renders it as the page's <h1>.",
|
|
99
|
+
id: "BLUME_AUDIT_H1_MISSING",
|
|
100
|
+
severity: "warning",
|
|
101
|
+
tier: "static",
|
|
102
|
+
title: "H1 tag missing or empty",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
category: "content",
|
|
106
|
+
fix: "Demote the extra `# Heading` in the body to `##` — Blume already renders `title` as the h1.",
|
|
107
|
+
id: "BLUME_AUDIT_H1_MULTIPLE",
|
|
108
|
+
severity: "warning",
|
|
109
|
+
tier: "static",
|
|
110
|
+
title: "Multiple H1 tags",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
category: "content",
|
|
114
|
+
fix: "Expand the page, or fold it into a larger one.",
|
|
115
|
+
id: "BLUME_AUDIT_LOW_WORD_COUNT",
|
|
116
|
+
severity: "info",
|
|
117
|
+
tier: "static",
|
|
118
|
+
title: "Low word count",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
category: "content",
|
|
122
|
+
fix: "Adjust the heading to the next level down — skipped levels break table-of-contents nesting and screen-reader outlines.",
|
|
123
|
+
id: "BLUME_AUDIT_HEADING_SKIP",
|
|
124
|
+
severity: "info",
|
|
125
|
+
tier: "static",
|
|
126
|
+
title: "Heading levels skip (e.g. h2 to h4)",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
category: "content",
|
|
130
|
+
fix: "Correct the `date`, or hold the page back until it is meant to be live.",
|
|
131
|
+
id: "BLUME_AUDIT_FUTURE_DATED_PAGE",
|
|
132
|
+
severity: "info",
|
|
133
|
+
tier: "static",
|
|
134
|
+
title: "Page is dated in the future",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
category: "content",
|
|
138
|
+
fix: "Restore the viewport <meta> in your ejected layout.",
|
|
139
|
+
id: "BLUME_AUDIT_VIEWPORT_MISSING",
|
|
140
|
+
severity: "error",
|
|
141
|
+
tier: "static",
|
|
142
|
+
title: "Viewport not set",
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
// Duplicates
|
|
146
|
+
{
|
|
147
|
+
category: "duplicates",
|
|
148
|
+
fix: "Give each page a distinct `title` — search engines show it as the result headline.",
|
|
149
|
+
id: "BLUME_AUDIT_DUPLICATE_TITLE",
|
|
150
|
+
severity: "warning",
|
|
151
|
+
tier: "static",
|
|
152
|
+
title: "Duplicate title",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
category: "duplicates",
|
|
156
|
+
fix: "Give each page a distinct `description`.",
|
|
157
|
+
id: "BLUME_AUDIT_DUPLICATE_DESCRIPTION",
|
|
158
|
+
severity: "warning",
|
|
159
|
+
tier: "static",
|
|
160
|
+
title: "Duplicate meta description",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
category: "duplicates",
|
|
164
|
+
fix: "Merge the pages, or set `seo.canonical` on all but one.",
|
|
165
|
+
id: "BLUME_AUDIT_DUPLICATE_CONTENT",
|
|
166
|
+
severity: "warning",
|
|
167
|
+
tier: "static",
|
|
168
|
+
title: "Duplicate pages without canonical",
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
// Indexability
|
|
172
|
+
{
|
|
173
|
+
category: "indexability",
|
|
174
|
+
fix: "Set `deployment.site` in blume.config.ts to the site's public URL.",
|
|
175
|
+
id: "BLUME_AUDIT_SITE_NOT_SET",
|
|
176
|
+
severity: "warning",
|
|
177
|
+
tier: "static",
|
|
178
|
+
title: "deployment.site is not set",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
category: "indexability",
|
|
182
|
+
fix: "Audit a production-like build (e.g. `VERCEL=1 VERCEL_PROJECT_PRODUCTION_URL=<host> blume build`) or the deployment itself with `--url <origin>`. Do not hardcode `deployment.site` — the platform sets it on every deploy.",
|
|
183
|
+
id: "BLUME_AUDIT_SITE_INFERRED_AT_DEPLOY",
|
|
184
|
+
severity: "info",
|
|
185
|
+
tier: "static",
|
|
186
|
+
title: "deployment.site is inferred at deploy time",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
category: "indexability",
|
|
190
|
+
fix: "Drop the canonical from noindex pages — Google treats the pairing as contradictory and may ignore one of the two.",
|
|
191
|
+
id: "BLUME_AUDIT_CANONICAL_ON_NOINDEX",
|
|
192
|
+
severity: "warning",
|
|
193
|
+
tier: "static",
|
|
194
|
+
title: "Page is noindex but declares a canonical",
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
category: "indexability",
|
|
198
|
+
fix: "Rebuild without `--preview` before deploying, or remove `draft: true` if the page is ready to ship.",
|
|
199
|
+
id: "BLUME_AUDIT_DRAFT_PAGE_PUBLISHED",
|
|
200
|
+
severity: "warning",
|
|
201
|
+
tier: "static",
|
|
202
|
+
title: "Draft page is in the build",
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
category: "indexability",
|
|
206
|
+
fix: "Set `deployment.site` so Blume can emit absolute canonical URLs.",
|
|
207
|
+
id: "BLUME_AUDIT_CANONICAL_MISSING",
|
|
208
|
+
severity: "warning",
|
|
209
|
+
tier: "static",
|
|
210
|
+
title: "Canonical URL missing",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
category: "indexability",
|
|
214
|
+
fix: "Point `seo.canonical` at this page, or remove it to use the default self-canonical.",
|
|
215
|
+
id: "BLUME_AUDIT_CANONICAL_NOT_SELF",
|
|
216
|
+
severity: "info",
|
|
217
|
+
tier: "static",
|
|
218
|
+
title: "Non-canonical page",
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
category: "indexability",
|
|
222
|
+
fix: "Point `seo.canonical` at a page that exists and doesn't redirect.",
|
|
223
|
+
id: "BLUME_AUDIT_CANONICAL_BAD_TARGET",
|
|
224
|
+
severity: "error",
|
|
225
|
+
tier: "static",
|
|
226
|
+
title: "Canonical points to a broken or redirecting page",
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
category: "indexability",
|
|
230
|
+
fix: "Use the same protocol in `seo.canonical` as in `deployment.site`.",
|
|
231
|
+
id: "BLUME_AUDIT_CANONICAL_PROTOCOL_MISMATCH",
|
|
232
|
+
severity: "error",
|
|
233
|
+
tier: "static",
|
|
234
|
+
title: "Canonical protocol does not match the site",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
category: "indexability",
|
|
238
|
+
fix: "Remove `noindex` from the page's frontmatter if it should be indexed.",
|
|
239
|
+
id: "BLUME_AUDIT_ROBOTS_META_UNEXPECTED",
|
|
240
|
+
severity: "info",
|
|
241
|
+
tier: "static",
|
|
242
|
+
title: "Page is not indexable",
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
category: "indexability",
|
|
246
|
+
fix: "Split the page — Googlebot stops reading an HTML document at 2 MB.",
|
|
247
|
+
id: "BLUME_AUDIT_HTML_TOO_LARGE",
|
|
248
|
+
severity: "error",
|
|
249
|
+
tier: "static",
|
|
250
|
+
title: "Page exceeds Googlebot's 2 MB crawl limit",
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
category: "indexability",
|
|
254
|
+
fix: "Remove the X-Robots-Tag header, or align it with the page's robots meta.",
|
|
255
|
+
id: "BLUME_AUDIT_ROBOTS_HEADER_CONFLICT",
|
|
256
|
+
severity: "error",
|
|
257
|
+
tier: "network",
|
|
258
|
+
title: "X-Robots-Tag header conflicts with the page's robots meta",
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
// Links
|
|
262
|
+
{
|
|
263
|
+
category: "links",
|
|
264
|
+
fix: "Fix the link target, or create the page it points at.",
|
|
265
|
+
id: "BLUME_AUDIT_LINK_TO_BROKEN",
|
|
266
|
+
severity: "error",
|
|
267
|
+
tier: "static",
|
|
268
|
+
title: "Page has links to a broken page",
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
category: "links",
|
|
272
|
+
fix: "Link straight to the destination instead of through the redirect.",
|
|
273
|
+
id: "BLUME_AUDIT_LINK_TO_REDIRECT",
|
|
274
|
+
severity: "warning",
|
|
275
|
+
tier: "static",
|
|
276
|
+
title: "Page has links to a redirect",
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
category: "links",
|
|
280
|
+
fix: "Link to this page from the body of a related page.",
|
|
281
|
+
id: "BLUME_AUDIT_ORPHAN_PAGE",
|
|
282
|
+
severity: "warning",
|
|
283
|
+
tier: "static",
|
|
284
|
+
title: "Orphan page (only reachable from navigation)",
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
category: "links",
|
|
288
|
+
fix: "Use a root-relative path (`/docs/x`) so the link survives previews and `basePath`.",
|
|
289
|
+
id: "BLUME_AUDIT_INTERNAL_LINK_ABSOLUTE",
|
|
290
|
+
severity: "warning",
|
|
291
|
+
tier: "static",
|
|
292
|
+
title: "Internal link hardcodes the site's own origin",
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
category: "links",
|
|
296
|
+
fix: 'Drop `rel="nofollow"` — it stops this page passing authority internally.',
|
|
297
|
+
id: "BLUME_AUDIT_INTERNAL_LINK_NOFOLLOW",
|
|
298
|
+
severity: "info",
|
|
299
|
+
tier: "static",
|
|
300
|
+
title: "Page has nofollow outgoing internal links",
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
category: "links",
|
|
304
|
+
fix: "Check `basePath` / `deployment.base` — a trailing slash there produces `//`.",
|
|
305
|
+
id: "BLUME_AUDIT_DOUBLE_SLASH_URL",
|
|
306
|
+
severity: "error",
|
|
307
|
+
tier: "static",
|
|
308
|
+
title: "Double slash in URL",
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
category: "links",
|
|
312
|
+
fix: "Point the fragment at a heading that exists on the target page, or fix the heading it meant.",
|
|
313
|
+
id: "BLUME_AUDIT_ANCHOR_BROKEN",
|
|
314
|
+
severity: "warning",
|
|
315
|
+
tier: "static",
|
|
316
|
+
title: "Link fragment matches no id on the target page",
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
category: "links",
|
|
320
|
+
fix: "Rename the source file to a lowercase, hyphenated slug — and add a redirect from the old URL if it was already published.",
|
|
321
|
+
id: "BLUME_AUDIT_URL_STYLE",
|
|
322
|
+
severity: "info",
|
|
323
|
+
tier: "static",
|
|
324
|
+
title: "URL contains uppercase, underscores, or spaces",
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
// Redirects
|
|
328
|
+
{
|
|
329
|
+
category: "redirects",
|
|
330
|
+
fix: "Point the redirect at a page that exists.",
|
|
331
|
+
id: "BLUME_AUDIT_REDIRECT_BROKEN",
|
|
332
|
+
severity: "error",
|
|
333
|
+
tier: "static",
|
|
334
|
+
title: "Broken redirect",
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
category: "redirects",
|
|
338
|
+
fix: "Break the cycle in `redirects` — it never resolves.",
|
|
339
|
+
id: "BLUME_AUDIT_REDIRECT_LOOP",
|
|
340
|
+
severity: "error",
|
|
341
|
+
tier: "static",
|
|
342
|
+
title: "Redirect loop",
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
category: "redirects",
|
|
346
|
+
fix: "Point every hop straight at the final destination.",
|
|
347
|
+
id: "BLUME_AUDIT_REDIRECT_CHAIN",
|
|
348
|
+
severity: "warning",
|
|
349
|
+
tier: "static",
|
|
350
|
+
title: "Redirect chain",
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
category: "redirects",
|
|
354
|
+
fix: "Use a real redirect in `blume.config.ts` instead of a meta refresh.",
|
|
355
|
+
id: "BLUME_AUDIT_META_REFRESH",
|
|
356
|
+
severity: "warning",
|
|
357
|
+
tier: "static",
|
|
358
|
+
title: "Meta refresh redirect",
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
category: "redirects",
|
|
362
|
+
fix: "Remove the redirect, or delete the page it shadows — the page wins and the redirect never fires.",
|
|
363
|
+
id: "BLUME_AUDIT_REDIRECT_SOURCE_IS_PAGE",
|
|
364
|
+
severity: "error",
|
|
365
|
+
tier: "static",
|
|
366
|
+
title: "Redirect source is also a real page",
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
category: "redirects",
|
|
370
|
+
fix: "Redirect to the HTTPS URL.",
|
|
371
|
+
id: "BLUME_AUDIT_REDIRECT_TO_HTTP",
|
|
372
|
+
severity: "error",
|
|
373
|
+
tier: "network",
|
|
374
|
+
title: "HTTPS to HTTP redirect",
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
// Social
|
|
378
|
+
{
|
|
379
|
+
category: "social",
|
|
380
|
+
fix: "Add a `description` — Blume fills the rest of the Open Graph tags for you.",
|
|
381
|
+
id: "BLUME_AUDIT_OG_INCOMPLETE",
|
|
382
|
+
severity: "warning",
|
|
383
|
+
tier: "static",
|
|
384
|
+
title: "Open Graph tags missing or incomplete",
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
category: "social",
|
|
388
|
+
fix: "Set `deployment.site` to turn on generated OG images, or set `seo.image` on the page.",
|
|
389
|
+
id: "BLUME_AUDIT_OG_IMAGE_MISSING",
|
|
390
|
+
severity: "warning",
|
|
391
|
+
tier: "static",
|
|
392
|
+
title: "Open Graph image missing",
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
category: "social",
|
|
396
|
+
fix: "Point `seo.image` at a file that exists, or rebuild — a dead og:image renders as a blank card everywhere the page is shared.",
|
|
397
|
+
id: "BLUME_AUDIT_OG_IMAGE_BROKEN",
|
|
398
|
+
severity: "warning",
|
|
399
|
+
tier: "static",
|
|
400
|
+
title: "Open Graph image is not in the build",
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
category: "social",
|
|
404
|
+
fix: "Use an image of at least 1200×630 — smaller ones render blurry or get cropped into small-card layouts.",
|
|
405
|
+
id: "BLUME_AUDIT_OG_IMAGE_SMALL",
|
|
406
|
+
severity: "warning",
|
|
407
|
+
tier: "static",
|
|
408
|
+
title: "Open Graph image is too small for large cards",
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
category: "social",
|
|
412
|
+
fix: "Align `og:url` with the page's canonical URL.",
|
|
413
|
+
id: "BLUME_AUDIT_OG_URL_MISMATCH",
|
|
414
|
+
severity: "warning",
|
|
415
|
+
tier: "static",
|
|
416
|
+
title: "Open Graph URL not matching canonical",
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
category: "social",
|
|
420
|
+
fix: "Set `seo.x.handle` in blume.config.ts so X can attribute the card.",
|
|
421
|
+
id: "BLUME_AUDIT_TWITTER_CARD_INCOMPLETE",
|
|
422
|
+
severity: "warning",
|
|
423
|
+
tier: "static",
|
|
424
|
+
title: "X (Twitter) card missing or incomplete",
|
|
425
|
+
},
|
|
426
|
+
|
|
427
|
+
// Localization
|
|
428
|
+
{
|
|
429
|
+
category: "i18n",
|
|
430
|
+
fix: "Restore the `lang` attribute on <html> in your ejected layout.",
|
|
431
|
+
id: "BLUME_AUDIT_HTML_LANG_MISSING",
|
|
432
|
+
severity: "error",
|
|
433
|
+
tier: "static",
|
|
434
|
+
title: "HTML lang attribute missing",
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
category: "i18n",
|
|
438
|
+
fix: "Use a valid BCP 47 tag (e.g. `en`, `en-GB`) for the locale.",
|
|
439
|
+
id: "BLUME_AUDIT_HTML_LANG_INVALID",
|
|
440
|
+
severity: "error",
|
|
441
|
+
tier: "static",
|
|
442
|
+
title: "HTML lang attribute invalid",
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
category: "i18n",
|
|
446
|
+
fix: "The page's <html lang> must match its own hreflang annotation.",
|
|
447
|
+
id: "BLUME_AUDIT_HREFLANG_LANG_MISMATCH",
|
|
448
|
+
severity: "error",
|
|
449
|
+
tier: "static",
|
|
450
|
+
title: "Hreflang and HTML lang mismatch",
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
category: "i18n",
|
|
454
|
+
fix: "Use a valid BCP 47 tag in the hreflang annotation.",
|
|
455
|
+
id: "BLUME_AUDIT_HREFLANG_INVALID",
|
|
456
|
+
severity: "error",
|
|
457
|
+
tier: "static",
|
|
458
|
+
title: "Hreflang annotation invalid",
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
category: "i18n",
|
|
462
|
+
fix: "A page's hreflang set must include a self-reference.",
|
|
463
|
+
id: "BLUME_AUDIT_HREFLANG_SELF_MISSING",
|
|
464
|
+
severity: "warning",
|
|
465
|
+
tier: "static",
|
|
466
|
+
title: "Self-reference hreflang annotation missing",
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
category: "i18n",
|
|
470
|
+
fix: "Add an `x-default` alternate pointing at the default-locale page.",
|
|
471
|
+
id: "BLUME_AUDIT_HREFLANG_XDEFAULT_MISSING",
|
|
472
|
+
severity: "info",
|
|
473
|
+
tier: "static",
|
|
474
|
+
title: "X-default hreflang annotation missing",
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
category: "i18n",
|
|
478
|
+
fix: "Every page in an hreflang group must link back to every other one.",
|
|
479
|
+
id: "BLUME_AUDIT_HREFLANG_NO_RETURN_TAG",
|
|
480
|
+
severity: "error",
|
|
481
|
+
tier: "static",
|
|
482
|
+
title: "Missing reciprocal hreflang (no return-tag)",
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
category: "i18n",
|
|
486
|
+
fix: "Point the hreflang alternate at a page that exists and is canonical.",
|
|
487
|
+
id: "BLUME_AUDIT_HREFLANG_BAD_TARGET",
|
|
488
|
+
severity: "error",
|
|
489
|
+
tier: "static",
|
|
490
|
+
title: "Hreflang points to a broken, redirecting, or non-canonical page",
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
category: "i18n",
|
|
494
|
+
fix: "Each language in an hreflang group must name exactly one page.",
|
|
495
|
+
id: "BLUME_AUDIT_HREFLANG_CONFLICT",
|
|
496
|
+
severity: "error",
|
|
497
|
+
tier: "static",
|
|
498
|
+
title: "Hreflang group has a language conflict",
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
// Assets
|
|
502
|
+
{
|
|
503
|
+
category: "assets",
|
|
504
|
+
fix: 'Add descriptive `alt` text, or `alt=""` if the image is decorative.',
|
|
505
|
+
id: "BLUME_AUDIT_IMAGE_ALT_MISSING",
|
|
506
|
+
severity: "warning",
|
|
507
|
+
tier: "static",
|
|
508
|
+
title: "Missing alt text",
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
category: "assets",
|
|
512
|
+
fix: "Fix the image path, or add the file to public/.",
|
|
513
|
+
id: "BLUME_AUDIT_IMAGE_BROKEN",
|
|
514
|
+
severity: "error",
|
|
515
|
+
tier: "static",
|
|
516
|
+
title: "Image broken",
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
category: "assets",
|
|
520
|
+
fix: "Compress the asset, or serve a modern format (WebP/AVIF).",
|
|
521
|
+
id: "BLUME_AUDIT_ASSET_TOO_LARGE",
|
|
522
|
+
severity: "warning",
|
|
523
|
+
tier: "static",
|
|
524
|
+
title: "Asset file size too large",
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
category: "assets",
|
|
528
|
+
fix: "Set `width` and `height` so the browser can reserve space (avoids layout shift).",
|
|
529
|
+
id: "BLUME_AUDIT_IMAGE_MISSING_DIMENSIONS",
|
|
530
|
+
severity: "warning",
|
|
531
|
+
tier: "static",
|
|
532
|
+
title: "Image has no width/height",
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
category: "assets",
|
|
536
|
+
fix: "Fix the reference, or restore the missing file.",
|
|
537
|
+
id: "BLUME_AUDIT_SUBRESOURCE_MISSING",
|
|
538
|
+
severity: "error",
|
|
539
|
+
tier: "static",
|
|
540
|
+
title: "Referenced script, style, or asset is missing from the build",
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
category: "assets",
|
|
544
|
+
fix: "Load the subresource over HTTPS — browsers block mixed content.",
|
|
545
|
+
id: "BLUME_AUDIT_MIXED_CONTENT",
|
|
546
|
+
severity: "error",
|
|
547
|
+
tier: "static",
|
|
548
|
+
title: "HTTPS/HTTP mixed content",
|
|
549
|
+
},
|
|
550
|
+
|
|
551
|
+
// Sitemap
|
|
552
|
+
{
|
|
553
|
+
category: "sitemap",
|
|
554
|
+
fix: "Remove `draft`/`hidden`/`noindex` from the page's frontmatter if it should be indexed.",
|
|
555
|
+
id: "BLUME_AUDIT_INDEXABLE_PAGE_NOT_IN_SITEMAP",
|
|
556
|
+
severity: "warning",
|
|
557
|
+
tier: "static",
|
|
558
|
+
title: "Indexable page not in sitemap",
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
category: "sitemap",
|
|
562
|
+
fix: "A noindex page should not be advertised in the sitemap.",
|
|
563
|
+
id: "BLUME_AUDIT_NOINDEX_IN_SITEMAP",
|
|
564
|
+
severity: "error",
|
|
565
|
+
tier: "static",
|
|
566
|
+
title: "Noindex page in sitemap",
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
category: "sitemap",
|
|
570
|
+
fix: "List only canonical URLs in the sitemap.",
|
|
571
|
+
id: "BLUME_AUDIT_NON_CANONICAL_IN_SITEMAP",
|
|
572
|
+
severity: "error",
|
|
573
|
+
tier: "static",
|
|
574
|
+
title: "Non-canonical page in sitemap",
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
category: "sitemap",
|
|
578
|
+
fix: "Remove the URL from the sitemap, or build the page it names.",
|
|
579
|
+
id: "BLUME_AUDIT_SITEMAP_BAD_URL",
|
|
580
|
+
severity: "error",
|
|
581
|
+
tier: "static",
|
|
582
|
+
title: "Sitemap names a page that does not exist or redirects",
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
category: "sitemap",
|
|
586
|
+
fix: "Sitemaps must be valid XML in the sitemaps.org urlset format.",
|
|
587
|
+
id: "BLUME_AUDIT_SITEMAP_INVALID",
|
|
588
|
+
severity: "error",
|
|
589
|
+
tier: "static",
|
|
590
|
+
title: "Sitemap has a syntax error or wrong format",
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
category: "sitemap",
|
|
594
|
+
fix: "Split the sitemap — the limits are 50 MB and 50,000 URLs.",
|
|
595
|
+
id: "BLUME_AUDIT_SITEMAP_TOO_LARGE",
|
|
596
|
+
severity: "error",
|
|
597
|
+
tier: "static",
|
|
598
|
+
title: "Sitemap exceeds 50 MB or 50,000 URLs",
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
category: "sitemap",
|
|
602
|
+
fix: "Use a real W3C date that is not in the future — search engines that catch a sitemap lying about freshness stop trusting its lastmod entirely.",
|
|
603
|
+
id: "BLUME_AUDIT_SITEMAP_LASTMOD_INVALID",
|
|
604
|
+
severity: "warning",
|
|
605
|
+
tier: "static",
|
|
606
|
+
title: "Sitemap lastmod is invalid or in the future",
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
category: "sitemap",
|
|
610
|
+
fix: "A sitemap may only list URLs on its own origin.",
|
|
611
|
+
id: "BLUME_AUDIT_SITEMAP_OUT_OF_SCOPE",
|
|
612
|
+
severity: "warning",
|
|
613
|
+
tier: "static",
|
|
614
|
+
title: "Sitemap includes URLs out of its scope",
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
category: "sitemap",
|
|
618
|
+
fix: "Make sitemap.xml reachable at the site root.",
|
|
619
|
+
id: "BLUME_AUDIT_SITEMAP_NOT_ACCESSIBLE",
|
|
620
|
+
severity: "error",
|
|
621
|
+
tier: "network",
|
|
622
|
+
title: "Sitemap is not accessible",
|
|
623
|
+
},
|
|
624
|
+
|
|
625
|
+
// robots.txt
|
|
626
|
+
{
|
|
627
|
+
category: "robots",
|
|
628
|
+
fix: "Set `seo.robots: true` to generate robots.txt.",
|
|
629
|
+
id: "BLUME_AUDIT_ROBOTS_MISSING",
|
|
630
|
+
severity: "warning",
|
|
631
|
+
tier: "static",
|
|
632
|
+
title: "robots.txt missing",
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
category: "robots",
|
|
636
|
+
fix: "Every robots.txt line must be a `Field: value` directive or a comment.",
|
|
637
|
+
id: "BLUME_AUDIT_ROBOTS_INVALID",
|
|
638
|
+
severity: "error",
|
|
639
|
+
tier: "static",
|
|
640
|
+
title: "robots.txt has a syntax error",
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
category: "robots",
|
|
644
|
+
fix: "A page can't be both disallowed in robots.txt and advertised in the sitemap.",
|
|
645
|
+
id: "BLUME_AUDIT_ROBOTS_DISALLOWS_INDEXABLE",
|
|
646
|
+
severity: "error",
|
|
647
|
+
tier: "static",
|
|
648
|
+
title: "robots.txt disallows a page that is in the sitemap",
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
category: "robots",
|
|
652
|
+
fix: "Set `deployment.site` so robots.txt can reference the sitemap.",
|
|
653
|
+
id: "BLUME_AUDIT_ROBOTS_SITEMAP_MISSING",
|
|
654
|
+
severity: "info",
|
|
655
|
+
tier: "static",
|
|
656
|
+
title: "robots.txt does not reference the sitemap",
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
category: "robots",
|
|
660
|
+
fix: "Make robots.txt reachable at the site root.",
|
|
661
|
+
id: "BLUME_AUDIT_ROBOTS_NOT_ACCESSIBLE",
|
|
662
|
+
severity: "error",
|
|
663
|
+
tier: "network",
|
|
664
|
+
title: "robots.txt is not accessible",
|
|
665
|
+
},
|
|
666
|
+
|
|
667
|
+
// AI discovery. `llms.txt` is the sitemap of the AI era, and no SEO crawler
|
|
668
|
+
// audits it — they audit for Google. Blume emits it, so Blume checks it.
|
|
669
|
+
{
|
|
670
|
+
category: "ai",
|
|
671
|
+
fix: "Rebuild — `ai.llmsTxt` is enabled but the build has no llms.txt. If that's intentional, set `ai.llmsTxt: false`.",
|
|
672
|
+
id: "BLUME_AUDIT_LLMS_TXT_MISSING",
|
|
673
|
+
severity: "warning",
|
|
674
|
+
tier: "static",
|
|
675
|
+
title: "llms.txt missing from the build",
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
category: "ai",
|
|
679
|
+
fix: "Rebuild so llms.txt matches the site — a stale entry sends an AI agent to a page that is not there.",
|
|
680
|
+
id: "BLUME_AUDIT_LLMS_TXT_STALE_ENTRY",
|
|
681
|
+
severity: "warning",
|
|
682
|
+
tier: "static",
|
|
683
|
+
title: "llms.txt lists a page the build does not serve",
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
category: "ai",
|
|
687
|
+
fix: "Rebuild so llms.txt matches the site; if the page is deliberately excluded, mark it `seo.noindex` or `sidebar.hidden`.",
|
|
688
|
+
id: "BLUME_AUDIT_LLMS_TXT_PAGE_MISSING",
|
|
689
|
+
severity: "warning",
|
|
690
|
+
tier: "static",
|
|
691
|
+
title: "Indexable page missing from llms.txt",
|
|
692
|
+
},
|
|
693
|
+
|
|
694
|
+
// Structured data. Note we validate only what Blume itself emits — we do not
|
|
695
|
+
// claim Google-rich-results or full schema.org validation (the former is an
|
|
696
|
+
// undocumented network API, the latter a vocabulary we don't bundle).
|
|
697
|
+
{
|
|
698
|
+
category: "structured-data",
|
|
699
|
+
fix: "The JSON-LD block must be valid JSON.",
|
|
700
|
+
id: "BLUME_AUDIT_JSONLD_INVALID",
|
|
701
|
+
severity: "error",
|
|
702
|
+
tier: "static",
|
|
703
|
+
title: "Structured data is not valid JSON",
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
category: "structured-data",
|
|
707
|
+
fix: "Every JSON-LD node needs `@context` and `@type`.",
|
|
708
|
+
id: "BLUME_AUDIT_JSONLD_INCOMPLETE",
|
|
709
|
+
severity: "warning",
|
|
710
|
+
tier: "static",
|
|
711
|
+
title: "Structured data is missing required properties",
|
|
712
|
+
},
|
|
713
|
+
|
|
714
|
+
// Network (`--url`)
|
|
715
|
+
{
|
|
716
|
+
category: "network",
|
|
717
|
+
fix: "The page is linked or in the sitemap but the deployment 404s it.",
|
|
718
|
+
id: "BLUME_AUDIT_HTTP_4XX",
|
|
719
|
+
severity: "error",
|
|
720
|
+
tier: "network",
|
|
721
|
+
title: "4XX page",
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
category: "network",
|
|
725
|
+
fix: "The deployment is erroring on this page.",
|
|
726
|
+
id: "BLUME_AUDIT_HTTP_5XX",
|
|
727
|
+
severity: "error",
|
|
728
|
+
tier: "network",
|
|
729
|
+
title: "5XX page",
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
category: "network",
|
|
733
|
+
fix: "The page did not respond in time.",
|
|
734
|
+
id: "BLUME_AUDIT_HTTP_TIMEOUT",
|
|
735
|
+
severity: "error",
|
|
736
|
+
tier: "network",
|
|
737
|
+
title: "Timed out",
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
category: "network",
|
|
741
|
+
fix: "Enable gzip or brotli on the host.",
|
|
742
|
+
id: "BLUME_AUDIT_NOT_COMPRESSED",
|
|
743
|
+
severity: "warning",
|
|
744
|
+
tier: "network",
|
|
745
|
+
title: "Not compressed",
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
category: "network",
|
|
749
|
+
fix: "The page was slow to respond.",
|
|
750
|
+
id: "BLUME_AUDIT_SLOW_RESPONSE",
|
|
751
|
+
severity: "warning",
|
|
752
|
+
tier: "network",
|
|
753
|
+
title: "Slow page",
|
|
754
|
+
},
|
|
755
|
+
|
|
756
|
+
// External (`--external`)
|
|
757
|
+
{
|
|
758
|
+
category: "network",
|
|
759
|
+
fix: "Fix or remove the outbound link.",
|
|
760
|
+
id: "BLUME_AUDIT_EXTERNAL_LINK_BROKEN",
|
|
761
|
+
severity: "error",
|
|
762
|
+
tier: "external",
|
|
763
|
+
title: "External link is broken",
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
category: "network",
|
|
767
|
+
fix: "Link straight to the destination.",
|
|
768
|
+
id: "BLUME_AUDIT_EXTERNAL_LINK_REDIRECT",
|
|
769
|
+
severity: "info",
|
|
770
|
+
tier: "external",
|
|
771
|
+
title: "External link redirects",
|
|
772
|
+
},
|
|
773
|
+
] as const satisfies readonly CheckMeta[];
|
|
774
|
+
|
|
775
|
+
export type CheckId = (typeof CHECKS)[number]["id"];
|
|
776
|
+
|
|
777
|
+
const BY_ID = new Map<string, CheckMeta>(
|
|
778
|
+
CHECKS.map((check) => [check.id, check])
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
export const checkMeta = (id: CheckId): CheckMeta => {
|
|
782
|
+
const meta = BY_ID.get(id);
|
|
783
|
+
if (!meta) {
|
|
784
|
+
throw new Error(`Unknown audit check: ${id}`);
|
|
785
|
+
}
|
|
786
|
+
return meta;
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
const DOCS_BASE = "https://useblume.dev/docs/reference/audit";
|
|
790
|
+
|
|
791
|
+
/** `BLUME_AUDIT_TITLE_TOO_LONG` -> `…/audit#title-too-long`. */
|
|
792
|
+
export const checkDocsUrl = (id: CheckId): string =>
|
|
793
|
+
`${DOCS_BASE}#${id.replace("BLUME_AUDIT_", "").toLowerCase().replaceAll("_", "-")}`;
|
|
794
|
+
|
|
795
|
+
/** Where a finding happened: the built URL, plus the source file that fixes it. */
|
|
796
|
+
export interface FindingSite {
|
|
797
|
+
url: string;
|
|
798
|
+
file?: string;
|
|
799
|
+
line?: number;
|
|
800
|
+
column?: number;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Build a diagnostic for a check. Severity, remediation, and the docs anchor all
|
|
805
|
+
* come from the catalog, so a check body only supplies what's specific to the
|
|
806
|
+
* occurrence: where it happened and what was actually wrong.
|
|
807
|
+
*/
|
|
808
|
+
export const finding = (
|
|
809
|
+
id: CheckId,
|
|
810
|
+
site: FindingSite,
|
|
811
|
+
detail: string,
|
|
812
|
+
fix?: string
|
|
813
|
+
): Diagnostic => {
|
|
814
|
+
const meta = checkMeta(id);
|
|
815
|
+
return {
|
|
816
|
+
code: id,
|
|
817
|
+
column: site.column,
|
|
818
|
+
docsUrl: checkDocsUrl(id),
|
|
819
|
+
file: site.file,
|
|
820
|
+
line: site.line,
|
|
821
|
+
message: detail,
|
|
822
|
+
severity: meta.severity,
|
|
823
|
+
suggestion: fix ?? meta.fix,
|
|
824
|
+
url: site.url,
|
|
825
|
+
};
|
|
826
|
+
};
|