search-auditor 0.0.0-stage → 0.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 ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-11-04)
4
+
5
+
6
+ ### New checks and features
7
+
8
+ * classic SEO and AI search checks in one report ([#1](https://github.com/a-bibikov/search-auditor/issues/1)) ([450ff97](https://github.com/a-bibikov/search-auditor/commit/450ff97db5f950e2708500ccd3389f4b97d5ab90))
9
+ * classic SEO and AI search checks in one report ([#1](https://github.com/a-bibikov/search-auditor/issues/1)) ([c272847](https://github.com/a-bibikov/search-auditor/commit/c272847b6b4ecaf5910e89bbc4e203480e00d324))
10
+
11
+
12
+ ### Fixes
13
+
14
+ * start versions at 0.1.0 ([#4](https://github.com/a-bibikov/search-auditor/issues/4)) ([d1d10c5](https://github.com/a-bibikov/search-auditor/commit/d1d10c5c25435f5eabdffd8c104473e82254ef45))
15
+ * start versions at 0.1.0 ([#4](https://github.com/a-bibikov/search-auditor/issues/4)) ([acbccc4](https://github.com/a-bibikov/search-auditor/commit/acbccc44da99a0eed0b2eeb42dbfa41d3330ccfc))
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aleksandr Bibikov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,146 @@
1
- # Temporary Holding Version
1
+ # search-auditor
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Can Google, Bing and AI search find, read and cite your site?** One crawl, one report: classic SEO checks — status codes, canonical, sitemap, titles, headings, broken links, hreflang, HTTPS — and the AI search checks of [ai-search-auditor](https://github.com/a-bibikov/ai-search-audit), with a score for each and one overall.
4
+
5
+ ```bash
6
+ npx search-auditor https://example.com
7
+ ```
8
+
9
+ No sign-up, no API keys. Node.js 22.18+.
10
+
11
+ ## Why both in one report
12
+
13
+ People find sites in two ways now: a list of links in Google, Bing or Yandex, and an answer from ChatGPT, Claude, Perplexity or Google AI Overviews that cites a few sources. Both start from the same thing — a crawler that can reach the page, read it and tell what it is about — and then they diverge. A broken canonical drops a page from Google; a heading that glues words together makes AI search misquote it.
14
+
15
+ Running two tools means two crawls, two reports and two scores that never meet. search-auditor crawls once, runs both rule sets on the same pages and puts the results side by side, so the first three things to fix are the three that cost the most, whichever kind of search they hurt.
16
+
17
+ ## What it checks
18
+
19
+ 24 classic search checks in ten groups, plus the 20 AI search checks of ai-search-auditor (turn them off with `--no-ai`). Nothing is checked twice: robots.txt for search crawlers, `noindex` and link cards are part of the AI search set.
20
+
21
+ **IDX. Indexing**
22
+ - **IDX-1** Audited pages answer 200
23
+ - **IDX-2** A page that does not exist answers 404, and no «not found» page answers 200
24
+
25
+ **SEC. HTTPS** (live sites only)
26
+ - **SEC-1** The site is on https, and http:// redirects to it
27
+ - **SEC-2** Pages do not load anything over http://
28
+
29
+ **CAN. Canonical**
30
+ - **CAN-1** Every page has a canonical
31
+ - **CAN-2** The canonical is an absolute address on this site
32
+ - **CAN-3** The canonical leads to a page that answers 200, not to an error or a redirect
33
+
34
+ **MAP. Sitemap**
35
+ - **MAP-1** There is a sitemap, and robots.txt points to it
36
+ - **MAP-2** Sitemap addresses do not redirect
37
+ - **MAP-3** Sitemap pages are their own canonical
38
+
39
+ **META. Titles and descriptions**
40
+ - **META-1** Every page has a title of about 10–60 characters
41
+ - **META-2** Every page has a description of about 50–160 characters
42
+ - **META-3** Titles and descriptions are not repeated across pages
43
+
44
+ **STR. Page structure**
45
+ - **STR-1** One H1 per page
46
+ - **STR-2** Heading levels do not skip
47
+ - **STR-3** The page declares its language
48
+ - **STR-4** Pages are set up for mobile
49
+
50
+ **LNK. Links**
51
+ - **LNK-1** Internal links work
52
+ - **LNK-2** Internal links do not go through redirects
53
+
54
+ **IMG. Images**
55
+ - **IMG-1** Images have alt text
56
+
57
+ **DATA. Structured data**
58
+ - **DATA-1** JSON-LD has no syntax errors
59
+
60
+ **LANG. Language versions** (sites with hreflang)
61
+ - **LANG-1** hreflang codes are valid
62
+ - **LANG-2** Language versions point at each other
63
+ - **LANG-3** There is an x-default
64
+
65
+ Levels: `error` — search engines cannot index the page or index the wrong one; `warn` — lowers rankings or clicks; `info` — a recommendation.
66
+
67
+ The report opens with the overall score and one per part («Classic search», «AI search»), then what to do first — the three changes worth the most, in plain words and with how many pages they touch — then the findings with fixes, and ends with a checklist of every check by group: ✓ OK, ✖ error, ⚠ warning, ℹ note, – not checked. A check that cannot run on a site — hreflang on a site in one language, HTTPS on a local build — is marked «not checked» with the reason, never as passed.
68
+
69
+ **Score:** each part is scored like ai-search-auditor: 100 minus 15 for each check with an error and 5 for each check with a warning; a warning that covers at least half of the audited pages costs 10. The overall score is the mean of the parts. A ≥ 90, B ≥ 80, C ≥ 70, D ≥ 60, F below; if fewer than two thirds of the checks could run, the grade is capped at B.
70
+
71
+ ## Usage
72
+
73
+ ```bash
74
+ # A live site
75
+ npx search-auditor https://example.com
76
+
77
+ # A local build: requests go to localhost, the report shows public URLs
78
+ npx search-auditor http://127.0.0.1:3000 --base https://example.com
79
+
80
+ # Every page in the sitemap instead of a sample of 20
81
+ npx search-auditor https://example.com --pages all
82
+
83
+ # Classic search only, report in Russian
84
+ npx search-auditor https://example.com --no-ai --lang ru
85
+
86
+ # JSON for scripts and agents
87
+ npx search-auditor https://example.com --json --out report.json
88
+ ```
89
+
90
+ | Option | |
91
+ |---|---|
92
+ | `--base <url>` | Public origin when auditing a local build |
93
+ | `--pages <n\|all>` | Pages to audit: a number sampled across templates (default 20), or `all` for every page in the sitemap |
94
+ | `--max-links <n>` | Internal links to request (default 100) |
95
+ | `--no-ai` | Classic search checks only |
96
+ | `--fail-on <level>` | Exit 1 on `error` (default), `warn` or `never` |
97
+ | `--lang <lang>` | Report language: `en` (default), `ru` |
98
+ | `--json` | JSON instead of Markdown |
99
+ | `--out <file>` | Also write the report to a file |
100
+ | `--config <file>` | Config file (default `search-auditor.config.json`) |
101
+ | `-v`, `--verbose` | Show progress |
102
+
103
+ Config — the options of ai-search-auditor plus two of its own:
104
+
105
+ ```json
106
+ {
107
+ "disable": ["STR-2", "C-7"],
108
+ "maxPages": 20,
109
+ "maxLinks": 100,
110
+ "ai": true,
111
+ "trainingBots": "any",
112
+ "lang": "en"
113
+ }
114
+ ```
115
+
116
+ `maxPages`: a number or `"all"`. With `"all"` every page in the sitemap is audited, so raise `maxLinks` too if you want every internal link checked.
117
+
118
+ In CI the Markdown report also goes to the job summary (`GITHUB_STEP_SUMMARY`).
119
+
120
+ ## As a library
121
+
122
+ ```js
123
+ import { auditSearch, toMarkdown } from "search-auditor";
124
+
125
+ const report = await auditSearch("https://example.com", { config: { lang: "en", ai: true } });
126
+ console.log(report.score, report.sections);
127
+ console.log(toMarkdown(report));
128
+ ```
129
+
130
+ `ai-search-auditor` is a peer dependency: the crawl, the scoring and the report come from it, so both parts always run on the same engine version. The SEO checks are also exported as `seoPlugin` — rules, texts, groups and the extra requests they need — for runners that bring their own crawl.
131
+
132
+ ## Roadmap
133
+
134
+ - **IDX-3** `noindex` and canonical do not contradict each other
135
+ - **IDX-4** robots.txt does not block CSS and JavaScript the page needs to render
136
+ - **CAN-4** Canonical chains: a canonical target whose own canonical points further
137
+ - **MAP-4** Every audited page is in the sitemap; the sitemap has no pages blocked by robots.txt
138
+ - **LNK-3** Orphan pages: in the sitemap, but no internal link leads to them
139
+ - **LNK-4** Redirect chains of more than one hop
140
+ - **IMG-2** Images are sized and lazy-loaded, so they do not slow the page down
141
+ - **PERF-1** Server response time and page weight
142
+ - **SOC-1** Twitter / X cards in addition to Open Graph
143
+
144
+ ## License
145
+
146
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { parseArgs } from "node:util";
4
+ import { DEFAULT_LANG, LANGS, isLang, parsePages, toMarkdown } from "ai-search-auditor";
5
+ import { auditSearch } from "./index.js";
6
+ const HELP = `search-auditor — one crawl, one report: can Google, Bing and AI search (ChatGPT, Claude, Perplexity) find, read and cite your site?
7
+
8
+ Usage:
9
+ search-auditor <url> [options]
10
+
11
+ Options:
12
+ --base <url> public origin when auditing a local build (links in sitemap point there)
13
+ --pages <n|all> pages to audit: a number sampled across templates (default 20), or all pages in the sitemap
14
+ --max-links <n> internal links to request (default 100)
15
+ --no-ai classic search checks only
16
+ --fail-on <level> exit 1 on: error (default), warn, never
17
+ --lang <lang> report language: ${Object.keys(LANGS).join(", ")} (default ${DEFAULT_LANG})
18
+ --json print the JSON report instead of Markdown
19
+ --out <file> also write the report to a file (.json or .md)
20
+ --config <file> config file (default: search-auditor.config.json if present)
21
+ -v, --verbose show progress
22
+ -h, --help
23
+
24
+ Examples:
25
+ npx search-auditor https://example.com
26
+ npx search-auditor http://127.0.0.1:3000 --base https://example.com --fail-on warn
27
+ npx search-auditor https://example.com --lang ru --no-ai
28
+ npx search-auditor https://example.com --pages all
29
+ `;
30
+ // An unknown or malformed flag gets one line with the help hint, not a stack trace.
31
+ let parsed;
32
+ try {
33
+ parsed = parseArgs({
34
+ allowPositionals: true,
35
+ allowNegative: true,
36
+ options: {
37
+ base: { type: "string" },
38
+ pages: { type: "string" },
39
+ "max-links": { type: "string" },
40
+ ai: { type: "boolean" },
41
+ "fail-on": { type: "string", default: "error" },
42
+ lang: { type: "string" },
43
+ json: { type: "boolean", default: false },
44
+ out: { type: "string" },
45
+ config: { type: "string" },
46
+ verbose: { type: "boolean", short: "v", default: false },
47
+ help: { type: "boolean", short: "h", default: false },
48
+ },
49
+ });
50
+ }
51
+ catch (e) {
52
+ process.stderr.write(`search-auditor: ${e.message.split(". To specify")[0]}\nRun with --help to see the options.\n`);
53
+ process.exit(2);
54
+ }
55
+ const { values, positionals } = parsed;
56
+ const target = positionals[0];
57
+ if (values.help || !target) {
58
+ process.stdout.write(HELP);
59
+ process.exit(values.help ? 0 : 2);
60
+ }
61
+ // A function declaration, not an arrow: TypeScript treats only declared `never` functions as the end of a branch.
62
+ function fail(message) {
63
+ process.stderr.write(`search-auditor: ${message}\n`);
64
+ process.exit(2);
65
+ }
66
+ const count = (name, v) => {
67
+ if (v === undefined)
68
+ return undefined;
69
+ const n = Number(v);
70
+ if (!Number.isInteger(n) || n < 1)
71
+ fail(`--${name} must be a whole number above 0, got «${v}»`);
72
+ return n;
73
+ };
74
+ const url = /^https?:\/\//.test(target) ? target : `https://${target}`;
75
+ const configPath = values.config ?? (existsSync("search-auditor.config.json") ? "search-auditor.config.json" : undefined);
76
+ const fileConfig = configPath ? JSON.parse(readFileSync(configPath, "utf8")) : {};
77
+ // Flags win over the config file; an unknown language stops before any request is made.
78
+ const lang = values.lang ?? fileConfig.lang ?? DEFAULT_LANG;
79
+ if (!isLang(lang))
80
+ fail(`unknown language «${lang}», use one of: ${Object.keys(LANGS).join(", ")}`);
81
+ if (!["error", "warn", "never"].includes(values["fail-on"]))
82
+ fail(`--fail-on must be error, warn or never, got «${values["fail-on"]}»`);
83
+ const pagesRaw = values.pages ?? fileConfig.maxPages;
84
+ const maxPages = pagesRaw === undefined ? undefined : parsePages(pagesRaw);
85
+ if (maxPages === null)
86
+ fail(`${values.pages !== undefined ? "--pages" : "maxPages in the config"} must be a whole number above 0 or «all», got «${pagesRaw}»`);
87
+ const maxLinks = count("max-links", values["max-links"]);
88
+ const config = {
89
+ ...fileConfig,
90
+ lang,
91
+ ...(maxPages ? { maxPages } : {}),
92
+ ...(maxLinks ? { maxLinks } : {}),
93
+ ...(values.ai === false ? { ai: false } : {}),
94
+ };
95
+ // Progress goes to stderr, so `--json > report.json` stays clean.
96
+ const log = values.verbose ? (line) => process.stderr.write(`· ${line}\n`) : undefined;
97
+ let report;
98
+ try {
99
+ report = await auditSearch(url, { base: values.base, config, log });
100
+ }
101
+ catch (e) {
102
+ fail(e.message);
103
+ }
104
+ const markdown = toMarkdown(report);
105
+ process.stdout.write(values.json ? JSON.stringify(report, null, 2) + "\n" : markdown);
106
+ if (values.out)
107
+ writeFileSync(values.out, values.out.endsWith(".json") ? JSON.stringify(report, null, 2) : markdown);
108
+ if (process.env.GITHUB_STEP_SUMMARY)
109
+ appendFileSync(process.env.GITHUB_STEP_SUMMARY, markdown);
110
+ const failOn = values["fail-on"];
111
+ const failed = failOn === "never" ? false : failOn === "warn" ? report.counts.error + report.counts.warn > 0 : report.counts.error > 0;
112
+ process.exit(failed ? 1 : 0);
@@ -0,0 +1,14 @@
1
+ import { type Site } from "ai-search-auditor";
2
+ import type { SeoExtras, SeoSite } from "./types.ts";
3
+ export interface CollectOptions {
4
+ maxLinks: number;
5
+ concurrency?: number;
6
+ log?: (line: string) => void;
7
+ }
8
+ /**
9
+ * Extra requests for the SEO checks, in the same run as the shared crawl: a made-up URL that must answer 404,
10
+ * canonical targets outside the sample, internal links (capped by maxLinks) and, for a live https site,
11
+ * the home page over plain http://.
12
+ */
13
+ export declare function collect(site: Site, opts: CollectOptions): Promise<SeoSite>;
14
+ export declare const seoOf: (site: Site) => SeoExtras;
@@ -0,0 +1,74 @@
1
+ import { fetchUrl, isSameSite, pool } from "ai-search-auditor";
2
+ import { norm } from "./rules/util.js";
3
+ const probeOf = (f, from) => ({
4
+ status: f.status,
5
+ redirected: Boolean(f.redirected),
6
+ finalUrl: f.finalUrl ?? f.publicUrl,
7
+ ...(f.error ? { error: f.error } : {}),
8
+ ...(from ? { from } : {}),
9
+ });
10
+ /** Links worth requesting: pages, not files a crawler would not index anyway. */
11
+ const isPageLike = (url) => !/\.(png|jpe?g|gif|webp|avif|svg|ico|css|js|mjs|map|woff2?|ttf|otf|mp4|webm|mp3|zip|gz)(\?|$)/i.test(new URL(url).pathname);
12
+ /**
13
+ * Extra requests for the SEO checks, in the same run as the shared crawl: a made-up URL that must answer 404,
14
+ * canonical targets outside the sample, internal links (capped by maxLinks) and, for a live https site,
15
+ * the home page over plain http://.
16
+ */
17
+ export async function collect(site, opts) {
18
+ const o = { target: site.target, base: site.base };
19
+ const conc = opts.concurrency ?? 6;
20
+ const log = opts.log ?? (() => { });
21
+ // A local build (--base) or a dev server: HTTPS and the http:// redirect are the live site's business.
22
+ const local = site.target !== site.base || /^(localhost|127\.\d+\.\d+\.\d+|\[::1\])$/.test(new URL(site.base).hostname);
23
+ // Everything the crawl already requested, so nothing is fetched twice.
24
+ const known = new Map();
25
+ for (const p of site.pages)
26
+ known.set(norm(p.fetched.publicUrl), probeOf(p.fetched));
27
+ for (const f of site.failed)
28
+ known.set(norm(f.publicUrl), probeOf(f));
29
+ const canonicalTargets = [
30
+ ...new Set(site.pages
31
+ .map((p) => {
32
+ try {
33
+ return p.canonical ? new URL(p.canonical, p.fetched.publicUrl).href : "";
34
+ }
35
+ catch {
36
+ return "";
37
+ }
38
+ })
39
+ .filter((u) => u && isSameSite(u, o) && !known.has(norm(u)))),
40
+ ].slice(0, 20);
41
+ const firstSeen = new Map();
42
+ for (const p of site.pages)
43
+ for (const href of p.links)
44
+ if (isSameSite(href, o) && isPageLike(href) && !firstSeen.has(norm(href)))
45
+ firstSeen.set(norm(href), p.fetched.publicUrl);
46
+ const linkTargets = [...firstSeen.keys()].filter((u) => !known.has(u));
47
+ const toCheck = linkTargets.slice(0, opts.maxLinks);
48
+ log(`${canonicalTargets.length} canonical targets, ${toCheck.length} of ${firstSeen.size} internal links`);
49
+ const light = { maxBytes: 65_536, timeoutMs: 10_000 };
50
+ const [probe404, httpHome, canonicals, linked] = await Promise.all([
51
+ fetchUrl(`${site.target}/__search-auditor-${Math.random().toString(36).slice(2, 10)}`, o, light),
52
+ !local && site.base.startsWith("https://") ? fetchUrl(`http://${new URL(site.base).host}/`, o, light) : Promise.resolve(null),
53
+ pool(canonicalTargets, conc, (u) => fetchUrl(u, o, light)),
54
+ pool(toCheck, conc, (u) => fetchUrl(u, o, light)),
55
+ ]);
56
+ const links = new Map();
57
+ for (const [url, from] of firstSeen) {
58
+ const done = known.get(url);
59
+ if (done)
60
+ links.set(url, { ...done, from });
61
+ }
62
+ toCheck.forEach((url, i) => links.set(url, probeOf(linked[i], firstSeen.get(url))));
63
+ const seo = {
64
+ local,
65
+ probe404,
66
+ httpHome,
67
+ canonicals: new Map(canonicalTargets.map((u, i) => [norm(u), probeOf(canonicals[i])])),
68
+ links,
69
+ linksFound: firstSeen.size,
70
+ linksChecked: links.size,
71
+ };
72
+ return Object.assign(site, { seo });
73
+ }
74
+ export const seoOf = (site) => site.seo;
@@ -0,0 +1,194 @@
1
+ /**
2
+ * English texts of the SEO checks: the reference for every other language. A new language is a file typed as
3
+ * `SeoMessages` (TypeScript lists what is missing) plus a line in ./index.ts. AI search texts come from ai-search-auditor.
4
+ */
5
+ export declare const en: {
6
+ report: {
7
+ title: (site: string) => string;
8
+ clean: string;
9
+ footer: (pages: number, date: string) => string;
10
+ classic: string;
11
+ ai: string;
12
+ };
13
+ groups: {
14
+ IDX: string;
15
+ CAN: string;
16
+ MAP: string;
17
+ META: string;
18
+ STR: string;
19
+ LNK: string;
20
+ IMG: string;
21
+ DATA: string;
22
+ LANG: string;
23
+ SEC: string;
24
+ };
25
+ rules: {
26
+ "IDX-1": {
27
+ title: string;
28
+ fix: string;
29
+ impact: string;
30
+ };
31
+ "IDX-2": {
32
+ title: string;
33
+ fix: string;
34
+ impact: string;
35
+ };
36
+ "CAN-1": {
37
+ title: string;
38
+ fix: string;
39
+ impact: string;
40
+ };
41
+ "CAN-2": {
42
+ title: string;
43
+ fix: string;
44
+ impact: string;
45
+ };
46
+ "CAN-3": {
47
+ title: string;
48
+ fix: string;
49
+ impact: string;
50
+ };
51
+ "MAP-1": {
52
+ title: string;
53
+ fix: string;
54
+ impact: string;
55
+ };
56
+ "MAP-2": {
57
+ title: string;
58
+ fix: string;
59
+ impact: string;
60
+ };
61
+ "MAP-3": {
62
+ title: string;
63
+ fix: string;
64
+ impact: string;
65
+ };
66
+ "META-1": {
67
+ title: string;
68
+ fix: string;
69
+ impact: string;
70
+ };
71
+ "META-2": {
72
+ title: string;
73
+ fix: string;
74
+ impact: string;
75
+ };
76
+ "META-3": {
77
+ title: string;
78
+ fix: string;
79
+ impact: string;
80
+ };
81
+ "STR-1": {
82
+ title: string;
83
+ fix: string;
84
+ impact: string;
85
+ };
86
+ "STR-2": {
87
+ title: string;
88
+ fix: string;
89
+ impact: string;
90
+ };
91
+ "STR-3": {
92
+ title: string;
93
+ fix: string;
94
+ impact: string;
95
+ };
96
+ "STR-4": {
97
+ title: string;
98
+ fix: string;
99
+ impact: string;
100
+ };
101
+ "LNK-1": {
102
+ title: string;
103
+ fix: string;
104
+ impact: string;
105
+ };
106
+ "LNK-2": {
107
+ title: string;
108
+ fix: string;
109
+ impact: string;
110
+ };
111
+ "IMG-1": {
112
+ title: string;
113
+ fix: string;
114
+ impact: string;
115
+ };
116
+ "DATA-1": {
117
+ title: string;
118
+ fix: string;
119
+ impact: string;
120
+ };
121
+ "LANG-1": {
122
+ title: string;
123
+ fix: string;
124
+ impact: string;
125
+ };
126
+ "LANG-2": {
127
+ title: string;
128
+ fix: string;
129
+ impact: string;
130
+ };
131
+ "LANG-3": {
132
+ title: string;
133
+ fix: string;
134
+ impact: string;
135
+ };
136
+ "SEC-1": {
137
+ title: string;
138
+ fix: string;
139
+ impact: string;
140
+ };
141
+ "SEC-2": {
142
+ title: string;
143
+ fix: string;
144
+ impact: string;
145
+ };
146
+ };
147
+ /** Finding messages. URLs and quotes come in already formatted. */
148
+ m: {
149
+ status: (status: string) => string;
150
+ soft404Probe: (url: string) => string;
151
+ soft404Page: (title: string) => string;
152
+ noCanonical: string;
153
+ relativeCanonical: (href: string) => string;
154
+ foreignCanonical: (href: string) => string;
155
+ canonicalStatus: (href: string, status: string) => string;
156
+ canonicalRedirects: (href: string, to: string) => string;
157
+ noSitemap: string;
158
+ sitemapNotInRobots: string;
159
+ sitemapRedirects: (to: string) => string;
160
+ sitemapCanonicalElsewhere: (href: string) => string;
161
+ noTitle: string;
162
+ titleLength: (n: number, title: string) => string;
163
+ noDescription: string;
164
+ descriptionLength: (n: number) => string;
165
+ sameTitle: (n: number, title: string) => string;
166
+ sameDescription: (n: number, text: string) => string;
167
+ noH1: string;
168
+ manyH1: (n: number) => string;
169
+ skippedLevel: (from: number, to: number, text: string) => string;
170
+ noLang: string;
171
+ noViewport: string;
172
+ brokenLink: (status: string, from: string) => string;
173
+ redirectLink: (to: string, from: string) => string;
174
+ linksNotChecked: (checked: number, found: number) => string;
175
+ imagesNoAlt: (n: number, total: number, example: string) => string;
176
+ jsonLdError: (error: string) => string;
177
+ badHreflang: (code: string) => string;
178
+ oneWayHreflang: (lang: string, href: string) => string;
179
+ noXDefault: (langs: string) => string;
180
+ notHttps: string;
181
+ httpNoRedirect: (to: string) => string;
182
+ mixedContent: (n: number, example: string) => string;
183
+ skipLocal: string;
184
+ skipNoSitemap: string;
185
+ skipNoHreflang: string;
186
+ skipOneLanguage: string;
187
+ skipNoImages: string;
188
+ skipNoJsonLd: string;
189
+ skipNoLinks: string;
190
+ skipNotHttps: string;
191
+ };
192
+ };
193
+ export type SeoMessages = typeof en;
194
+ export type SeoRuleId = keyof SeoMessages["rules"];