@aexol/spectral 0.9.42 → 0.9.43
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/extensions/aexol-mcp.d.ts.map +1 -1
- package/dist/extensions/aexol-mcp.js +44 -9
- package/dist/extensions/browser/browser-service.d.ts +4 -1
- package/dist/extensions/browser/browser-service.d.ts.map +1 -1
- package/dist/extensions/browser/browser-service.js +34 -7
- package/dist/extensions/browser/config.d.ts +3 -0
- package/dist/extensions/browser/config.d.ts.map +1 -1
- package/dist/extensions/browser/config.js +1 -0
- package/dist/extensions/browser/index.d.ts.map +1 -1
- package/dist/extensions/browser/index.js +3 -0
- package/dist/extensions/browser/record/record-tool.d.ts.map +1 -1
- package/dist/extensions/browser/record/record-tool.js +27 -4
- package/dist/extensions/browser/scenario/markdown-parser.d.ts +44 -0
- package/dist/extensions/browser/scenario/markdown-parser.d.ts.map +1 -0
- package/dist/extensions/browser/scenario/markdown-parser.js +159 -0
- package/dist/extensions/browser/scenario/scenario-tool.d.ts +4 -0
- package/dist/extensions/browser/scenario/scenario-tool.d.ts.map +1 -0
- package/dist/extensions/browser/scenario/scenario-tool.js +381 -0
- package/dist/extensions/browser/tools/core.js +1 -1
- package/dist/extensions/browser/tools/screenshot.d.ts.map +1 -1
- package/dist/extensions/browser/tools/screenshot.js +2 -1
- package/dist/extensions/seo/analyzers/agent-ux.d.ts +5 -0
- package/dist/extensions/seo/analyzers/agent-ux.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/agent-ux.js +95 -0
- package/dist/extensions/seo/analyzers/content.d.ts +16 -0
- package/dist/extensions/seo/analyzers/content.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/content.js +174 -0
- package/dist/extensions/seo/analyzers/drift.d.ts +13 -0
- package/dist/extensions/seo/analyzers/drift.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/drift.js +204 -0
- package/dist/extensions/seo/analyzers/images.d.ts +10 -0
- package/dist/extensions/seo/analyzers/images.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/images.js +49 -0
- package/dist/extensions/seo/analyzers/parasite.d.ts +18 -0
- package/dist/extensions/seo/analyzers/parasite.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/parasite.js +142 -0
- package/dist/extensions/seo/analyzers/preload.d.ts +12 -0
- package/dist/extensions/seo/analyzers/preload.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/preload.js +108 -0
- package/dist/extensions/seo/analyzers/schema.d.ts +21 -0
- package/dist/extensions/seo/analyzers/schema.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/schema.js +324 -0
- package/dist/extensions/seo/analyzers/sitemap.d.ts +20 -0
- package/dist/extensions/seo/analyzers/sitemap.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/sitemap.js +121 -0
- package/dist/extensions/seo/analyzers/technical.d.ts +11 -0
- package/dist/extensions/seo/analyzers/technical.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/technical.js +268 -0
- package/dist/extensions/seo/config.d.ts +24 -0
- package/dist/extensions/seo/config.d.ts.map +1 -0
- package/dist/extensions/seo/config.js +48 -0
- package/dist/extensions/seo/index.d.ts +16 -0
- package/dist/extensions/seo/index.d.ts.map +1 -0
- package/dist/extensions/seo/index.js +88 -0
- package/dist/extensions/seo/tools/agent-ux.d.ts +7 -0
- package/dist/extensions/seo/tools/agent-ux.d.ts.map +1 -0
- package/dist/extensions/seo/tools/agent-ux.js +63 -0
- package/dist/extensions/seo/tools/content.d.ts +7 -0
- package/dist/extensions/seo/tools/content.d.ts.map +1 -0
- package/dist/extensions/seo/tools/content.js +55 -0
- package/dist/extensions/seo/tools/drift.d.ts +7 -0
- package/dist/extensions/seo/tools/drift.d.ts.map +1 -0
- package/dist/extensions/seo/tools/drift.js +104 -0
- package/dist/extensions/seo/tools/fetch.d.ts +7 -0
- package/dist/extensions/seo/tools/fetch.d.ts.map +1 -0
- package/dist/extensions/seo/tools/fetch.js +123 -0
- package/dist/extensions/seo/tools/full-audit.d.ts +10 -0
- package/dist/extensions/seo/tools/full-audit.d.ts.map +1 -0
- package/dist/extensions/seo/tools/full-audit.js +191 -0
- package/dist/extensions/seo/tools/helpers.d.ts +13 -0
- package/dist/extensions/seo/tools/helpers.d.ts.map +1 -0
- package/dist/extensions/seo/tools/helpers.js +24 -0
- package/dist/extensions/seo/tools/images.d.ts +7 -0
- package/dist/extensions/seo/tools/images.d.ts.map +1 -0
- package/dist/extensions/seo/tools/images.js +46 -0
- package/dist/extensions/seo/tools/parasite.d.ts +7 -0
- package/dist/extensions/seo/tools/parasite.d.ts.map +1 -0
- package/dist/extensions/seo/tools/parasite.js +53 -0
- package/dist/extensions/seo/tools/parse.d.ts +7 -0
- package/dist/extensions/seo/tools/parse.d.ts.map +1 -0
- package/dist/extensions/seo/tools/parse.js +66 -0
- package/dist/extensions/seo/tools/preload.d.ts +7 -0
- package/dist/extensions/seo/tools/preload.d.ts.map +1 -0
- package/dist/extensions/seo/tools/preload.js +52 -0
- package/dist/extensions/seo/tools/report.d.ts +4 -0
- package/dist/extensions/seo/tools/report.d.ts.map +1 -0
- package/dist/extensions/seo/tools/report.js +167 -0
- package/dist/extensions/seo/tools/schema.d.ts +7 -0
- package/dist/extensions/seo/tools/schema.d.ts.map +1 -0
- package/dist/extensions/seo/tools/schema.js +105 -0
- package/dist/extensions/seo/tools/sitemap.d.ts +7 -0
- package/dist/extensions/seo/tools/sitemap.d.ts.map +1 -0
- package/dist/extensions/seo/tools/sitemap.js +85 -0
- package/dist/extensions/seo/tools/technical.d.ts +7 -0
- package/dist/extensions/seo/tools/technical.d.ts.map +1 -0
- package/dist/extensions/seo/tools/technical.js +62 -0
- package/dist/extensions/seo/types.d.ts +289 -0
- package/dist/extensions/seo/types.d.ts.map +1 -0
- package/dist/extensions/seo/types.js +7 -0
- package/dist/extensions/seo/utils/fetcher.d.ts +28 -0
- package/dist/extensions/seo/utils/fetcher.d.ts.map +1 -0
- package/dist/extensions/seo/utils/fetcher.js +118 -0
- package/dist/extensions/seo/utils/parser.d.ts +8 -0
- package/dist/extensions/seo/utils/parser.d.ts.map +1 -0
- package/dist/extensions/seo/utils/parser.js +265 -0
- package/dist/extensions/seo/utils/renderer.d.ts +39 -0
- package/dist/extensions/seo/utils/renderer.d.ts.map +1 -0
- package/dist/extensions/seo/utils/renderer.js +190 -0
- package/dist/extensions/seo/utils/url-safety.d.ts +31 -0
- package/dist/extensions/seo/utils/url-safety.d.ts.map +1 -0
- package/dist/extensions/seo/utils/url-safety.js +200 -0
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/native-extensions.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const SPECULATION_BLOCK_RE = /<script\b[^>]*\btype\s*=\s*["']speculationrules["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
2
|
+
const PRELOAD_LINK_RE = /<link\b[^>]*\brel\s*=\s*["']preload["'][^>]*>/gi;
|
|
3
|
+
const PRERENDER_LINK_RE = /<link\b[^>]*\brel\s*=\s*["']prerender["'][^>]*>/gi;
|
|
4
|
+
const FETCHPRIORITY_HIGH_RE = /\bfetchpriority\s*=\s*["']high["']/gi;
|
|
5
|
+
const LCP_IMG_HINT_RE = /<(?:img|video|source)\b[^>]*\bfetchpriority\s*=\s*["']high["']/gi;
|
|
6
|
+
const UNLOAD_LISTENER_RE = /(?:addEventListener\s*\(\s*["']?unload["']?|\.onunload\s*=|on\s*\(\s*["']?unload["'])/i;
|
|
7
|
+
const BEFOREUNLOAD_LISTENER_RE = /(?:addEventListener\s*\(\s*["']?beforeunload["']?|\.onbeforeunload\s*=|on\s*\(\s*["']?beforeunload["'])/i;
|
|
8
|
+
function extractSpeculationActions(body) {
|
|
9
|
+
const actions = new Set();
|
|
10
|
+
try {
|
|
11
|
+
const payload = JSON.parse(body.trim());
|
|
12
|
+
if (Array.isArray(payload.prefetch))
|
|
13
|
+
actions.add("prefetch");
|
|
14
|
+
if (Array.isArray(payload.prerender))
|
|
15
|
+
actions.add("prerender");
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// invalid JSON
|
|
19
|
+
}
|
|
20
|
+
return [...actions].sort();
|
|
21
|
+
}
|
|
22
|
+
export function auditPreload(fetchResult) {
|
|
23
|
+
const html = fetchResult.content;
|
|
24
|
+
const headers = fetchResult.headers;
|
|
25
|
+
// Speculation rules.
|
|
26
|
+
const speculationBlocks = [];
|
|
27
|
+
let match;
|
|
28
|
+
while ((match = SPECULATION_BLOCK_RE.exec(html)) !== null) {
|
|
29
|
+
speculationBlocks.push(match[1]);
|
|
30
|
+
}
|
|
31
|
+
const actions = new Set();
|
|
32
|
+
for (const block of speculationBlocks) {
|
|
33
|
+
for (const a of extractSpeculationActions(block)) {
|
|
34
|
+
actions.add(a);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const speculationHeader = Object.keys(headers).some((k) => k.toLowerCase() === "speculation-rules");
|
|
38
|
+
// Preload hints.
|
|
39
|
+
const preloadMatches = html.match(PRELOAD_LINK_RE) ?? [];
|
|
40
|
+
const preloadHints = preloadMatches.length;
|
|
41
|
+
// Deprecated prerender links.
|
|
42
|
+
const prerenderMatches = html.match(PRERENDER_LINK_RE) ?? [];
|
|
43
|
+
const prerenderLinks = prerenderMatches.length;
|
|
44
|
+
// bfcache signals.
|
|
45
|
+
const ccValue = Object.entries(headers).find(([k]) => k.toLowerCase() === "cache-control")?.[1] ?? "";
|
|
46
|
+
const cacheControlNoStore = ccValue.toLowerCase().includes("no-store");
|
|
47
|
+
const hasUnload = UNLOAD_LISTENER_RE.test(html);
|
|
48
|
+
const hasBeforeunload = BEFOREUNLOAD_LISTENER_RE.test(html);
|
|
49
|
+
// LCP resource hints.
|
|
50
|
+
const fetchpriorityMatches = html.match(FETCHPRIORITY_HIGH_RE) ?? [];
|
|
51
|
+
const lcpImgHint = LCP_IMG_HINT_RE.test(html);
|
|
52
|
+
// Score.
|
|
53
|
+
let score = 0;
|
|
54
|
+
const recommendations = [];
|
|
55
|
+
if (speculationBlocks.length > 0 || speculationHeader) {
|
|
56
|
+
score += 25;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
recommendations.push('Add <script type="speculationrules"> for prefetch+prerender on top user-paths. Saves entire next-navigation paint cost.');
|
|
60
|
+
}
|
|
61
|
+
if (lcpImgHint) {
|
|
62
|
+
score += 25;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
recommendations.push('Mark the LCP hero image with fetchpriority="high" so the browser preloads it ahead of other resources.');
|
|
66
|
+
}
|
|
67
|
+
if (!cacheControlNoStore && !hasUnload) {
|
|
68
|
+
score += 25;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
if (cacheControlNoStore) {
|
|
72
|
+
recommendations.push('Remove "Cache-Control: no-store" to enable bfcache restoration on back/forward navigation.');
|
|
73
|
+
}
|
|
74
|
+
if (hasUnload) {
|
|
75
|
+
recommendations.push('Remove "unload" event listeners (use "pagehide" instead) — they block bfcache.');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (preloadHints > 0) {
|
|
79
|
+
score += 25;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
recommendations.push('Add <link rel="preload"> for critical resources (fonts, CSS, hero image) to speed up LCP.');
|
|
83
|
+
}
|
|
84
|
+
if (prerenderLinks > 0) {
|
|
85
|
+
recommendations.push('Replace deprecated <link rel="prerender"> with Speculation Rules API for modern prerendering.');
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
url: fetchResult.finalUrl,
|
|
89
|
+
speculationRules: {
|
|
90
|
+
inlineBlocks: speculationBlocks.length,
|
|
91
|
+
headerPresent: speculationHeader,
|
|
92
|
+
actions: [...actions],
|
|
93
|
+
},
|
|
94
|
+
preloadHints,
|
|
95
|
+
prerenderLinks,
|
|
96
|
+
bfcacheSignals: {
|
|
97
|
+
cacheControlNoStore,
|
|
98
|
+
unloadListener: hasUnload,
|
|
99
|
+
beforeunloadListener: hasBeforeunload,
|
|
100
|
+
},
|
|
101
|
+
lcpResourceHints: {
|
|
102
|
+
preloadLcpCandidate: lcpImgHint,
|
|
103
|
+
fetchpriorityHigh: fetchpriorityMatches.length,
|
|
104
|
+
},
|
|
105
|
+
score,
|
|
106
|
+
recommendations,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema.org analyzer — detection, validation, and generation.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the Python `schema_generate.py` and `seo-schema/SKILL.md` in
|
|
5
|
+
* claude-seo. Generates JSON-LD for high-leverage types and validates existing
|
|
6
|
+
* markup against Google's rich result requirements.
|
|
7
|
+
*/
|
|
8
|
+
import type { SchemaAuditResult } from "../types.js";
|
|
9
|
+
/** Audit schema markup on a page. */
|
|
10
|
+
export declare function auditSchema(html: string, url?: string): SchemaAuditResult;
|
|
11
|
+
export interface SchemaGenerateParams {
|
|
12
|
+
type: "Organization" | "LocalBusiness" | "Article" | "BlogPosting" | "Product" | "BreadcrumbList" | "WebSite" | "Person" | "ProfilePage" | "DiscussionForumPosting" | "OrderAction" | "Reservation";
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
/** Generate JSON-LD for the specified schema type. */
|
|
16
|
+
export declare function generateSchema(params: SchemaGenerateParams): {
|
|
17
|
+
jsonLd: unknown;
|
|
18
|
+
type: string;
|
|
19
|
+
required: string[];
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/schema.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAe,MAAM,aAAa,CAAC;AA2FlE,qCAAqC;AACrC,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,iBAAiB,CA2DzE;AAID,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,cAAc,GAAG,eAAe,GAAG,SAAS,GAAG,aAAa,GAAG,SAAS,GAC5E,gBAAgB,GAAG,SAAS,GAAG,QAAQ,GAAG,aAAa,GAAG,wBAAwB,GAClF,aAAa,GAAG,aAAa,CAAC;IAChC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,sDAAsD;AACtD,wBAAgB,cAAc,CAAC,MAAM,EAAE,oBAAoB,GAAG;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAsMlH"}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { extractJsonLd, parsePage } from "../utils/parser.js";
|
|
2
|
+
/** Schema types deprecated by Google (no rich results as of 2026). */
|
|
3
|
+
const DEPRECATED_TYPES = new Set([
|
|
4
|
+
"HowTo",
|
|
5
|
+
"SpecialAnnouncement",
|
|
6
|
+
"CourseInfo",
|
|
7
|
+
"EstimatedSalary",
|
|
8
|
+
"LearningVideo",
|
|
9
|
+
"ClaimReview",
|
|
10
|
+
"VehicleListing",
|
|
11
|
+
"PracticeProblem",
|
|
12
|
+
"Dataset",
|
|
13
|
+
"FAQPage",
|
|
14
|
+
]);
|
|
15
|
+
/** Active schema types that Google supports for rich results. */
|
|
16
|
+
const ACTIVE_TYPES = new Set([
|
|
17
|
+
"Organization", "LocalBusiness", "SoftwareApplication", "WebApplication",
|
|
18
|
+
"Product", "ProductGroup", "Offer", "Service", "Article", "BlogPosting",
|
|
19
|
+
"NewsArticle", "Review", "AggregateRating", "BreadcrumbList", "WebSite",
|
|
20
|
+
"WebPage", "Person", "ProfilePage", "ContactPage", "VideoObject",
|
|
21
|
+
"ImageObject", "Event", "JobPosting", "Course", "DiscussionForumPosting",
|
|
22
|
+
"Reservation", "OrderAction",
|
|
23
|
+
]);
|
|
24
|
+
/** Required properties per schema type (subset of Google's requirements). */
|
|
25
|
+
const REQUIRED_PROPERTIES = {
|
|
26
|
+
Article: ["headline", "author", "datePublished", "image", "publisher"],
|
|
27
|
+
BlogPosting: ["headline", "author", "datePublished", "image"],
|
|
28
|
+
NewsArticle: ["headline", "datePublished", "author", "image", "publisher"],
|
|
29
|
+
Product: ["name", "offers"],
|
|
30
|
+
Organization: ["name", "url"],
|
|
31
|
+
LocalBusiness: ["name", "address"],
|
|
32
|
+
Event: ["name", "startDate", "location"],
|
|
33
|
+
Review: ["itemReviewed", "author", "reviewRating"],
|
|
34
|
+
BreadcrumbList: ["itemListElement"],
|
|
35
|
+
VideoObject: ["name", "uploadDate"],
|
|
36
|
+
JobPosting: ["title", "description", "hiringOrganization"],
|
|
37
|
+
Course: ["name", "provider"],
|
|
38
|
+
};
|
|
39
|
+
/** Validate a parsed JSON-LD object and return issues. */
|
|
40
|
+
function validateSchemaItem(item) {
|
|
41
|
+
if (!item || typeof item !== "object") {
|
|
42
|
+
return { type: "Unknown", issues: ["Invalid schema object"] };
|
|
43
|
+
}
|
|
44
|
+
const obj = item;
|
|
45
|
+
const rawType = obj["@type"];
|
|
46
|
+
// Handle @type arrays (e.g. ["Article", "NewsArticle"]) and full URLs.
|
|
47
|
+
const type = typeof rawType === "string"
|
|
48
|
+
? rawType.replace("https://schema.org/", "")
|
|
49
|
+
: Array.isArray(rawType) && rawType.length > 0
|
|
50
|
+
? rawType[0].replace("https://schema.org/", "")
|
|
51
|
+
: "Unknown";
|
|
52
|
+
const issues = [];
|
|
53
|
+
if (!obj["@context"]) {
|
|
54
|
+
issues.push("Missing @context");
|
|
55
|
+
}
|
|
56
|
+
// Check required properties.
|
|
57
|
+
const required = REQUIRED_PROPERTIES[type];
|
|
58
|
+
if (required) {
|
|
59
|
+
for (const prop of required) {
|
|
60
|
+
if (!(prop in obj)) {
|
|
61
|
+
issues.push(`Missing required property: ${prop}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Check for relative URLs.
|
|
66
|
+
const jsonStr = JSON.stringify(obj);
|
|
67
|
+
if (jsonStr.includes('"url":"/') || jsonStr.includes('"url": "/')) {
|
|
68
|
+
issues.push("Contains relative URL — should be absolute");
|
|
69
|
+
}
|
|
70
|
+
// Check date formats.
|
|
71
|
+
for (const dateField of ["datePublished", "dateModified", "startDate", "endDate", "uploadDate"]) {
|
|
72
|
+
const val = obj[dateField];
|
|
73
|
+
if (typeof val === "string" && val.length > 0) {
|
|
74
|
+
if (isNaN(Date.parse(val))) {
|
|
75
|
+
issues.push(`Invalid date format for ${dateField}: ${val}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { type, issues };
|
|
80
|
+
}
|
|
81
|
+
/** Audit schema markup on a page. */
|
|
82
|
+
export function auditSchema(html, url) {
|
|
83
|
+
const pageData = parsePage(html, url);
|
|
84
|
+
const detectedSchemas = pageData.schemaBlocks;
|
|
85
|
+
const deprecatedTypes = [];
|
|
86
|
+
const relativeUrls = [];
|
|
87
|
+
const invalidDates = [];
|
|
88
|
+
let missingContext = false;
|
|
89
|
+
// Re-validate with deeper checks using parsed JSON-LD.
|
|
90
|
+
const jsonLdItems = extractJsonLd(html);
|
|
91
|
+
for (const item of jsonLdItems) {
|
|
92
|
+
const { type, issues } = validateSchemaItem(item);
|
|
93
|
+
if (DEPRECATED_TYPES.has(type)) {
|
|
94
|
+
deprecatedTypes.push(type);
|
|
95
|
+
}
|
|
96
|
+
for (const issue of issues) {
|
|
97
|
+
if (issue.includes("relative URL"))
|
|
98
|
+
relativeUrls.push(type);
|
|
99
|
+
if (issue.includes("Invalid date"))
|
|
100
|
+
invalidDates.push(type);
|
|
101
|
+
if (issue.includes("Missing @context"))
|
|
102
|
+
missingContext = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const recommendations = [];
|
|
106
|
+
if (deprecatedTypes.length > 0) {
|
|
107
|
+
recommendations.push(`Deprecated schema types detected: ${deprecatedTypes.join(", ")}. These no longer produce rich results.`);
|
|
108
|
+
}
|
|
109
|
+
if (missingContext) {
|
|
110
|
+
recommendations.push('Add "@context": "https://schema.org" to all JSON-LD blocks.');
|
|
111
|
+
}
|
|
112
|
+
if (relativeUrls.length > 0) {
|
|
113
|
+
recommendations.push("Use absolute URLs in all schema properties.");
|
|
114
|
+
}
|
|
115
|
+
if (detectedSchemas.length === 0) {
|
|
116
|
+
recommendations.push("No structured data found. Add JSON-LD markup for rich result eligibility.");
|
|
117
|
+
}
|
|
118
|
+
// Recommend types based on page content.
|
|
119
|
+
const hasArticle = pageData.wordCount > 300 && (pageData.title ?? "").length > 0;
|
|
120
|
+
if (hasArticle && !detectedSchemas.some((s) => ["Article", "BlogPosting", "NewsArticle"].includes(s.type))) {
|
|
121
|
+
recommendations.push("Consider adding Article or BlogPosting schema for content pages.");
|
|
122
|
+
}
|
|
123
|
+
if (detectedSchemas.length > 0 && !detectedSchemas.some((s) => s.type === "BreadcrumbList")) {
|
|
124
|
+
recommendations.push("Add BreadcrumbList schema to improve navigation rich results.");
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
url,
|
|
128
|
+
detectedSchemas,
|
|
129
|
+
deprecatedTypes,
|
|
130
|
+
missingContext,
|
|
131
|
+
relativeUrls,
|
|
132
|
+
invalidDates,
|
|
133
|
+
recommendations,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Generate JSON-LD for the specified schema type. */
|
|
137
|
+
export function generateSchema(params) {
|
|
138
|
+
const { type, ...rest } = params;
|
|
139
|
+
const context = "https://schema.org";
|
|
140
|
+
switch (type) {
|
|
141
|
+
case "Organization":
|
|
142
|
+
return {
|
|
143
|
+
jsonLd: {
|
|
144
|
+
"@context": context,
|
|
145
|
+
"@type": "Organization",
|
|
146
|
+
name: rest.name ?? "[Company Name]",
|
|
147
|
+
url: rest.url ?? "[Website URL]",
|
|
148
|
+
logo: rest.logo ?? "[Logo URL]",
|
|
149
|
+
sameAs: rest.sameAs ?? [],
|
|
150
|
+
...(rest.contactPoint ? { contactPoint: rest.contactPoint } : {}),
|
|
151
|
+
},
|
|
152
|
+
type: "Organization",
|
|
153
|
+
required: ["name", "url"],
|
|
154
|
+
};
|
|
155
|
+
case "LocalBusiness":
|
|
156
|
+
return {
|
|
157
|
+
jsonLd: {
|
|
158
|
+
"@context": context,
|
|
159
|
+
"@type": "LocalBusiness",
|
|
160
|
+
name: rest.name ?? "[Business Name]",
|
|
161
|
+
address: rest.address ?? {
|
|
162
|
+
"@type": "PostalAddress",
|
|
163
|
+
streetAddress: "[Street]",
|
|
164
|
+
addressLocality: "[City]",
|
|
165
|
+
addressRegion: "[State]",
|
|
166
|
+
postalCode: "[ZIP]",
|
|
167
|
+
addressCountry: "[Country]",
|
|
168
|
+
},
|
|
169
|
+
telephone: rest.telephone ?? "[Phone]",
|
|
170
|
+
openingHours: rest.openingHours ?? "Mo-Fr 09:00-17:00",
|
|
171
|
+
...(rest.geo ? { geo: rest.geo } : {}),
|
|
172
|
+
},
|
|
173
|
+
type: "LocalBusiness",
|
|
174
|
+
required: ["name", "address"],
|
|
175
|
+
};
|
|
176
|
+
case "Article":
|
|
177
|
+
case "BlogPosting":
|
|
178
|
+
return {
|
|
179
|
+
jsonLd: {
|
|
180
|
+
"@context": context,
|
|
181
|
+
"@type": type,
|
|
182
|
+
headline: rest.headline ?? "[Title]",
|
|
183
|
+
author: { "@type": "Person", name: rest.author ?? "[Author Name]" },
|
|
184
|
+
datePublished: rest.datePublished ?? "[YYYY-MM-DD]",
|
|
185
|
+
dateModified: rest.dateModified ?? rest.datePublished ?? "[YYYY-MM-DD]",
|
|
186
|
+
image: rest.image ?? "[Image URL]",
|
|
187
|
+
publisher: {
|
|
188
|
+
"@type": "Organization",
|
|
189
|
+
name: rest.publisher ?? "[Publisher]",
|
|
190
|
+
logo: { "@type": "ImageObject", url: rest.publisherLogo ?? "[Logo URL]" },
|
|
191
|
+
},
|
|
192
|
+
...(rest.url ? { mainEntityOfPage: { "@type": "WebPage", "@id": rest.url } } : {}),
|
|
193
|
+
},
|
|
194
|
+
type,
|
|
195
|
+
required: ["headline", "author", "datePublished", "image", "publisher"],
|
|
196
|
+
};
|
|
197
|
+
case "Product":
|
|
198
|
+
return {
|
|
199
|
+
jsonLd: {
|
|
200
|
+
"@context": context,
|
|
201
|
+
"@type": "Product",
|
|
202
|
+
name: rest.name ?? "[Product Name]",
|
|
203
|
+
image: rest.image ?? "[Image URL]",
|
|
204
|
+
description: rest.description ?? "[Description]",
|
|
205
|
+
offers: rest.offers ?? {
|
|
206
|
+
"@type": "Offer",
|
|
207
|
+
price: rest.price ?? "0",
|
|
208
|
+
priceCurrency: rest.priceCurrency ?? "USD",
|
|
209
|
+
availability: `https://schema.org/${rest.availability ?? "InStock"}`,
|
|
210
|
+
},
|
|
211
|
+
...(rest.brand ? { brand: { "@type": "Brand", name: rest.brand } } : {}),
|
|
212
|
+
...(rest.aggregateRating ? { aggregateRating: rest.aggregateRating } : {}),
|
|
213
|
+
},
|
|
214
|
+
type: "Product",
|
|
215
|
+
required: ["name", "offers"],
|
|
216
|
+
};
|
|
217
|
+
case "BreadcrumbList":
|
|
218
|
+
return {
|
|
219
|
+
jsonLd: {
|
|
220
|
+
"@context": context,
|
|
221
|
+
"@type": "BreadcrumbList",
|
|
222
|
+
itemListElement: (rest.items ?? []).map((item, i) => ({
|
|
223
|
+
"@type": "ListItem",
|
|
224
|
+
position: i + 1,
|
|
225
|
+
name: item.name,
|
|
226
|
+
item: item.url,
|
|
227
|
+
})),
|
|
228
|
+
},
|
|
229
|
+
type: "BreadcrumbList",
|
|
230
|
+
required: ["itemListElement"],
|
|
231
|
+
};
|
|
232
|
+
case "WebSite":
|
|
233
|
+
return {
|
|
234
|
+
jsonLd: {
|
|
235
|
+
"@context": context,
|
|
236
|
+
"@type": "WebSite",
|
|
237
|
+
name: rest.name ?? "[Site Name]",
|
|
238
|
+
url: rest.url ?? "[Site URL]",
|
|
239
|
+
...(rest.potentialAction ? { potentialAction: rest.potentialAction } : {}),
|
|
240
|
+
},
|
|
241
|
+
type: "WebSite",
|
|
242
|
+
required: ["name", "url"],
|
|
243
|
+
};
|
|
244
|
+
case "Person":
|
|
245
|
+
case "ProfilePage":
|
|
246
|
+
return {
|
|
247
|
+
jsonLd: {
|
|
248
|
+
"@context": context,
|
|
249
|
+
"@type": type,
|
|
250
|
+
name: rest.name ?? "[Name]",
|
|
251
|
+
url: rest.url ?? "[Profile URL]",
|
|
252
|
+
...(rest.sameAs ? { sameAs: rest.sameAs } : {}),
|
|
253
|
+
...(rest.knowsAbout ? { knowsAbout: rest.knowsAbout } : {}),
|
|
254
|
+
...(rest.jobTitle ? { jobTitle: rest.jobTitle } : {}),
|
|
255
|
+
...(rest.worksFor ? { worksFor: { "@type": "Organization", name: rest.worksFor } } : {}),
|
|
256
|
+
...(rest.image ? { image: rest.image } : {}),
|
|
257
|
+
},
|
|
258
|
+
type,
|
|
259
|
+
required: ["name", "url"],
|
|
260
|
+
};
|
|
261
|
+
case "DiscussionForumPosting":
|
|
262
|
+
return {
|
|
263
|
+
jsonLd: {
|
|
264
|
+
"@context": context,
|
|
265
|
+
"@type": "DiscussionForumPosting",
|
|
266
|
+
headline: rest.headline ?? "[Headline]",
|
|
267
|
+
author: { "@type": "Person", name: rest.author ?? "[Author]" },
|
|
268
|
+
datePublished: rest.datePublished ?? "[YYYY-MM-DD]",
|
|
269
|
+
url: rest.url ?? "[URL]",
|
|
270
|
+
mainEntityOfPage: { "@type": "WebPage", "@id": rest.url ?? "[URL]" },
|
|
271
|
+
...(rest.text ? { text: rest.text } : {}),
|
|
272
|
+
...(rest.dateModified ? { dateModified: rest.dateModified } : {}),
|
|
273
|
+
...(rest.commentCount !== undefined ? { commentCount: rest.commentCount } : {}),
|
|
274
|
+
},
|
|
275
|
+
type: "DiscussionForumPosting",
|
|
276
|
+
required: ["headline", "author", "datePublished", "url"],
|
|
277
|
+
};
|
|
278
|
+
case "OrderAction":
|
|
279
|
+
return {
|
|
280
|
+
jsonLd: {
|
|
281
|
+
"@context": context,
|
|
282
|
+
"@type": "OrderAction",
|
|
283
|
+
name: rest.name ?? "Order online",
|
|
284
|
+
target: {
|
|
285
|
+
"@type": "EntryPoint",
|
|
286
|
+
urlTemplate: rest.orderUrl ?? "[Order URL]",
|
|
287
|
+
inLanguage: "en-US",
|
|
288
|
+
actionPlatform: [
|
|
289
|
+
"https://schema.org/DesktopWebPlatform",
|
|
290
|
+
"https://schema.org/MobileWebPlatform",
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
deliveryMethod: rest.deliveryMethod ?? [
|
|
294
|
+
"https://schema.org/OnSitePickup",
|
|
295
|
+
"https://schema.org/ParcelService",
|
|
296
|
+
],
|
|
297
|
+
merchant: { "@type": "Organization", name: rest.merchant ?? "[Merchant]" },
|
|
298
|
+
},
|
|
299
|
+
type: "OrderAction",
|
|
300
|
+
required: ["orderUrl", "merchant"],
|
|
301
|
+
};
|
|
302
|
+
case "Reservation":
|
|
303
|
+
return {
|
|
304
|
+
jsonLd: {
|
|
305
|
+
"@context": context,
|
|
306
|
+
"@type": rest.kind ?? "FoodEstablishmentReservation",
|
|
307
|
+
reservationStatus: "https://schema.org/ReservationConfirmed",
|
|
308
|
+
provider: { "@type": "Organization", name: rest.provider ?? "[Provider]" },
|
|
309
|
+
reservationFor: {
|
|
310
|
+
"@type": rest.kind === "FoodEstablishmentReservation" ? "FoodEstablishment" : "Place",
|
|
311
|
+
name: rest.reservationForName ?? rest.provider ?? "[Name]",
|
|
312
|
+
},
|
|
313
|
+
startTime: rest.start ?? "[Start Time]",
|
|
314
|
+
...(rest.end ? { endTime: rest.end } : {}),
|
|
315
|
+
...(rest.partySize !== undefined ? { partySize: rest.partySize } : {}),
|
|
316
|
+
...(rest.reservationId ? { reservationId: rest.reservationId } : {}),
|
|
317
|
+
},
|
|
318
|
+
type: "Reservation",
|
|
319
|
+
required: ["provider", "start"],
|
|
320
|
+
};
|
|
321
|
+
default:
|
|
322
|
+
return { jsonLd: { "@context": context, "@type": "Thing" }, type: "Unknown", required: [] };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XML sitemap analyzer and generator.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the Python `seo-sitemap/SKILL.md` directives in claude-seo.
|
|
5
|
+
* Parses sitemap XML (including sitemap index files) and generates valid
|
|
6
|
+
* XML sitemaps from URL lists.
|
|
7
|
+
*/
|
|
8
|
+
import type { SitemapAnalysisResult } from "../types.js";
|
|
9
|
+
import type { SeoConfig } from "../config.js";
|
|
10
|
+
/** Analyze a sitemap at a given URL. */
|
|
11
|
+
export declare function analyzeSitemap(sitemapUrl: string, config: SeoConfig): Promise<SitemapAnalysisResult>;
|
|
12
|
+
export interface SitemapGenerateEntry {
|
|
13
|
+
loc: string;
|
|
14
|
+
lastmod?: string;
|
|
15
|
+
changefreq?: string;
|
|
16
|
+
priority?: number;
|
|
17
|
+
}
|
|
18
|
+
/** Generate an XML sitemap from a list of URLs. */
|
|
19
|
+
export declare function generateSitemap(entries: SitemapGenerateEntry[]): string;
|
|
20
|
+
//# sourceMappingURL=sitemap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/sitemap.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AA8C9C,wCAAwC;AACxC,wBAAsB,cAAc,CAClC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,SAAS,GAChB,OAAO,CAAC,qBAAqB,CAAC,CA4DhC;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,mDAAmD;AACnD,wBAAgB,eAAe,CAAC,OAAO,EAAE,oBAAoB,EAAE,GAAG,MAAM,CAuBvE"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { fetchPage } from "../utils/fetcher.js";
|
|
2
|
+
/** Parse a sitemap XML string into URLs. */
|
|
3
|
+
function parseSitemapXml(xml) {
|
|
4
|
+
const urls = [];
|
|
5
|
+
const childSitemaps = [];
|
|
6
|
+
const isIndex = /<sitemapindex/i.test(xml);
|
|
7
|
+
if (isIndex) {
|
|
8
|
+
const sitemapRegex = /<sitemap>([\s\S]*?)<\/sitemap>/gi;
|
|
9
|
+
let match;
|
|
10
|
+
while ((match = sitemapRegex.exec(xml)) !== null) {
|
|
11
|
+
const locMatch = /<loc>(.*?)<\/loc>/i.exec(match[1]);
|
|
12
|
+
if (locMatch) {
|
|
13
|
+
childSitemaps.push(locMatch[1].trim());
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return { urls, isIndex: true, childSitemaps };
|
|
17
|
+
}
|
|
18
|
+
const urlRegex = /<url>([\s\S]*?)<\/url>/gi;
|
|
19
|
+
let match;
|
|
20
|
+
while ((match = urlRegex.exec(xml)) !== null) {
|
|
21
|
+
const block = match[1];
|
|
22
|
+
const loc = /<loc>(.*?)<\/loc>/i.exec(block)?.[1]?.trim();
|
|
23
|
+
if (loc) {
|
|
24
|
+
urls.push({
|
|
25
|
+
loc,
|
|
26
|
+
lastmod: /<lastmod>(.*?)<\/lastmod>/i.exec(block)?.[1]?.trim(),
|
|
27
|
+
changefreq: /<changefreq>(.*?)<\/changefreq>/i.exec(block)?.[1]?.trim(),
|
|
28
|
+
priority: /<priority>(.*?)<\/priority>/i.exec(block)?.[1]?.trim(),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { urls, isIndex: false, childSitemaps };
|
|
33
|
+
}
|
|
34
|
+
/** Analyze a sitemap at a given URL. */
|
|
35
|
+
export async function analyzeSitemap(sitemapUrl, config) {
|
|
36
|
+
const result = await fetchPage(sitemapUrl, config);
|
|
37
|
+
const warnings = [];
|
|
38
|
+
if (result.statusCode !== 200) {
|
|
39
|
+
return {
|
|
40
|
+
url: sitemapUrl,
|
|
41
|
+
urlCount: 0,
|
|
42
|
+
urls: [],
|
|
43
|
+
lastmodCoverage: 0,
|
|
44
|
+
invalidEntries: 0,
|
|
45
|
+
sizeBytes: result.contentLengthBytes,
|
|
46
|
+
isSitemapIndex: false,
|
|
47
|
+
childSitemaps: [],
|
|
48
|
+
warnings: [`Sitemap returned status ${result.statusCode}`],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const parsed = parseSitemapXml(result.content);
|
|
52
|
+
// Validate URLs.
|
|
53
|
+
let invalidEntries = 0;
|
|
54
|
+
const validUrls = [];
|
|
55
|
+
for (const u of parsed.urls) {
|
|
56
|
+
try {
|
|
57
|
+
new URL(u.loc);
|
|
58
|
+
validUrls.push(u.loc);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
invalidEntries++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Calculate lastmod coverage.
|
|
65
|
+
const withLastmod = parsed.urls.filter((u) => u.lastmod).length;
|
|
66
|
+
const lastmodCoverage = parsed.urls.length > 0
|
|
67
|
+
? Math.round((withLastmod / parsed.urls.length) * 100)
|
|
68
|
+
: 0;
|
|
69
|
+
// Size warnings.
|
|
70
|
+
if (result.contentLengthBytes > 50_000_000) {
|
|
71
|
+
warnings.push("Sitemap exceeds 50MB uncompressed limit. Split into multiple sitemaps.");
|
|
72
|
+
}
|
|
73
|
+
if (parsed.urls.length > 50_000) {
|
|
74
|
+
warnings.push("Sitemap contains more than 50,000 URLs. Split into multiple sitemaps.");
|
|
75
|
+
}
|
|
76
|
+
if (lastmodCoverage < 50 && parsed.urls.length > 0) {
|
|
77
|
+
warnings.push(`Only ${lastmodCoverage}% of URLs have lastmod dates. Adding lastmod improves crawl efficiency.`);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
url: result.finalUrl,
|
|
81
|
+
urlCount: parsed.urls.length,
|
|
82
|
+
urls: validUrls,
|
|
83
|
+
lastmodCoverage,
|
|
84
|
+
invalidEntries,
|
|
85
|
+
sizeBytes: result.contentLengthBytes,
|
|
86
|
+
isSitemapIndex: parsed.isIndex,
|
|
87
|
+
childSitemaps: parsed.childSitemaps,
|
|
88
|
+
warnings,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** Generate an XML sitemap from a list of URLs. */
|
|
92
|
+
export function generateSitemap(entries) {
|
|
93
|
+
const lines = [
|
|
94
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
95
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
96
|
+
];
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
lines.push(" <url>");
|
|
99
|
+
lines.push(` <loc>${escapeXml(entry.loc)}</loc>`);
|
|
100
|
+
if (entry.lastmod) {
|
|
101
|
+
lines.push(` <lastmod>${escapeXml(entry.lastmod)}</lastmod>`);
|
|
102
|
+
}
|
|
103
|
+
if (entry.changefreq) {
|
|
104
|
+
lines.push(` <changefreq>${escapeXml(entry.changefreq)}</changefreq>`);
|
|
105
|
+
}
|
|
106
|
+
if (entry.priority !== undefined) {
|
|
107
|
+
lines.push(` <priority>${entry.priority.toFixed(1)}</priority>`);
|
|
108
|
+
}
|
|
109
|
+
lines.push(" </url>");
|
|
110
|
+
}
|
|
111
|
+
lines.push("</urlset>");
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
function escapeXml(str) {
|
|
115
|
+
return str
|
|
116
|
+
.replace(/&/g, "&")
|
|
117
|
+
.replace(/</g, "<")
|
|
118
|
+
.replace(/>/g, ">")
|
|
119
|
+
.replace(/"/g, """)
|
|
120
|
+
.replace(/'/g, "'");
|
|
121
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Technical SEO audit analyzer.
|
|
3
|
+
*
|
|
4
|
+
* Ported from the Python `seo-technical/SKILL.md` directives in claude-seo.
|
|
5
|
+
* Audits crawlability, indexability, security, URL structure, mobile,
|
|
6
|
+
* and AI crawler management — all using local HTTP fetches only.
|
|
7
|
+
*/
|
|
8
|
+
import type { SeoConfig } from "../config.js";
|
|
9
|
+
import type { FetchResult, PageData, TechnicalAuditResult } from "../types.js";
|
|
10
|
+
export declare function auditTechnical(url: string, config: SeoConfig, pageData?: PageData, fetchResult?: FetchResult): Promise<TechnicalAuditResult>;
|
|
11
|
+
//# sourceMappingURL=technical.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"technical.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/technical.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAc,WAAW,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AA2D3F,wBAAsB,cAAc,CAClC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,EACjB,QAAQ,CAAC,EAAE,QAAQ,EACnB,WAAW,CAAC,EAAE,WAAW,GACxB,OAAO,CAAC,oBAAoB,CAAC,CA+N/B"}
|