@jay-framework/seo-validator 0.19.1 → 0.19.2
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.js +67 -0
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -4,11 +4,18 @@ const validate = (ctx) => {
|
|
|
4
4
|
let hasH1 = false;
|
|
5
5
|
let h1Count = 0;
|
|
6
6
|
let lastHeadingLevel = 0;
|
|
7
|
+
let hasMain = false;
|
|
8
|
+
let hasImage = false;
|
|
9
|
+
let hasFetchPriorityHigh = false;
|
|
7
10
|
walkElements(ctx.body, ctx, (el) => {
|
|
8
11
|
const tag = el.rawTagName?.toLowerCase();
|
|
9
12
|
if (!tag)
|
|
10
13
|
return;
|
|
11
14
|
if (tag === "img") {
|
|
15
|
+
hasImage = true;
|
|
16
|
+
if (el.getAttribute?.("fetchpriority") === "high") {
|
|
17
|
+
hasFetchPriorityHigh = true;
|
|
18
|
+
}
|
|
12
19
|
const alt = el.getAttribute?.("alt");
|
|
13
20
|
if (alt === void 0 || alt === null) {
|
|
14
21
|
findings.push({
|
|
@@ -63,6 +70,9 @@ const validate = (ctx) => {
|
|
|
63
70
|
}
|
|
64
71
|
}
|
|
65
72
|
}
|
|
73
|
+
if (tag === "main") {
|
|
74
|
+
hasMain = true;
|
|
75
|
+
}
|
|
66
76
|
const headingMatch = tag.match(/^h([1-6])$/);
|
|
67
77
|
if (headingMatch) {
|
|
68
78
|
const level = parseInt(headingMatch[1], 10);
|
|
@@ -96,6 +106,63 @@ const validate = (ctx) => {
|
|
|
96
106
|
element: "<h1>"
|
|
97
107
|
});
|
|
98
108
|
}
|
|
109
|
+
if (!hasMain) {
|
|
110
|
+
findings.push({
|
|
111
|
+
severity: "warning",
|
|
112
|
+
message: "Page has no <main> landmark — helps search engines identify primary content",
|
|
113
|
+
suggestion: "Wrap the primary page content in a <main> element. Each page should have one <main> landmark.",
|
|
114
|
+
element: "<main>"
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (hasImage && !hasFetchPriorityHigh) {
|
|
118
|
+
findings.push({
|
|
119
|
+
severity: "warning",
|
|
120
|
+
message: 'No image has fetchpriority="high" — the LCP image should be prioritized',
|
|
121
|
+
suggestion: 'Add fetchpriority="high" to the largest above-the-fold image (the LCP candidate). This tells the browser to download it first, improving Largest Contentful Paint.',
|
|
122
|
+
element: "<img>",
|
|
123
|
+
attribute: "fetchpriority"
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (ctx.head) {
|
|
127
|
+
if (!ctx.head.title) {
|
|
128
|
+
findings.push({
|
|
129
|
+
severity: "warning",
|
|
130
|
+
message: "Page has no <title> element",
|
|
131
|
+
suggestion: "Add <title>Page Title</title> in <head>. The title appears in search results and browser tabs.",
|
|
132
|
+
element: "<title>"
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const hasDescription = ctx.head.meta.some((m) => m.name?.toLowerCase() === "description");
|
|
136
|
+
if (!hasDescription) {
|
|
137
|
+
findings.push({
|
|
138
|
+
severity: "warning",
|
|
139
|
+
message: 'Page has no <meta name="description">',
|
|
140
|
+
suggestion: 'Add <meta name="description" content="..."> in <head>. Search engines use this for result snippets.',
|
|
141
|
+
element: "<meta>",
|
|
142
|
+
attribute: "name"
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const hasCanonical = ctx.head.links.some((l) => l.rel === "canonical");
|
|
146
|
+
if (!hasCanonical) {
|
|
147
|
+
findings.push({
|
|
148
|
+
severity: "warning",
|
|
149
|
+
message: 'Page has no <link rel="canonical">',
|
|
150
|
+
suggestion: 'Add <link rel="canonical" href="..."> in <head> to specify the preferred URL. This prevents duplicate content issues in search engines.',
|
|
151
|
+
element: "<link>",
|
|
152
|
+
attribute: "rel"
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const robotsMeta = ctx.head.meta.find((m) => m.name?.toLowerCase() === "robots");
|
|
156
|
+
if (robotsMeta && /noindex/i.test(robotsMeta.content)) {
|
|
157
|
+
findings.push({
|
|
158
|
+
severity: "warning",
|
|
159
|
+
message: 'Page has <meta name="robots" content="noindex"> — it will not appear in search results',
|
|
160
|
+
suggestion: "Remove noindex from the robots meta tag if this page should be indexed. If intentional (e.g. admin pages), this warning can be ignored.",
|
|
161
|
+
element: "<meta>",
|
|
162
|
+
attribute: "content"
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
99
166
|
return findings;
|
|
100
167
|
};
|
|
101
168
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jay-framework/seo-validator",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SEO validation plugin for Jay Framework — checks jay-html templates for SEO best practices",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"test": "vitest run"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@jay-framework/compiler-shared": "^0.19.
|
|
27
|
+
"@jay-framework/compiler-shared": "^0.19.2"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@jay-framework/dev-environment": "^0.19.
|
|
31
|
-
"@jay-framework/jay-stack-cli": "^0.19.
|
|
30
|
+
"@jay-framework/dev-environment": "^0.19.2",
|
|
31
|
+
"@jay-framework/jay-stack-cli": "^0.19.2",
|
|
32
32
|
"@types/node": "^22.15.21",
|
|
33
33
|
"node-html-parser": "^6.1.0",
|
|
34
34
|
"rimraf": "^5.0.5",
|