search-auditor 0.1.1 → 0.2.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 +8 -0
- package/README.md +87 -38
- package/dist/cli.js +17 -9
- package/dist/collect.d.ts +5 -1
- package/dist/collect.js +186 -11
- package/dist/i18n/en.d.ts +462 -7
- package/dist/i18n/en.js +480 -25
- package/dist/i18n/index.d.ts +924 -14
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/ru.js +484 -24
- package/dist/index.d.ts +3 -145
- package/dist/index.js +14 -53
- package/dist/rules/can/can-1.d.ts +2 -0
- package/dist/rules/can/can-1.js +14 -0
- package/dist/rules/can/can-2.d.ts +2 -0
- package/dist/rules/can/can-2.js +20 -0
- package/dist/rules/can/can-3.d.ts +2 -0
- package/dist/rules/can/can-3.js +32 -0
- package/dist/rules/can/can-4.d.ts +2 -0
- package/dist/rules/can/can-4.js +18 -0
- package/dist/rules/can/can-5.d.ts +2 -0
- package/dist/rules/can/can-5.js +17 -0
- package/dist/rules/can/can-6.d.ts +2 -0
- package/dist/rules/can/can-6.js +22 -0
- package/dist/rules/can/can-7.d.ts +2 -0
- package/dist/rules/can/can-7.js +32 -0
- package/dist/rules/can/can-8.d.ts +2 -0
- package/dist/rules/can/can-8.js +23 -0
- package/dist/rules/can/can-9.d.ts +2 -0
- package/dist/rules/can/can-9.js +24 -0
- package/dist/rules/can/index.d.ts +7 -0
- package/dist/rules/can/index.js +10 -0
- package/dist/rules/can/shared.d.ts +11 -0
- package/dist/rules/can/shared.js +26 -0
- package/dist/rules/data/data-1.d.ts +2 -0
- package/dist/rules/data/data-1.js +16 -0
- package/dist/rules/data/data-2.d.ts +2 -0
- package/dist/rules/data/data-2.js +25 -0
- package/dist/rules/data/data-3.d.ts +2 -0
- package/dist/rules/data/data-3.js +29 -0
- package/dist/rules/data/data-4.d.ts +2 -0
- package/dist/rules/data/data-4.js +17 -0
- package/dist/rules/data/data-5.d.ts +2 -0
- package/dist/rules/data/data-5.js +28 -0
- package/dist/rules/data/data-6.d.ts +2 -0
- package/dist/rules/data/data-6.js +16 -0
- package/dist/rules/data/index.d.ts +8 -0
- package/dist/rules/data/index.js +7 -0
- package/dist/rules/data/shared.d.ts +22 -0
- package/dist/rules/data/shared.js +59 -0
- package/dist/rules/idx/idx-1.d.ts +2 -0
- package/dist/rules/idx/idx-1.js +15 -0
- package/dist/rules/idx/idx-2.d.ts +2 -0
- package/dist/rules/idx/idx-2.js +22 -0
- package/dist/rules/idx/idx-3.d.ts +2 -0
- package/dist/rules/idx/idx-3.js +35 -0
- package/dist/rules/idx/idx-4.d.ts +2 -0
- package/dist/rules/idx/idx-4.js +40 -0
- package/dist/rules/idx/idx-5.d.ts +2 -0
- package/dist/rules/idx/idx-5.js +24 -0
- package/dist/rules/idx/idx-6.d.ts +2 -0
- package/dist/rules/idx/idx-6.js +25 -0
- package/dist/rules/idx/idx-7.d.ts +2 -0
- package/dist/rules/idx/idx-7.js +29 -0
- package/dist/rules/idx/idx-8.d.ts +2 -0
- package/dist/rules/idx/idx-8.js +25 -0
- package/dist/rules/idx/idx-9.d.ts +2 -0
- package/dist/rules/idx/idx-9.js +22 -0
- package/dist/rules/idx/index.d.ts +7 -0
- package/dist/rules/idx/index.js +10 -0
- package/dist/rules/idx/shared.d.ts +18 -0
- package/dist/rules/idx/shared.js +18 -0
- package/dist/rules/img/img-1.d.ts +2 -0
- package/dist/rules/img/img-1.js +18 -0
- package/dist/rules/img/img-2.d.ts +2 -0
- package/dist/rules/img/img-2.js +20 -0
- package/dist/rules/img/img-3.d.ts +2 -0
- package/dist/rules/img/img-3.js +24 -0
- package/dist/rules/img/img-4.d.ts +2 -0
- package/dist/rules/img/img-4.js +19 -0
- package/dist/rules/img/img-5.d.ts +2 -0
- package/dist/rules/img/img-5.js +18 -0
- package/dist/rules/img/img-6.d.ts +2 -0
- package/dist/rules/img/img-6.js +20 -0
- package/dist/rules/img/img-7.d.ts +2 -0
- package/dist/rules/img/img-7.js +22 -0
- package/dist/rules/img/index.d.ts +7 -0
- package/dist/rules/img/index.js +8 -0
- package/dist/rules/img/shared.d.ts +26 -0
- package/dist/rules/img/shared.js +38 -0
- package/dist/rules/index.d.ts +1 -1
- package/dist/rules/index.js +23 -6
- package/dist/rules/lang/index.d.ts +8 -0
- package/dist/rules/lang/index.js +9 -0
- package/dist/rules/lang/lang-1.d.ts +2 -0
- package/dist/rules/lang/lang-1.js +17 -0
- package/dist/rules/lang/lang-2.d.ts +2 -0
- package/dist/rules/lang/lang-2.js +30 -0
- package/dist/rules/lang/lang-3.d.ts +2 -0
- package/dist/rules/lang/lang-3.js +22 -0
- package/dist/rules/lang/lang-4.d.ts +2 -0
- package/dist/rules/lang/lang-4.js +23 -0
- package/dist/rules/lang/lang-5.d.ts +2 -0
- package/dist/rules/lang/lang-5.js +33 -0
- package/dist/rules/lang/lang-6.d.ts +2 -0
- package/dist/rules/lang/lang-6.js +25 -0
- package/dist/rules/lang/lang-7.d.ts +2 -0
- package/dist/rules/lang/lang-7.js +25 -0
- package/dist/rules/lang/lang-8.d.ts +2 -0
- package/dist/rules/lang/lang-8.js +19 -0
- package/dist/rules/lang/shared.d.ts +7 -0
- package/dist/rules/lang/shared.js +5 -0
- package/dist/rules/lnk/index.d.ts +8 -0
- package/dist/rules/lnk/index.js +8 -0
- package/dist/rules/lnk/lnk-1.d.ts +2 -0
- package/dist/rules/lnk/lnk-1.js +22 -0
- package/dist/rules/lnk/lnk-2.d.ts +2 -0
- package/dist/rules/lnk/lnk-2.js +18 -0
- package/dist/rules/lnk/lnk-3.d.ts +2 -0
- package/dist/rules/lnk/lnk-3.js +32 -0
- package/dist/rules/lnk/lnk-4.d.ts +2 -0
- package/dist/rules/lnk/lnk-4.js +18 -0
- package/dist/rules/lnk/lnk-5.d.ts +2 -0
- package/dist/rules/lnk/lnk-5.js +22 -0
- package/dist/rules/lnk/lnk-6.d.ts +2 -0
- package/dist/rules/lnk/lnk-6.js +24 -0
- package/dist/rules/lnk/lnk-7.d.ts +2 -0
- package/dist/rules/lnk/lnk-7.js +26 -0
- package/dist/rules/lnk/shared.d.ts +20 -0
- package/dist/rules/lnk/shared.js +29 -0
- package/dist/rules/map/index.d.ts +7 -0
- package/dist/rules/map/index.js +9 -0
- package/dist/rules/map/map-1.d.ts +2 -0
- package/dist/rules/map/map-1.js +15 -0
- package/dist/rules/map/map-2.d.ts +2 -0
- package/dist/rules/map/map-2.js +26 -0
- package/dist/rules/map/map-3.d.ts +2 -0
- package/dist/rules/map/map-3.js +24 -0
- package/dist/rules/map/map-4.d.ts +2 -0
- package/dist/rules/map/map-4.js +34 -0
- package/dist/rules/map/map-5.d.ts +2 -0
- package/dist/rules/map/map-5.js +28 -0
- package/dist/rules/map/map-6.d.ts +2 -0
- package/dist/rules/map/map-6.js +22 -0
- package/dist/rules/map/map-7.d.ts +2 -0
- package/dist/rules/map/map-7.js +18 -0
- package/dist/rules/map/map-8.d.ts +2 -0
- package/dist/rules/map/map-8.js +31 -0
- package/dist/rules/map/shared.d.ts +14 -0
- package/dist/rules/map/shared.js +12 -0
- package/dist/rules/meta/index.d.ts +8 -0
- package/dist/rules/meta/index.js +10 -0
- package/dist/rules/meta/meta-1.d.ts +2 -0
- package/dist/rules/meta/meta-1.js +20 -0
- package/dist/rules/meta/meta-2.d.ts +2 -0
- package/dist/rules/meta/meta-2.js +19 -0
- package/dist/rules/meta/meta-3.d.ts +2 -0
- package/dist/rules/meta/meta-3.js +26 -0
- package/dist/rules/meta/meta-4.d.ts +2 -0
- package/dist/rules/meta/meta-4.js +22 -0
- package/dist/rules/meta/meta-5.d.ts +2 -0
- package/dist/rules/meta/meta-5.js +17 -0
- package/dist/rules/meta/meta-6.d.ts +2 -0
- package/dist/rules/meta/meta-6.js +18 -0
- package/dist/rules/meta/meta-7.d.ts +2 -0
- package/dist/rules/meta/meta-7.js +18 -0
- package/dist/rules/meta/meta-8.d.ts +2 -0
- package/dist/rules/meta/meta-8.js +17 -0
- package/dist/rules/meta/meta-9.d.ts +2 -0
- package/dist/rules/meta/meta-9.js +16 -0
- package/dist/rules/meta/shared.d.ts +15 -0
- package/dist/rules/meta/shared.js +9 -0
- package/dist/rules/sec/index.d.ts +8 -0
- package/dist/rules/sec/index.js +8 -0
- package/dist/rules/sec/sec-1.d.ts +2 -0
- package/dist/rules/sec/sec-1.js +21 -0
- package/dist/rules/sec/sec-2.d.ts +2 -0
- package/dist/rules/sec/sec-2.js +17 -0
- package/dist/rules/sec/sec-3.d.ts +2 -0
- package/dist/rules/sec/sec-3.js +18 -0
- package/dist/rules/sec/sec-4.d.ts +2 -0
- package/dist/rules/sec/sec-4.js +23 -0
- package/dist/rules/sec/sec-5.d.ts +2 -0
- package/dist/rules/sec/sec-5.js +23 -0
- package/dist/rules/sec/sec-6.d.ts +2 -0
- package/dist/rules/sec/sec-6.js +32 -0
- package/dist/rules/sec/sec-7.d.ts +2 -0
- package/dist/rules/sec/sec-7.js +26 -0
- package/dist/rules/sec/shared.d.ts +10 -0
- package/dist/rules/sec/shared.js +8 -0
- package/dist/rules/str/index.d.ts +8 -0
- package/dist/rules/str/index.js +11 -0
- package/dist/rules/str/shared.d.ts +14 -0
- package/dist/rules/str/shared.js +16 -0
- package/dist/rules/str/str-1.d.ts +2 -0
- package/dist/rules/str/str-1.js +17 -0
- package/dist/rules/str/str-10.d.ts +2 -0
- package/dist/rules/str/str-10.js +15 -0
- package/dist/rules/str/str-2.d.ts +2 -0
- package/dist/rules/str/str-2.js +19 -0
- package/dist/rules/str/str-3.d.ts +2 -0
- package/dist/rules/str/str-3.js +14 -0
- package/dist/rules/str/str-4.d.ts +2 -0
- package/dist/rules/str/str-4.js +14 -0
- package/dist/rules/str/str-5.d.ts +2 -0
- package/dist/rules/str/str-5.js +19 -0
- package/dist/rules/str/str-6.d.ts +2 -0
- package/dist/rules/str/str-6.js +22 -0
- package/dist/rules/str/str-7.d.ts +2 -0
- package/dist/rules/str/str-7.js +18 -0
- package/dist/rules/str/str-8.d.ts +2 -0
- package/dist/rules/str/str-8.js +24 -0
- package/dist/rules/str/str-9.d.ts +2 -0
- package/dist/rules/str/str-9.js +14 -0
- package/dist/rules/util.d.ts +1 -1
- package/dist/trace.d.ts +20 -0
- package/dist/trace.js +77 -0
- package/dist/types.d.ts +86 -6
- package/package.json +4 -9
- package/dist/rules/links.d.ts +0 -2
- package/dist/rules/links.js +0 -95
- package/dist/rules/page.d.ts +0 -2
- package/dist/rules/page.js +0 -145
- package/dist/rules/site.d.ts +0 -2
- package/dist/rules/site.js +0 -177
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** IMG-4: Photos are served in modern formats. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { imgs, NEGOTIATED } from "./shared.js";
|
|
4
|
+
export const img4 = {
|
|
5
|
+
id: "IMG-4",
|
|
6
|
+
group: "IMG",
|
|
7
|
+
level: "info",
|
|
8
|
+
applies: (site, t) => (site.pages.some((p) => p.images.length) ? null : t.skipNoImages),
|
|
9
|
+
run(site, _c, t) {
|
|
10
|
+
const { out, add } = finder(this);
|
|
11
|
+
for (const p of site.pages) {
|
|
12
|
+
// JPEG and PNG with no WebP or AVIF next to them; a <picture> with a modern source or an image CDN is fine.
|
|
13
|
+
const old = imgs(p.fetched.body).filter((i) => /\.(jpe?g|png)(\?|#|$)/i.test(i.src) && !i.modernSource && !NEGOTIATED.test(i.src));
|
|
14
|
+
if (old.length)
|
|
15
|
+
add(p.fetched.publicUrl, t.oldFormats(old.length, old[0].src));
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** IMG-5: Images open. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { seoOf } from "../../collect.js";
|
|
4
|
+
import { rel, statusOf } from "../util.js";
|
|
5
|
+
export const img5 = {
|
|
6
|
+
id: "IMG-5",
|
|
7
|
+
group: "IMG",
|
|
8
|
+
level: "error",
|
|
9
|
+
applies: (site, t) => (seoOf(site).images.size ? null : t.skipNoOwnImages),
|
|
10
|
+
run(site, _c, t) {
|
|
11
|
+
const { out, add } = finder(this);
|
|
12
|
+
for (const [url, p] of seoOf(site).images)
|
|
13
|
+
if (p.status === 0 || p.status >= 400)
|
|
14
|
+
for (const page of p.from ?? [])
|
|
15
|
+
add(page, t.brokenImage(rel(url, site.base), statusOf(p)));
|
|
16
|
+
return out;
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** IMG-6: Images are not heavier than they need to be. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { seoOf } from "../../collect.js";
|
|
4
|
+
import { rel } from "../util.js";
|
|
5
|
+
import { MAX_IMAGE_BYTES } from "./shared.js";
|
|
6
|
+
export const img6 = {
|
|
7
|
+
id: "IMG-6",
|
|
8
|
+
group: "IMG",
|
|
9
|
+
level: "warn",
|
|
10
|
+
applies: (site, t) => (seoOf(site).images.size ? null : t.skipNoOwnImages),
|
|
11
|
+
run(site, _c, t) {
|
|
12
|
+
const { out, add } = finder(this);
|
|
13
|
+
// By Content-Length: a server that does not send it is not guessed at.
|
|
14
|
+
for (const [url, p] of seoOf(site).images)
|
|
15
|
+
if (p.status === 200 && (p.bytes ?? 0) > MAX_IMAGE_BYTES)
|
|
16
|
+
for (const page of p.from ?? [])
|
|
17
|
+
add(page, t.heavyImage(rel(url, site.base), Math.round(p.bytes / 1024), MAX_IMAGE_BYTES / 1024));
|
|
18
|
+
return out;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** IMG-7: Alt text describes the image. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { short } from "../util.js";
|
|
4
|
+
import { imgs, MAX_ALT, POOR_ALT } from "./shared.js";
|
|
5
|
+
export const img7 = {
|
|
6
|
+
id: "IMG-7",
|
|
7
|
+
group: "IMG",
|
|
8
|
+
level: "info",
|
|
9
|
+
applies: (site, t) => (site.pages.some((p) => p.images.length) ? null : t.skipNoImages),
|
|
10
|
+
run(site, _c, t) {
|
|
11
|
+
const { out, add } = finder(this);
|
|
12
|
+
for (const p of site.pages) {
|
|
13
|
+
// An empty alt is a deliberate «decorative»; a file name, «image» or a whole paragraph is not a description.
|
|
14
|
+
const poor = imgs(p.fetched.body)
|
|
15
|
+
.map((i) => (i.alt ?? "").trim())
|
|
16
|
+
.filter((alt) => alt && (POOR_ALT.test(alt) || alt.length > MAX_ALT));
|
|
17
|
+
if (poor.length)
|
|
18
|
+
add(p.fetched.publicUrl, t.poorAlt(poor.length, short(poor[0], 40)));
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Images (IMG): can image search and screen readers tell what an image shows, and do images keep the page fast.
|
|
3
|
+
* Alt text on every image that describes it, width and height so the layout does not jump, lazy loading below the
|
|
4
|
+
* first screen but not for the first image, modern formats, images that open, and none heavier than it needs to be.
|
|
5
|
+
*/
|
|
6
|
+
import type { SeoRule } from "../util.ts";
|
|
7
|
+
export declare const imgRules: SeoRule[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { img1 } from "./img-1.js";
|
|
2
|
+
import { img2 } from "./img-2.js";
|
|
3
|
+
import { img3 } from "./img-3.js";
|
|
4
|
+
import { img4 } from "./img-4.js";
|
|
5
|
+
import { img5 } from "./img-5.js";
|
|
6
|
+
import { img6 } from "./img-6.js";
|
|
7
|
+
import { img7 } from "./img-7.js";
|
|
8
|
+
export const imgRules = [img1, img2, img3, img4, img5, img6, img7];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** One <img> of the raw HTML, with the attributes the checks need. */
|
|
2
|
+
export interface Img {
|
|
3
|
+
src: string;
|
|
4
|
+
alt: string | null;
|
|
5
|
+
width: string | null;
|
|
6
|
+
height: string | null;
|
|
7
|
+
loading: string;
|
|
8
|
+
/** Inside a <picture> that offers WebP or AVIF. */
|
|
9
|
+
modernSource: boolean;
|
|
10
|
+
/** Characters of text before the image: a rough stand-in for «is it on the first screen». */
|
|
11
|
+
textBefore: number;
|
|
12
|
+
}
|
|
13
|
+
/** Every <img> in document order, and whether a <picture> around it offers a modern format. */
|
|
14
|
+
export declare function imgs(html: string): Img[];
|
|
15
|
+
/** An image heavier than this slows the page more than it needs to. */
|
|
16
|
+
export declare const MAX_IMAGE_BYTES: number;
|
|
17
|
+
/** More text than this before an image and it is below the first screen, where lazy loading is right. */
|
|
18
|
+
export declare const FIRST_SCREEN_TEXT = 1000;
|
|
19
|
+
/** From this many images on a page, the ones below the first screen should load lazily. */
|
|
20
|
+
export declare const LAZY_FROM = 5;
|
|
21
|
+
/** Alt text longer than this is a paragraph, not a description: screen readers read it in full. */
|
|
22
|
+
export declare const MAX_ALT = 150;
|
|
23
|
+
/** URLs that already serve WebP or AVIF to browsers that take it: image CDNs and framework image endpoints. */
|
|
24
|
+
export declare const NEGOTIATED: RegExp;
|
|
25
|
+
/** Alt text that names a file or a kind of thing, not what the image shows. */
|
|
26
|
+
export declare const POOR_ALT: RegExp;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Constants and helpers shared by the IMG checks. */
|
|
2
|
+
import { decode, textContent } from "auditor-engine";
|
|
3
|
+
const attr = (tag, name) => {
|
|
4
|
+
const m = tag.match(new RegExp(`\\s${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i"));
|
|
5
|
+
return m ? (m[1] ?? m[2] ?? m[3] ?? "") : null;
|
|
6
|
+
};
|
|
7
|
+
/** Every <img> in document order, and whether a <picture> around it offers a modern format. */
|
|
8
|
+
export function imgs(html) {
|
|
9
|
+
const pictures = [...html.matchAll(/<picture\b[\s\S]*?<\/picture>/gi)].map((m) => ({
|
|
10
|
+
start: m.index,
|
|
11
|
+
end: m.index + m[0].length,
|
|
12
|
+
modern: /<source\b[^>]*type\s*=\s*["']?image\/(webp|avif)/i.test(m[0]),
|
|
13
|
+
}));
|
|
14
|
+
return [...html.matchAll(/<img\b[^>]*>/gi)].map((m) => {
|
|
15
|
+
const tag = m[0];
|
|
16
|
+
return {
|
|
17
|
+
src: decode(attr(tag, "src") ?? attr(tag, "data-src") ?? ""),
|
|
18
|
+
alt: attr(tag, "alt") === null ? null : decode(attr(tag, "alt")),
|
|
19
|
+
width: attr(tag, "width"),
|
|
20
|
+
height: attr(tag, "height"),
|
|
21
|
+
loading: (attr(tag, "loading") ?? "").toLowerCase(),
|
|
22
|
+
modernSource: pictures.some((p) => p.modern && m.index > p.start && m.index < p.end),
|
|
23
|
+
textBefore: textContent(html.slice(0, m.index).replace(/<(script|style|head)\b[\s\S]*?<\/\1>/gi, "")).replace(/\s+/g, " ").trim().length,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** An image heavier than this slows the page more than it needs to. */
|
|
28
|
+
export const MAX_IMAGE_BYTES = 500 * 1024;
|
|
29
|
+
/** More text than this before an image and it is below the first screen, where lazy loading is right. */
|
|
30
|
+
export const FIRST_SCREEN_TEXT = 1000;
|
|
31
|
+
/** From this many images on a page, the ones below the first screen should load lazily. */
|
|
32
|
+
export const LAZY_FROM = 5;
|
|
33
|
+
/** Alt text longer than this is a paragraph, not a description: screen readers read it in full. */
|
|
34
|
+
export const MAX_ALT = 150;
|
|
35
|
+
/** URLs that already serve WebP or AVIF to browsers that take it: image CDNs and framework image endpoints. */
|
|
36
|
+
export const NEGOTIATED = /\/_next\/image|\/_vercel\/image|\/cdn-cgi\/image\/|[?&](fm|format|f)=(webp|avif|auto)|[,/]f_auto|\/image\/upload\//i;
|
|
37
|
+
/** Alt text that names a file or a kind of thing, not what the image shows. */
|
|
38
|
+
export const POOR_ALT = /^(img|image|dsc|dscn|photo|pic|picture|screenshot|screen shot|снимок|фото|изображение|картинка|рисунок)?[\s_-]*\d*(\.(jpe?g|png|webp|gif|svg|avif))?$/i;
|
package/dist/rules/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SeoRule } from "./util.ts";
|
|
2
2
|
export type { SeoRule };
|
|
3
|
-
/** Group ids in checklist order: the order a site owner fixes things in. */
|
|
3
|
+
/** Group ids in checklist order: the order a site owner fixes things in. One module per group, named after it. */
|
|
4
4
|
export declare const SEO_GROUPS: readonly ["IDX", "SEC", "CAN", "MAP", "META", "STR", "LNK", "IMG", "DATA", "LANG"];
|
|
5
5
|
export declare const seoRules: SeoRule[];
|
package/dist/rules/index.js
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { canRules } from "./can/index.js";
|
|
2
|
+
import { dataRules } from "./data/index.js";
|
|
3
|
+
import { idxRules } from "./idx/index.js";
|
|
4
|
+
import { imgRules } from "./img/index.js";
|
|
5
|
+
import { langRules } from "./lang/index.js";
|
|
6
|
+
import { lnkRules } from "./lnk/index.js";
|
|
7
|
+
import { mapRules } from "./map/index.js";
|
|
8
|
+
import { metaRules } from "./meta/index.js";
|
|
9
|
+
import { secRules } from "./sec/index.js";
|
|
10
|
+
import { strRules } from "./str/index.js";
|
|
11
|
+
/** Group ids in checklist order: the order a site owner fixes things in. One module per group, named after it. */
|
|
5
12
|
export const SEO_GROUPS = ["IDX", "SEC", "CAN", "MAP", "META", "STR", "LNK", "IMG", "DATA", "LANG"];
|
|
6
|
-
const
|
|
7
|
-
|
|
13
|
+
export const seoRules = [
|
|
14
|
+
...idxRules,
|
|
15
|
+
...secRules,
|
|
16
|
+
...canRules,
|
|
17
|
+
...mapRules,
|
|
18
|
+
...metaRules,
|
|
19
|
+
...strRules,
|
|
20
|
+
...lnkRules,
|
|
21
|
+
...imgRules,
|
|
22
|
+
...dataRules,
|
|
23
|
+
...langRules,
|
|
24
|
+
];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language versions (LANG): hreflang for multilingual sites, so search engines show each visitor their language.
|
|
3
|
+
* Valid codes, each language once, full addresses, every version listing itself and the others back, an x-default,
|
|
4
|
+
* targets that are working canonical pages, and a hreflang language that matches the page it points to.
|
|
5
|
+
* Only on sites that use hreflang.
|
|
6
|
+
*/
|
|
7
|
+
import type { SeoRule } from "../util.ts";
|
|
8
|
+
export declare const langRules: SeoRule[];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { lang1 } from "./lang-1.js";
|
|
2
|
+
import { lang2 } from "./lang-2.js";
|
|
3
|
+
import { lang3 } from "./lang-3.js";
|
|
4
|
+
import { lang4 } from "./lang-4.js";
|
|
5
|
+
import { lang5 } from "./lang-5.js";
|
|
6
|
+
import { lang6 } from "./lang-6.js";
|
|
7
|
+
import { lang7 } from "./lang-7.js";
|
|
8
|
+
import { lang8 } from "./lang-8.js";
|
|
9
|
+
export const langRules = [lang1, lang2, lang3, lang4, lang5, lang6, lang7, lang8];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** LANG-1: hreflang codes are valid. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { HREFLANG, hasHreflang } from "./shared.js";
|
|
4
|
+
export const lang1 = {
|
|
5
|
+
id: "LANG-1",
|
|
6
|
+
group: "LANG",
|
|
7
|
+
level: "warn",
|
|
8
|
+
applies: (site, t) => (hasHreflang(site) ? null : t.skipNoHreflang),
|
|
9
|
+
run(site, _c, t) {
|
|
10
|
+
const { out, add } = finder(this);
|
|
11
|
+
for (const p of site.pages)
|
|
12
|
+
for (const h of p.hreflang)
|
|
13
|
+
if (!HREFLANG.test(h.lang))
|
|
14
|
+
add(p.fetched.publicUrl, t.badHreflang(h.lang));
|
|
15
|
+
return out;
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** LANG-2: Language versions point at each other. */
|
|
2
|
+
import { finder, isSameSite } from "auditor-engine";
|
|
3
|
+
import { absolute, norm, rel } from "../util.js";
|
|
4
|
+
import { hasHreflang } from "./shared.js";
|
|
5
|
+
export const lang2 = {
|
|
6
|
+
id: "LANG-2",
|
|
7
|
+
group: "LANG",
|
|
8
|
+
level: "warn",
|
|
9
|
+
applies: (site, t) => (hasHreflang(site) ? null : t.skipNoHreflang),
|
|
10
|
+
run(site, _c, t) {
|
|
11
|
+
const { out, add } = finder(this);
|
|
12
|
+
const o = { target: site.target, base: site.base };
|
|
13
|
+
// Only pairs where both pages were audited: a translation outside the sample is not known to be one-way.
|
|
14
|
+
const audited = new Map(site.pages.map((p) => [norm(p.fetched.publicUrl), p]));
|
|
15
|
+
for (const p of site.pages) {
|
|
16
|
+
const self = norm(p.fetched.publicUrl);
|
|
17
|
+
for (const h of p.hreflang) {
|
|
18
|
+
if (h.lang.toLowerCase() === "x-default")
|
|
19
|
+
continue;
|
|
20
|
+
const href = absolute(h.href, p.fetched.publicUrl);
|
|
21
|
+
if (!href || !isSameSite(href, o) || norm(href) === self)
|
|
22
|
+
continue;
|
|
23
|
+
const other = audited.get(norm(href));
|
|
24
|
+
if (other && !other.hreflang.some((b) => norm(absolute(b.href, other.fetched.publicUrl)) === self))
|
|
25
|
+
add(p.fetched.publicUrl, t.oneWayHreflang(h.lang, rel(href, site.base)));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** LANG-3: There is an x-default for other languages. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { hasHreflang } from "./shared.js";
|
|
4
|
+
export const lang3 = {
|
|
5
|
+
id: "LANG-3",
|
|
6
|
+
group: "LANG",
|
|
7
|
+
level: "info",
|
|
8
|
+
applies(site, t) {
|
|
9
|
+
if (!hasHreflang(site))
|
|
10
|
+
return t.skipNoHreflang;
|
|
11
|
+
return site.pages.some((p) => new Set(p.hreflang.filter((h) => h.lang.toLowerCase() !== "x-default").map((h) => h.lang.toLowerCase())).size > 1) ? null : t.skipOneLanguage;
|
|
12
|
+
},
|
|
13
|
+
run(site, _c, t) {
|
|
14
|
+
const { out, add } = finder(this);
|
|
15
|
+
for (const p of site.pages) {
|
|
16
|
+
const langs = [...new Set(p.hreflang.map((h) => h.lang.toLowerCase()))];
|
|
17
|
+
if (langs.length > 1 && !langs.includes("x-default"))
|
|
18
|
+
add(p.fetched.publicUrl, t.noXDefault(langs.join(", ")));
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** LANG-4: Every version lists itself in hreflang. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { absolute, norm } from "../util.js";
|
|
4
|
+
import { hasHreflang } from "./shared.js";
|
|
5
|
+
export const lang4 = {
|
|
6
|
+
id: "LANG-4",
|
|
7
|
+
group: "LANG",
|
|
8
|
+
level: "warn",
|
|
9
|
+
applies: (site, t) => (hasHreflang(site) ? null : t.skipNoHreflang),
|
|
10
|
+
run(site, _c, t) {
|
|
11
|
+
const { out, add } = finder(this);
|
|
12
|
+
// Google wants each version in its own list: a set that leaves the page out may be ignored.
|
|
13
|
+
for (const p of site.pages) {
|
|
14
|
+
if (!p.hreflang.length)
|
|
15
|
+
continue;
|
|
16
|
+
const self = norm(p.fetched.publicUrl);
|
|
17
|
+
const lists = p.hreflang.some((h) => h.lang.toLowerCase() !== "x-default" && norm(absolute(h.href, p.fetched.publicUrl)) === self);
|
|
18
|
+
if (!lists)
|
|
19
|
+
add(p.fetched.publicUrl, t.noSelfHreflang);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** LANG-5: hreflang leads to working canonical pages. */
|
|
2
|
+
import { finder, isSameSite } from "auditor-engine";
|
|
3
|
+
import { absolute, norm, rel, statusOf } from "../util.js";
|
|
4
|
+
import { hasHreflang } from "./shared.js";
|
|
5
|
+
export const lang5 = {
|
|
6
|
+
id: "LANG-5",
|
|
7
|
+
group: "LANG",
|
|
8
|
+
level: "warn",
|
|
9
|
+
applies: (site, t) => (hasHreflang(site) ? null : t.skipNoHreflang),
|
|
10
|
+
run(site, _c, t) {
|
|
11
|
+
const { out, add } = finder(this);
|
|
12
|
+
const o = { target: site.target, base: site.base };
|
|
13
|
+
// What is known about each address: audited pages, and those that failed. Others are not guessed at.
|
|
14
|
+
const pages = new Map(site.pages.map((p) => [norm(p.fetched.publicUrl), p]));
|
|
15
|
+
const failed = new Map(site.failed.map((f) => [norm(f.publicUrl), f]));
|
|
16
|
+
for (const p of site.pages)
|
|
17
|
+
for (const h of p.hreflang) {
|
|
18
|
+
const href = absolute(h.href, p.fetched.publicUrl);
|
|
19
|
+
if (!href || !isSameSite(href, o))
|
|
20
|
+
continue;
|
|
21
|
+
const where = rel(href, site.base);
|
|
22
|
+
const bad = failed.get(norm(href));
|
|
23
|
+
const target = pages.get(norm(href));
|
|
24
|
+
if (bad)
|
|
25
|
+
add(p.fetched.publicUrl, t.hreflangStatus(h.lang, where, statusOf(bad)));
|
|
26
|
+
else if (target?.fetched.redirected && target.fetched.finalUrl && norm(target.fetched.finalUrl) !== norm(href))
|
|
27
|
+
add(p.fetched.publicUrl, t.hreflangRedirects(h.lang, where, rel(target.fetched.finalUrl, site.base)));
|
|
28
|
+
else if (target?.canonical && norm(absolute(target.canonical, href)) !== norm(href))
|
|
29
|
+
add(p.fetched.publicUrl, t.hreflangNotCanonical(h.lang, where, rel(absolute(target.canonical, href), site.base)));
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** LANG-6: The hreflang language matches the language of the page it points to. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { absolute, norm, rel } from "../util.js";
|
|
4
|
+
import { hasHreflang, primary } from "./shared.js";
|
|
5
|
+
export const lang6 = {
|
|
6
|
+
id: "LANG-6",
|
|
7
|
+
group: "LANG",
|
|
8
|
+
level: "warn",
|
|
9
|
+
applies: (site, t) => (hasHreflang(site) ? null : t.skipNoHreflang),
|
|
10
|
+
run(site, _c, t) {
|
|
11
|
+
const { out, add } = finder(this);
|
|
12
|
+
const pages = new Map(site.pages.map((p) => [norm(p.fetched.publicUrl), p]));
|
|
13
|
+
for (const p of site.pages)
|
|
14
|
+
for (const h of p.hreflang) {
|
|
15
|
+
if (h.lang.toLowerCase() === "x-default")
|
|
16
|
+
continue;
|
|
17
|
+
const target = pages.get(norm(absolute(h.href, p.fetched.publicUrl)));
|
|
18
|
+
const declared = target ? primary(target.htmlLang) : "";
|
|
19
|
+
// Compared by language only: hreflang="en-GB" on a page with lang="en" is fine.
|
|
20
|
+
if (target && declared && declared !== primary(h.lang))
|
|
21
|
+
add(p.fetched.publicUrl, t.hreflangLangMismatch(h.lang, rel(target.fetched.publicUrl, site.base), target.htmlLang));
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** LANG-7: Each language is listed once. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { absolute, norm } from "../util.js";
|
|
4
|
+
import { hasHreflang } from "./shared.js";
|
|
5
|
+
export const lang7 = {
|
|
6
|
+
id: "LANG-7",
|
|
7
|
+
group: "LANG",
|
|
8
|
+
level: "warn",
|
|
9
|
+
applies: (site, t) => (hasHreflang(site) ? null : t.skipNoHreflang),
|
|
10
|
+
run(site, _c, t) {
|
|
11
|
+
const { out, add } = finder(this);
|
|
12
|
+
for (const p of site.pages) {
|
|
13
|
+
// The same code for two different addresses: search engines cannot tell which one to show and may drop both.
|
|
14
|
+
const byCode = new Map();
|
|
15
|
+
for (const h of p.hreflang) {
|
|
16
|
+
const code = h.lang.toLowerCase();
|
|
17
|
+
byCode.set(code, new Set([...(byCode.get(code) ?? []), norm(absolute(h.href, p.fetched.publicUrl))]));
|
|
18
|
+
}
|
|
19
|
+
const twice = [...byCode].filter(([, urls]) => urls.size > 1).map(([code]) => code);
|
|
20
|
+
if (twice.length)
|
|
21
|
+
add(p.fetched.publicUrl, t.duplicateHreflang(twice.join(", ")));
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** LANG-8: hreflang addresses are full. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { hasHreflang } from "./shared.js";
|
|
4
|
+
export const lang8 = {
|
|
5
|
+
id: "LANG-8",
|
|
6
|
+
group: "LANG",
|
|
7
|
+
level: "warn",
|
|
8
|
+
applies: (site, t) => (hasHreflang(site) ? null : t.skipNoHreflang),
|
|
9
|
+
run(site, _c, t) {
|
|
10
|
+
const { out, add } = finder(this);
|
|
11
|
+
// Google requires fully qualified addresses in hreflang: «/ru/» is not one.
|
|
12
|
+
for (const p of site.pages) {
|
|
13
|
+
const relative = p.hreflang.filter((h) => !/^https?:\/\//i.test(h.href.trim()));
|
|
14
|
+
if (relative.length)
|
|
15
|
+
add(p.fetched.publicUrl, t.relativeHreflang(relative.length, relative[0].href));
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Constants and helpers shared by the LANG checks. */
|
|
2
|
+
import type { SeoRule } from "../util.ts";
|
|
3
|
+
/** ISO 639-1 language, optional ISO 15924 script and region (ISO 3166-1 or UN M.49), or x-default. */
|
|
4
|
+
export declare const HREFLANG: RegExp;
|
|
5
|
+
export declare const hasHreflang: (site: Parameters<SeoRule["run"]>[0]) => boolean;
|
|
6
|
+
/** The language of a code or of <html lang>, without script and region: «en» for en-GB, «zh» for zh-Hant. */
|
|
7
|
+
export declare const primary: (code: string) => string;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** ISO 639-1 language, optional ISO 15924 script and region (ISO 3166-1 or UN M.49), or x-default. */
|
|
2
|
+
export const HREFLANG = /^(x-default|[a-z]{2,3}(-[a-z]{4})?(-([a-z]{2}|\d{3}))?)$/i;
|
|
3
|
+
export const hasHreflang = (site) => site.pages.some((p) => p.hreflang.length);
|
|
4
|
+
/** The language of a code or of <html lang>, without script and region: «en» for en-GB, «zh» for zh-Hant. */
|
|
5
|
+
export const primary = (code) => code.trim().toLowerCase().split(/[-_]/)[0] ?? "";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Links (LNK): how the pages link to each other, and whether those links work for people and crawlers.
|
|
3
|
+
* No broken links and no links through redirects; every page linked from another (on a full audit); links a crawler
|
|
4
|
+
* can follow, without nofollow on the site's own pages, with text that says where they go; and links to other sites
|
|
5
|
+
* that still exist. Problems are reported on the page that has the link: that is where they get fixed.
|
|
6
|
+
*/
|
|
7
|
+
import type { SeoRule } from "../util.ts";
|
|
8
|
+
export declare const lnkRules: SeoRule[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { lnk1 } from "./lnk-1.js";
|
|
2
|
+
import { lnk2 } from "./lnk-2.js";
|
|
3
|
+
import { lnk3 } from "./lnk-3.js";
|
|
4
|
+
import { lnk4 } from "./lnk-4.js";
|
|
5
|
+
import { lnk5 } from "./lnk-5.js";
|
|
6
|
+
import { lnk6 } from "./lnk-6.js";
|
|
7
|
+
import { lnk7 } from "./lnk-7.js";
|
|
8
|
+
export const lnkRules = [lnk1, lnk2, lnk3, lnk4, lnk5, lnk6, lnk7];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** LNK-1: Internal links work. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { seoOf } from "../../collect.js";
|
|
4
|
+
import { rel, statusOf } from "../util.js";
|
|
5
|
+
export const lnk1 = {
|
|
6
|
+
id: "LNK-1",
|
|
7
|
+
group: "LNK",
|
|
8
|
+
level: "error",
|
|
9
|
+
applies: (site, t) => (seoOf(site).linksFound ? null : t.skipNoLinks),
|
|
10
|
+
run(site, _c, t) {
|
|
11
|
+
const { out, add } = finder(this);
|
|
12
|
+
const { links, linksFound, linksChecked } = seoOf(site);
|
|
13
|
+
// Reported on the pages that have the link: that is where it gets fixed.
|
|
14
|
+
for (const [url, p] of links)
|
|
15
|
+
if (p.status === 0 || p.status >= 400)
|
|
16
|
+
for (const page of p.from ?? [site.base])
|
|
17
|
+
add(page, t.brokenLink(rel(url, site.base), statusOf(p)));
|
|
18
|
+
if (linksChecked < linksFound)
|
|
19
|
+
add("site", t.linksNotChecked(linksChecked, linksFound), "info");
|
|
20
|
+
return out;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** LNK-2: Internal links do not go through redirects. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { seoOf } from "../../collect.js";
|
|
4
|
+
import { norm, rel } from "../util.js";
|
|
5
|
+
export const lnk2 = {
|
|
6
|
+
id: "LNK-2",
|
|
7
|
+
group: "LNK",
|
|
8
|
+
level: "warn",
|
|
9
|
+
applies: (site, t) => (seoOf(site).linksFound ? null : t.skipNoLinks),
|
|
10
|
+
run(site, _c, t) {
|
|
11
|
+
const { out, add } = finder(this);
|
|
12
|
+
for (const [url, p] of seoOf(site).links)
|
|
13
|
+
if (p.status > 0 && p.status < 400 && p.redirected && norm(p.finalUrl) !== norm(url))
|
|
14
|
+
for (const page of p.from ?? [site.base])
|
|
15
|
+
add(page, t.redirectLink(rel(url, site.base), rel(p.finalUrl, site.base)));
|
|
16
|
+
return out;
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** LNK-3: Every page is linked from another page. */
|
|
2
|
+
import { finder, isSameSite } from "auditor-engine";
|
|
3
|
+
import { norm } from "../util.js";
|
|
4
|
+
/** Every same-site sitemap address was audited: only then is «no page links here» a fact, not a gap in the sample. */
|
|
5
|
+
const fullyAudited = (site) => {
|
|
6
|
+
const o = { target: site.target, base: site.base };
|
|
7
|
+
const audited = new Set([...site.pages.map((p) => p.fetched.publicUrl), ...site.failed.map((f) => f.publicUrl)].map(norm));
|
|
8
|
+
const listed = (site.sitemap?.urls ?? []).map((u) => u.loc).filter((u) => isSameSite(u, o));
|
|
9
|
+
return listed.length > 0 && listed.every((u) => audited.has(norm(u)));
|
|
10
|
+
};
|
|
11
|
+
export const lnk3 = {
|
|
12
|
+
id: "LNK-3",
|
|
13
|
+
group: "LNK",
|
|
14
|
+
level: "warn",
|
|
15
|
+
applies: (site, t) => (!site.sitemap ? t.skipNoSitemap : fullyAudited(site) ? null : t.skipPartialSample),
|
|
16
|
+
run(site, _c, t) {
|
|
17
|
+
const { out, add } = finder(this);
|
|
18
|
+
const home = norm(`${site.base}/`);
|
|
19
|
+
const linked = new Set();
|
|
20
|
+
for (const p of site.pages)
|
|
21
|
+
for (const href of p.links)
|
|
22
|
+
if (norm(href) !== norm(p.fetched.publicUrl))
|
|
23
|
+
linked.add(norm(href));
|
|
24
|
+
// A page no other page links to is found only through the sitemap, and gets no weight from the site.
|
|
25
|
+
for (const p of site.pages) {
|
|
26
|
+
const self = norm(p.fetched.publicUrl);
|
|
27
|
+
if (self !== home && !linked.has(self))
|
|
28
|
+
add(p.fetched.publicUrl, t.orphan);
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** LNK-4: Links can be followed by a crawler. */
|
|
2
|
+
import { finder } from "auditor-engine";
|
|
3
|
+
import { anchors } from "./shared.js";
|
|
4
|
+
export const lnk4 = {
|
|
5
|
+
id: "LNK-4",
|
|
6
|
+
group: "LNK",
|
|
7
|
+
level: "warn",
|
|
8
|
+
run(site, _c, t) {
|
|
9
|
+
const { out, add } = finder(this);
|
|
10
|
+
for (const p of site.pages) {
|
|
11
|
+
// No href, a bare «#» or javascript: — a crawler has nowhere to go. An <a name> or <a id> target is fine.
|
|
12
|
+
const dead = anchors(p.fetched.body).filter((a) => a.href === null ? !a.named : a.href.trim() === "#" || /^\s*javascript:/i.test(a.href));
|
|
13
|
+
if (dead.length)
|
|
14
|
+
add(p.fetched.publicUrl, t.uncrawlableLinks(dead.length, dead[0].text || dead[0].label || "…"));
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
},
|
|
18
|
+
};
|