svelte-vitals 0.12.0 → 0.14.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-NBNJRN52.js → chunk-YMRN4VPN.js} +100 -15
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -164,6 +164,11 @@ function valueFromNodes(nodes) {
|
|
|
164
164
|
const text = nodes.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
|
|
165
165
|
return text.trim().length > 0 ? "static" : "absent";
|
|
166
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
|
+
}
|
|
167
172
|
function attrText(attributes, name) {
|
|
168
173
|
const attr = findAttr(attributes, name);
|
|
169
174
|
if (!attr) return void 0;
|
|
@@ -210,37 +215,63 @@ function tagsFromHead(head) {
|
|
|
210
215
|
const children = head?.fragment?.nodes ?? [];
|
|
211
216
|
for (const node of children) {
|
|
212
217
|
if (node?.type === "TitleElement") {
|
|
213
|
-
|
|
218
|
+
const titleNodes = node.fragment?.nodes ?? [];
|
|
219
|
+
const text = textFromNodes(titleNodes);
|
|
220
|
+
tags.push({ kind: "title", value: valueFromNodes(titleNodes), ...text !== void 0 ? { text } : {} });
|
|
214
221
|
continue;
|
|
215
222
|
}
|
|
216
223
|
if (node?.type !== "RegularElement") continue;
|
|
217
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
|
+
}
|
|
218
230
|
const name = attrText(node.attributes, "name");
|
|
219
231
|
const property = attrText(node.attributes, "property");
|
|
220
232
|
const content = name === "robots" ? attrText(node.attributes, "content") : void 0;
|
|
221
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;
|
|
222
236
|
tags.push({
|
|
223
237
|
kind: "meta",
|
|
224
238
|
...name ? { name } : {},
|
|
225
239
|
...property ? { property } : {},
|
|
226
|
-
value:
|
|
227
|
-
...noindex ? { noindex: true } : {}
|
|
240
|
+
value: contentValue,
|
|
241
|
+
...noindex ? { noindex: true } : {},
|
|
242
|
+
...descText !== void 0 ? { text: descText } : {}
|
|
228
243
|
});
|
|
229
244
|
} else if (node.name === "link") {
|
|
230
245
|
const rel = attrText(node.attributes, "rel");
|
|
231
246
|
const hasAs = findAttr(node.attributes, "as") !== void 0;
|
|
232
247
|
const asLiteral = attrText(node.attributes, "as");
|
|
233
248
|
const hasCrossorigin = findAttr(node.attributes, "crossorigin") !== void 0;
|
|
249
|
+
const hreflang = attrText(node.attributes, "hreflang");
|
|
250
|
+
const href = attrText(node.attributes, "href");
|
|
234
251
|
tags.push({
|
|
235
252
|
kind: "link",
|
|
236
253
|
...rel ? { rel } : {},
|
|
237
254
|
value: attrValue(node.attributes, "href"),
|
|
238
255
|
...hasAs ? { hasAs: true } : {},
|
|
239
256
|
...asLiteral ? { as: asLiteral } : {},
|
|
240
|
-
...hasCrossorigin ? { hasCrossorigin: true } : {}
|
|
257
|
+
...hasCrossorigin ? { hasCrossorigin: true } : {},
|
|
258
|
+
// Keep a literal empty hreflang="" (present-but-invalid) so SEO026 can flag it.
|
|
259
|
+
...hreflang !== void 0 ? { hreflang } : {},
|
|
260
|
+
...href ? { href } : {}
|
|
241
261
|
});
|
|
242
|
-
} else if (node.name === "script"
|
|
243
|
-
|
|
262
|
+
} else if (node.name === "script") {
|
|
263
|
+
const type = attrText(node.attributes, "type");
|
|
264
|
+
if (type === "application/ld+json") {
|
|
265
|
+
const nodes = node.fragment?.nodes ?? [];
|
|
266
|
+
const raw = textFromNodes(nodes);
|
|
267
|
+
tags.push({ kind: "jsonld", value: valueFromNodes(nodes), ...raw !== void 0 ? { jsonld: raw } : {} });
|
|
268
|
+
} else {
|
|
269
|
+
const src = attrText(node.attributes, "src");
|
|
270
|
+
if (src) {
|
|
271
|
+
const blocking = findAttr(node.attributes, "defer") === void 0 && findAttr(node.attributes, "async") === void 0 && type !== "module";
|
|
272
|
+
tags.push({ kind: "script", value: "static", href: src, ...blocking ? { blocking: true } : {} });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
244
275
|
}
|
|
245
276
|
}
|
|
246
277
|
return tags;
|
|
@@ -252,6 +283,12 @@ function attrValueOf(attr) {
|
|
|
252
283
|
if (v && v.type === "ExpressionTag") return "dynamic";
|
|
253
284
|
return "absent";
|
|
254
285
|
}
|
|
286
|
+
function attrTextOf(attr) {
|
|
287
|
+
const v = attr?.value;
|
|
288
|
+
if (!Array.isArray(v) || v.some((n) => n?.type === "ExpressionTag")) return void 0;
|
|
289
|
+
const text = v.filter((n) => n?.type === "Text").map((n) => String(n.data ?? "")).join("");
|
|
290
|
+
return text.trim().length > 0 ? text : void 0;
|
|
291
|
+
}
|
|
255
292
|
function collectComponents(node, acc) {
|
|
256
293
|
if (Array.isArray(node)) {
|
|
257
294
|
for (const child of node) collectComponents(child, acc);
|
|
@@ -283,6 +320,10 @@ function collectImages(node, source, acc) {
|
|
|
283
320
|
hasWidth: hasSpread || Boolean(findAttr(attrs, "width")),
|
|
284
321
|
hasHeight: hasSpread || Boolean(findAttr(attrs, "height")),
|
|
285
322
|
hasLoading: hasSpread || Boolean(findAttr(attrs, "loading")),
|
|
323
|
+
hasAlt: hasSpread || Boolean(findAttr(attrs, "alt")),
|
|
324
|
+
// A literal loading="lazy" only — a spread or dynamic loading={…} must not be flagged.
|
|
325
|
+
lazy: attrText(attrs, "loading") === "lazy",
|
|
326
|
+
hasSrcset: hasSpread || Boolean(findAttr(attrs, "srcset")),
|
|
286
327
|
line: lineOf(source, node.start)
|
|
287
328
|
});
|
|
288
329
|
}
|
|
@@ -290,6 +331,20 @@ function collectImages(node, source, acc) {
|
|
|
290
331
|
if (key in node) collectImages(node[key], source, acc);
|
|
291
332
|
}
|
|
292
333
|
}
|
|
334
|
+
function collectHeadings(node, source, acc) {
|
|
335
|
+
if (Array.isArray(node)) {
|
|
336
|
+
for (const child of node) collectHeadings(child, source, acc);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (!node || typeof node !== "object") return;
|
|
340
|
+
if (node.type === "SvelteHead") return;
|
|
341
|
+
if (node.type === "RegularElement" && typeof node.name === "string" && /^h[1-6]$/.test(node.name)) {
|
|
342
|
+
acc.push({ level: Number(node.name[1]), line: lineOf(source, node.start) });
|
|
343
|
+
}
|
|
344
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
345
|
+
if (key in node) collectHeadings(node[key], source, acc);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
293
348
|
function parseFile(source, filename) {
|
|
294
349
|
const ast = parse(source, { modern: true, filename });
|
|
295
350
|
const heads = [];
|
|
@@ -298,11 +353,14 @@ function parseFile(source, filename) {
|
|
|
298
353
|
collectComponents(ast.fragment ?? ast, components);
|
|
299
354
|
const images = [];
|
|
300
355
|
collectImages(ast.fragment ?? ast, source, images);
|
|
356
|
+
const headings = [];
|
|
357
|
+
collectHeadings(ast.fragment ?? ast, source, headings);
|
|
301
358
|
return {
|
|
302
359
|
headTags: heads.flatMap(tagsFromHead),
|
|
303
360
|
components,
|
|
304
361
|
imports: collectImports(ast),
|
|
305
|
-
images
|
|
362
|
+
images,
|
|
363
|
+
headings
|
|
306
364
|
};
|
|
307
365
|
}
|
|
308
366
|
|
|
@@ -319,10 +377,20 @@ var svelteMetaTagsAdapter = {
|
|
|
319
377
|
resolve(use) {
|
|
320
378
|
const tags = [];
|
|
321
379
|
const attrs = use.attributes;
|
|
322
|
-
const
|
|
323
|
-
|
|
380
|
+
const titleAttr = findAttr2(attrs, "title");
|
|
381
|
+
const templateAttr = findAttr2(attrs, "titleTemplate");
|
|
382
|
+
const title = titleAttr ?? templateAttr;
|
|
383
|
+
if (title) {
|
|
384
|
+
const value = attrValueOf(title);
|
|
385
|
+
const text = titleAttr && !templateAttr && value === "static" ? attrTextOf(titleAttr) : void 0;
|
|
386
|
+
tags.push({ kind: "title", value, ...text !== void 0 ? { text } : {} });
|
|
387
|
+
}
|
|
324
388
|
const description = findAttr2(attrs, "description");
|
|
325
|
-
if (description)
|
|
389
|
+
if (description) {
|
|
390
|
+
const value = attrValueOf(description);
|
|
391
|
+
const text = value === "static" ? attrTextOf(description) : void 0;
|
|
392
|
+
tags.push({ kind: "meta", name: "description", value, ...text !== void 0 ? { text } : {} });
|
|
393
|
+
}
|
|
326
394
|
const canonical = findAttr2(attrs, "canonical");
|
|
327
395
|
if (canonical) tags.push({ kind: "link", rel: "canonical", value: attrValueOf(canonical) });
|
|
328
396
|
const robots = findAttr2(attrs, "robots");
|
|
@@ -345,9 +413,17 @@ var svelteSeoAdapter = {
|
|
|
345
413
|
const tags = [];
|
|
346
414
|
const attrs = use.attributes;
|
|
347
415
|
const title = findAttr3(attrs, "title");
|
|
348
|
-
if (title)
|
|
416
|
+
if (title) {
|
|
417
|
+
const value = attrValueOf(title);
|
|
418
|
+
const text = value === "static" ? attrTextOf(title) : void 0;
|
|
419
|
+
tags.push({ kind: "title", value, ...text !== void 0 ? { text } : {} });
|
|
420
|
+
}
|
|
349
421
|
const description = findAttr3(attrs, "description");
|
|
350
|
-
if (description)
|
|
422
|
+
if (description) {
|
|
423
|
+
const value = attrValueOf(description);
|
|
424
|
+
const text = value === "static" ? attrTextOf(description) : void 0;
|
|
425
|
+
tags.push({ kind: "meta", name: "description", value, ...text !== void 0 ? { text } : {} });
|
|
426
|
+
}
|
|
351
427
|
const canonical = findAttr3(attrs, "canonical");
|
|
352
428
|
if (canonical) tags.push({ kind: "link", rel: "canonical", value: attrValueOf(canonical) });
|
|
353
429
|
const openGraph = findAttr3(attrs, "openGraph");
|
|
@@ -381,6 +457,8 @@ function tagKey(tag) {
|
|
|
381
457
|
return `link:${tag.rel ?? "?"}`;
|
|
382
458
|
case "jsonld":
|
|
383
459
|
return "jsonld";
|
|
460
|
+
case "script":
|
|
461
|
+
return `script:${tag.href ?? "?"}`;
|
|
384
462
|
}
|
|
385
463
|
}
|
|
386
464
|
function resolveComponentPath(source, fromFileRel) {
|
|
@@ -466,12 +544,16 @@ async function resolveRoute(rt, cwd, pageRel, config) {
|
|
|
466
544
|
let broadOwn = false;
|
|
467
545
|
let broadInherited = false;
|
|
468
546
|
const images = [];
|
|
547
|
+
const headings = [];
|
|
469
548
|
for (const { rel, isPage } of files) {
|
|
470
549
|
const source = await rt.readFile(rt.join(cwd, rel));
|
|
471
550
|
const parsed = parseFile(source, rel);
|
|
472
551
|
for (const img of parsed.images) {
|
|
473
552
|
images.push({ ...img, file: rel });
|
|
474
553
|
}
|
|
554
|
+
for (const heading of parsed.headings) {
|
|
555
|
+
headings.push({ ...heading, file: rel });
|
|
556
|
+
}
|
|
475
557
|
const resolved = await resolveFileTags(rt, cwd, rel, parsed, config, MAX_DEPTH, /* @__PURE__ */ new Set([rel]));
|
|
476
558
|
for (const tag of resolved.tags) {
|
|
477
559
|
composed.set(tagKey(tag), { ...tag, presence: isPage ? "own" : "inherited", file: rel });
|
|
@@ -491,7 +573,8 @@ async function resolveRoute(rt, cwd, pageRel, config) {
|
|
|
491
573
|
const route = deriveRoute(pageRel);
|
|
492
574
|
return {
|
|
493
575
|
head: { route, source: "static", tags: [...composed.values()], file: pageRel },
|
|
494
|
-
images: { route, images }
|
|
576
|
+
images: { route, images },
|
|
577
|
+
headings: { route, headings }
|
|
495
578
|
};
|
|
496
579
|
}
|
|
497
580
|
async function collectRoutes(rt, cwd, config = defaultConfig) {
|
|
@@ -499,7 +582,8 @@ async function collectRoutes(rt, cwd, config = defaultConfig) {
|
|
|
499
582
|
const facts = await Promise.all(pages.map((page) => resolveRoute(rt, cwd, page, config)));
|
|
500
583
|
return {
|
|
501
584
|
heads: facts.map((f) => f.head),
|
|
502
|
-
images: facts.map((f) => f.images)
|
|
585
|
+
images: facts.map((f) => f.images),
|
|
586
|
+
headings: facts.map((f) => f.headings)
|
|
503
587
|
};
|
|
504
588
|
}
|
|
505
589
|
|
|
@@ -582,9 +666,10 @@ async function analyzeProject(opts = {}) {
|
|
|
582
666
|
const collected = await collectRoutes(rt, cwd, config);
|
|
583
667
|
const heads = collected.heads.filter((h) => matches(h.route));
|
|
584
668
|
const images = collected.images.filter((i) => matches(i.route));
|
|
669
|
+
const headings = collected.headings.filter((h) => matches(h.route));
|
|
585
670
|
const project = await collectProjectFacts(rt, cwd);
|
|
586
671
|
const rules = selectRules(allRules2, config);
|
|
587
|
-
const results = applyRuleSeverities(await runRules(rules, { heads, images, project, config }), config);
|
|
672
|
+
const results = applyRuleSeverities(await runRules(rules, { heads, images, headings, project, config }), config);
|
|
588
673
|
return { results, config, version: readPackageVersion() };
|
|
589
674
|
}
|
|
590
675
|
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.14.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.15.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^24.7.0"
|