@jay-framework/seo-validator 0.19.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/dist/index.d.ts +5 -0
- package/dist/index.js +103 -0
- package/package.json +40 -0
- package/plugin.yaml +6 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { walkElements } from "@jay-framework/compiler-shared";
|
|
2
|
+
const validate = (ctx) => {
|
|
3
|
+
const findings = [];
|
|
4
|
+
let hasH1 = false;
|
|
5
|
+
let h1Count = 0;
|
|
6
|
+
let lastHeadingLevel = 0;
|
|
7
|
+
walkElements(ctx.body, ctx, (el) => {
|
|
8
|
+
const tag = el.rawTagName?.toLowerCase();
|
|
9
|
+
if (!tag)
|
|
10
|
+
return;
|
|
11
|
+
if (tag === "img") {
|
|
12
|
+
const alt = el.getAttribute?.("alt");
|
|
13
|
+
if (alt === void 0 || alt === null) {
|
|
14
|
+
findings.push({
|
|
15
|
+
severity: "warning",
|
|
16
|
+
message: "Image missing alt attribute — hurts SEO and accessibility",
|
|
17
|
+
suggestion: 'Add an alt attribute with descriptive text. For decorative images use alt="".',
|
|
18
|
+
element: "<img>",
|
|
19
|
+
attribute: "alt"
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const width = el.getAttribute?.("width");
|
|
23
|
+
const height = el.getAttribute?.("height");
|
|
24
|
+
const srcset = el.getAttribute?.("srcset");
|
|
25
|
+
if (!width || !height) {
|
|
26
|
+
const style = el.getAttribute?.("style") || "";
|
|
27
|
+
const hasInlineWidth = /width\s*:/.test(style);
|
|
28
|
+
const hasInlineHeight = /height\s*:/.test(style);
|
|
29
|
+
if ((!hasInlineWidth || !hasInlineHeight) && !srcset) {
|
|
30
|
+
findings.push({
|
|
31
|
+
severity: "warning",
|
|
32
|
+
message: "Image missing explicit dimensions — causes layout shift (CLS)",
|
|
33
|
+
suggestion: 'Add width and height attributes to prevent Cumulative Layout Shift. Example: <img width="800" height="600" ... />. For responsive images, use srcset with sizes. CLS is a Core Web Vital that affects search ranking.',
|
|
34
|
+
element: "<img>",
|
|
35
|
+
attribute: "width"
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const loading = el.getAttribute?.("loading");
|
|
40
|
+
if (!loading) {
|
|
41
|
+
findings.push({
|
|
42
|
+
severity: "warning",
|
|
43
|
+
message: "Image without loading attribute — consider lazy loading for performance",
|
|
44
|
+
suggestion: 'Add loading="lazy" to defer off-screen images. Use loading="eager" only for above-the-fold images.',
|
|
45
|
+
element: "<img>",
|
|
46
|
+
attribute: "loading"
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (tag === "a") {
|
|
51
|
+
const href = el.getAttribute?.("href");
|
|
52
|
+
const text = el.textContent?.trim();
|
|
53
|
+
if (href && (!text || text.length === 0) && !el.querySelector?.("img")) {
|
|
54
|
+
const ariaLabel = el.getAttribute?.("aria-label");
|
|
55
|
+
if (!ariaLabel) {
|
|
56
|
+
findings.push({
|
|
57
|
+
severity: "warning",
|
|
58
|
+
message: "Anchor element has no visible text or aria-label — bad for SEO link signals",
|
|
59
|
+
suggestion: "Add descriptive text content inside the <a> tag, or add an aria-label attribute.",
|
|
60
|
+
element: "<a>",
|
|
61
|
+
attribute: "href"
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const headingMatch = tag.match(/^h([1-6])$/);
|
|
67
|
+
if (headingMatch) {
|
|
68
|
+
const level = parseInt(headingMatch[1], 10);
|
|
69
|
+
if (level === 1) {
|
|
70
|
+
hasH1 = true;
|
|
71
|
+
h1Count++;
|
|
72
|
+
}
|
|
73
|
+
if (lastHeadingLevel > 0 && level > lastHeadingLevel + 1) {
|
|
74
|
+
findings.push({
|
|
75
|
+
severity: "warning",
|
|
76
|
+
message: `Heading level skipped: <h${lastHeadingLevel}> followed by <h${level}>`,
|
|
77
|
+
suggestion: `Use <h${lastHeadingLevel + 1}> instead of <h${level}> to maintain heading hierarchy. Search engines use heading structure to understand content organization.`,
|
|
78
|
+
element: `<h${level}>`
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
lastHeadingLevel = level;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
if (!hasH1) {
|
|
85
|
+
findings.push({
|
|
86
|
+
severity: "warning",
|
|
87
|
+
message: "Page has no <h1> element — the primary heading is important for SEO",
|
|
88
|
+
suggestion: "Add an <h1> element with the main page title or topic. Each page should have exactly one <h1>.",
|
|
89
|
+
element: "<h1>"
|
|
90
|
+
});
|
|
91
|
+
} else if (h1Count > 1) {
|
|
92
|
+
findings.push({
|
|
93
|
+
severity: "warning",
|
|
94
|
+
message: `Page has ${h1Count} <h1> elements — should have exactly one`,
|
|
95
|
+
suggestion: "Keep only one <h1> for the primary page heading. Use <h2> or lower for secondary headings.",
|
|
96
|
+
element: "<h1>"
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return findings;
|
|
100
|
+
};
|
|
101
|
+
export {
|
|
102
|
+
validate
|
|
103
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jay-framework/seo-validator",
|
|
3
|
+
"version": "0.19.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "SEO validation plugin for Jay Framework — checks jay-html templates for SEO best practices",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"plugin.yaml"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./dist/index.js",
|
|
14
|
+
"./plugin.yaml": "./plugin.yaml"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "npm run clean && npm run build:server && npm run build:types && npm run validate",
|
|
18
|
+
"validate": "jay-stack-cli validate-plugin",
|
|
19
|
+
"build:server": "vite build --ssr",
|
|
20
|
+
"build:types": "tsup lib/index.ts --dts-only --format esm",
|
|
21
|
+
"build:check-types": "tsc",
|
|
22
|
+
"clean": "rimraf dist",
|
|
23
|
+
"confirm": "npm run clean && npm run build && npm run test",
|
|
24
|
+
"test": "vitest run"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@jay-framework/compiler-shared": "^0.19.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@jay-framework/dev-environment": "^0.19.0",
|
|
31
|
+
"@jay-framework/jay-stack-cli": "^0.19.0",
|
|
32
|
+
"@types/node": "^22.15.21",
|
|
33
|
+
"node-html-parser": "^6.1.0",
|
|
34
|
+
"rimraf": "^5.0.5",
|
|
35
|
+
"tsup": "^8.0.1",
|
|
36
|
+
"typescript": "^5.3.3",
|
|
37
|
+
"vite": "^5.0.11",
|
|
38
|
+
"vitest": "^1.2.1"
|
|
39
|
+
}
|
|
40
|
+
}
|