@rivi-software/agentic-seo-check 0.4.0 → 0.5.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/README.md +29 -5
- package/dist/cli.js +20 -0
- package/dist/cli.js.map +1 -1
- package/dist/core/aeo/audit.d.ts +8 -0
- package/dist/core/aeo/audit.js +101 -17
- package/dist/core/aeo/audit.js.map +1 -1
- package/dist/core/aeo/bots.d.ts +48 -0
- package/dist/core/aeo/bots.js +151 -5
- package/dist/core/aeo/bots.js.map +1 -1
- package/dist/core/aeo/fixes.d.ts +38 -0
- package/dist/core/aeo/fixes.js +111 -0
- package/dist/core/aeo/fixes.js.map +1 -0
- package/dist/core/aeo/site-rules.d.ts +5 -0
- package/dist/core/aeo/site-rules.js +293 -10
- package/dist/core/aeo/site-rules.js.map +1 -1
- package/dist/core/audit.d.ts +8 -0
- package/dist/core/audit.js +144 -25
- package/dist/core/audit.js.map +1 -1
- package/dist/core/crawl.js +43 -4
- package/dist/core/crawl.js.map +1 -1
- package/dist/core/html/answer.d.ts +5 -2
- package/dist/core/html/answer.js +25 -7
- package/dist/core/html/answer.js.map +1 -1
- package/dist/core/html/dom.d.ts +13 -1
- package/dist/core/html/dom.js +30 -2
- package/dist/core/html/dom.js.map +1 -1
- package/dist/core/html/extract.js +122 -30
- package/dist/core/html/extract.js.map +1 -1
- package/dist/core/html/head.js +114 -19
- package/dist/core/html/head.js.map +1 -1
- package/dist/core/html/resources.js +15 -7
- package/dist/core/html/resources.js.map +1 -1
- package/dist/core/html/structured-data.js +21 -5
- package/dist/core/html/structured-data.js.map +1 -1
- package/dist/core/lighthouse.d.ts +2 -5
- package/dist/core/lighthouse.js +290 -5
- package/dist/core/lighthouse.js.map +1 -1
- package/dist/core/performance-fixes.d.ts +25 -0
- package/dist/core/performance-fixes.js +446 -0
- package/dist/core/performance-fixes.js.map +1 -0
- package/dist/core/performance-rules.js +56 -8
- package/dist/core/performance-rules.js.map +1 -1
- package/dist/core/recheck.d.ts +68 -0
- package/dist/core/recheck.js +247 -0
- package/dist/core/recheck.js.map +1 -0
- package/dist/core/render-fixes.d.ts +27 -0
- package/dist/core/render-fixes.js +164 -0
- package/dist/core/render-fixes.js.map +1 -0
- package/dist/core/render-rules.js +60 -21
- package/dist/core/render-rules.js.map +1 -1
- package/dist/core/report/markdown.js +68 -1
- package/dist/core/report/markdown.js.map +1 -1
- package/dist/core/robots.d.ts +5 -0
- package/dist/core/robots.js +8 -0
- package/dist/core/robots.js.map +1 -1
- package/dist/core/rules/aeo.js +339 -25
- package/dist/core/rules/aeo.js.map +1 -1
- package/dist/core/rules/canonical.js +107 -11
- package/dist/core/rules/canonical.js.map +1 -1
- package/dist/core/rules/content.js +65 -7
- package/dist/core/rules/content.js.map +1 -1
- package/dist/core/rules/document.js +73 -8
- package/dist/core/rules/document.js.map +1 -1
- package/dist/core/rules/fix.d.ts +55 -0
- package/dist/core/rules/fix.js +159 -0
- package/dist/core/rules/fix.js.map +1 -0
- package/dist/core/rules/head.js +81 -4
- package/dist/core/rules/head.js.map +1 -1
- package/dist/core/rules/headings.js +92 -9
- package/dist/core/rules/headings.js.map +1 -1
- package/dist/core/rules/hreflang.js +79 -9
- package/dist/core/rules/hreflang.js.map +1 -1
- package/dist/core/rules/images.js +49 -0
- package/dist/core/rules/images.js.map +1 -1
- package/dist/core/rules/index.d.ts +11 -0
- package/dist/core/rules/index.js +21 -1
- package/dist/core/rules/index.js.map +1 -1
- package/dist/core/rules/indexing.js +108 -1
- package/dist/core/rules/indexing.js.map +1 -1
- package/dist/core/rules/keyword.js +91 -1
- package/dist/core/rules/keyword.js.map +1 -1
- package/dist/core/rules/links.js +42 -0
- package/dist/core/rules/links.js.map +1 -1
- package/dist/core/rules/loading.js +54 -1
- package/dist/core/rules/loading.js.map +1 -1
- package/dist/core/rules/meta.js +36 -5
- package/dist/core/rules/meta.js.map +1 -1
- package/dist/core/rules/page-fixes.d.ts +39 -0
- package/dist/core/rules/page-fixes.js +107 -0
- package/dist/core/rules/page-fixes.js.map +1 -0
- package/dist/core/rules/quoted-tags.d.ts +23 -0
- package/dist/core/rules/quoted-tags.js +79 -0
- package/dist/core/rules/quoted-tags.js.map +1 -0
- package/dist/core/rules/response.js +71 -7
- package/dist/core/rules/response.js.map +1 -1
- package/dist/core/rules/rule.d.ts +17 -4
- package/dist/core/rules/rule.js +11 -2
- package/dist/core/rules/rule.js.map +1 -1
- package/dist/core/rules/social.js +101 -10
- package/dist/core/rules/social.js.map +1 -1
- package/dist/core/rules/structured-data-fixes.d.ts +33 -0
- package/dist/core/rules/structured-data-fixes.js +469 -0
- package/dist/core/rules/structured-data-fixes.js.map +1 -0
- package/dist/core/rules/structured-data-values.js +242 -31
- package/dist/core/rules/structured-data-values.js.map +1 -1
- package/dist/core/rules/structured-data.js +171 -4
- package/dist/core/rules/structured-data.js.map +1 -1
- package/dist/core/rules/title.js +39 -5
- package/dist/core/rules/title.js.map +1 -1
- package/dist/core/rules/urls.js +166 -0
- package/dist/core/rules/urls.js.map +1 -1
- package/dist/core/site-fixes.d.ts +111 -0
- package/dist/core/site-fixes.js +650 -0
- package/dist/core/site-fixes.js.map +1 -0
- package/dist/core/site-rules.js +474 -53
- package/dist/core/site-rules.js.map +1 -1
- package/dist/core/sitemap.d.ts +16 -0
- package/dist/core/sitemap.js +34 -0
- package/dist/core/sitemap.js.map +1 -1
- package/dist/core/types.d.ts +223 -3
- package/dist/core/types.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +16 -2
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/skills/seo-audit/SKILL.md +22 -5
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/** A slot in `code` for words the author writes, such as a title: `{{hint}}`. */
|
|
2
|
+
export const slot = (hint) => `{{${hint}}}`;
|
|
3
|
+
/** A value for a double-quoted HTML attribute. Text that would read as a character reference keeps its `&` escaped. */
|
|
4
|
+
export const escapeAttribute = (value) => value.replaceAll(/&(?=#?\w+;)/gu, "&").replaceAll('"', """);
|
|
5
|
+
/** Text for an HTML element's content. */
|
|
6
|
+
export const escapeText = (value) => value.replaceAll("&", "&").replaceAll("<", "<");
|
|
7
|
+
const edit = (target, action, parts) => {
|
|
8
|
+
// Key order is the order an agent reads an edit in: what, where, what is there, what to write.
|
|
9
|
+
const out = { in: target, action };
|
|
10
|
+
if (parts.url) {
|
|
11
|
+
out.url = parts.url;
|
|
12
|
+
}
|
|
13
|
+
if (parts.at) {
|
|
14
|
+
out.at = parts.at;
|
|
15
|
+
}
|
|
16
|
+
if (parts.current !== undefined) {
|
|
17
|
+
out.current = parts.current;
|
|
18
|
+
}
|
|
19
|
+
if (parts.code !== undefined) {
|
|
20
|
+
out.code = parts.code;
|
|
21
|
+
}
|
|
22
|
+
if (parts.write) {
|
|
23
|
+
out.write = parts.write;
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
};
|
|
27
|
+
/** Write `code` at `at` (a CSS selector: "head" adds to <head>) in `target`. */
|
|
28
|
+
export const addEdit = (target, at, code, options = {}) => edit(target, "add", { ...options, at, code });
|
|
29
|
+
/** Swap `current` for `code` in `target`. */
|
|
30
|
+
export const replaceEdit = (target, current, code, options = {}) => edit(target, "replace", { ...options, current, code });
|
|
31
|
+
/** Delete `current` from `target`. */
|
|
32
|
+
export const removeEdit = (target, current, options = {}) => edit(target, "remove", { ...options, current });
|
|
33
|
+
export const addHtml = (at, code, write) => addEdit("html", at, code, { write });
|
|
34
|
+
export const replaceHtml = (current, code, options = {}) => replaceEdit("html", current, code, options);
|
|
35
|
+
export const removeHtml = (current, at) => removeEdit("html", current, { at });
|
|
36
|
+
/**
|
|
37
|
+
* A change to a whole file the site serves (a script split, an image re-encoded) that no code can
|
|
38
|
+
* show: a replace at "whole file" with no `current`. `code` is a slot, and `write` says what to
|
|
39
|
+
* change and by how much, since a measurement is not text an agent can find in the file.
|
|
40
|
+
*/
|
|
41
|
+
export const rebuildEdit = (url, hint, write) => edit("asset", "replace", { url, at: "whole file", code: slot(hint), write });
|
|
42
|
+
/** A quote the extractor cut short, which no longer matches the source as a whole. */
|
|
43
|
+
export const isCut = (quoted) => quoted.endsWith("…");
|
|
44
|
+
const isSpace = (ch) => ch === " " || ch === "\t" || ch === "\n" || ch === "\r" || ch === "\f";
|
|
45
|
+
/** The first offset from `i` on where `stop` holds, or the end of `text`. */
|
|
46
|
+
const scan = (text, i, stop) => {
|
|
47
|
+
let j = i;
|
|
48
|
+
while (j < text.length && !stop(text[j])) {
|
|
49
|
+
j += 1;
|
|
50
|
+
}
|
|
51
|
+
return j;
|
|
52
|
+
};
|
|
53
|
+
const notSpace = (ch) => !isSpace(ch);
|
|
54
|
+
/** Where an attribute value that starts at `i` ends, or -1 when its quote never closes. */
|
|
55
|
+
const valueEnd = (tag, i) => {
|
|
56
|
+
const q = tag[i];
|
|
57
|
+
if (q === '"' || q === "'") {
|
|
58
|
+
const endQuote = tag.indexOf(q, i + 1);
|
|
59
|
+
return endQuote === -1 ? -1 : endQuote + 1;
|
|
60
|
+
}
|
|
61
|
+
return scan(tag, i, (ch) => isSpace(ch) || ch === ">");
|
|
62
|
+
};
|
|
63
|
+
/** Where an attribute whose name ends at `i` ends: after its value, if it has one. */
|
|
64
|
+
const attributeEnd = (tag, i) => {
|
|
65
|
+
const eq = scan(tag, i, notSpace);
|
|
66
|
+
return tag[eq] === "=" ? valueEnd(tag, scan(tag, eq + 1, notSpace)) : i;
|
|
67
|
+
};
|
|
68
|
+
const closesTag = (tag, i) => tag[i] === ">" || (tag[i] === "/" && tag[i + 1] === ">");
|
|
69
|
+
/**
|
|
70
|
+
* The attributes of a start tag as written, read the way an HTML tokenizer does: a value in quotes
|
|
71
|
+
* may hold anything, so `title="alt=x"` holds no alt attribute. Null for text that is not one
|
|
72
|
+
* whole start tag.
|
|
73
|
+
*/
|
|
74
|
+
const attributesOf = (tag) => {
|
|
75
|
+
if (!tag.startsWith("<") || isCut(tag)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const spans = [];
|
|
79
|
+
let i = scan(tag, 1, (ch) => isSpace(ch) || ch === ">" || ch === "/");
|
|
80
|
+
while (i < tag.length) {
|
|
81
|
+
const start = i;
|
|
82
|
+
i = scan(tag, i, (ch) => notSpace(ch) && ch !== "/");
|
|
83
|
+
// A lone "/" between attributes is ignored, as the tokenizer does.
|
|
84
|
+
while (tag[i] === "/" && tag[i + 1] !== ">") {
|
|
85
|
+
i = scan(tag, i + 1, (ch) => notSpace(ch) && ch !== "/");
|
|
86
|
+
}
|
|
87
|
+
if (closesTag(tag, i)) {
|
|
88
|
+
return { spans, close: start };
|
|
89
|
+
}
|
|
90
|
+
const nameEnd = scan(tag, i, (ch) => isSpace(ch) || "/>=".includes(ch ?? ""));
|
|
91
|
+
const name = tag.slice(i, nameEnd).toLowerCase();
|
|
92
|
+
i = attributeEnd(tag, nameEnd);
|
|
93
|
+
if (i === -1) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
spans.push({ name, start, end: i });
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* `tag` with attribute `name` set to `value` (replaced where the tag has it, else added before the
|
|
102
|
+
* `>`), or null when `tag` is not one whole start tag. `value` is written as given: escape it with
|
|
103
|
+
* `escapeAttribute` unless it holds a `{{…}}` slot.
|
|
104
|
+
*/
|
|
105
|
+
export const withAttribute = (tag, name, value) => {
|
|
106
|
+
const parsed = attributesOf(tag);
|
|
107
|
+
if (!parsed) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
// `true` writes a boolean attribute bare, the way authors write `defer` or `async`.
|
|
111
|
+
const attribute = value === true ? ` ${name}` : ` ${name}="${value}"`;
|
|
112
|
+
const lower = name.toLowerCase();
|
|
113
|
+
const existing = parsed.spans.find((s) => s.name === lower);
|
|
114
|
+
if (existing) {
|
|
115
|
+
return `${tag.slice(0, existing.start)}${attribute}${tag.slice(existing.end)}`;
|
|
116
|
+
}
|
|
117
|
+
return `${tag.slice(0, parsed.close)}${attribute}${tag.slice(parsed.close)}`;
|
|
118
|
+
};
|
|
119
|
+
/** `tag` without attribute `name`, or null when `tag` is not one whole start tag. */
|
|
120
|
+
export const withoutAttribute = (tag, name) => {
|
|
121
|
+
const parsed = attributesOf(tag);
|
|
122
|
+
if (!parsed) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const lower = name.toLowerCase();
|
|
126
|
+
let out = tag;
|
|
127
|
+
// Right to left, so earlier offsets stay valid.
|
|
128
|
+
for (const span of parsed.spans.toReversed()) {
|
|
129
|
+
if (span.name === lower) {
|
|
130
|
+
out = `${out.slice(0, span.start)}${out.slice(span.end)}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* The edit that sets attribute `name` on the element `tag` quotes: a replace of the whole tag when
|
|
137
|
+
* it could be rewritten, else an add of the attribute on the element at `at`.
|
|
138
|
+
*/
|
|
139
|
+
export const setAttributeEdit = (tag, at, name, value, write) => {
|
|
140
|
+
const rewritten = tag ? withAttribute(tag, name, value) : null;
|
|
141
|
+
if (tag && rewritten) {
|
|
142
|
+
return replaceHtml(tag, rewritten, { at, write });
|
|
143
|
+
}
|
|
144
|
+
return addHtml(at, value === true ? name : `${name}="${value}"`, write);
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* `outer`, an element quoted whole (`<title>Old</title>`), with `content` between its tags; null
|
|
148
|
+
* when the quote was cut short.
|
|
149
|
+
*/
|
|
150
|
+
export const withContent = (outer, content) => {
|
|
151
|
+
const parsed = attributesOf(outer);
|
|
152
|
+
const endTag = outer.lastIndexOf("</");
|
|
153
|
+
if (!parsed || endTag === -1) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const startEnd = outer.indexOf(">", parsed.close) + 1;
|
|
157
|
+
return `${outer.slice(0, startEnd)}${content}${outer.slice(endTag)}`;
|
|
158
|
+
};
|
|
159
|
+
//# sourceMappingURL=fix.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fix.js","sourceRoot":"","sources":["../../../src/core/rules/fix.ts"],"names":[],"mappings":"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"}
|
package/dist/core/rules/head.js
CHANGED
|
@@ -1,8 +1,56 @@
|
|
|
1
1
|
import { normalizeUrl, resolveUrl } from "../url.js";
|
|
2
|
+
import { isLocalHost } from "./canonical.js";
|
|
3
|
+
import { addHtml, escapeAttribute, isCut, removeEdit, replaceEdit, } from "./fix.js";
|
|
4
|
+
import { liveUrl, ORIGIN_WRITE } from "./quoted-tags.js";
|
|
2
5
|
import { cap, defineRule, GOOGLE_DOCS } from "./rule.js";
|
|
3
6
|
/** The HTML spec's limit for the element that declares the encoding. */
|
|
4
7
|
const CHARSET_PRESCAN_BYTES = 1024;
|
|
5
8
|
const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
9
|
+
/**
|
|
10
|
+
* The edits that move what ended <head> to the start of <body>. A browser already shows it there,
|
|
11
|
+
* so the page looks the same; only the metadata after it comes back into <head>.
|
|
12
|
+
*/
|
|
13
|
+
const moveToBody = (source, described, write) => {
|
|
14
|
+
// Without a verbatim quote, the short description is the best handle on the element.
|
|
15
|
+
const quoted = source ?? described;
|
|
16
|
+
if (!source || isCut(source)) {
|
|
17
|
+
return [
|
|
18
|
+
removeEdit("html", quoted, {
|
|
19
|
+
at: "head",
|
|
20
|
+
write: `${write} Move the whole element there, with its content and end tag.`,
|
|
21
|
+
}),
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
return [
|
|
25
|
+
removeEdit("html", source, { at: "head" }),
|
|
26
|
+
addHtml("body", source, write),
|
|
27
|
+
];
|
|
28
|
+
};
|
|
29
|
+
/** Moves metadata elements from <body> to <head>, where search engines read them. */
|
|
30
|
+
const moveToHead = (sources, write) => cap(sources, 5).flatMap((source) => [
|
|
31
|
+
removeEdit("html", source, { at: "body" }),
|
|
32
|
+
addHtml("head", source, write),
|
|
33
|
+
]);
|
|
34
|
+
/** "HTTP/1.1 200 OK" for the response, as near as the audit knows it. */
|
|
35
|
+
const statusLine = (response) => response?.status && response.status !== 200
|
|
36
|
+
? `HTTP/1.1 ${response.status}`
|
|
37
|
+
: "HTTP/1.1 200 OK";
|
|
38
|
+
/** The server redirect that replaces a meta refresh to `target`, and the removal of the tag. */
|
|
39
|
+
const serverRedirect = (page, response, target, tag) => {
|
|
40
|
+
const local = isLocalHost(page.url) || page.url.startsWith("file:");
|
|
41
|
+
const location = liveUrl(target, local);
|
|
42
|
+
const redirect = replaceEdit("http", statusLine(response), `HTTP/1.1 301 Moved Permanently\nLocation: ${location}`, {
|
|
43
|
+
write: [
|
|
44
|
+
local ? ORIGIN_WRITE : undefined,
|
|
45
|
+
`Set in the server, host or framework config (a redirects file or rule). If the server cannot redirect, keep the meta refresh with content="0; url=${target}", which Google reads as a permanent redirect.`,
|
|
46
|
+
]
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.join(" "),
|
|
49
|
+
});
|
|
50
|
+
return tag
|
|
51
|
+
? [redirect, removeEdit("html", tag, { at: 'meta[http-equiv="refresh"]' })]
|
|
52
|
+
: [redirect];
|
|
53
|
+
};
|
|
6
54
|
export const headRules = [
|
|
7
55
|
defineRule({
|
|
8
56
|
id: "head.closed-early",
|
|
@@ -20,6 +68,7 @@ export const headRules = [
|
|
|
20
68
|
return make(`A <noscript> in <head> holds ${closedBy}, which ends <head> for a parser that does not run JavaScript, so it reads the ${plural(afterClose.length, "metadata element")} after it as <body>: ${cap(afterClose, 5).join(", ")}.`, {
|
|
21
69
|
severity: "info",
|
|
22
70
|
recommendation: "Move the <noscript> block below every <title>, <meta> and <link>, or right after the opening <body> tag, where tag managers and ad pixels document it. Inside a <noscript> in <head>, only <link>, <meta> and <style> keep <head> open.",
|
|
71
|
+
fix: moveToBody(page.head.sources.closedBy, closedBy, "Right after the opening <body> tag, where tag managers and ad pixels document their <noscript> block."),
|
|
23
72
|
selector: "head > noscript",
|
|
24
73
|
evidence: {
|
|
25
74
|
closedBy,
|
|
@@ -32,8 +81,17 @@ export const headRules = [
|
|
|
32
81
|
const onlyViewport = afterClose.every((m) => m === '<meta name="viewport">');
|
|
33
82
|
// An invisible character is removed, not moved: it usually comes from a file saved with a BOM.
|
|
34
83
|
const invisible = closedBy.startsWith("invisible character");
|
|
84
|
+
const source = page.head.sources.closedBy;
|
|
35
85
|
return make(`<head> ends at ${closedBy}, so Google ignores the ${plural(afterClose.length, "metadata element")} after it: ${cap(afterClose, 5).join(", ")}.`, {
|
|
36
86
|
severity: onlyViewport ? "warning" : "error",
|
|
87
|
+
fix: invisible && source
|
|
88
|
+
? [
|
|
89
|
+
removeEdit("html", source, {
|
|
90
|
+
at: "head",
|
|
91
|
+
write: "Delete the invisible character. A byte order mark (U+FEFF) comes from a template or include saved as UTF-8 with BOM: save it without BOM. A no-break space comes from an in <head>.",
|
|
92
|
+
}),
|
|
93
|
+
]
|
|
94
|
+
: moveToBody(source, closedBy, "Right after the opening <body> tag, where the browser already shows it, so the page looks the same."),
|
|
37
95
|
recommendation: invisible
|
|
38
96
|
? "Remove the invisible character from <head>. A U+FEFF byte order mark usually comes from a template or include file saved as UTF-8 with BOM: save it as UTF-8 without BOM. A U+00A0 comes from an in the template's <head>."
|
|
39
97
|
: undefined,
|
|
@@ -55,7 +113,11 @@ export const headRules = [
|
|
|
55
113
|
}, ({ page }, make) => {
|
|
56
114
|
const { inBody, streamedInBody } = page.head;
|
|
57
115
|
if (inBody.length > 0) {
|
|
58
|
-
return make(`${plural(inBody.length, "metadata element")} ${inBody.length === 1 ? "is" : "are"} in <body>: ${cap(inBody, 5).join(", ")}.`, {
|
|
116
|
+
return make(`${plural(inBody.length, "metadata element")} ${inBody.length === 1 ? "is" : "are"} in <body>: ${cap(inBody, 5).join(", ")}.`, {
|
|
117
|
+
fix: moveToHead(page.head.sources.inBody, "In a framework, render it through the route's head API (metadata export, <Head>, useHead, <svelte:head>) instead of in the page body."),
|
|
118
|
+
selector: "body",
|
|
119
|
+
evidence: { elements: cap(inBody) },
|
|
120
|
+
});
|
|
59
121
|
}
|
|
60
122
|
// Next.js (15.2+) streams metadata from an async generateMetadata this way to clients it does
|
|
61
123
|
// not count as HTML-limited bots; Googlebot reads it after rendering.
|
|
@@ -63,6 +125,7 @@ export const headRules = [
|
|
|
63
125
|
? make(`${plural(streamedInBody.length, "metadata element")} ${streamedInBody.length === 1 ? "is" : "are"} streamed into a hidden part of <body> (React or Next.js streaming metadata): ${cap(streamedInBody, 5).join(", ")}.`, {
|
|
64
126
|
severity: "info",
|
|
65
127
|
recommendation: "Google renders the page and reads streamed metadata, so this is fine for Google Search. Crawlers that do not run JavaScript only get it in <head> if the framework holds the response for them: in Next.js, add their User-Agents to htmlLimitedBots in next.config, or make generateMetadata static.",
|
|
128
|
+
fix: moveToHead(page.head.sources.streamedInBody, "In Next.js these tags come from an async generateMetadata: make it static, or list the crawlers in htmlLimitedBots in next.config, so the server writes them in <head>."),
|
|
66
129
|
selector: "body [hidden]",
|
|
67
130
|
evidence: { elements: cap(streamedInBody), streamed: true },
|
|
68
131
|
})
|
|
@@ -75,8 +138,9 @@ export const headRules = [
|
|
|
75
138
|
description: 'A <meta http-equiv="refresh"> sends visitors to another URL. Google reads an instant one as a permanent redirect (info) and a delayed one as a temporary redirect (warning), and prefers a redirect from the server.',
|
|
76
139
|
recommendation: "Redirect on the server with a 301 (or 308) to the new URL. If the server cannot redirect, set the meta refresh to 0 seconds, which Google reads as a permanent redirect.",
|
|
77
140
|
docs: GOOGLE_DOCS.redirects,
|
|
78
|
-
}, ({ page }, make) => {
|
|
79
|
-
const
|
|
141
|
+
}, ({ page, response }, make) => {
|
|
142
|
+
const index = page.head.refresh.findIndex((r) => r.url);
|
|
143
|
+
const redirect = page.head.refresh[index];
|
|
80
144
|
if (!redirect?.url) {
|
|
81
145
|
return null;
|
|
82
146
|
}
|
|
@@ -91,6 +155,7 @@ export const headRules = [
|
|
|
91
155
|
? `Page redirects to ${target} with a meta refresh after ${plural(delay, "second")}, which Google treats as a temporary redirect.`
|
|
92
156
|
: `Page redirects to ${target} with an instant meta refresh.`, {
|
|
93
157
|
severity: delay > 0 ? "warning" : "info",
|
|
158
|
+
fix: serverRedirect(page, response, target, page.head.sources.refresh[index]),
|
|
94
159
|
selector: 'meta[http-equiv="refresh"]',
|
|
95
160
|
evidence: { target, seconds: redirect.seconds },
|
|
96
161
|
});
|
|
@@ -110,7 +175,19 @@ export const headRules = [
|
|
|
110
175
|
if (/charset=/iu.test(response?.contentType ?? "")) {
|
|
111
176
|
return null;
|
|
112
177
|
}
|
|
113
|
-
|
|
178
|
+
const tag = page.markup.charset;
|
|
179
|
+
// A <meta charset="utf-8"> moved first; an http-equiv declaration becomes the short form.
|
|
180
|
+
const code = tag && /\scharset\s*=/iu.test(tag) && !/http-equiv/iu.test(tag)
|
|
181
|
+
? tag
|
|
182
|
+
: `<meta charset="${escapeAttribute(page.charset ?? "utf-8")}">`;
|
|
183
|
+
return make(`The charset declaration ends at byte ${end}, past the first ${CHARSET_PRESCAN_BYTES}.`, {
|
|
184
|
+
fix: [
|
|
185
|
+
...(tag ? [removeEdit("html", tag, { at: "head" })] : []),
|
|
186
|
+
addHtml("head", code, "First in <head>, before any script, style or comment."),
|
|
187
|
+
],
|
|
188
|
+
selector: "head > meta[charset]",
|
|
189
|
+
evidence: { endsAtByte: end },
|
|
190
|
+
});
|
|
114
191
|
}),
|
|
115
192
|
];
|
|
116
193
|
//# sourceMappingURL=head.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"head.js","sourceRoot":"","sources":["../../../src/core/rules/head.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"head.js","sourceRoot":"","sources":["../../../src/core/rules/head.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,37 @@
|
|
|
1
|
+
import { addHtml, removeEdit, replaceHtml, setAttributeEdit, slot, withContent, } from "./fix.js";
|
|
2
|
+
import { innerOf, renamed } from "./quoted-tags.js";
|
|
1
3
|
import { cap, defineRule } from "./rule.js";
|
|
4
|
+
const headingElements = (page) => page.headings.map((h, i) => ({
|
|
5
|
+
...h,
|
|
6
|
+
outer: page.markup.headings[i] ?? `<h${h.level}>`,
|
|
7
|
+
}));
|
|
8
|
+
/** An h2-h6 with neither text nor markup, which headings.empty removes. */
|
|
9
|
+
const isRemovable = (h) => h.level > 1 && !h.text && (innerOf(h.outer) ?? "").trim() === "";
|
|
10
|
+
const STYLE_NOTE = "If the heading should keep its look, give it a class that carries the old style.";
|
|
11
|
+
/** The edit that turns `h` into a heading of `level`, keeping its attributes and content. */
|
|
12
|
+
const relevel = (h, level, write) => {
|
|
13
|
+
const code = renamed(h.outer, `h${h.level}`, `h${level}`);
|
|
14
|
+
return code
|
|
15
|
+
? replaceHtml(h.outer, code, { write })
|
|
16
|
+
: replaceHtml(h.outer, `<h${level}>${slot("heading text")}</h${level}>`, {
|
|
17
|
+
write: `The same heading as an <h${level}>; its quote is cut short, so keep its attributes and content. ${write}`,
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The edit that gives a heading without text its text: alt text for an image it holds, else the
|
|
22
|
+
* text itself, beside any other markup it holds so nothing is lost.
|
|
23
|
+
*/
|
|
24
|
+
const nameHeading = (page, h, hint, write) => {
|
|
25
|
+
const image = page.images.find((img) => h.outer.includes(img.tag));
|
|
26
|
+
if (image) {
|
|
27
|
+
return setAttributeEdit(image.tag, `h${h.level} img`, "alt", slot(hint), `${write} The heading holds only this image, so its alt text is the heading.`);
|
|
28
|
+
}
|
|
29
|
+
const inner = innerOf(h.outer);
|
|
30
|
+
const code = withContent(h.outer, `${slot(hint)}${inner ?? ""}`);
|
|
31
|
+
return code
|
|
32
|
+
? replaceHtml(h.outer, code, { write })
|
|
33
|
+
: addHtml(`h${h.level}`, slot(hint), write);
|
|
34
|
+
};
|
|
2
35
|
export const headingRules = [
|
|
3
36
|
defineRule({
|
|
4
37
|
id: "h1.missing",
|
|
@@ -7,7 +40,12 @@ export const headingRules = [
|
|
|
7
40
|
description: "The page has no <h1>.",
|
|
8
41
|
recommendation: "Add one <h1> that states the main topic of the page. It is usually the visible page heading.",
|
|
9
42
|
}, ({ page }, make) => page.h1s.length === 0
|
|
10
|
-
? make("Page has no <h1> heading.", {
|
|
43
|
+
? make("Page has no <h1> heading.", {
|
|
44
|
+
fix: [
|
|
45
|
+
addHtml("body", `<h1>${slot("page heading")}</h1>`, "The visible heading of the page, saying what it is about, at the top of its main content. If the page already shows its title in another element, make that element the <h1>."),
|
|
46
|
+
],
|
|
47
|
+
selector: "h1",
|
|
48
|
+
})
|
|
11
49
|
: null),
|
|
12
50
|
defineRule({
|
|
13
51
|
id: "h1.multiple",
|
|
@@ -15,12 +53,19 @@ export const headingRules = [
|
|
|
15
53
|
severity: "info",
|
|
16
54
|
description: "The page has more than one <h1>.",
|
|
17
55
|
recommendation: "Prefer a single <h1> for the main topic and use <h2>-<h6> for sections. Multiple H1s are valid HTML but make the main topic less clear.",
|
|
18
|
-
}, ({ page }, make) =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
56
|
+
}, ({ page }, make) => {
|
|
57
|
+
const h1s = headingElements(page).filter((h) => h.level === 1);
|
|
58
|
+
// The first <h1> stays the main heading; the others become section headings.
|
|
59
|
+
return h1s.length > 1
|
|
60
|
+
? make(`Page has ${page.h1s.length} <h1> headings.`, {
|
|
61
|
+
fix: h1s
|
|
62
|
+
.slice(1)
|
|
63
|
+
.map((h) => relevel(h, 2, `This keeps the first <h1>, "${h1s[0]?.text.slice(0, 60) ?? ""}", as the page's main heading. ${STYLE_NOTE}`)),
|
|
64
|
+
selector: "h1",
|
|
65
|
+
evidence: { h1s: cap(page.h1s) },
|
|
66
|
+
})
|
|
67
|
+
: null;
|
|
68
|
+
}),
|
|
24
69
|
defineRule({
|
|
25
70
|
id: "h1.empty",
|
|
26
71
|
category: "headings",
|
|
@@ -28,7 +73,13 @@ export const headingRules = [
|
|
|
28
73
|
description: "An <h1> exists but contains no text.",
|
|
29
74
|
recommendation: "Put the page heading text inside the <h1>. If it wraps only an image, give the image descriptive alt text.",
|
|
30
75
|
}, ({ page }, make) => page.h1s.length > 0 && page.h1s.every((h) => !h)
|
|
31
|
-
? make("All <h1> elements are empty.", {
|
|
76
|
+
? make("All <h1> elements are empty.", {
|
|
77
|
+
fix: headingElements(page)
|
|
78
|
+
.filter((h) => h.level === 1)
|
|
79
|
+
.slice(0, 1)
|
|
80
|
+
.map((h) => nameHeading(page, h, "page heading", "The visible heading of the page, saying what it is about.")),
|
|
81
|
+
selector: "h1",
|
|
82
|
+
})
|
|
32
83
|
: null),
|
|
33
84
|
defineRule({
|
|
34
85
|
id: "h1.too-long",
|
|
@@ -38,8 +89,17 @@ export const headingRules = [
|
|
|
38
89
|
recommendation: "Keep the H1 concise; move detail into the intro paragraph.",
|
|
39
90
|
}, ({ page, thresholds }, make) => {
|
|
40
91
|
const long = page.h1s.filter((h) => h.length > thresholds.h1MaxChars);
|
|
92
|
+
const fix = headingElements(page)
|
|
93
|
+
.filter((h) => h.level === 1 && h.text.length > thresholds.h1MaxChars)
|
|
94
|
+
.map((h) => {
|
|
95
|
+
const code = withContent(h.outer, slot("shorter heading"));
|
|
96
|
+
const write = `The heading in at most ${thresholds.h1MaxChars} characters; move the detail into the paragraph below it.`;
|
|
97
|
+
return code
|
|
98
|
+
? replaceHtml(h.outer, code, { write })
|
|
99
|
+
: addHtml("h1", slot("shorter heading"), write);
|
|
100
|
+
});
|
|
41
101
|
return long.length
|
|
42
|
-
? make(`${long.length} <h1> heading(s) exceed ${thresholds.h1MaxChars} characters.`, { selector: "h1", evidence: { h1s: cap(long) } })
|
|
102
|
+
? make(`${long.length} <h1> heading(s) exceed ${thresholds.h1MaxChars} characters.`, { fix, selector: "h1", evidence: { h1s: cap(long) } })
|
|
43
103
|
: null;
|
|
44
104
|
}),
|
|
45
105
|
defineRule({
|
|
@@ -50,8 +110,15 @@ export const headingRules = [
|
|
|
50
110
|
recommendation: "Remove empty heading tags or fill them with text. Empty headings confuse outline parsers and screen readers.",
|
|
51
111
|
}, ({ page }, make) => {
|
|
52
112
|
const empty = page.headings.filter((h) => h.level > 1 && !h.text);
|
|
113
|
+
// A heading with no text and no markup is removed; one that holds an icon or image gets a name.
|
|
114
|
+
const fix = headingElements(page)
|
|
115
|
+
.filter((h) => h.level > 1 && !h.text)
|
|
116
|
+
.map((h) => isRemovable(h)
|
|
117
|
+
? removeEdit("html", h.outer, { at: `h${h.level}:empty` })
|
|
118
|
+
: nameHeading(page, h, "section heading", "The heading of the section below it."));
|
|
53
119
|
return empty.length
|
|
54
120
|
? make(`${empty.length} empty heading element(s) found.`, {
|
|
121
|
+
fix,
|
|
55
122
|
evidence: { levels: cap(empty.map((h) => `h${h.level}`)) },
|
|
56
123
|
})
|
|
57
124
|
: null;
|
|
@@ -71,8 +138,24 @@ export const headingRules = [
|
|
|
71
138
|
}
|
|
72
139
|
prev = h.level;
|
|
73
140
|
}
|
|
141
|
+
// Renumber so no level is skipped: a heading that skips moves up, and the headings under it
|
|
142
|
+
// move with it, so the fix leaves no new skip behind.
|
|
143
|
+
const fix = [];
|
|
144
|
+
let prevNew = 0;
|
|
145
|
+
for (const h of headingElements(page)) {
|
|
146
|
+
// headings.empty removes an empty heading; renumbering it too would give two edits one tag.
|
|
147
|
+
if (isRemovable(h)) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const level = prevNew > 0 && h.level > prevNew + 1 ? prevNew + 1 : h.level;
|
|
151
|
+
if (level !== h.level) {
|
|
152
|
+
fix.push(relevel(h, level, STYLE_NOTE));
|
|
153
|
+
}
|
|
154
|
+
prevNew = level;
|
|
155
|
+
}
|
|
74
156
|
return skips.length
|
|
75
157
|
? make(`${skips.length} heading level skip(s) found.`, {
|
|
158
|
+
fix,
|
|
76
159
|
evidence: { skips: cap(skips) },
|
|
77
160
|
})
|
|
78
161
|
: null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"headings.js","sourceRoot":"","sources":["../../../src/core/rules/headings.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"headings.js","sourceRoot":"","sources":["../../../src/core/rules/headings.ts"],"names":[],"mappings":"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"}
|