svelte-vitals 0.11.0 → 0.13.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/bin.js +1 -1
- package/dist/{chunk-QI6NLWDD.js → chunk-Y4JY3ODE.js} +101 -14
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -95,13 +95,28 @@ async function detectAppHtmlLang(rt, cwd) {
|
|
|
95
95
|
if (!await rt.exists(appHtmlPath)) return { presence: "none", value: "absent" };
|
|
96
96
|
return detectHtmlLang(await rt.readFile(appHtmlPath));
|
|
97
97
|
}
|
|
98
|
+
async function robotsRefsSitemap(rt, cwd) {
|
|
99
|
+
const p = rt.join(cwd, "static/robots.txt");
|
|
100
|
+
if (!await rt.exists(p)) return void 0;
|
|
101
|
+
try {
|
|
102
|
+
return /^\s*sitemap:/im.test(await rt.readFile(p));
|
|
103
|
+
} catch {
|
|
104
|
+
return void 0;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
98
107
|
async function collectProjectFacts(rt, cwd) {
|
|
99
108
|
const [hasRobotsTxt, hasSitemap, htmlLang] = await Promise.all([
|
|
100
109
|
existsAny(rt, cwd, ROBOTS_SOURCE_PATHS),
|
|
101
110
|
existsAny(rt, cwd, SITEMAP_SOURCE_PATHS),
|
|
102
111
|
detectAppHtmlLang(rt, cwd)
|
|
103
112
|
]);
|
|
104
|
-
|
|
113
|
+
const robotsReferencesSitemap = await robotsRefsSitemap(rt, cwd);
|
|
114
|
+
return {
|
|
115
|
+
hasRobotsTxt,
|
|
116
|
+
hasSitemap,
|
|
117
|
+
htmlLang,
|
|
118
|
+
...robotsReferencesSitemap !== void 0 ? { robotsReferencesSitemap } : {}
|
|
119
|
+
};
|
|
105
120
|
}
|
|
106
121
|
|
|
107
122
|
// src/providers/source/parse.ts
|
|
@@ -149,6 +164,11 @@ function valueFromNodes(nodes) {
|
|
|
149
164
|
const text = nodes.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
|
|
150
165
|
return text.trim().length > 0 ? "static" : "absent";
|
|
151
166
|
}
|
|
167
|
+
function textFromNodes(nodes) {
|
|
168
|
+
if (!Array.isArray(nodes) || nodes.some((n) => n?.type === "ExpressionTag")) return void 0;
|
|
169
|
+
const text = nodes.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
|
|
170
|
+
return text.trim().length > 0 ? text : void 0;
|
|
171
|
+
}
|
|
152
172
|
function attrText(attributes, name) {
|
|
153
173
|
const attr = findAttr(attributes, name);
|
|
154
174
|
if (!attr) return void 0;
|
|
@@ -195,34 +215,52 @@ function tagsFromHead(head) {
|
|
|
195
215
|
const children = head?.fragment?.nodes ?? [];
|
|
196
216
|
for (const node of children) {
|
|
197
217
|
if (node?.type === "TitleElement") {
|
|
198
|
-
|
|
218
|
+
const titleNodes = node.fragment?.nodes ?? [];
|
|
219
|
+
const text = textFromNodes(titleNodes);
|
|
220
|
+
tags.push({ kind: "title", value: valueFromNodes(titleNodes), ...text !== void 0 ? { text } : {} });
|
|
199
221
|
continue;
|
|
200
222
|
}
|
|
201
223
|
if (node?.type !== "RegularElement") continue;
|
|
202
224
|
if (node.name === "meta") {
|
|
225
|
+
const charset = attrValue(node.attributes, "charset");
|
|
226
|
+
if (charset !== "absent") {
|
|
227
|
+
tags.push({ kind: "meta", name: "charset", value: charset });
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
203
230
|
const name = attrText(node.attributes, "name");
|
|
204
231
|
const property = attrText(node.attributes, "property");
|
|
232
|
+
const content = name === "robots" ? attrText(node.attributes, "content") : void 0;
|
|
233
|
+
const noindex = content !== void 0 && /(^|[\s,])(noindex|none)([\s,]|$)/i.test(content);
|
|
234
|
+
const contentValue = attrValue(node.attributes, "content");
|
|
235
|
+
const descText = name === "description" && contentValue === "static" ? attrText(node.attributes, "content") : void 0;
|
|
205
236
|
tags.push({
|
|
206
237
|
kind: "meta",
|
|
207
238
|
...name ? { name } : {},
|
|
208
239
|
...property ? { property } : {},
|
|
209
|
-
value:
|
|
240
|
+
value: contentValue,
|
|
241
|
+
...noindex ? { noindex: true } : {},
|
|
242
|
+
...descText !== void 0 ? { text: descText } : {}
|
|
210
243
|
});
|
|
211
244
|
} else if (node.name === "link") {
|
|
212
245
|
const rel = attrText(node.attributes, "rel");
|
|
213
246
|
const hasAs = findAttr(node.attributes, "as") !== void 0;
|
|
214
247
|
const asLiteral = attrText(node.attributes, "as");
|
|
215
248
|
const hasCrossorigin = findAttr(node.attributes, "crossorigin") !== void 0;
|
|
249
|
+
const hreflang = attrText(node.attributes, "hreflang");
|
|
216
250
|
tags.push({
|
|
217
251
|
kind: "link",
|
|
218
252
|
...rel ? { rel } : {},
|
|
219
253
|
value: attrValue(node.attributes, "href"),
|
|
220
254
|
...hasAs ? { hasAs: true } : {},
|
|
221
255
|
...asLiteral ? { as: asLiteral } : {},
|
|
222
|
-
...hasCrossorigin ? { hasCrossorigin: true } : {}
|
|
256
|
+
...hasCrossorigin ? { hasCrossorigin: true } : {},
|
|
257
|
+
// Keep a literal empty hreflang="" (present-but-invalid) so SEO026 can flag it.
|
|
258
|
+
...hreflang !== void 0 ? { hreflang } : {}
|
|
223
259
|
});
|
|
224
260
|
} else if (node.name === "script" && attrText(node.attributes, "type") === "application/ld+json") {
|
|
225
|
-
|
|
261
|
+
const nodes = node.fragment?.nodes ?? [];
|
|
262
|
+
const raw = textFromNodes(nodes);
|
|
263
|
+
tags.push({ kind: "jsonld", value: valueFromNodes(nodes), ...raw !== void 0 ? { jsonld: raw } : {} });
|
|
226
264
|
}
|
|
227
265
|
}
|
|
228
266
|
return tags;
|
|
@@ -234,6 +272,12 @@ function attrValueOf(attr) {
|
|
|
234
272
|
if (v && v.type === "ExpressionTag") return "dynamic";
|
|
235
273
|
return "absent";
|
|
236
274
|
}
|
|
275
|
+
function attrTextOf(attr) {
|
|
276
|
+
const v = attr?.value;
|
|
277
|
+
if (!Array.isArray(v) || v.some((n) => n?.type === "ExpressionTag")) return void 0;
|
|
278
|
+
const text = v.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
|
|
279
|
+
return text.trim().length > 0 ? text : void 0;
|
|
280
|
+
}
|
|
237
281
|
function collectComponents(node, acc) {
|
|
238
282
|
if (Array.isArray(node)) {
|
|
239
283
|
for (const child of node) collectComponents(child, acc);
|
|
@@ -265,6 +309,7 @@ function collectImages(node, source, acc) {
|
|
|
265
309
|
hasWidth: hasSpread || Boolean(findAttr(attrs, "width")),
|
|
266
310
|
hasHeight: hasSpread || Boolean(findAttr(attrs, "height")),
|
|
267
311
|
hasLoading: hasSpread || Boolean(findAttr(attrs, "loading")),
|
|
312
|
+
hasAlt: hasSpread || Boolean(findAttr(attrs, "alt")),
|
|
268
313
|
line: lineOf(source, node.start)
|
|
269
314
|
});
|
|
270
315
|
}
|
|
@@ -272,6 +317,20 @@ function collectImages(node, source, acc) {
|
|
|
272
317
|
if (key in node) collectImages(node[key], source, acc);
|
|
273
318
|
}
|
|
274
319
|
}
|
|
320
|
+
function collectHeadings(node, source, acc) {
|
|
321
|
+
if (Array.isArray(node)) {
|
|
322
|
+
for (const child of node) collectHeadings(child, source, acc);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (!node || typeof node !== "object") return;
|
|
326
|
+
if (node.type === "SvelteHead") return;
|
|
327
|
+
if (node.type === "RegularElement" && typeof node.name === "string" && /^h[1-6]$/.test(node.name)) {
|
|
328
|
+
acc.push({ level: Number(node.name[1]), line: lineOf(source, node.start) });
|
|
329
|
+
}
|
|
330
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
331
|
+
if (key in node) collectHeadings(node[key], source, acc);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
275
334
|
function parseFile(source, filename) {
|
|
276
335
|
const ast = parse(source, { modern: true, filename });
|
|
277
336
|
const heads = [];
|
|
@@ -280,11 +339,14 @@ function parseFile(source, filename) {
|
|
|
280
339
|
collectComponents(ast.fragment ?? ast, components);
|
|
281
340
|
const images = [];
|
|
282
341
|
collectImages(ast.fragment ?? ast, source, images);
|
|
342
|
+
const headings = [];
|
|
343
|
+
collectHeadings(ast.fragment ?? ast, source, headings);
|
|
283
344
|
return {
|
|
284
345
|
headTags: heads.flatMap(tagsFromHead),
|
|
285
346
|
components,
|
|
286
347
|
imports: collectImports(ast),
|
|
287
|
-
images
|
|
348
|
+
images,
|
|
349
|
+
headings
|
|
288
350
|
};
|
|
289
351
|
}
|
|
290
352
|
|
|
@@ -301,10 +363,20 @@ var svelteMetaTagsAdapter = {
|
|
|
301
363
|
resolve(use) {
|
|
302
364
|
const tags = [];
|
|
303
365
|
const attrs = use.attributes;
|
|
304
|
-
const
|
|
305
|
-
|
|
366
|
+
const titleAttr = findAttr2(attrs, "title");
|
|
367
|
+
const templateAttr = findAttr2(attrs, "titleTemplate");
|
|
368
|
+
const title = titleAttr ?? templateAttr;
|
|
369
|
+
if (title) {
|
|
370
|
+
const value = attrValueOf(title);
|
|
371
|
+
const text = titleAttr && !templateAttr && value === "static" ? attrTextOf(titleAttr) : void 0;
|
|
372
|
+
tags.push({ kind: "title", value, ...text !== void 0 ? { text } : {} });
|
|
373
|
+
}
|
|
306
374
|
const description = findAttr2(attrs, "description");
|
|
307
|
-
if (description)
|
|
375
|
+
if (description) {
|
|
376
|
+
const value = attrValueOf(description);
|
|
377
|
+
const text = value === "static" ? attrTextOf(description) : void 0;
|
|
378
|
+
tags.push({ kind: "meta", name: "description", value, ...text !== void 0 ? { text } : {} });
|
|
379
|
+
}
|
|
308
380
|
const canonical = findAttr2(attrs, "canonical");
|
|
309
381
|
if (canonical) tags.push({ kind: "link", rel: "canonical", value: attrValueOf(canonical) });
|
|
310
382
|
const robots = findAttr2(attrs, "robots");
|
|
@@ -327,9 +399,17 @@ var svelteSeoAdapter = {
|
|
|
327
399
|
const tags = [];
|
|
328
400
|
const attrs = use.attributes;
|
|
329
401
|
const title = findAttr3(attrs, "title");
|
|
330
|
-
if (title)
|
|
402
|
+
if (title) {
|
|
403
|
+
const value = attrValueOf(title);
|
|
404
|
+
const text = value === "static" ? attrTextOf(title) : void 0;
|
|
405
|
+
tags.push({ kind: "title", value, ...text !== void 0 ? { text } : {} });
|
|
406
|
+
}
|
|
331
407
|
const description = findAttr3(attrs, "description");
|
|
332
|
-
if (description)
|
|
408
|
+
if (description) {
|
|
409
|
+
const value = attrValueOf(description);
|
|
410
|
+
const text = value === "static" ? attrTextOf(description) : void 0;
|
|
411
|
+
tags.push({ kind: "meta", name: "description", value, ...text !== void 0 ? { text } : {} });
|
|
412
|
+
}
|
|
333
413
|
const canonical = findAttr3(attrs, "canonical");
|
|
334
414
|
if (canonical) tags.push({ kind: "link", rel: "canonical", value: attrValueOf(canonical) });
|
|
335
415
|
const openGraph = findAttr3(attrs, "openGraph");
|
|
@@ -448,12 +528,16 @@ async function resolveRoute(rt, cwd, pageRel, config) {
|
|
|
448
528
|
let broadOwn = false;
|
|
449
529
|
let broadInherited = false;
|
|
450
530
|
const images = [];
|
|
531
|
+
const headings = [];
|
|
451
532
|
for (const { rel, isPage } of files) {
|
|
452
533
|
const source = await rt.readFile(rt.join(cwd, rel));
|
|
453
534
|
const parsed = parseFile(source, rel);
|
|
454
535
|
for (const img of parsed.images) {
|
|
455
536
|
images.push({ ...img, file: rel });
|
|
456
537
|
}
|
|
538
|
+
for (const heading of parsed.headings) {
|
|
539
|
+
headings.push({ ...heading, file: rel });
|
|
540
|
+
}
|
|
457
541
|
const resolved = await resolveFileTags(rt, cwd, rel, parsed, config, MAX_DEPTH, /* @__PURE__ */ new Set([rel]));
|
|
458
542
|
for (const tag of resolved.tags) {
|
|
459
543
|
composed.set(tagKey(tag), { ...tag, presence: isPage ? "own" : "inherited", file: rel });
|
|
@@ -473,7 +557,8 @@ async function resolveRoute(rt, cwd, pageRel, config) {
|
|
|
473
557
|
const route = deriveRoute(pageRel);
|
|
474
558
|
return {
|
|
475
559
|
head: { route, source: "static", tags: [...composed.values()], file: pageRel },
|
|
476
|
-
images: { route, images }
|
|
560
|
+
images: { route, images },
|
|
561
|
+
headings: { route, headings }
|
|
477
562
|
};
|
|
478
563
|
}
|
|
479
564
|
async function collectRoutes(rt, cwd, config = defaultConfig) {
|
|
@@ -481,7 +566,8 @@ async function collectRoutes(rt, cwd, config = defaultConfig) {
|
|
|
481
566
|
const facts = await Promise.all(pages.map((page) => resolveRoute(rt, cwd, page, config)));
|
|
482
567
|
return {
|
|
483
568
|
heads: facts.map((f) => f.head),
|
|
484
|
-
images: facts.map((f) => f.images)
|
|
569
|
+
images: facts.map((f) => f.images),
|
|
570
|
+
headings: facts.map((f) => f.headings)
|
|
485
571
|
};
|
|
486
572
|
}
|
|
487
573
|
|
|
@@ -564,9 +650,10 @@ async function analyzeProject(opts = {}) {
|
|
|
564
650
|
const collected = await collectRoutes(rt, cwd, config);
|
|
565
651
|
const heads = collected.heads.filter((h) => matches(h.route));
|
|
566
652
|
const images = collected.images.filter((i) => matches(i.route));
|
|
653
|
+
const headings = collected.headings.filter((h) => matches(h.route));
|
|
567
654
|
const project = await collectProjectFacts(rt, cwd);
|
|
568
655
|
const rules = selectRules(allRules2, config);
|
|
569
|
-
const results = applyRuleSeverities(await runRules(rules, { heads, images, project, config }), config);
|
|
656
|
+
const results = applyRuleSeverities(await runRules(rules, { heads, images, headings, project, config }), config);
|
|
570
657
|
return { results, config, version: readPackageVersion() };
|
|
571
658
|
}
|
|
572
659
|
async function run(opts = {}) {
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-vitals",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "A SvelteKit SEO checker — not a runtime Web Vitals reporter. Static analysis of your routes' head metadata.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"mri": "^1.2.0",
|
|
43
43
|
"svelte": "^5.56.3",
|
|
44
44
|
"tinyglobby": "^0.2.17",
|
|
45
|
-
"@svelte-vitals/core": "0.
|
|
45
|
+
"@svelte-vitals/core": "0.14.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^24.7.0"
|