vercel-seo-audit 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/README.md +208 -0
- package/bin/vercel-seo-audit.js +2 -0
- package/dist/audit/favicon.d.ts +3 -0
- package/dist/audit/favicon.d.ts.map +1 -0
- package/dist/audit/favicon.js +73 -0
- package/dist/audit/favicon.js.map +1 -0
- package/dist/audit/index.d.ts +7 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +7 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/metadata.d.ts +3 -0
- package/dist/audit/metadata.d.ts.map +1 -0
- package/dist/audit/metadata.js +183 -0
- package/dist/audit/metadata.js.map +1 -0
- package/dist/audit/nextjs.d.ts +3 -0
- package/dist/audit/nextjs.d.ts.map +1 -0
- package/dist/audit/nextjs.js +104 -0
- package/dist/audit/nextjs.js.map +1 -0
- package/dist/audit/redirects.d.ts +3 -0
- package/dist/audit/redirects.d.ts.map +1 -0
- package/dist/audit/redirects.js +134 -0
- package/dist/audit/redirects.js.map +1 -0
- package/dist/audit/robots.d.ts +3 -0
- package/dist/audit/robots.d.ts.map +1 -0
- package/dist/audit/robots.js +115 -0
- package/dist/audit/robots.js.map +1 -0
- package/dist/audit/sitemap.d.ts +3 -0
- package/dist/audit/sitemap.d.ts.map +1 -0
- package/dist/audit/sitemap.js +152 -0
- package/dist/audit/sitemap.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +51 -0
- package/dist/cli.js.map +1 -0
- package/dist/constants.d.ts +11 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +11 -0
- package/dist/constants.js.map +1 -0
- package/dist/runner.d.ts +6 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +55 -0
- package/dist/runner.js.map +1 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/html-parser.d.ts +15 -0
- package/dist/utils/html-parser.d.ts.map +1 -0
- package/dist/utils/html-parser.js +65 -0
- package/dist/utils/html-parser.js.map +1 -0
- package/dist/utils/http.d.ts +14 -0
- package/dist/utils/http.d.ts.map +1 -0
- package/dist/utils/http.js +88 -0
- package/dist/utils/http.js.map +1 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/output.d.ts +4 -0
- package/dist/utils/output.d.ts.map +1 -0
- package/dist/utils/output.js +72 -0
- package/dist/utils/output.js.map +1 -0
- package/dist/utils/url.d.ts +10 -0
- package/dist/utils/url.d.ts.map +1 -0
- package/dist/utils/url.js +51 -0
- package/dist/utils/url.js.map +1 -0
- package/dist/utils/xml-parser.d.ts +13 -0
- package/dist/utils/xml-parser.d.ts.map +1 -0
- package/dist/utils/xml-parser.js +41 -0
- package/dist/utils/xml-parser.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { COMMON_PAGES } from '../constants.js';
|
|
2
|
+
import { followRedirectChain, fetchPage, fetchWithoutRedirect, } from '../utils/http.js';
|
|
3
|
+
import { getMetaRefresh } from '../utils/html-parser.js';
|
|
4
|
+
import { toHttpUrl, isHttps, hasTrailingSlash, addTrailingSlash, removeTrailingSlash, getOrigin, } from '../utils/url.js';
|
|
5
|
+
export async function auditRedirects(ctx) {
|
|
6
|
+
const findings = [];
|
|
7
|
+
const { normalizedUrl, fetchOptions } = ctx;
|
|
8
|
+
// 1. Homepage redirect chain
|
|
9
|
+
const chain = await followRedirectChain(normalizedUrl, fetchOptions);
|
|
10
|
+
if (chain.isCircular) {
|
|
11
|
+
findings.push({
|
|
12
|
+
code: 'REDIRECT_LOOP',
|
|
13
|
+
severity: 'error',
|
|
14
|
+
category: 'redirect',
|
|
15
|
+
message: 'Redirect loop detected on homepage',
|
|
16
|
+
explanation: 'A redirect loop prevents search engines and users from reaching your page, causing crawl failures.',
|
|
17
|
+
suggestion: 'Check your server configuration and middleware for circular redirects.',
|
|
18
|
+
details: { hops: chain.hops },
|
|
19
|
+
url: normalizedUrl,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
else if (chain.hops.length > 1) {
|
|
23
|
+
findings.push({
|
|
24
|
+
code: 'REDIRECT_CHAIN',
|
|
25
|
+
severity: 'warning',
|
|
26
|
+
category: 'redirect',
|
|
27
|
+
message: `Redirect chain with ${chain.hops.length} hops detected`,
|
|
28
|
+
explanation: 'Long redirect chains slow down page loading and may cause search engines to drop the page from the index.',
|
|
29
|
+
suggestion: 'Reduce the chain to a single redirect by pointing directly to the final URL.',
|
|
30
|
+
details: { hops: chain.hops, finalUrl: chain.finalUrl },
|
|
31
|
+
url: normalizedUrl,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
// 2. HTTP → HTTPS check
|
|
35
|
+
if (isHttps(normalizedUrl)) {
|
|
36
|
+
try {
|
|
37
|
+
const httpUrl = toHttpUrl(normalizedUrl);
|
|
38
|
+
const httpChain = await followRedirectChain(httpUrl, fetchOptions);
|
|
39
|
+
if (isHttps(httpChain.finalUrl)) {
|
|
40
|
+
findings.push({
|
|
41
|
+
code: 'HTTP_TO_HTTPS_REDIRECT',
|
|
42
|
+
severity: 'pass',
|
|
43
|
+
category: 'redirect',
|
|
44
|
+
message: 'HTTP correctly redirects to HTTPS',
|
|
45
|
+
explanation: 'HTTP to HTTPS redirects ensure users always reach the secure version of your site.',
|
|
46
|
+
suggestion: 'No action needed.',
|
|
47
|
+
url: httpUrl,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
findings.push({
|
|
52
|
+
code: 'HTTP_NO_HTTPS_REDIRECT',
|
|
53
|
+
severity: 'warning',
|
|
54
|
+
category: 'redirect',
|
|
55
|
+
message: 'HTTP does not redirect to HTTPS',
|
|
56
|
+
explanation: 'Without an HTTP→HTTPS redirect, search engines may index the insecure version of your site.',
|
|
57
|
+
suggestion: 'Configure your server or Vercel project to redirect HTTP traffic to HTTPS.',
|
|
58
|
+
url: httpUrl,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// HTTP variant may not exist — not an issue
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// 3. Trailing slash check
|
|
67
|
+
try {
|
|
68
|
+
const withSlash = addTrailingSlash(normalizedUrl);
|
|
69
|
+
const withoutSlash = removeTrailingSlash(normalizedUrl);
|
|
70
|
+
const testUrl = hasTrailingSlash(normalizedUrl) ? withoutSlash : withSlash;
|
|
71
|
+
const slashRes = await fetchWithoutRedirect(testUrl, fetchOptions);
|
|
72
|
+
if (slashRes.status >= 300 && slashRes.status < 400) {
|
|
73
|
+
const status = slashRes.status;
|
|
74
|
+
findings.push({
|
|
75
|
+
code: 'TRAILING_SLASH_REDIRECT',
|
|
76
|
+
severity: status === 308 ? 'info' : 'info',
|
|
77
|
+
category: 'redirect',
|
|
78
|
+
message: `Trailing slash ${hasTrailingSlash(normalizedUrl) ? 'removal' : 'addition'} causes ${status} redirect`,
|
|
79
|
+
explanation: 'Inconsistent trailing slash handling can create duplicate content issues for search engines.',
|
|
80
|
+
suggestion: 'Ensure consistent trailing slash behavior across your site. In Next.js, use the trailingSlash config option.',
|
|
81
|
+
details: { testedUrl: testUrl, status },
|
|
82
|
+
url: testUrl,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Not critical
|
|
88
|
+
}
|
|
89
|
+
// 4. Meta refresh detection
|
|
90
|
+
try {
|
|
91
|
+
const page = await fetchPage(normalizedUrl, fetchOptions);
|
|
92
|
+
const metaRefreshUrl = getMetaRefresh(page.body);
|
|
93
|
+
if (metaRefreshUrl) {
|
|
94
|
+
findings.push({
|
|
95
|
+
code: 'META_REFRESH_REDIRECT',
|
|
96
|
+
severity: 'warning',
|
|
97
|
+
category: 'redirect',
|
|
98
|
+
message: 'Meta refresh redirect detected on homepage',
|
|
99
|
+
explanation: 'Meta refresh redirects are slower than server-side redirects and may confuse search engines.',
|
|
100
|
+
suggestion: 'Replace the meta refresh with a 301 server-side redirect.',
|
|
101
|
+
details: { targetUrl: metaRefreshUrl },
|
|
102
|
+
url: normalizedUrl,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Not critical
|
|
108
|
+
}
|
|
109
|
+
// 5. Common page redirect checks
|
|
110
|
+
const origin = getOrigin(normalizedUrl);
|
|
111
|
+
for (const path of COMMON_PAGES) {
|
|
112
|
+
try {
|
|
113
|
+
const pageUrl = `${origin}${path}`;
|
|
114
|
+
const pageChain = await followRedirectChain(pageUrl, fetchOptions);
|
|
115
|
+
if (pageChain.hops.length > 1) {
|
|
116
|
+
findings.push({
|
|
117
|
+
code: 'COMMON_PAGE_REDIRECT',
|
|
118
|
+
severity: 'info',
|
|
119
|
+
category: 'redirect',
|
|
120
|
+
message: `${path} has a ${pageChain.hops.length}-hop redirect chain`,
|
|
121
|
+
explanation: 'Redirect chains on commonly linked pages waste crawl budget.',
|
|
122
|
+
suggestion: 'Reduce to a single redirect or update internal links to point to the final URL.',
|
|
123
|
+
details: { hops: pageChain.hops, finalUrl: pageChain.finalUrl },
|
|
124
|
+
url: pageUrl,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Page may not exist
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return findings;
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=redirects.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirects.js","sourceRoot":"","sources":["../../src/audit/redirects.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"robots.d.ts","sourceRoot":"","sources":["../../src/audit/robots.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAuC9D,wBAAsB,WAAW,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAgG5E"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { DEFAULT_PATHS } from '../constants.js';
|
|
2
|
+
import { fetchPage } from '../utils/http.js';
|
|
3
|
+
import { getOrigin } from '../utils/url.js';
|
|
4
|
+
function parseRobotsTxt(txt) {
|
|
5
|
+
const lines = txt.split('\n').map((l) => l.trim());
|
|
6
|
+
const rules = [];
|
|
7
|
+
const sitemaps = [];
|
|
8
|
+
let current = null;
|
|
9
|
+
for (const line of lines) {
|
|
10
|
+
if (line.startsWith('#') || line === '')
|
|
11
|
+
continue;
|
|
12
|
+
const [key, ...rest] = line.split(':');
|
|
13
|
+
const value = rest.join(':').trim();
|
|
14
|
+
const keyLower = key.toLowerCase().trim();
|
|
15
|
+
if (keyLower === 'user-agent') {
|
|
16
|
+
current = { userAgent: value, disallow: [], allow: [] };
|
|
17
|
+
rules.push(current);
|
|
18
|
+
}
|
|
19
|
+
else if (keyLower === 'disallow' && current) {
|
|
20
|
+
if (value)
|
|
21
|
+
current.disallow.push(value);
|
|
22
|
+
}
|
|
23
|
+
else if (keyLower === 'allow' && current) {
|
|
24
|
+
if (value)
|
|
25
|
+
current.allow.push(value);
|
|
26
|
+
}
|
|
27
|
+
else if (keyLower === 'sitemap') {
|
|
28
|
+
if (value)
|
|
29
|
+
sitemaps.push(value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { rules, sitemaps };
|
|
33
|
+
}
|
|
34
|
+
export async function auditRobots(ctx) {
|
|
35
|
+
const findings = [];
|
|
36
|
+
const origin = getOrigin(ctx.normalizedUrl);
|
|
37
|
+
const robotsUrl = `${origin}${DEFAULT_PATHS.robotsTxt}`;
|
|
38
|
+
let robotsTxt;
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetchPage(robotsUrl, ctx.fetchOptions);
|
|
41
|
+
if (res.status === 200) {
|
|
42
|
+
robotsTxt = res.body;
|
|
43
|
+
// Store for cross-module use
|
|
44
|
+
ctx.robotsTxt = robotsTxt;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
findings.push({
|
|
48
|
+
code: 'ROBOTS_MISSING',
|
|
49
|
+
severity: 'warning',
|
|
50
|
+
category: 'robots',
|
|
51
|
+
message: 'robots.txt not found',
|
|
52
|
+
explanation: 'Without a robots.txt, search engines have no guidance on which pages to crawl or avoid.',
|
|
53
|
+
suggestion: 'Create a robots.txt file at the root of your site. In Next.js App Router, use the metadata API.',
|
|
54
|
+
url: robotsUrl,
|
|
55
|
+
});
|
|
56
|
+
return findings;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
findings.push({
|
|
61
|
+
code: 'ROBOTS_MISSING',
|
|
62
|
+
severity: 'warning',
|
|
63
|
+
category: 'robots',
|
|
64
|
+
message: 'robots.txt could not be fetched',
|
|
65
|
+
explanation: 'Without a robots.txt, search engines have no guidance on which pages to crawl or avoid.',
|
|
66
|
+
suggestion: 'Ensure robots.txt is accessible at the root of your domain.',
|
|
67
|
+
url: robotsUrl,
|
|
68
|
+
});
|
|
69
|
+
return findings;
|
|
70
|
+
}
|
|
71
|
+
const { rules, sitemaps } = parseRobotsTxt(robotsTxt);
|
|
72
|
+
// Check for blocking all crawlers
|
|
73
|
+
for (const rule of rules) {
|
|
74
|
+
const ua = rule.userAgent.toLowerCase();
|
|
75
|
+
if (ua === '*' && rule.disallow.includes('/')) {
|
|
76
|
+
findings.push({
|
|
77
|
+
code: 'ROBOTS_BLOCKS_ALL',
|
|
78
|
+
severity: 'error',
|
|
79
|
+
category: 'robots',
|
|
80
|
+
message: 'robots.txt blocks all crawlers',
|
|
81
|
+
explanation: 'Disallow: / for all user agents prevents search engines from indexing any page on your site.',
|
|
82
|
+
suggestion: 'Remove or scope the Disallow: / rule unless you intentionally want to prevent indexing.',
|
|
83
|
+
details: { rule },
|
|
84
|
+
url: robotsUrl,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if ((ua === 'googlebot' || ua === 'googlebot-news' || ua === 'googlebot-image') &&
|
|
88
|
+
rule.disallow.includes('/')) {
|
|
89
|
+
findings.push({
|
|
90
|
+
code: 'ROBOTS_BLOCKS_GOOGLEBOT',
|
|
91
|
+
severity: 'error',
|
|
92
|
+
category: 'robots',
|
|
93
|
+
message: `robots.txt blocks ${rule.userAgent}`,
|
|
94
|
+
explanation: 'Blocking Googlebot prevents Google from indexing your site.',
|
|
95
|
+
suggestion: `Remove the Disallow: / for ${rule.userAgent} unless intentional.`,
|
|
96
|
+
details: { rule },
|
|
97
|
+
url: robotsUrl,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Check for sitemap directive
|
|
102
|
+
if (sitemaps.length === 0) {
|
|
103
|
+
findings.push({
|
|
104
|
+
code: 'ROBOTS_NO_SITEMAP',
|
|
105
|
+
severity: 'info',
|
|
106
|
+
category: 'robots',
|
|
107
|
+
message: 'No Sitemap directive found in robots.txt',
|
|
108
|
+
explanation: 'Declaring your sitemap URL in robots.txt helps search engines discover it faster.',
|
|
109
|
+
suggestion: 'Add a Sitemap: https://yourdomain.com/sitemap.xml line to robots.txt.',
|
|
110
|
+
url: robotsUrl,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return findings;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=robots.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"robots.js","sourceRoot":"","sources":["../../src/audit/robots.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../src/audit/sitemap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAM9D,wBAAsB,YAAY,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAiK7E"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { DEFAULT_PATHS, SITEMAP_SAMPLE_SIZE } from '../constants.js';
|
|
2
|
+
import { fetchPage, fetchHead, followRedirectChain } from '../utils/http.js';
|
|
3
|
+
import { parseSitemapXml } from '../utils/xml-parser.js';
|
|
4
|
+
import { getOrigin } from '../utils/url.js';
|
|
5
|
+
export async function auditSitemap(ctx) {
|
|
6
|
+
const findings = [];
|
|
7
|
+
const origin = getOrigin(ctx.normalizedUrl);
|
|
8
|
+
const sitemapUrl = `${origin}${DEFAULT_PATHS.sitemapXml}`;
|
|
9
|
+
// 1. Check if sitemap is redirected
|
|
10
|
+
const chain = await followRedirectChain(sitemapUrl, ctx.fetchOptions);
|
|
11
|
+
if (chain.hops.length > 0) {
|
|
12
|
+
findings.push({
|
|
13
|
+
code: 'SITEMAP_REDIRECTED',
|
|
14
|
+
severity: 'warning',
|
|
15
|
+
category: 'sitemap',
|
|
16
|
+
message: 'Sitemap URL is redirected',
|
|
17
|
+
explanation: 'Some search engines may not follow redirects to sitemaps, which could cause discovery issues.',
|
|
18
|
+
suggestion: 'Serve the sitemap directly at /sitemap.xml without redirects.',
|
|
19
|
+
details: { hops: chain.hops, finalUrl: chain.finalUrl },
|
|
20
|
+
url: sitemapUrl,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
// 2. Fetch and parse the sitemap
|
|
24
|
+
let sitemapBody;
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetchPage(sitemapUrl, ctx.fetchOptions);
|
|
27
|
+
if (res.status !== 200) {
|
|
28
|
+
findings.push({
|
|
29
|
+
code: 'SITEMAP_MISSING',
|
|
30
|
+
severity: 'warning',
|
|
31
|
+
category: 'sitemap',
|
|
32
|
+
message: 'sitemap.xml not found',
|
|
33
|
+
explanation: 'Without a sitemap, search engines rely solely on crawling to discover your pages, which may miss content.',
|
|
34
|
+
suggestion: 'Generate a sitemap.xml. In Next.js App Router, export a sitemap function from app/sitemap.ts.',
|
|
35
|
+
url: sitemapUrl,
|
|
36
|
+
});
|
|
37
|
+
return findings;
|
|
38
|
+
}
|
|
39
|
+
sitemapBody = res.body;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
findings.push({
|
|
43
|
+
code: 'SITEMAP_MISSING',
|
|
44
|
+
severity: 'warning',
|
|
45
|
+
category: 'sitemap',
|
|
46
|
+
message: 'sitemap.xml could not be fetched',
|
|
47
|
+
explanation: 'Without a sitemap, search engines rely solely on crawling to discover your pages.',
|
|
48
|
+
suggestion: 'Ensure sitemap.xml is accessible at the root of your domain.',
|
|
49
|
+
url: sitemapUrl,
|
|
50
|
+
});
|
|
51
|
+
return findings;
|
|
52
|
+
}
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = parseSitemapXml(sitemapBody);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
findings.push({
|
|
59
|
+
code: 'SITEMAP_MISSING',
|
|
60
|
+
severity: 'error',
|
|
61
|
+
category: 'sitemap',
|
|
62
|
+
message: 'sitemap.xml contains invalid XML',
|
|
63
|
+
explanation: 'Search engines cannot read malformed sitemap files.',
|
|
64
|
+
suggestion: 'Validate and fix your sitemap XML structure.',
|
|
65
|
+
url: sitemapUrl,
|
|
66
|
+
});
|
|
67
|
+
return findings;
|
|
68
|
+
}
|
|
69
|
+
// 3. Handle sitemap index
|
|
70
|
+
if (parsed.type === 'sitemapindex') {
|
|
71
|
+
findings.push({
|
|
72
|
+
code: 'SITEMAP_MISSING',
|
|
73
|
+
severity: 'pass',
|
|
74
|
+
category: 'sitemap',
|
|
75
|
+
message: `Sitemap index found with ${parsed.sitemaps.length} sitemap(s)`,
|
|
76
|
+
explanation: 'A sitemap index is a valid approach for organizing large sitemaps.',
|
|
77
|
+
suggestion: 'No action needed.',
|
|
78
|
+
details: { sitemaps: parsed.sitemaps },
|
|
79
|
+
url: sitemapUrl,
|
|
80
|
+
});
|
|
81
|
+
return findings;
|
|
82
|
+
}
|
|
83
|
+
// 4. Check for empty sitemap
|
|
84
|
+
if (parsed.urls.length === 0) {
|
|
85
|
+
findings.push({
|
|
86
|
+
code: 'SITEMAP_EMPTY',
|
|
87
|
+
severity: 'warning',
|
|
88
|
+
category: 'sitemap',
|
|
89
|
+
message: 'Sitemap contains no URLs',
|
|
90
|
+
explanation: 'An empty sitemap provides no value for search engine crawling.',
|
|
91
|
+
suggestion: 'Add your site pages to the sitemap.',
|
|
92
|
+
url: sitemapUrl,
|
|
93
|
+
});
|
|
94
|
+
return findings;
|
|
95
|
+
}
|
|
96
|
+
// 5. Sample URLs for status checks
|
|
97
|
+
const sample = parsed.urls.slice(0, SITEMAP_SAMPLE_SIZE);
|
|
98
|
+
let errorCount = 0;
|
|
99
|
+
for (const entry of sample) {
|
|
100
|
+
try {
|
|
101
|
+
const { status } = await fetchHead(entry.loc, ctx.fetchOptions);
|
|
102
|
+
if (status >= 400) {
|
|
103
|
+
errorCount++;
|
|
104
|
+
findings.push({
|
|
105
|
+
code: 'SITEMAP_URL_ERROR',
|
|
106
|
+
severity: 'warning',
|
|
107
|
+
category: 'sitemap',
|
|
108
|
+
message: `Sitemap URL returns ${status}: ${entry.loc}`,
|
|
109
|
+
explanation: 'Sitemap URLs returning error status codes waste crawl budget and signal poor site quality.',
|
|
110
|
+
suggestion: 'Remove broken URLs from the sitemap or fix the underlying pages.',
|
|
111
|
+
details: { status },
|
|
112
|
+
url: entry.loc,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Network error for one URL shouldn't stop the audit
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (errorCount === 0) {
|
|
121
|
+
findings.push({
|
|
122
|
+
code: 'SITEMAP_MISSING',
|
|
123
|
+
severity: 'pass',
|
|
124
|
+
category: 'sitemap',
|
|
125
|
+
message: `Sitemap found with ${parsed.urls.length} URLs (${sample.length} sampled, all OK)`,
|
|
126
|
+
explanation: 'Your sitemap is valid and URLs are accessible.',
|
|
127
|
+
suggestion: 'No action needed.',
|
|
128
|
+
url: sitemapUrl,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// 6. Cross-reference with robots.txt Sitemap directive
|
|
132
|
+
if (ctx.robotsTxt) {
|
|
133
|
+
const robotsSitemaps = ctx.robotsTxt
|
|
134
|
+
.split('\n')
|
|
135
|
+
.filter((l) => l.toLowerCase().trim().startsWith('sitemap:'))
|
|
136
|
+
.map((l) => l.split(':').slice(1).join(':').trim());
|
|
137
|
+
if (robotsSitemaps.length > 0 && !robotsSitemaps.includes(sitemapUrl) && !robotsSitemaps.includes(chain.finalUrl)) {
|
|
138
|
+
findings.push({
|
|
139
|
+
code: 'SITEMAP_ROBOTS_MISMATCH',
|
|
140
|
+
severity: 'info',
|
|
141
|
+
category: 'sitemap',
|
|
142
|
+
message: 'Sitemap URL in robots.txt does not match /sitemap.xml',
|
|
143
|
+
explanation: 'Mismatched sitemap URLs between robots.txt and the default location may confuse crawlers.',
|
|
144
|
+
suggestion: 'Ensure robots.txt Sitemap directive matches your actual sitemap URL.',
|
|
145
|
+
details: { robotsSitemaps },
|
|
146
|
+
url: sitemapUrl,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return findings;
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=sitemap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap.js","sourceRoot":"","sources":["../../src/audit/sitemap.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { runAudit } from './runner.js';
|
|
3
|
+
import { formatReport, formatJson } from './utils/output.js';
|
|
4
|
+
const program = new Command();
|
|
5
|
+
program
|
|
6
|
+
.name('vercel-seo-audit')
|
|
7
|
+
.description('Diagnose SEO and indexing issues for Next.js/Vercel websites')
|
|
8
|
+
.version('0.1.0')
|
|
9
|
+
.argument('<url>', 'URL to audit (e.g. https://example.com)')
|
|
10
|
+
.option('--json', 'Output results as JSON')
|
|
11
|
+
.option('--verbose', 'Show detailed information for each finding')
|
|
12
|
+
.option('--timeout <ms>', 'Request timeout in milliseconds', '10000')
|
|
13
|
+
.action(async (url, options) => {
|
|
14
|
+
const timeout = parseInt(options.timeout, 10);
|
|
15
|
+
if (isNaN(timeout) || timeout <= 0) {
|
|
16
|
+
console.error('Error: --timeout must be a positive number');
|
|
17
|
+
process.exit(2);
|
|
18
|
+
}
|
|
19
|
+
// Validate URL
|
|
20
|
+
try {
|
|
21
|
+
const testUrl = /^https?:\/\//i.test(url) ? url : `https://${url}`;
|
|
22
|
+
new URL(testUrl);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
console.error(`Error: Invalid URL "${url}"`);
|
|
26
|
+
process.exit(2);
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const report = await runAudit(url, {
|
|
30
|
+
verbose: options.verbose,
|
|
31
|
+
timeout,
|
|
32
|
+
});
|
|
33
|
+
if (options.json) {
|
|
34
|
+
console.log(formatJson(report));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
console.log(formatReport(report, options.verbose ?? false));
|
|
38
|
+
}
|
|
39
|
+
// Exit code based on findings
|
|
40
|
+
if (report.summary.errors > 0) {
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
console.error('Fatal error:', err instanceof Error ? err.message : err);
|
|
47
|
+
process.exit(2);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
program.parse();
|
|
51
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE7D,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,kBAAkB,CAAC;KACxB,WAAW,CAAC,8DAA8D,CAAC;KAC3E,OAAO,CAAC,OAAO,CAAC;KAChB,QAAQ,CAAC,OAAO,EAAE,yCAAyC,CAAC;KAC5D,MAAM,CAAC,QAAQ,EAAE,wBAAwB,CAAC;KAC1C,MAAM,CAAC,WAAW,EAAE,4CAA4C,CAAC;KACjE,MAAM,CAAC,gBAAgB,EAAE,iCAAiC,EAAE,OAAO,CAAC;KACpE,MAAM,CAAC,KAAK,EAAE,GAAW,EAAE,OAA+D,EAAE,EAAE;IAC7F,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAC9C,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,OAAO,IAAI,CAAC,EAAE,CAAC;QACnC,OAAO,CAAC,KAAK,CAAC,4CAA4C,CAAC,CAAC;QAC5D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,eAAe;IACf,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,GAAG,EAAE,CAAC;QACnE,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IACnB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,KAAK,CAAC,uBAAuB,GAAG,GAAG,CAAC,CAAC;QAC7C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,GAAG,EAAE;YACjC,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,OAAO;SACR,CAAC,CAAC;QAEH,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;YACjB,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;QAClC,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC;QAC9D,CAAC;QAED,8BAA8B;QAC9B,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACxE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,EAAE,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const DEFAULT_TIMEOUT = 10000;
|
|
2
|
+
export declare const MAX_REDIRECTS = 20;
|
|
3
|
+
export declare const USER_AGENT = "vercel-seo-audit/0.1.0";
|
|
4
|
+
export declare const SITEMAP_SAMPLE_SIZE = 10;
|
|
5
|
+
export declare const COMMON_PAGES: string[];
|
|
6
|
+
export declare const DEFAULT_PATHS: {
|
|
7
|
+
readonly robotsTxt: "/robots.txt";
|
|
8
|
+
readonly sitemapXml: "/sitemap.xml";
|
|
9
|
+
readonly faviconIco: "/favicon.ico";
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,QAAS,CAAC;AACtC,eAAO,MAAM,aAAa,KAAK,CAAC;AAChC,eAAO,MAAM,UAAU,2BAA2B,CAAC;AACnD,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,eAAO,MAAM,YAAY,UAA8C,CAAC;AAExE,eAAO,MAAM,aAAa;;;;CAIhB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const DEFAULT_TIMEOUT = 10_000;
|
|
2
|
+
export const MAX_REDIRECTS = 20;
|
|
3
|
+
export const USER_AGENT = 'vercel-seo-audit/0.1.0';
|
|
4
|
+
export const SITEMAP_SAMPLE_SIZE = 10;
|
|
5
|
+
export const COMMON_PAGES = ['/about', '/contact', '/blog', '/pricing'];
|
|
6
|
+
export const DEFAULT_PATHS = {
|
|
7
|
+
robotsTxt: '/robots.txt',
|
|
8
|
+
sitemapXml: '/sitemap.xml',
|
|
9
|
+
faviconIco: '/favicon.ico',
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC;AACtC,MAAM,CAAC,MAAM,aAAa,GAAG,EAAE,CAAC;AAChC,MAAM,CAAC,MAAM,UAAU,GAAG,wBAAwB,CAAC;AACnD,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAEtC,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;AAExE,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,SAAS,EAAE,aAAa;IACxB,UAAU,EAAE,cAAc;IAC1B,UAAU,EAAE,cAAc;CAClB,CAAC"}
|
package/dist/runner.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,WAAW,EAEZ,MAAM,YAAY,CAAC;AA+CpB,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,MAAM,EACX,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GACjD,OAAO,CAAC,WAAW,CAAC,CAuCtB"}
|
package/dist/runner.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { normalizeUrl } from './utils/url.js';
|
|
2
|
+
import { auditRedirects, auditRobots, auditSitemap, auditMetadata, auditFavicon, auditNextjs, } from './audit/index.js';
|
|
3
|
+
const phase1Modules = [
|
|
4
|
+
{ name: 'robots', run: auditRobots },
|
|
5
|
+
{ name: 'redirects', run: auditRedirects },
|
|
6
|
+
];
|
|
7
|
+
const phase2Modules = [
|
|
8
|
+
{ name: 'sitemap', run: auditSitemap },
|
|
9
|
+
{ name: 'metadata', run: auditMetadata },
|
|
10
|
+
{ name: 'favicon', run: auditFavicon },
|
|
11
|
+
{ name: 'nextjs', run: auditNextjs },
|
|
12
|
+
];
|
|
13
|
+
async function runModules(modules, ctx) {
|
|
14
|
+
const results = await Promise.allSettled(modules.map(async (mod) => {
|
|
15
|
+
const findings = await mod.run(ctx);
|
|
16
|
+
return { module: mod.name, findings };
|
|
17
|
+
}));
|
|
18
|
+
return results
|
|
19
|
+
.filter((r) => r.status === 'fulfilled')
|
|
20
|
+
.map((r) => r.value);
|
|
21
|
+
}
|
|
22
|
+
export async function runAudit(url, opts = {}) {
|
|
23
|
+
const start = Date.now();
|
|
24
|
+
const normalized = normalizeUrl(url);
|
|
25
|
+
const fetchOptions = {
|
|
26
|
+
timeout: opts.timeout,
|
|
27
|
+
};
|
|
28
|
+
const ctx = {
|
|
29
|
+
url,
|
|
30
|
+
normalizedUrl: normalized,
|
|
31
|
+
fetchOptions,
|
|
32
|
+
verbose: opts.verbose ?? false,
|
|
33
|
+
};
|
|
34
|
+
// Phase 1: robots + redirects (parallel)
|
|
35
|
+
const phase1Results = await runModules(phase1Modules, ctx);
|
|
36
|
+
// Phase 2: sitemap, metadata, favicon, nextjs (parallel)
|
|
37
|
+
const phase2Results = await runModules(phase2Modules, ctx);
|
|
38
|
+
const allModules = [...phase1Results, ...phase2Results];
|
|
39
|
+
// Compute summary
|
|
40
|
+
const allFindings = allModules.flatMap((m) => m.findings);
|
|
41
|
+
const summary = {
|
|
42
|
+
errors: allFindings.filter((f) => f.severity === 'error').length,
|
|
43
|
+
warnings: allFindings.filter((f) => f.severity === 'warning').length,
|
|
44
|
+
info: allFindings.filter((f) => f.severity === 'info').length,
|
|
45
|
+
passed: allFindings.filter((f) => f.severity === 'pass').length,
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
url: normalized,
|
|
49
|
+
timestamp: new Date().toISOString(),
|
|
50
|
+
duration: Date.now() - start,
|
|
51
|
+
summary,
|
|
52
|
+
modules: allModules,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EACL,cAAc,EACd,WAAW,EACX,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,WAAW,GACZ,MAAM,kBAAkB,CAAC;AAO1B,MAAM,aAAa,GAAkB;IACnC,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,WAAW,EAAE;IACpC,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,cAAc,EAAE;CAC3C,CAAC;AAEF,MAAM,aAAa,GAAkB;IACnC,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,YAAY,EAAE;IACtC,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,aAAa,EAAE;IACxC,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,YAAY,EAAE;IACtC,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,WAAW,EAAE;CACrC,CAAC;AAEF,KAAK,UAAU,UAAU,CACvB,OAAsB,EACtB,GAAiB;IAEjB,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CACtC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACxB,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACpC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,IAAI,EAAE,QAAQ,EAA8B,CAAC;IACpE,CAAC,CAAC,CACH,CAAC;IAEF,OAAO,OAAO;SACX,MAAM,CACL,CAAC,CAAC,EAAkD,EAAE,CACpD,CAAC,CAAC,MAAM,KAAK,WAAW,CAC3B;SACA,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,GAAW,EACX,OAAgD,EAAE;IAElD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,MAAM,UAAU,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAErC,MAAM,YAAY,GAAiB;QACjC,OAAO,EAAE,IAAI,CAAC,OAAO;KACtB,CAAC;IAEF,MAAM,GAAG,GAAiB;QACxB,GAAG;QACH,aAAa,EAAE,UAAU;QACzB,YAAY;QACZ,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,KAAK;KAC/B,CAAC;IAEF,yCAAyC;IACzC,MAAM,aAAa,GAAG,MAAM,UAAU,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;IAE3D,yDAAyD;IACzD,MAAM,aAAa,GAAG,MAAM,UAAU,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;IAE3D,MAAM,UAAU,GAAG,CAAC,GAAG,aAAa,EAAE,GAAG,aAAa,CAAC,CAAC;IAExD,kBAAkB;IAClB,MAAM,WAAW,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC1D,MAAM,OAAO,GAAG;QACd,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,MAAM;QAChE,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,MAAM;QACpE,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC,MAAM;QAC7D,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,CAAC,MAAM;KAChE,CAAC;IAEF,OAAO;QACL,GAAG,EAAE,UAAU;QACf,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;QAC5B,OAAO;QACP,OAAO,EAAE,UAAU;KACpB,CAAC;AACJ,CAAC"}
|