@rivi-software/agentic-seo-check 0.0.0-stage → 0.1.1

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.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -2
  3. package/dist/brand.d.ts +11 -0
  4. package/dist/brand.js +22 -0
  5. package/dist/brand.js.map +1 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +194 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/core/audit.d.ts +72 -0
  10. package/dist/core/audit.js +452 -0
  11. package/dist/core/audit.js.map +1 -0
  12. package/dist/core/concurrency.d.ts +4 -0
  13. package/dist/core/concurrency.js +39 -0
  14. package/dist/core/concurrency.js.map +1 -0
  15. package/dist/core/crawl.d.ts +40 -0
  16. package/dist/core/crawl.js +324 -0
  17. package/dist/core/crawl.js.map +1 -0
  18. package/dist/core/fetch.d.ts +27 -0
  19. package/dist/core/fetch.js +186 -0
  20. package/dist/core/fetch.js.map +1 -0
  21. package/dist/core/html/dom.d.ts +13 -0
  22. package/dist/core/html/dom.js +17 -0
  23. package/dist/core/html/dom.js.map +1 -0
  24. package/dist/core/html/extract.d.ts +10 -0
  25. package/dist/core/html/extract.js +207 -0
  26. package/dist/core/html/extract.js.map +1 -0
  27. package/dist/core/html/structured-data.d.ts +21 -0
  28. package/dist/core/html/structured-data.js +220 -0
  29. package/dist/core/html/structured-data.js.map +1 -0
  30. package/dist/core/html/text.d.ts +23 -0
  31. package/dist/core/html/text.js +152 -0
  32. package/dist/core/html/text.js.map +1 -0
  33. package/dist/core/lighthouse.d.ts +50 -0
  34. package/dist/core/lighthouse.js +216 -0
  35. package/dist/core/lighthouse.js.map +1 -0
  36. package/dist/core/performance-rules.d.ts +3 -0
  37. package/dist/core/performance-rules.js +125 -0
  38. package/dist/core/performance-rules.js.map +1 -0
  39. package/dist/core/render-rules.d.ts +8 -0
  40. package/dist/core/render-rules.js +144 -0
  41. package/dist/core/render-rules.js.map +1 -0
  42. package/dist/core/render.d.ts +22 -0
  43. package/dist/core/render.js +79 -0
  44. package/dist/core/render.js.map +1 -0
  45. package/dist/core/report/markdown.d.ts +2 -0
  46. package/dist/core/report/markdown.js +131 -0
  47. package/dist/core/report/markdown.js.map +1 -0
  48. package/dist/core/robots.d.ts +11 -0
  49. package/dist/core/robots.js +48 -0
  50. package/dist/core/robots.js.map +1 -0
  51. package/dist/core/rules/canonical.d.ts +2 -0
  52. package/dist/core/rules/canonical.js +141 -0
  53. package/dist/core/rules/canonical.js.map +1 -0
  54. package/dist/core/rules/content.d.ts +1 -0
  55. package/dist/core/rules/content.js +55 -0
  56. package/dist/core/rules/content.js.map +1 -0
  57. package/dist/core/rules/document.d.ts +1 -0
  58. package/dist/core/rules/document.js +83 -0
  59. package/dist/core/rules/document.js.map +1 -0
  60. package/dist/core/rules/headings.d.ts +1 -0
  61. package/dist/core/rules/headings.js +81 -0
  62. package/dist/core/rules/headings.js.map +1 -0
  63. package/dist/core/rules/hreflang.d.ts +1 -0
  64. package/dist/core/rules/hreflang.js +100 -0
  65. package/dist/core/rules/hreflang.js.map +1 -0
  66. package/dist/core/rules/images.d.ts +1 -0
  67. package/dist/core/rules/images.js +77 -0
  68. package/dist/core/rules/images.js.map +1 -0
  69. package/dist/core/rules/index.d.ts +14 -0
  70. package/dist/core/rules/index.js +77 -0
  71. package/dist/core/rules/index.js.map +1 -0
  72. package/dist/core/rules/indexing.d.ts +10 -0
  73. package/dist/core/rules/indexing.js +75 -0
  74. package/dist/core/rules/indexing.js.map +1 -0
  75. package/dist/core/rules/keyword.d.ts +1 -0
  76. package/dist/core/rules/keyword.js +111 -0
  77. package/dist/core/rules/keyword.js.map +1 -0
  78. package/dist/core/rules/links.d.ts +1 -0
  79. package/dist/core/rules/links.js +107 -0
  80. package/dist/core/rules/links.js.map +1 -0
  81. package/dist/core/rules/meta.d.ts +1 -0
  82. package/dist/core/rules/meta.js +81 -0
  83. package/dist/core/rules/meta.js.map +1 -0
  84. package/dist/core/rules/response.d.ts +1 -0
  85. package/dist/core/rules/response.js +93 -0
  86. package/dist/core/rules/response.js.map +1 -0
  87. package/dist/core/rules/rule.d.ts +42 -0
  88. package/dist/core/rules/rule.js +51 -0
  89. package/dist/core/rules/rule.js.map +1 -0
  90. package/dist/core/rules/social.d.ts +1 -0
  91. package/dist/core/rules/social.js +115 -0
  92. package/dist/core/rules/social.js.map +1 -0
  93. package/dist/core/rules/structured-data.d.ts +25 -0
  94. package/dist/core/rules/structured-data.js +381 -0
  95. package/dist/core/rules/structured-data.js.map +1 -0
  96. package/dist/core/rules/title.d.ts +1 -0
  97. package/dist/core/rules/title.js +74 -0
  98. package/dist/core/rules/title.js.map +1 -0
  99. package/dist/core/score.d.ts +3 -0
  100. package/dist/core/score.js +17 -0
  101. package/dist/core/score.js.map +1 -0
  102. package/dist/core/site-rules.d.ts +64 -0
  103. package/dist/core/site-rules.js +462 -0
  104. package/dist/core/site-rules.js.map +1 -0
  105. package/dist/core/sitemap.d.ts +13 -0
  106. package/dist/core/sitemap.js +140 -0
  107. package/dist/core/sitemap.js.map +1 -0
  108. package/dist/core/static.d.ts +9 -0
  109. package/dist/core/static.js +281 -0
  110. package/dist/core/static.js.map +1 -0
  111. package/dist/core/summary.d.ts +2 -0
  112. package/dist/core/summary.js +33 -0
  113. package/dist/core/summary.js.map +1 -0
  114. package/dist/core/types.d.ts +296 -0
  115. package/dist/core/types.js +33 -0
  116. package/dist/core/types.js.map +1 -0
  117. package/dist/core/url.d.ts +20 -0
  118. package/dist/core/url.js +191 -0
  119. package/dist/core/url.js.map +1 -0
  120. package/dist/index.d.ts +11 -0
  121. package/dist/index.js +10 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/mcp-bin.d.ts +2 -0
  124. package/dist/mcp-bin.js +10 -0
  125. package/dist/mcp-bin.js.map +1 -0
  126. package/dist/mcp.d.ts +5 -0
  127. package/dist/mcp.js +380 -0
  128. package/dist/mcp.js.map +1 -0
  129. package/dist/version.d.ts +1 -0
  130. package/dist/version.js +6 -0
  131. package/dist/version.js.map +1 -0
  132. package/package.json +107 -3
  133. package/skills/seo-audit/SKILL.md +53 -0
@@ -0,0 +1,107 @@
1
+ import { isGenericAnchor } from "../html/text.js";
2
+ import { cap, defineRule, GOOGLE_DOCS } from "./rule.js";
3
+ export const linkRules = [
4
+ defineRule({
5
+ id: "links.no-internal",
6
+ category: "links",
7
+ severity: "warning",
8
+ description: "The page has no internal links, so crawlers cannot reach other pages from it and it passes no authority onward.",
9
+ recommendation: "Link to related pages and the main navigation with plain <a href> elements.",
10
+ docs: GOOGLE_DOCS.links,
11
+ }, ({ page }, make) => page.links.some((l) => l.internal)
12
+ ? null
13
+ : make("Page contains no internal links.", {
14
+ evidence: { totalLinks: page.links.length },
15
+ })),
16
+ defineRule({
17
+ id: "links.empty-anchor",
18
+ category: "links",
19
+ severity: "warning",
20
+ description: "Links without any accessible name (no text, aria-label, title or image alt).",
21
+ recommendation: "Add descriptive link text, or aria-label for icon links. Empty links tell crawlers nothing about the target page.",
22
+ docs: GOOGLE_DOCS.links,
23
+ }, ({ page }, make) => {
24
+ const empty = page.links.filter((l) => l.url && !l.hasAccessibleName);
25
+ return empty.length
26
+ ? make(`${empty.length} link(s) have no accessible text.`, {
27
+ evidence: {
28
+ count: empty.length,
29
+ examples: cap(empty.map((l) => l.href)),
30
+ },
31
+ })
32
+ : null;
33
+ }),
34
+ defineRule({
35
+ id: "links.generic-anchor-text",
36
+ category: "links",
37
+ severity: "info",
38
+ description: 'Links whose text is generic ("click here", "read more", "learn more").',
39
+ recommendation: 'Use anchor text that describes the destination, e.g. "pricing plans" instead of "click here".',
40
+ docs: GOOGLE_DOCS.links,
41
+ }, ({ page }, make) => {
42
+ const generic = page.links.filter((l) => l.url && l.text && isGenericAnchor(l.text));
43
+ return generic.length
44
+ ? make(`${generic.length} link(s) use generic anchor text.`, {
45
+ evidence: {
46
+ count: generic.length,
47
+ examples: cap(generic.map((l) => ({ text: l.text, href: l.href }))),
48
+ },
49
+ })
50
+ : null;
51
+ }),
52
+ defineRule({
53
+ id: "links.uncrawlable-href",
54
+ category: "links",
55
+ severity: "info",
56
+ description: 'Anchors whose href is "#", empty, or a javascript: URL. Crawlers cannot follow them.',
57
+ recommendation: "Use <button> for actions. For navigation, give the anchor a real URL even when JavaScript intercepts the click.",
58
+ docs: GOOGLE_DOCS.links,
59
+ }, ({ page }, make) => {
60
+ const bad = page.links.filter((l) => l.href === "" || l.href === "#" || /^javascript:/iu.test(l.href));
61
+ return bad.length
62
+ ? make(`${bad.length} anchor(s) have an uncrawlable href.`, {
63
+ evidence: {
64
+ count: bad.length,
65
+ examples: cap(bad.map((l) => ({ href: l.href, text: l.text }))),
66
+ },
67
+ })
68
+ : null;
69
+ }),
70
+ defineRule({
71
+ id: "links.insecure-internal",
72
+ category: "links",
73
+ severity: "warning",
74
+ description: "Internal links use http:// while the page is served over https://.",
75
+ recommendation: "Update internal links to https:// (or use relative URLs) to avoid an extra redirect on every click and crawl.",
76
+ docs: GOOGLE_DOCS.redirects,
77
+ }, ({ page, source }, make) => {
78
+ if (source !== "url" || !page.url.startsWith("https://")) {
79
+ return null;
80
+ }
81
+ const insecure = page.links.filter((l) => l.internal && l.url?.startsWith("http://"));
82
+ return insecure.length
83
+ ? make(`${insecure.length} internal link(s) point to http:// URLs.`, {
84
+ evidence: {
85
+ count: insecure.length,
86
+ examples: cap(insecure.map((l) => l.url)),
87
+ },
88
+ })
89
+ : null;
90
+ }),
91
+ defineRule({
92
+ id: "links.nofollow-internal",
93
+ category: "links",
94
+ severity: "info",
95
+ description: 'Internal links carry rel="nofollow".',
96
+ recommendation: "Remove nofollow from internal links unless the target is intentionally kept out of the index; nofollow on internal links wastes crawl signals.",
97
+ docs: GOOGLE_DOCS.links,
98
+ }, ({ page }, make) => {
99
+ const nf = page.links.filter((l) => l.internal && l.nofollow);
100
+ return nf.length
101
+ ? make(`${nf.length} internal link(s) are rel="nofollow".`, {
102
+ evidence: { count: nf.length, examples: cap(nf.map((l) => l.url)) },
103
+ })
104
+ : null;
105
+ }),
106
+ ];
107
+ //# sourceMappingURL=links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.js","sourceRoot":"","sources":["../../../src/core/rules/links.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare const metaRules: import("./rule.js").Rule[];
@@ -0,0 +1,81 @@
1
+ import { normalizeForComparison } from "../html/text.js";
2
+ import { defineRule, GOOGLE_DOCS } from "./rule.js";
3
+ const SELECTOR = 'head > meta[name="description"]';
4
+ export const metaRules = [
5
+ defineRule({
6
+ id: "meta-description.missing",
7
+ category: "meta",
8
+ severity: "warning",
9
+ description: 'No <meta name="description"> is present or it is empty.',
10
+ recommendation: "Add a unique meta description (about 70-160 characters) that summarises the page and invites the click. Search engines may show it as the snippet.",
11
+ docs: GOOGLE_DOCS.snippets,
12
+ }, ({ page }, make) => page.metaDescription
13
+ ? null
14
+ : make("Page has no meta description.", { selector: SELECTOR })),
15
+ defineRule({
16
+ id: "meta-description.multiple",
17
+ category: "meta",
18
+ severity: "warning",
19
+ description: "More than one meta description is present.",
20
+ recommendation: "Keep a single meta description; remove or merge the others.",
21
+ docs: GOOGLE_DOCS.snippets,
22
+ }, ({ page }, make) => page.metaDescriptions.length > 1
23
+ ? make(`Page has ${page.metaDescriptions.length} meta description tags.`, {
24
+ selector: SELECTOR,
25
+ evidence: { descriptions: page.metaDescriptions },
26
+ })
27
+ : null),
28
+ defineRule({
29
+ id: "meta-description.too-long",
30
+ category: "meta",
31
+ severity: "warning",
32
+ description: "Meta description exceeds the configured maximum (default 160 characters) and will be truncated.",
33
+ recommendation: "Trim the description to about 160 characters, front-loading the key message.",
34
+ docs: GOOGLE_DOCS.snippets,
35
+ }, ({ page, thresholds }, make) => page.metaDescription &&
36
+ page.metaDescription.length > thresholds.descriptionMaxChars
37
+ ? make(`Meta description is ${page.metaDescription.length} characters (max ${thresholds.descriptionMaxChars}).`, {
38
+ selector: SELECTOR,
39
+ evidence: {
40
+ description: page.metaDescription,
41
+ length: page.metaDescription.length,
42
+ max: thresholds.descriptionMaxChars,
43
+ },
44
+ })
45
+ : null),
46
+ defineRule({
47
+ id: "meta-description.too-short",
48
+ category: "meta",
49
+ severity: "info",
50
+ description: "Meta description is shorter than the configured minimum (default 70 characters).",
51
+ recommendation: "Expand the description so it summarises the page content and its value to the visitor.",
52
+ docs: GOOGLE_DOCS.snippets,
53
+ }, ({ page, thresholds }, make) => page.metaDescription &&
54
+ page.metaDescription.length < thresholds.descriptionMinChars
55
+ ? make(`Meta description is only ${page.metaDescription.length} characters (recommended at least ${thresholds.descriptionMinChars}).`, {
56
+ selector: SELECTOR,
57
+ evidence: {
58
+ description: page.metaDescription,
59
+ length: page.metaDescription.length,
60
+ min: thresholds.descriptionMinChars,
61
+ },
62
+ })
63
+ : null),
64
+ defineRule({
65
+ id: "meta-description.same-as-title",
66
+ category: "meta",
67
+ severity: "info",
68
+ description: "Meta description is identical to the title.",
69
+ recommendation: "Write a description that adds information beyond the title instead of repeating it.",
70
+ docs: GOOGLE_DOCS.snippets,
71
+ }, ({ page }, make) => page.metaDescription &&
72
+ page.title &&
73
+ normalizeForComparison(page.metaDescription) ===
74
+ normalizeForComparison(page.title)
75
+ ? make("Meta description repeats the title verbatim.", {
76
+ selector: SELECTOR,
77
+ evidence: { title: page.title },
78
+ })
79
+ : null),
80
+ ];
81
+ //# sourceMappingURL=meta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta.js","sourceRoot":"","sources":["../../../src/core/rules/meta.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare const responseRules: import("./rule.js").Rule[];
@@ -0,0 +1,93 @@
1
+ import { isLocalHost } from "./canonical.js";
2
+ import { defineRule, GOOGLE_DOCS } from "./rule.js";
3
+ export const responseRules = [
4
+ defineRule({
5
+ id: "response.error-status",
6
+ category: "response",
7
+ severity: "error",
8
+ description: "The URL returned a 4xx or 5xx status code.",
9
+ recommendation: "Fix the route or server error. If the page was removed on purpose, return 410 (or 404) and remove internal links and sitemap entries pointing to it.",
10
+ }, ({ response }, make) => response?.status && response.status >= 400
11
+ ? make(`URL returned HTTP ${response.status}.`, {
12
+ evidence: { status: response.status, finalUrl: response.finalUrl },
13
+ })
14
+ : null),
15
+ defineRule({
16
+ id: "response.redirected",
17
+ category: "response",
18
+ severity: "info",
19
+ description: "The requested URL redirected to another URL.",
20
+ recommendation: "Link directly to the final URL (internal links, sitemap, canonical) so crawlers and users skip the redirect.",
21
+ docs: GOOGLE_DOCS.redirects,
22
+ }, ({ response }, make) => {
23
+ const [hop] = response?.redirectChain ?? [];
24
+ return response && hop && response.redirectChain.length === 1
25
+ ? make(`Request redirected (${hop.status}) to ${response.finalUrl}.`, {
26
+ evidence: {
27
+ chain: response.redirectChain,
28
+ finalUrl: response.finalUrl,
29
+ },
30
+ })
31
+ : null;
32
+ }),
33
+ defineRule({
34
+ id: "response.redirect-chain",
35
+ category: "response",
36
+ severity: "warning",
37
+ description: "The URL goes through two or more redirects before reaching the final page.",
38
+ recommendation: "Collapse the chain so the first URL redirects straight to the final destination. Each hop costs crawl budget and loses some signal.",
39
+ docs: GOOGLE_DOCS.redirects,
40
+ }, ({ response }, make) => response && response.redirectChain.length >= 2
41
+ ? make(`Redirect chain of ${response.redirectChain.length} hops ends at ${response.finalUrl}.`, {
42
+ evidence: {
43
+ chain: response.redirectChain,
44
+ finalUrl: response.finalUrl,
45
+ },
46
+ })
47
+ : null),
48
+ defineRule({
49
+ id: "response.slow",
50
+ category: "response",
51
+ severity: "warning",
52
+ description: "The HTML response took longer than the configured threshold (default 2000 ms).",
53
+ recommendation: "Reduce server response time: cache the HTML, avoid blocking data fetches during render, use a CDN. Google recommends TTFB under 800 ms.",
54
+ docs: GOOGLE_DOCS.cwv,
55
+ }, ({ response, thresholds }, make) => response?.timeMs !== null &&
56
+ response?.timeMs !== undefined &&
57
+ response.timeMs > thresholds.slowResponseMs
58
+ ? make(`HTML response took ${response.timeMs} ms (single measurement; threshold ${thresholds.slowResponseMs} ms).`, { evidence: { timeMs: response.timeMs } })
59
+ : null),
60
+ defineRule({
61
+ id: "response.not-html",
62
+ category: "response",
63
+ severity: "warning",
64
+ description: "The response Content-Type is not HTML.",
65
+ recommendation: "Serve pages with Content-Type: text/html; charset=utf-8.",
66
+ }, ({ response }, make) => {
67
+ const contentType = response?.contentType;
68
+ if (!contentType) {
69
+ return null;
70
+ }
71
+ const ct = contentType.toLowerCase();
72
+ return !ct.includes("html") && !ct.includes("xhtml")
73
+ ? make(`Content-Type is "${contentType}", not HTML.`, {
74
+ evidence: { contentType },
75
+ })
76
+ : null;
77
+ }),
78
+ defineRule({
79
+ id: "response.not-https",
80
+ category: "response",
81
+ severity: "warning",
82
+ description: "The final URL is served over plain http.",
83
+ recommendation: "Serve the site over https and redirect http to https. HTTPS is a (light) ranking signal and required for many browser features.",
84
+ }, ({ response, source }, make) => source === "url" &&
85
+ response &&
86
+ response.finalUrl.startsWith("http://") &&
87
+ !isLocalHost(response.finalUrl)
88
+ ? make("Page is served over http:// instead of https://.", {
89
+ evidence: { finalUrl: response.finalUrl },
90
+ })
91
+ : null),
92
+ ];
93
+ //# sourceMappingURL=response.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response.js","sourceRoot":"","sources":["../../../src/core/rules/response.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import type { ExtractedPage, Finding, PageSource, ResponseInfo, RuleMeta, Severity, Thresholds } from "../types.js";
2
+ export interface PageContext {
3
+ page: ExtractedPage;
4
+ source: PageSource;
5
+ response?: ResponseInfo;
6
+ keyword?: string;
7
+ thresholds: Thresholds;
8
+ depth?: number;
9
+ }
10
+ export interface Rule {
11
+ meta: RuleMeta;
12
+ check: (ctx: PageContext) => Finding[];
13
+ }
14
+ export interface FindingExtras {
15
+ severity?: Severity;
16
+ recommendation?: string;
17
+ selector?: string;
18
+ evidence?: Finding["evidence"];
19
+ }
20
+ /** Build a finding from rule metadata, overriding only what differs for this instance. */
21
+ export declare const finding: (meta: RuleMeta, message: string, extras?: FindingExtras) => Finding;
22
+ export declare const defineRule: (meta: RuleMeta, check: (ctx: PageContext, make: (message: string, extras?: FindingExtras) => Finding) => Finding[] | Finding | null | undefined) => Rule;
23
+ export declare const cap: <T>(items: T[], max?: number) => T[];
24
+ export declare const GOOGLE_DOCS: {
25
+ readonly titles: "https://developers.google.com/search/docs/appearance/title-link";
26
+ readonly snippets: "https://developers.google.com/search/docs/appearance/snippet";
27
+ readonly canonical: "https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls";
28
+ readonly robotsMeta: "https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag";
29
+ readonly robotsTxt: "https://developers.google.com/search/docs/crawling-indexing/robots/intro";
30
+ readonly sitemaps: "https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview";
31
+ readonly hreflang: "https://developers.google.com/search/docs/specialty/international/localized-versions";
32
+ readonly images: "https://developers.google.com/search/docs/appearance/google-images";
33
+ readonly links: "https://developers.google.com/search/docs/crawling-indexing/links-crawlable";
34
+ readonly structuredData: "https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data";
35
+ readonly javascript: "https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics";
36
+ readonly cwv: "https://web.dev/articles/vitals";
37
+ readonly mobile: "https://developers.google.com/search/docs/crawling-indexing/mobile/mobile-sites-mobile-first-indexing";
38
+ readonly redirects: "https://developers.google.com/search/docs/crawling-indexing/301-redirects";
39
+ readonly softs404: "https://developers.google.com/search/docs/crawling-indexing/http-network-errors#soft-404-errors";
40
+ readonly og: "https://ogp.me/";
41
+ readonly twitter: "https://developer.x.com/en/docs/x-for-websites/cards/overview/markup";
42
+ };
@@ -0,0 +1,51 @@
1
+ /** Build a finding from rule metadata, overriding only what differs for this instance. */
2
+ export const finding = (meta, message, extras = {}) => {
3
+ const f = {
4
+ rule: meta.id,
5
+ severity: extras.severity ?? meta.severity,
6
+ category: meta.category,
7
+ message,
8
+ recommendation: extras.recommendation ?? meta.recommendation,
9
+ };
10
+ if (extras.selector) {
11
+ f.selector = extras.selector;
12
+ }
13
+ if (extras.evidence) {
14
+ f.evidence = extras.evidence;
15
+ }
16
+ if (meta.docs) {
17
+ f.docs = meta.docs;
18
+ }
19
+ return f;
20
+ };
21
+ export const defineRule = (meta, check) => ({
22
+ meta,
23
+ check(ctx) {
24
+ const result = check(ctx, (message, extras) => finding(meta, message, extras));
25
+ if (!result) {
26
+ return [];
27
+ }
28
+ return Array.isArray(result) ? result : [result];
29
+ },
30
+ });
31
+ export const cap = (items, max = 10) => items.length > max ? items.slice(0, max) : items;
32
+ export const GOOGLE_DOCS = {
33
+ titles: "https://developers.google.com/search/docs/appearance/title-link",
34
+ snippets: "https://developers.google.com/search/docs/appearance/snippet",
35
+ canonical: "https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls",
36
+ robotsMeta: "https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag",
37
+ robotsTxt: "https://developers.google.com/search/docs/crawling-indexing/robots/intro",
38
+ sitemaps: "https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview",
39
+ hreflang: "https://developers.google.com/search/docs/specialty/international/localized-versions",
40
+ images: "https://developers.google.com/search/docs/appearance/google-images",
41
+ links: "https://developers.google.com/search/docs/crawling-indexing/links-crawlable",
42
+ structuredData: "https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data",
43
+ javascript: "https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics",
44
+ cwv: "https://web.dev/articles/vitals",
45
+ mobile: "https://developers.google.com/search/docs/crawling-indexing/mobile/mobile-sites-mobile-first-indexing",
46
+ redirects: "https://developers.google.com/search/docs/crawling-indexing/301-redirects",
47
+ softs404: "https://developers.google.com/search/docs/crawling-indexing/http-network-errors#soft-404-errors",
48
+ og: "https://ogp.me/",
49
+ twitter: "https://developer.x.com/en/docs/x-for-websites/cards/overview/markup",
50
+ };
51
+ //# sourceMappingURL=rule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule.js","sourceRoot":"","sources":["../../../src/core/rules/rule.ts"],"names":[],"mappings":"AA+BA,0FAA0F;AAC1F,MAAM,CAAC,MAAM,OAAO,GAAG,CACrB,IAAc,EACd,OAAe,EACf,MAAM,GAAkB,EAAE,EACjB,EAAE;IACX,MAAM,CAAC,GAAY;QACjB,IAAI,EAAE,IAAI,CAAC,EAAE;QACb,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ;QAC1C,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,OAAO;QACP,cAAc,EAAE,MAAM,CAAC,cAAc,IAAI,IAAI,CAAC,cAAc;KAC7D,CAAC;IAEF,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,CAAC,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IAC/B,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,CAAC,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IAC/B,CAAC;IAED,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IACrB,CAAC;IAED,OAAO,CAAC,CAAC;AACX,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,CACxB,IAAc,EACd,KAG2C,EACrC,EAAE,CAAC,CAAC;IACV,IAAI;IACJ,KAAK,CAAC,GAAG;QACP,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAC5C,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAC/B,CAAC;QAEF,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IACnD,CAAC;CACF,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,GAAG,GAAG,CAAI,KAAU,EAAE,GAAG,GAAG,EAAE,EAAO,EAAE,CAClD,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AAEnD,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,MAAM,EAAE,iEAAiE;IACzE,QAAQ,EAAE,8DAA8D;IACxE,SAAS,EACP,wFAAwF;IAC1F,UAAU,EACR,6EAA6E;IAC/E,SAAS,EACP,0EAA0E;IAC5E,QAAQ,EACN,+EAA+E;IACjF,QAAQ,EACN,sFAAsF;IACxF,MAAM,EAAE,oEAAoE;IAC5E,KAAK,EACH,6EAA6E;IAC/E,cAAc,EACZ,4FAA4F;IAC9F,UAAU,EACR,8FAA8F;IAChG,GAAG,EAAE,iCAAiC;IACtC,MAAM,EACJ,uGAAuG;IACzG,SAAS,EACP,2EAA2E;IAC7E,QAAQ,EACN,iGAAiG;IACnG,EAAE,EAAE,iBAAiB;IACrB,OAAO,EACL,sEAAsE;CAChE,CAAC"}
@@ -0,0 +1 @@
1
+ export declare const socialRules: import("./rule.js").Rule[];
@@ -0,0 +1,115 @@
1
+ import { normalizeUrl } from "../url.js";
2
+ import { defineRule, GOOGLE_DOCS } from "./rule.js";
3
+ const OG_CORE = ["title", "description", "image"];
4
+ const OG_RECOMMENDED = [
5
+ "title",
6
+ "description",
7
+ "image",
8
+ "url",
9
+ "type",
10
+ ];
11
+ const TWITTER_CARDS = new Set([
12
+ "summary",
13
+ "summary_large_image",
14
+ "app",
15
+ "player",
16
+ ]);
17
+ export const socialRules = [
18
+ defineRule({
19
+ id: "og.missing",
20
+ category: "social",
21
+ severity: "warning",
22
+ description: "No Open Graph tags (og:title, og:description, og:image) are present.",
23
+ recommendation: "Add og:title, og:description, og:image (absolute URL, at least 1200x630), og:url and og:type so shares on social platforms and chat apps render a rich preview.",
24
+ docs: GOOGLE_DOCS.og,
25
+ }, ({ page }, make) => OG_CORE.some((k) => page.og[k])
26
+ ? null
27
+ : make("Page has no Open Graph tags.", {
28
+ selector: 'head > meta[property^="og:"]',
29
+ })),
30
+ defineRule({
31
+ id: "og.incomplete",
32
+ category: "social",
33
+ severity: "info",
34
+ description: "Some Open Graph tags are present but one or more of og:title, og:description, og:image, og:url, og:type is missing.",
35
+ recommendation: "Fill in the missing Open Graph properties. og:image and og:description have the biggest effect on share previews.",
36
+ docs: GOOGLE_DOCS.og,
37
+ }, ({ page }, make) => {
38
+ if (!OG_CORE.some((k) => page.og[k])) {
39
+ return null;
40
+ }
41
+ const missing = OG_RECOMMENDED.filter((k) => !page.og[k]);
42
+ return missing.length
43
+ ? make(`Open Graph is missing: ${missing.map((m) => `og:${m}`).join(", ")}.`, {
44
+ selector: 'head > meta[property^="og:"]',
45
+ evidence: { missing, present: Object.keys(page.og) },
46
+ })
47
+ : null;
48
+ }),
49
+ defineRule({
50
+ id: "og.image-not-absolute",
51
+ category: "social",
52
+ severity: "warning",
53
+ description: "og:image is not an absolute URL.",
54
+ recommendation: "Use a full https:// URL for og:image; most platforms ignore relative image paths.",
55
+ docs: GOOGLE_DOCS.og,
56
+ }, ({ page }, make) => {
57
+ const img = page.og["image"];
58
+ return img && !/^https?:\/\//iu.test(img)
59
+ ? make(`og:image "${img}" is not an absolute URL.`, {
60
+ selector: 'head > meta[property="og:image"]',
61
+ evidence: { image: img },
62
+ })
63
+ : null;
64
+ }),
65
+ defineRule({
66
+ id: "og.url-mismatch",
67
+ category: "social",
68
+ severity: "info",
69
+ description: "og:url differs from the canonical URL.",
70
+ recommendation: "Set og:url to the same URL as the canonical so share counts and previews consolidate.",
71
+ docs: GOOGLE_DOCS.og,
72
+ }, ({ page }, make) => {
73
+ const ogUrl = page.og["url"];
74
+ if (!ogUrl || !page.canonical) {
75
+ return null;
76
+ }
77
+ const a = normalizeUrl(ogUrl);
78
+ const b = normalizeUrl(page.canonical);
79
+ return a && b && a !== b
80
+ ? make("og:url does not match the canonical URL.", {
81
+ selector: 'head > meta[property="og:url"]',
82
+ evidence: { ogUrl, canonical: page.canonical },
83
+ })
84
+ : null;
85
+ }),
86
+ defineRule({
87
+ id: "twitter.card-missing",
88
+ category: "social",
89
+ severity: "info",
90
+ description: "No twitter:card meta tag is present.",
91
+ recommendation: 'Add <meta name="twitter:card" content="summary_large_image">; X/Twitter falls back to Open Graph for the other fields.',
92
+ docs: GOOGLE_DOCS.twitter,
93
+ }, ({ page }, make) => page.twitter["card"]
94
+ ? null
95
+ : make("Page has no twitter:card tag.", {
96
+ selector: 'head > meta[name="twitter:card"]',
97
+ })),
98
+ defineRule({
99
+ id: "twitter.card-invalid",
100
+ category: "social",
101
+ severity: "warning",
102
+ description: "twitter:card has a value other than summary, summary_large_image, app or player.",
103
+ recommendation: "Use one of the supported card types, usually summary_large_image.",
104
+ docs: GOOGLE_DOCS.twitter,
105
+ }, ({ page }, make) => {
106
+ const { card } = page.twitter;
107
+ return card && !TWITTER_CARDS.has(card.toLowerCase())
108
+ ? make(`twitter:card "${card}" is not a valid card type.`, {
109
+ selector: 'head > meta[name="twitter:card"]',
110
+ evidence: { card },
111
+ })
112
+ : null;
113
+ }),
114
+ ];
115
+ //# sourceMappingURL=social.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"social.js","sourceRoot":"","sources":["../../../src/core/rules/social.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { StructuredDataItem } from "../types.js";
2
+ interface SchemaSpec {
3
+ required: string[];
4
+ recommended: string[];
5
+ /** At least one of these groups must be present (each group = list of alternatives). */
6
+ oneOf?: string[][];
7
+ docs?: string;
8
+ }
9
+ export declare const SCHEMA_SPECS: Record<string, SchemaSpec>;
10
+ interface Issue {
11
+ type: string;
12
+ missing: string[];
13
+ path: string;
14
+ }
15
+ /** What one structured-data item lacks or gets wrong, against the specs above. */
16
+ export interface ItemValidation {
17
+ required: Issue[];
18
+ recommended: Issue[];
19
+ badDates: string[];
20
+ unknownTypes: string[];
21
+ }
22
+ /** Walk one structured-data item (and nested typed objects) collecting spec violations. */
23
+ export declare const validateItem: (item: StructuredDataItem) => ItemValidation;
24
+ export declare const structuredDataRules: import("./rule.js").Rule[];
25
+ export {};