@rivi-software/agentic-seo-check 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +160 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +149 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/audit.d.ts +72 -0
- package/dist/core/audit.js +337 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/concurrency.d.ts +4 -0
- package/dist/core/concurrency.js +29 -0
- package/dist/core/concurrency.js.map +1 -0
- package/dist/core/crawl.d.ts +40 -0
- package/dist/core/crawl.js +195 -0
- package/dist/core/crawl.js.map +1 -0
- package/dist/core/fetch.d.ts +27 -0
- package/dist/core/fetch.js +135 -0
- package/dist/core/fetch.js.map +1 -0
- package/dist/core/html/extract.d.ts +10 -0
- package/dist/core/html/extract.js +150 -0
- package/dist/core/html/extract.js.map +1 -0
- package/dist/core/html/structured-data.d.ts +11 -0
- package/dist/core/html/structured-data.js +185 -0
- package/dist/core/html/structured-data.js.map +1 -0
- package/dist/core/html/text.d.ts +23 -0
- package/dist/core/html/text.js +149 -0
- package/dist/core/html/text.js.map +1 -0
- package/dist/core/lighthouse.d.ts +49 -0
- package/dist/core/lighthouse.js +140 -0
- package/dist/core/lighthouse.js.map +1 -0
- package/dist/core/performance-rules.d.ts +3 -0
- package/dist/core/performance-rules.js +53 -0
- package/dist/core/performance-rules.js.map +1 -0
- package/dist/core/render-rules.d.ts +7 -0
- package/dist/core/render-rules.js +77 -0
- package/dist/core/render-rules.js.map +1 -0
- package/dist/core/render.d.ts +22 -0
- package/dist/core/render.js +73 -0
- package/dist/core/render.js.map +1 -0
- package/dist/core/report/markdown.d.ts +2 -0
- package/dist/core/report/markdown.js +152 -0
- package/dist/core/report/markdown.js.map +1 -0
- package/dist/core/robots.d.ts +11 -0
- package/dist/core/robots.js +33 -0
- package/dist/core/robots.js.map +1 -0
- package/dist/core/rules/canonical.d.ts +2 -0
- package/dist/core/rules/canonical.js +105 -0
- package/dist/core/rules/canonical.js.map +1 -0
- package/dist/core/rules/content.d.ts +1 -0
- package/dist/core/rules/content.js +41 -0
- package/dist/core/rules/content.js.map +1 -0
- package/dist/core/rules/document.d.ts +1 -0
- package/dist/core/rules/document.js +60 -0
- package/dist/core/rules/document.js.map +1 -0
- package/dist/core/rules/headings.d.ts +1 -0
- package/dist/core/rules/headings.js +61 -0
- package/dist/core/rules/headings.js.map +1 -0
- package/dist/core/rules/hreflang.d.ts +1 -0
- package/dist/core/rules/hreflang.js +68 -0
- package/dist/core/rules/hreflang.js.map +1 -0
- package/dist/core/rules/images.d.ts +1 -0
- package/dist/core/rules/images.js +59 -0
- package/dist/core/rules/images.js.map +1 -0
- package/dist/core/rules/index.d.ts +14 -0
- package/dist/core/rules/index.js +76 -0
- package/dist/core/rules/index.js.map +1 -0
- package/dist/core/rules/indexing.d.ts +10 -0
- package/dist/core/rules/indexing.js +53 -0
- package/dist/core/rules/indexing.js.map +1 -0
- package/dist/core/rules/keyword.d.ts +1 -0
- package/dist/core/rules/keyword.js +74 -0
- package/dist/core/rules/keyword.js.map +1 -0
- package/dist/core/rules/links.d.ts +1 -0
- package/dist/core/rules/links.js +72 -0
- package/dist/core/rules/links.js.map +1 -0
- package/dist/core/rules/meta.d.ts +1 -0
- package/dist/core/rules/meta.js +60 -0
- package/dist/core/rules/meta.js.map +1 -0
- package/dist/core/rules/response.d.ts +1 -0
- package/dist/core/rules/response.js +59 -0
- package/dist/core/rules/response.js.map +1 -0
- package/dist/core/rules/rule.d.ts +42 -0
- package/dist/core/rules/rule.js +51 -0
- package/dist/core/rules/rule.js.map +1 -0
- package/dist/core/rules/social.d.ts +1 -0
- package/dist/core/rules/social.js +74 -0
- package/dist/core/rules/social.js.map +1 -0
- package/dist/core/rules/structured-data.d.ts +23 -0
- package/dist/core/rules/structured-data.js +169 -0
- package/dist/core/rules/structured-data.js.map +1 -0
- package/dist/core/rules/title.d.ts +1 -0
- package/dist/core/rules/title.js +59 -0
- package/dist/core/rules/title.js.map +1 -0
- package/dist/core/score.d.ts +3 -0
- package/dist/core/score.js +15 -0
- package/dist/core/score.js.map +1 -0
- package/dist/core/site-rules.d.ts +64 -0
- package/dist/core/site-rules.js +251 -0
- package/dist/core/site-rules.js.map +1 -0
- package/dist/core/sitemap.d.ts +13 -0
- package/dist/core/sitemap.js +95 -0
- package/dist/core/sitemap.js.map +1 -0
- package/dist/core/static.d.ts +9 -0
- package/dist/core/static.js +184 -0
- package/dist/core/static.js.map +1 -0
- package/dist/core/summary.d.ts +2 -0
- package/dist/core/summary.js +28 -0
- package/dist/core/summary.js.map +1 -0
- package/dist/core/types.d.ts +296 -0
- package/dist/core/types.js +26 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/url.d.ts +20 -0
- package/dist/core/url.js +139 -0
- package/dist/core/url.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-bin.d.ts +2 -0
- package/dist/mcp-bin.js +7 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/mcp.d.ts +5 -0
- package/dist/mcp.js +190 -0
- package/dist/mcp.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/package.json +89 -3
- package/skills/seo-audit/SKILL.md +53 -0
|
@@ -0,0 +1,59 @@
|
|
|
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 ? make(`URL returned HTTP ${response.status}.`, { evidence: { status: response.status, finalUrl: response.finalUrl } }) : null)),
|
|
11
|
+
defineRule({
|
|
12
|
+
id: 'response.redirected',
|
|
13
|
+
category: 'response',
|
|
14
|
+
severity: 'info',
|
|
15
|
+
description: 'The requested URL redirected to another URL.',
|
|
16
|
+
recommendation: 'Link directly to the final URL (internal links, sitemap, canonical) so crawlers and users skip the redirect.',
|
|
17
|
+
docs: GOOGLE_DOCS.redirects,
|
|
18
|
+
}, ({ response }, make) => response && response.redirectChain.length === 1
|
|
19
|
+
? make(`Request redirected (${response.redirectChain[0].status}) to ${response.finalUrl}.`, { evidence: { chain: response.redirectChain, finalUrl: response.finalUrl } })
|
|
20
|
+
: null),
|
|
21
|
+
defineRule({
|
|
22
|
+
id: 'response.redirect-chain',
|
|
23
|
+
category: 'response',
|
|
24
|
+
severity: 'warning',
|
|
25
|
+
description: 'The URL goes through two or more redirects before reaching the final page.',
|
|
26
|
+
recommendation: 'Collapse the chain so the first URL redirects straight to the final destination. Each hop costs crawl budget and loses some signal.',
|
|
27
|
+
docs: GOOGLE_DOCS.redirects,
|
|
28
|
+
}, ({ response }, make) => response && response.redirectChain.length >= 2
|
|
29
|
+
? make(`Redirect chain of ${response.redirectChain.length} hops ends at ${response.finalUrl}.`, { evidence: { chain: response.redirectChain, finalUrl: response.finalUrl } })
|
|
30
|
+
: null),
|
|
31
|
+
defineRule({
|
|
32
|
+
id: 'response.slow',
|
|
33
|
+
category: 'response',
|
|
34
|
+
severity: 'warning',
|
|
35
|
+
description: 'The HTML response took longer than the configured threshold (default 2000 ms).',
|
|
36
|
+
recommendation: 'Reduce server response time: cache the HTML, avoid blocking data fetches during render, use a CDN. Google recommends TTFB under 800 ms.',
|
|
37
|
+
docs: GOOGLE_DOCS.cwv,
|
|
38
|
+
}, ({ response, thresholds }, make) => response?.timeMs !== null && response?.timeMs !== undefined && response.timeMs > thresholds.slowResponseMs
|
|
39
|
+
? make(`HTML response took ${response.timeMs} ms (single measurement; threshold ${thresholds.slowResponseMs} ms).`, { evidence: { timeMs: response.timeMs } })
|
|
40
|
+
: null),
|
|
41
|
+
defineRule({
|
|
42
|
+
id: 'response.not-html',
|
|
43
|
+
category: 'response',
|
|
44
|
+
severity: 'warning',
|
|
45
|
+
description: 'The response Content-Type is not HTML.',
|
|
46
|
+
recommendation: 'Serve pages with Content-Type: text/html; charset=utf-8.',
|
|
47
|
+
}, ({ response }, make) => {
|
|
48
|
+
const ct = response?.contentType?.toLowerCase();
|
|
49
|
+
return ct && !ct.includes('html') && !ct.includes('xhtml') ? make(`Content-Type is "${response.contentType}", not HTML.`, { evidence: { contentType: response.contentType } }) : null;
|
|
50
|
+
}),
|
|
51
|
+
defineRule({
|
|
52
|
+
id: 'response.not-https',
|
|
53
|
+
category: 'response',
|
|
54
|
+
severity: 'warning',
|
|
55
|
+
description: 'The final URL is served over plain http.',
|
|
56
|
+
recommendation: 'Serve the site over https and redirect http to https. HTTPS is a (light) ranking signal and required for many browser features.',
|
|
57
|
+
}, ({ response, source }, make) => source === 'url' && response && response.finalUrl.startsWith('http://') && !isLocalHost(response.finalUrl) ? make('Page is served over http:// instead of https://.', { evidence: { finalUrl: response.finalUrl } }) : null),
|
|
58
|
+
];
|
|
59
|
+
//# 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?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
/** Build a finding from rule metadata, overriding only what differs for this instance. */
|
|
21
|
+
export declare function finding(meta: RuleMeta, message: string, extras?: FindingExtras): Finding;
|
|
22
|
+
export declare function defineRule(meta: RuleMeta, check: (ctx: PageContext, make: (message: string, extras?: FindingExtras) => Finding) => Finding[] | Finding | null | undefined | void): Rule;
|
|
23
|
+
export declare function 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 function 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
|
+
if (extras.evidence)
|
|
13
|
+
f.evidence = extras.evidence;
|
|
14
|
+
if (meta.docs)
|
|
15
|
+
f.docs = meta.docs;
|
|
16
|
+
return f;
|
|
17
|
+
}
|
|
18
|
+
export function defineRule(meta, check) {
|
|
19
|
+
return {
|
|
20
|
+
meta,
|
|
21
|
+
check(ctx) {
|
|
22
|
+
const result = check(ctx, (message, extras) => finding(meta, message, extras));
|
|
23
|
+
if (!result)
|
|
24
|
+
return [];
|
|
25
|
+
return Array.isArray(result) ? result : [result];
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function cap(items, max = 10) {
|
|
30
|
+
return items.length > max ? items.slice(0, max) : items;
|
|
31
|
+
}
|
|
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":"AAuBA,0FAA0F;AAC1F,MAAM,UAAU,OAAO,CAAC,IAAc,EAAE,OAAe,EAAE,SAAwB,EAAE;IACjF,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;IACF,IAAI,MAAM,CAAC,QAAQ;QAAE,CAAC,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IAClD,IAAI,MAAM,CAAC,QAAQ;QAAE,CAAC,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IAClD,IAAI,IAAI,CAAC,IAAI;QAAE,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IAClC,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,UAAU,UAAU,CACxB,IAAc,EACd,KAAsI;IAEtI,OAAO;QACL,IAAI;QACJ,KAAK,CAAC,GAAG;YACP,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,MAAM;gBAAE,OAAO,EAAE,CAAC;YACvB,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QACnD,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,GAAG,CAAI,KAAU,EAAE,GAAG,GAAG,EAAE;IACzC,OAAO,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AAC1D,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,MAAM,EAAE,iEAAiE;IACzE,QAAQ,EAAE,8DAA8D;IACxE,SAAS,EAAE,wFAAwF;IACnG,UAAU,EAAE,6EAA6E;IACzF,SAAS,EAAE,0EAA0E;IACrF,QAAQ,EAAE,+EAA+E;IACzF,QAAQ,EAAE,sFAAsF;IAChG,MAAM,EAAE,oEAAoE;IAC5E,KAAK,EAAE,6EAA6E;IACpF,cAAc,EAAE,4FAA4F;IAC5G,UAAU,EAAE,8FAA8F;IAC1G,GAAG,EAAE,iCAAiC;IACtC,MAAM,EAAE,uGAAuG;IAC/G,SAAS,EAAE,2EAA2E;IACtF,QAAQ,EAAE,iGAAiG;IAC3G,EAAE,EAAE,iBAAiB;IACrB,OAAO,EAAE,sEAAsE;CACvE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const socialRules: import("./rule.js").Rule[];
|
|
@@ -0,0 +1,74 @@
|
|
|
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 = ['title', 'description', 'image', 'url', 'type'];
|
|
5
|
+
const TWITTER_CARDS = new Set(['summary', 'summary_large_image', 'app', 'player']);
|
|
6
|
+
export const socialRules = [
|
|
7
|
+
defineRule({
|
|
8
|
+
id: 'og.missing',
|
|
9
|
+
category: 'social',
|
|
10
|
+
severity: 'warning',
|
|
11
|
+
description: 'No Open Graph tags (og:title, og:description, og:image) are present.',
|
|
12
|
+
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.',
|
|
13
|
+
docs: GOOGLE_DOCS.og,
|
|
14
|
+
}, ({ page }, make) => (OG_CORE.some((k) => page.og[k]) ? null : make('Page has no Open Graph tags.', { selector: 'head > meta[property^="og:"]' }))),
|
|
15
|
+
defineRule({
|
|
16
|
+
id: 'og.incomplete',
|
|
17
|
+
category: 'social',
|
|
18
|
+
severity: 'info',
|
|
19
|
+
description: 'Some Open Graph tags are present but one or more of og:title, og:description, og:image, og:url, og:type is missing.',
|
|
20
|
+
recommendation: 'Fill in the missing Open Graph properties. og:image and og:description have the biggest effect on share previews.',
|
|
21
|
+
docs: GOOGLE_DOCS.og,
|
|
22
|
+
}, ({ page }, make) => {
|
|
23
|
+
if (!OG_CORE.some((k) => page.og[k]))
|
|
24
|
+
return null;
|
|
25
|
+
const missing = OG_RECOMMENDED.filter((k) => !page.og[k]);
|
|
26
|
+
return missing.length ? make(`Open Graph is missing: ${missing.map((m) => `og:${m}`).join(', ')}.`, { selector: 'head > meta[property^="og:"]', evidence: { missing, present: Object.keys(page.og) } }) : null;
|
|
27
|
+
}),
|
|
28
|
+
defineRule({
|
|
29
|
+
id: 'og.image-not-absolute',
|
|
30
|
+
category: 'social',
|
|
31
|
+
severity: 'warning',
|
|
32
|
+
description: 'og:image is not an absolute URL.',
|
|
33
|
+
recommendation: 'Use a full https:// URL for og:image; most platforms ignore relative image paths.',
|
|
34
|
+
docs: GOOGLE_DOCS.og,
|
|
35
|
+
}, ({ page }, make) => {
|
|
36
|
+
const img = page.og['image'];
|
|
37
|
+
return img && !/^https?:\/\//i.test(img) ? make(`og:image "${img}" is not an absolute URL.`, { selector: 'head > meta[property="og:image"]', evidence: { image: img } }) : null;
|
|
38
|
+
}),
|
|
39
|
+
defineRule({
|
|
40
|
+
id: 'og.url-mismatch',
|
|
41
|
+
category: 'social',
|
|
42
|
+
severity: 'info',
|
|
43
|
+
description: 'og:url differs from the canonical URL.',
|
|
44
|
+
recommendation: 'Set og:url to the same URL as the canonical so share counts and previews consolidate.',
|
|
45
|
+
docs: GOOGLE_DOCS.og,
|
|
46
|
+
}, ({ page }, make) => {
|
|
47
|
+
const ogUrl = page.og['url'];
|
|
48
|
+
if (!ogUrl || !page.canonical)
|
|
49
|
+
return null;
|
|
50
|
+
const a = normalizeUrl(ogUrl);
|
|
51
|
+
const b = normalizeUrl(page.canonical);
|
|
52
|
+
return a && b && a !== b ? make('og:url does not match the canonical URL.', { selector: 'head > meta[property="og:url"]', evidence: { ogUrl, canonical: page.canonical } }) : null;
|
|
53
|
+
}),
|
|
54
|
+
defineRule({
|
|
55
|
+
id: 'twitter.card-missing',
|
|
56
|
+
category: 'social',
|
|
57
|
+
severity: 'info',
|
|
58
|
+
description: 'No twitter:card meta tag is present.',
|
|
59
|
+
recommendation: 'Add <meta name="twitter:card" content="summary_large_image">; X/Twitter falls back to Open Graph for the other fields.',
|
|
60
|
+
docs: GOOGLE_DOCS.twitter,
|
|
61
|
+
}, ({ page }, make) => (page.twitter['card'] ? null : make('Page has no twitter:card tag.', { selector: 'head > meta[name="twitter:card"]' }))),
|
|
62
|
+
defineRule({
|
|
63
|
+
id: 'twitter.card-invalid',
|
|
64
|
+
category: 'social',
|
|
65
|
+
severity: 'warning',
|
|
66
|
+
description: 'twitter:card has a value other than summary, summary_large_image, app or player.',
|
|
67
|
+
recommendation: 'Use one of the supported card types, usually summary_large_image.',
|
|
68
|
+
docs: GOOGLE_DOCS.twitter,
|
|
69
|
+
}, ({ page }, make) => {
|
|
70
|
+
const card = page.twitter['card'];
|
|
71
|
+
return card && !TWITTER_CARDS.has(card.toLowerCase()) ? make(`twitter:card "${card}" is not a valid card type.`, { selector: 'head > meta[name="twitter:card"]', evidence: { card } }) : null;
|
|
72
|
+
}),
|
|
73
|
+
];
|
|
74
|
+
//# 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,23 @@
|
|
|
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
|
+
/** Walk one structured-data item (and nested typed objects) collecting spec violations. */
|
|
16
|
+
export declare function validateItem(item: StructuredDataItem): {
|
|
17
|
+
required: Issue[];
|
|
18
|
+
recommended: Issue[];
|
|
19
|
+
badDates: string[];
|
|
20
|
+
unknownTypes: string[];
|
|
21
|
+
};
|
|
22
|
+
export declare const structuredDataRules: import("./rule.js").Rule[];
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { typesOf } from '../html/structured-data.js';
|
|
2
|
+
import { cap, defineRule, GOOGLE_DOCS } from './rule.js';
|
|
3
|
+
const G = 'https://developers.google.com/search/docs/appearance/structured-data/';
|
|
4
|
+
const ARTICLE = { required: [], recommended: ['headline', 'image', 'datePublished', 'dateModified', 'author'], docs: `${G}article` };
|
|
5
|
+
export const SCHEMA_SPECS = {
|
|
6
|
+
Article: ARTICLE,
|
|
7
|
+
NewsArticle: ARTICLE,
|
|
8
|
+
BlogPosting: ARTICLE,
|
|
9
|
+
TechArticle: ARTICLE,
|
|
10
|
+
Product: { required: ['name'], recommended: ['image', 'description', 'brand', 'sku', 'offers'], oneOf: [['offers', 'review', 'aggregateRating']], docs: `${G}product` },
|
|
11
|
+
Offer: { required: ['price', 'priceCurrency'], recommended: ['availability', 'url', 'priceValidUntil'], docs: `${G}product` },
|
|
12
|
+
AggregateOffer: { required: ['lowPrice', 'priceCurrency'], recommended: ['highPrice', 'offerCount'], docs: `${G}product` },
|
|
13
|
+
Organization: { required: ['name'], recommended: ['url', 'logo', 'sameAs'], docs: `${G}organization` },
|
|
14
|
+
WebSite: { required: ['name', 'url'], recommended: [], docs: `${G}sitelinks-searchbox` },
|
|
15
|
+
WebPage: { required: [], recommended: ['name'] },
|
|
16
|
+
BreadcrumbList: { required: ['itemListElement'], recommended: [], docs: `${G}breadcrumb` },
|
|
17
|
+
ListItem: { required: ['position'], recommended: ['name', 'item'], docs: `${G}breadcrumb` },
|
|
18
|
+
ItemList: { required: ['itemListElement'], recommended: [], docs: `${G}carousel` },
|
|
19
|
+
FAQPage: { required: ['mainEntity'], recommended: [], docs: `${G}faqpage` },
|
|
20
|
+
Question: { required: ['name', 'acceptedAnswer'], recommended: [], docs: `${G}faqpage` },
|
|
21
|
+
Answer: { required: ['text'], recommended: [], docs: `${G}faqpage` },
|
|
22
|
+
LocalBusiness: { required: ['name', 'address'], recommended: ['telephone', 'url', 'image', 'openingHoursSpecification', 'priceRange', 'geo'], docs: `${G}local-business` },
|
|
23
|
+
Event: { required: ['name', 'startDate', 'location'], recommended: ['endDate', 'description', 'image', 'offers', 'performer', 'organizer', 'eventStatus', 'eventAttendanceMode'], docs: `${G}event` },
|
|
24
|
+
Recipe: { required: ['name', 'image'], recommended: ['author', 'datePublished', 'description', 'prepTime', 'cookTime', 'totalTime', 'recipeYield', 'recipeIngredient', 'recipeInstructions', 'aggregateRating'], docs: `${G}recipe` },
|
|
25
|
+
VideoObject: { required: ['name', 'thumbnailUrl', 'uploadDate'], recommended: ['description', 'duration', 'contentUrl', 'embedUrl'], docs: `${G}video` },
|
|
26
|
+
Person: { required: ['name'], recommended: ['url', 'sameAs', 'jobTitle'] },
|
|
27
|
+
JobPosting: { required: ['title', 'description', 'datePosted', 'hiringOrganization', 'jobLocation'], recommended: ['validThrough', 'employmentType', 'baseSalary', 'identifier'], docs: `${G}job-posting` },
|
|
28
|
+
HowTo: { required: ['name', 'step'], recommended: ['image', 'totalTime', 'estimatedCost', 'supply', 'tool'], docs: `${G}how-to` },
|
|
29
|
+
Review: { required: ['itemReviewed', 'reviewRating', 'author'], recommended: ['datePublished', 'reviewBody'], docs: `${G}review-snippet` },
|
|
30
|
+
Rating: { required: ['ratingValue'], recommended: ['bestRating', 'worstRating'], docs: `${G}review-snippet` },
|
|
31
|
+
AggregateRating: { required: ['ratingValue'], recommended: ['bestRating'], oneOf: [['ratingCount', 'reviewCount']], docs: `${G}review-snippet` },
|
|
32
|
+
SoftwareApplication: { required: ['name'], recommended: ['applicationCategory', 'operatingSystem', 'offers', 'aggregateRating'], oneOf: [['offers', 'aggregateRating']], docs: `${G}software-app` },
|
|
33
|
+
Course: { required: ['name', 'description', 'provider'], recommended: [], docs: `${G}course` },
|
|
34
|
+
Dataset: { required: ['name', 'description'], recommended: ['url', 'creator', 'license', 'distribution'], docs: `${G}dataset` },
|
|
35
|
+
ImageObject: { required: [], recommended: ['contentUrl', 'license', 'creator'], oneOf: [['contentUrl', 'url']], docs: `${G}image-license-metadata` },
|
|
36
|
+
PostalAddress: { required: [], recommended: ['streetAddress', 'addressLocality', 'postalCode', 'addressCountry'] },
|
|
37
|
+
};
|
|
38
|
+
const DATE_PROPS = new Set(['datePublished', 'dateModified', 'startDate', 'endDate', 'uploadDate', 'datePosted', 'validThrough', 'dateCreated', 'expires']);
|
|
39
|
+
const ISO_DATE = /^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$/;
|
|
40
|
+
function isPresent(value) {
|
|
41
|
+
if (value === null || value === undefined)
|
|
42
|
+
return false;
|
|
43
|
+
if (typeof value === 'string')
|
|
44
|
+
return value.trim().length > 0;
|
|
45
|
+
if (Array.isArray(value))
|
|
46
|
+
return value.length > 0;
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
/** Walk one structured-data item (and nested typed objects) collecting spec violations. */
|
|
50
|
+
export function validateItem(item) {
|
|
51
|
+
const required = [];
|
|
52
|
+
const recommended = [];
|
|
53
|
+
const badDates = [];
|
|
54
|
+
const unknownTypes = [];
|
|
55
|
+
const visited = new Set();
|
|
56
|
+
const walk = (node, path, depth) => {
|
|
57
|
+
if (depth > 6 || !node || typeof node !== 'object' || visited.has(node))
|
|
58
|
+
return;
|
|
59
|
+
visited.add(node);
|
|
60
|
+
if (Array.isArray(node)) {
|
|
61
|
+
node.forEach((n, i) => walk(n, `${path}[${i}]`, depth));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const obj = node;
|
|
65
|
+
const types = typesOf(obj['@type']);
|
|
66
|
+
for (const type of types) {
|
|
67
|
+
const spec = SCHEMA_SPECS[type];
|
|
68
|
+
if (!spec) {
|
|
69
|
+
unknownTypes.push(type);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const missingReq = spec.required.filter((p) => !isPresent(obj[p]));
|
|
73
|
+
for (const group of spec.oneOf ?? []) {
|
|
74
|
+
if (!group.some((p) => isPresent(obj[p])))
|
|
75
|
+
missingReq.push(`one of: ${group.join(' | ')}`);
|
|
76
|
+
}
|
|
77
|
+
if (missingReq.length)
|
|
78
|
+
required.push({ type, missing: missingReq, path });
|
|
79
|
+
const missingRec = spec.recommended.filter((p) => !isPresent(obj[p]));
|
|
80
|
+
if (missingRec.length)
|
|
81
|
+
recommended.push({ type, missing: missingRec, path });
|
|
82
|
+
}
|
|
83
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
84
|
+
if (DATE_PROPS.has(key) && typeof value === 'string' && !ISO_DATE.test(value.trim()))
|
|
85
|
+
badDates.push(`${path ? `${path}.` : ''}${key}="${value}"`);
|
|
86
|
+
if (value && typeof value === 'object')
|
|
87
|
+
walk(value, path ? `${path}.${key}` : key, depth + 1);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
walk(item.data, '', 0);
|
|
91
|
+
return { required, recommended, badDates, unknownTypes };
|
|
92
|
+
}
|
|
93
|
+
export const structuredDataRules = [
|
|
94
|
+
defineRule({
|
|
95
|
+
id: 'structured-data.invalid-json',
|
|
96
|
+
category: 'structured-data',
|
|
97
|
+
severity: 'error',
|
|
98
|
+
description: 'A JSON-LD <script> block contains invalid JSON and will be ignored entirely.',
|
|
99
|
+
recommendation: 'Fix the JSON syntax (trailing commas, unescaped quotes, HTML comments inside the script). Serialise with JSON.stringify instead of hand-writing it.',
|
|
100
|
+
docs: GOOGLE_DOCS.structuredData,
|
|
101
|
+
}, ({ page }, make) => page.structuredDataErrors.map((e) => make(`Invalid JSON-LD: ${e.message}`, { selector: 'script[type="application/ld+json"]', evidence: { snippet: e.snippet } }))),
|
|
102
|
+
defineRule({
|
|
103
|
+
id: 'structured-data.missing',
|
|
104
|
+
category: 'structured-data',
|
|
105
|
+
severity: 'info',
|
|
106
|
+
description: 'The page has no structured data (JSON-LD or microdata).',
|
|
107
|
+
recommendation: 'Add JSON-LD for the page type (Article, Product, Organization, BreadcrumbList, FAQPage, ...) to qualify for rich results.',
|
|
108
|
+
docs: GOOGLE_DOCS.structuredData,
|
|
109
|
+
}, ({ page }, make) => (page.structuredData.length === 0 && page.structuredDataErrors.length === 0 ? make('Page has no structured data.') : null)),
|
|
110
|
+
defineRule({
|
|
111
|
+
id: 'structured-data.missing-context',
|
|
112
|
+
category: 'structured-data',
|
|
113
|
+
severity: 'warning',
|
|
114
|
+
description: 'A JSON-LD item has no @context or a context other than schema.org.',
|
|
115
|
+
recommendation: 'Set "@context": "https://schema.org" on every top-level JSON-LD object.',
|
|
116
|
+
docs: GOOGLE_DOCS.structuredData,
|
|
117
|
+
}, ({ page }, make) => {
|
|
118
|
+
const bad = page.structuredData.filter((i) => i.source === 'json-ld' && !(i.context ?? '').toLowerCase().includes('schema.org'));
|
|
119
|
+
return bad.length ? make(`${bad.length} JSON-LD item(s) lack a schema.org @context.`, { selector: 'script[type="application/ld+json"]', evidence: { contexts: cap(bad.map((i) => i.context)), types: cap(bad.flatMap((i) => i.types)) } }) : null;
|
|
120
|
+
}),
|
|
121
|
+
defineRule({
|
|
122
|
+
id: 'structured-data.missing-type',
|
|
123
|
+
category: 'structured-data',
|
|
124
|
+
severity: 'warning',
|
|
125
|
+
description: 'A structured data item has no @type (or itemtype).',
|
|
126
|
+
recommendation: 'Add "@type" (e.g. "Article") to every structured data object.',
|
|
127
|
+
docs: GOOGLE_DOCS.structuredData,
|
|
128
|
+
}, ({ page }, make) => {
|
|
129
|
+
const bad = page.structuredData.filter((i) => i.types.length === 0);
|
|
130
|
+
return bad.length ? make(`${bad.length} structured data item(s) have no @type.`, { evidence: { sources: cap(bad.map((i) => i.source)), keys: cap(bad.map((i) => Object.keys(i.data).slice(0, 8))) } }) : null;
|
|
131
|
+
}),
|
|
132
|
+
defineRule({
|
|
133
|
+
id: 'structured-data.missing-required',
|
|
134
|
+
category: 'structured-data',
|
|
135
|
+
severity: 'warning',
|
|
136
|
+
description: 'A structured data item of a known type is missing properties Google requires for rich results.',
|
|
137
|
+
recommendation: 'Add the missing properties listed in evidence. Validate with https://search.google.com/test/rich-results after fixing.',
|
|
138
|
+
docs: GOOGLE_DOCS.structuredData,
|
|
139
|
+
}, ({ page }, make) => page.structuredData.flatMap((item) => {
|
|
140
|
+
const { required } = validateItem(item);
|
|
141
|
+
return required.map((iss) => make(`${iss.type} is missing required: ${iss.missing.join(', ')}.`, {
|
|
142
|
+
selector: item.source === 'json-ld' ? 'script[type="application/ld+json"]' : `[itemtype*="${iss.type}"]`,
|
|
143
|
+
evidence: { type: iss.type, missing: iss.missing, path: iss.path || '(root)', source: item.source, docs: SCHEMA_SPECS[iss.type]?.docs },
|
|
144
|
+
}));
|
|
145
|
+
})),
|
|
146
|
+
defineRule({
|
|
147
|
+
id: 'structured-data.missing-recommended',
|
|
148
|
+
category: 'structured-data',
|
|
149
|
+
severity: 'info',
|
|
150
|
+
description: 'A structured data item is missing properties Google recommends for richer results.',
|
|
151
|
+
recommendation: 'Add the recommended properties where the data exists; they unlock additional rich-result features.',
|
|
152
|
+
docs: GOOGLE_DOCS.structuredData,
|
|
153
|
+
}, ({ page }, make) => page.structuredData.flatMap((item) => {
|
|
154
|
+
const { recommended } = validateItem(item);
|
|
155
|
+
return recommended.map((iss) => make(`${iss.type} is missing recommended: ${iss.missing.join(', ')}.`, { evidence: { type: iss.type, missing: iss.missing, path: iss.path || '(root)', source: item.source } }));
|
|
156
|
+
})),
|
|
157
|
+
defineRule({
|
|
158
|
+
id: 'structured-data.invalid-date',
|
|
159
|
+
category: 'structured-data',
|
|
160
|
+
severity: 'warning',
|
|
161
|
+
description: 'Date properties in structured data that are not ISO 8601.',
|
|
162
|
+
recommendation: 'Use ISO 8601 dates such as 2026-03-14 or 2026-03-14T09:30:00+01:00.',
|
|
163
|
+
docs: GOOGLE_DOCS.structuredData,
|
|
164
|
+
}, ({ page }, make) => {
|
|
165
|
+
const bad = page.structuredData.flatMap((i) => validateItem(i).badDates);
|
|
166
|
+
return bad.length ? make(`${bad.length} structured data date(s) are not ISO 8601.`, { evidence: { examples: cap(bad) } }) : null;
|
|
167
|
+
}),
|
|
168
|
+
];
|
|
169
|
+
//# sourceMappingURL=structured-data.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structured-data.js","sourceRoot":"","sources":["../../../src/core/rules/structured-data.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const titleRules: import("./rule.js").Rule[];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { isGenericTitle } from '../html/text.js';
|
|
2
|
+
import { defineRule, GOOGLE_DOCS } from './rule.js';
|
|
3
|
+
export const titleRules = [
|
|
4
|
+
defineRule({
|
|
5
|
+
id: 'title.missing',
|
|
6
|
+
category: 'title',
|
|
7
|
+
severity: 'error',
|
|
8
|
+
description: 'The page has no <title> element or it is empty.',
|
|
9
|
+
recommendation: 'Add a unique, descriptive <title> in <head> (about 30-60 characters) that states what the page is about.',
|
|
10
|
+
docs: GOOGLE_DOCS.titles,
|
|
11
|
+
}, ({ page }, make) => (page.title ? null : make('Page has no <title> or the title is empty.', { selector: 'head > title' }))),
|
|
12
|
+
defineRule({
|
|
13
|
+
id: 'title.multiple',
|
|
14
|
+
category: 'title',
|
|
15
|
+
severity: 'warning',
|
|
16
|
+
description: 'More than one <title> element is present.',
|
|
17
|
+
recommendation: 'Keep exactly one <title>. Browsers and crawlers may pick different ones when several exist.',
|
|
18
|
+
docs: GOOGLE_DOCS.titles,
|
|
19
|
+
}, ({ page }, make) => page.titles.length > 1
|
|
20
|
+
? make(`Page has ${page.titles.length} <title> elements.`, { selector: 'title', evidence: { titles: page.titles } })
|
|
21
|
+
: null),
|
|
22
|
+
defineRule({
|
|
23
|
+
id: 'title.too-long',
|
|
24
|
+
category: 'title',
|
|
25
|
+
severity: 'warning',
|
|
26
|
+
description: 'Title is longer than the configured maximum (default 60 characters) and will be truncated in search results.',
|
|
27
|
+
recommendation: 'Shorten the title to roughly 60 characters or fewer, keeping the most important words first.',
|
|
28
|
+
docs: GOOGLE_DOCS.titles,
|
|
29
|
+
}, ({ page, thresholds }, make) => page.title && page.title.length > thresholds.titleMaxChars
|
|
30
|
+
? make(`Title is ${page.title.length} characters (max ${thresholds.titleMaxChars}).`, {
|
|
31
|
+
selector: 'head > title',
|
|
32
|
+
evidence: { title: page.title, length: page.title.length, max: thresholds.titleMaxChars },
|
|
33
|
+
})
|
|
34
|
+
: null),
|
|
35
|
+
defineRule({
|
|
36
|
+
id: 'title.too-short',
|
|
37
|
+
category: 'title',
|
|
38
|
+
severity: 'info',
|
|
39
|
+
description: 'Title is shorter than the configured minimum (default 30 characters).',
|
|
40
|
+
recommendation: 'Consider a more descriptive title that includes the page topic and, where sensible, the brand.',
|
|
41
|
+
docs: GOOGLE_DOCS.titles,
|
|
42
|
+
}, ({ page, thresholds }, make) => page.title && page.title.length < thresholds.titleMinChars
|
|
43
|
+
? make(`Title is only ${page.title.length} characters (recommended at least ${thresholds.titleMinChars}).`, {
|
|
44
|
+
selector: 'head > title',
|
|
45
|
+
evidence: { title: page.title, length: page.title.length, min: thresholds.titleMinChars },
|
|
46
|
+
})
|
|
47
|
+
: null),
|
|
48
|
+
defineRule({
|
|
49
|
+
id: 'title.generic',
|
|
50
|
+
category: 'title',
|
|
51
|
+
severity: 'warning',
|
|
52
|
+
description: 'Title is a framework default or placeholder such as "React App", "Create Next App", "Home" or "Untitled".',
|
|
53
|
+
recommendation: 'Replace the placeholder with a title that describes this page. In frameworks, set it per route (metadata export, <Head>, useHead, <svelte:head>, etc.).',
|
|
54
|
+
docs: GOOGLE_DOCS.titles,
|
|
55
|
+
}, ({ page }, make) => page.title && isGenericTitle(page.title)
|
|
56
|
+
? make(`Title "${page.title}" looks like a placeholder or framework default.`, { selector: 'head > title', evidence: { title: page.title } })
|
|
57
|
+
: null),
|
|
58
|
+
];
|
|
59
|
+
//# sourceMappingURL=title.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"title.js","sourceRoot":"","sources":["../../../src/core/rules/title.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const PAGE_WEIGHTS = { error: 15, warning: 5, info: 1 };
|
|
2
|
+
const SITE_WEIGHTS = { error: 10, warning: 3, info: 0.5 };
|
|
3
|
+
export function pageScore(counts) {
|
|
4
|
+
const deduction = counts.error * PAGE_WEIGHTS.error + counts.warning * PAGE_WEIGHTS.warning + counts.info * PAGE_WEIGHTS.info;
|
|
5
|
+
return clamp(Math.round(100 - deduction));
|
|
6
|
+
}
|
|
7
|
+
export function siteScore(pages, siteFindings) {
|
|
8
|
+
const avg = pages.length ? pages.reduce((sum, p) => sum + p.score, 0) / pages.length : 100;
|
|
9
|
+
const deduction = siteFindings.reduce((sum, f) => sum + SITE_WEIGHTS[f.severity], 0);
|
|
10
|
+
return clamp(Math.round(avg - deduction));
|
|
11
|
+
}
|
|
12
|
+
function clamp(n) {
|
|
13
|
+
return Math.max(0, Math.min(100, n));
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=score.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"score.js","sourceRoot":"","sources":["../../src/core/score.ts"],"names":[],"mappings":"AAEA,MAAM,YAAY,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAW,CAAC;AACjE,MAAM,YAAY,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,GAAG,EAAW,CAAC;AAEnE,MAAM,UAAU,SAAS,CAAC,MAAsB;IAC9C,MAAM,SAAS,GAAG,MAAM,CAAC,KAAK,GAAG,YAAY,CAAC,KAAK,GAAG,MAAM,CAAC,OAAO,GAAG,YAAY,CAAC,OAAO,GAAG,MAAM,CAAC,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC;IAC9H,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,SAAS,CAAC,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,KAAkB,EAAE,YAAuB;IACnE,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC;IAC3F,MAAM,SAAS,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;IACrF,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,SAAS,CAAC,CAAC,CAAC;AAC5C,CAAC;AAED,SAAS,KAAK,CAAC,CAAS;IACtB,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;AACvC,CAAC"}
|