svelte-vitals 0.12.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-NBNJRN52.js → chunk-Y4JY3ODE.js} +83 -14
- 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,52 @@ 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");
|
|
234
250
|
tags.push({
|
|
235
251
|
kind: "link",
|
|
236
252
|
...rel ? { rel } : {},
|
|
237
253
|
value: attrValue(node.attributes, "href"),
|
|
238
254
|
...hasAs ? { hasAs: true } : {},
|
|
239
255
|
...asLiteral ? { as: asLiteral } : {},
|
|
240
|
-
...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 } : {}
|
|
241
259
|
});
|
|
242
260
|
} else if (node.name === "script" && attrText(node.attributes, "type") === "application/ld+json") {
|
|
243
|
-
|
|
261
|
+
const nodes = node.fragment?.nodes ?? [];
|
|
262
|
+
const raw = textFromNodes(nodes);
|
|
263
|
+
tags.push({ kind: "jsonld", value: valueFromNodes(nodes), ...raw !== void 0 ? { jsonld: raw } : {} });
|
|
244
264
|
}
|
|
245
265
|
}
|
|
246
266
|
return tags;
|
|
@@ -252,6 +272,12 @@ function attrValueOf(attr) {
|
|
|
252
272
|
if (v && v.type === "ExpressionTag") return "dynamic";
|
|
253
273
|
return "absent";
|
|
254
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
|
+
}
|
|
255
281
|
function collectComponents(node, acc) {
|
|
256
282
|
if (Array.isArray(node)) {
|
|
257
283
|
for (const child of node) collectComponents(child, acc);
|
|
@@ -283,6 +309,7 @@ function collectImages(node, source, acc) {
|
|
|
283
309
|
hasWidth: hasSpread || Boolean(findAttr(attrs, "width")),
|
|
284
310
|
hasHeight: hasSpread || Boolean(findAttr(attrs, "height")),
|
|
285
311
|
hasLoading: hasSpread || Boolean(findAttr(attrs, "loading")),
|
|
312
|
+
hasAlt: hasSpread || Boolean(findAttr(attrs, "alt")),
|
|
286
313
|
line: lineOf(source, node.start)
|
|
287
314
|
});
|
|
288
315
|
}
|
|
@@ -290,6 +317,20 @@ function collectImages(node, source, acc) {
|
|
|
290
317
|
if (key in node) collectImages(node[key], source, acc);
|
|
291
318
|
}
|
|
292
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
|
+
}
|
|
293
334
|
function parseFile(source, filename) {
|
|
294
335
|
const ast = parse(source, { modern: true, filename });
|
|
295
336
|
const heads = [];
|
|
@@ -298,11 +339,14 @@ function parseFile(source, filename) {
|
|
|
298
339
|
collectComponents(ast.fragment ?? ast, components);
|
|
299
340
|
const images = [];
|
|
300
341
|
collectImages(ast.fragment ?? ast, source, images);
|
|
342
|
+
const headings = [];
|
|
343
|
+
collectHeadings(ast.fragment ?? ast, source, headings);
|
|
301
344
|
return {
|
|
302
345
|
headTags: heads.flatMap(tagsFromHead),
|
|
303
346
|
components,
|
|
304
347
|
imports: collectImports(ast),
|
|
305
|
-
images
|
|
348
|
+
images,
|
|
349
|
+
headings
|
|
306
350
|
};
|
|
307
351
|
}
|
|
308
352
|
|
|
@@ -319,10 +363,20 @@ var svelteMetaTagsAdapter = {
|
|
|
319
363
|
resolve(use) {
|
|
320
364
|
const tags = [];
|
|
321
365
|
const attrs = use.attributes;
|
|
322
|
-
const
|
|
323
|
-
|
|
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
|
+
}
|
|
324
374
|
const description = findAttr2(attrs, "description");
|
|
325
|
-
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
|
+
}
|
|
326
380
|
const canonical = findAttr2(attrs, "canonical");
|
|
327
381
|
if (canonical) tags.push({ kind: "link", rel: "canonical", value: attrValueOf(canonical) });
|
|
328
382
|
const robots = findAttr2(attrs, "robots");
|
|
@@ -345,9 +399,17 @@ var svelteSeoAdapter = {
|
|
|
345
399
|
const tags = [];
|
|
346
400
|
const attrs = use.attributes;
|
|
347
401
|
const title = findAttr3(attrs, "title");
|
|
348
|
-
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
|
+
}
|
|
349
407
|
const description = findAttr3(attrs, "description");
|
|
350
|
-
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
|
+
}
|
|
351
413
|
const canonical = findAttr3(attrs, "canonical");
|
|
352
414
|
if (canonical) tags.push({ kind: "link", rel: "canonical", value: attrValueOf(canonical) });
|
|
353
415
|
const openGraph = findAttr3(attrs, "openGraph");
|
|
@@ -466,12 +528,16 @@ async function resolveRoute(rt, cwd, pageRel, config) {
|
|
|
466
528
|
let broadOwn = false;
|
|
467
529
|
let broadInherited = false;
|
|
468
530
|
const images = [];
|
|
531
|
+
const headings = [];
|
|
469
532
|
for (const { rel, isPage } of files) {
|
|
470
533
|
const source = await rt.readFile(rt.join(cwd, rel));
|
|
471
534
|
const parsed = parseFile(source, rel);
|
|
472
535
|
for (const img of parsed.images) {
|
|
473
536
|
images.push({ ...img, file: rel });
|
|
474
537
|
}
|
|
538
|
+
for (const heading of parsed.headings) {
|
|
539
|
+
headings.push({ ...heading, file: rel });
|
|
540
|
+
}
|
|
475
541
|
const resolved = await resolveFileTags(rt, cwd, rel, parsed, config, MAX_DEPTH, /* @__PURE__ */ new Set([rel]));
|
|
476
542
|
for (const tag of resolved.tags) {
|
|
477
543
|
composed.set(tagKey(tag), { ...tag, presence: isPage ? "own" : "inherited", file: rel });
|
|
@@ -491,7 +557,8 @@ async function resolveRoute(rt, cwd, pageRel, config) {
|
|
|
491
557
|
const route = deriveRoute(pageRel);
|
|
492
558
|
return {
|
|
493
559
|
head: { route, source: "static", tags: [...composed.values()], file: pageRel },
|
|
494
|
-
images: { route, images }
|
|
560
|
+
images: { route, images },
|
|
561
|
+
headings: { route, headings }
|
|
495
562
|
};
|
|
496
563
|
}
|
|
497
564
|
async function collectRoutes(rt, cwd, config = defaultConfig) {
|
|
@@ -499,7 +566,8 @@ async function collectRoutes(rt, cwd, config = defaultConfig) {
|
|
|
499
566
|
const facts = await Promise.all(pages.map((page) => resolveRoute(rt, cwd, page, config)));
|
|
500
567
|
return {
|
|
501
568
|
heads: facts.map((f) => f.head),
|
|
502
|
-
images: facts.map((f) => f.images)
|
|
569
|
+
images: facts.map((f) => f.images),
|
|
570
|
+
headings: facts.map((f) => f.headings)
|
|
503
571
|
};
|
|
504
572
|
}
|
|
505
573
|
|
|
@@ -582,9 +650,10 @@ async function analyzeProject(opts = {}) {
|
|
|
582
650
|
const collected = await collectRoutes(rt, cwd, config);
|
|
583
651
|
const heads = collected.heads.filter((h) => matches(h.route));
|
|
584
652
|
const images = collected.images.filter((i) => matches(i.route));
|
|
653
|
+
const headings = collected.headings.filter((h) => matches(h.route));
|
|
585
654
|
const project = await collectProjectFacts(rt, cwd);
|
|
586
655
|
const rules = selectRules(allRules2, config);
|
|
587
|
-
const results = applyRuleSeverities(await runRules(rules, { heads, images, project, config }), config);
|
|
656
|
+
const results = applyRuleSeverities(await runRules(rules, { heads, images, headings, project, config }), config);
|
|
588
657
|
return { results, config, version: readPackageVersion() };
|
|
589
658
|
}
|
|
590
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"
|