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
package/README.md
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# vercel-seo-audit
|
|
2
|
+
|
|
3
|
+
> **If you're using Vercel and Google hates your site, this is for you.**
|
|
4
|
+
|
|
5
|
+
A fast, developer-friendly CLI that explains **why Google isn’t indexing your Next.js site** — beyond the vague stuff in Search Console.
|
|
6
|
+
It detects the misconfigs that silently kill crawling and indexing: **redirect traps (308), missing robots/sitemap, noindex headers, canonical mismatches, and Vercel/Next.js quirks**.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Why this exists
|
|
11
|
+
|
|
12
|
+
Google Search Console often reports symptoms like:
|
|
13
|
+
- *“Page with redirect”*
|
|
14
|
+
- *“Discovered – currently not indexed”*
|
|
15
|
+
- *“Alternate page with proper canonical”*
|
|
16
|
+
|
|
17
|
+
…but doesn’t tell you **what’s actually wrong** or **what to change**.
|
|
18
|
+
|
|
19
|
+
`vercel-seo-audit` turns those symptoms into **actionable fixes**.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx vercel-seo-audit https://yoursite.com
|
|
27
|
+
````
|
|
28
|
+
|
|
29
|
+
Install globally (optional):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm i -g vercel-seo-audit
|
|
33
|
+
vercel-seo-audit https://yoursite.com
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Example output
|
|
39
|
+
|
|
40
|
+
```txt
|
|
41
|
+
SEO Audit Report for https://yusufhan.dev/
|
|
42
|
+
Completed in 1118ms at 2026-01-31T12:30:54.448Z
|
|
43
|
+
|
|
44
|
+
Summary:
|
|
45
|
+
⚠ 2 warnings
|
|
46
|
+
ℹ 1 info
|
|
47
|
+
✔ 1 passed
|
|
48
|
+
|
|
49
|
+
ROBOTS
|
|
50
|
+
────────────────────────────────────────
|
|
51
|
+
⚠ [WARNING] robots.txt not found
|
|
52
|
+
→ Create a robots.txt at /robots.txt
|
|
53
|
+
|
|
54
|
+
SITEMAP
|
|
55
|
+
────────────────────────────────────────
|
|
56
|
+
⚠ [WARNING] sitemap.xml not found
|
|
57
|
+
→ Add app/sitemap.ts in Next.js App Router
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Basic audit
|
|
66
|
+
vercel-seo-audit https://yoursite.com
|
|
67
|
+
|
|
68
|
+
# JSON output (pipe to jq, save to file, feed to CI)
|
|
69
|
+
vercel-seo-audit https://yoursite.com --json
|
|
70
|
+
|
|
71
|
+
# Verbose mode — raw HTTP details for each finding
|
|
72
|
+
vercel-seo-audit https://yoursite.com --verbose
|
|
73
|
+
|
|
74
|
+
# Custom timeout (default: 10s)
|
|
75
|
+
vercel-seo-audit https://yoursite.com --timeout 15000
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## What it checks
|
|
81
|
+
|
|
82
|
+
### Redirects
|
|
83
|
+
|
|
84
|
+
* Redirect chains & loops (homepage + common pages)
|
|
85
|
+
* HTTP → HTTPS redirect
|
|
86
|
+
* Trailing slash consistency (catches Next.js **308 traps**)
|
|
87
|
+
* Meta refresh redirects (`<meta http-equiv="refresh">`)
|
|
88
|
+
* Samples common routes: `/about`, `/contact`, `/blog`, `/pricing`
|
|
89
|
+
|
|
90
|
+
### robots.txt
|
|
91
|
+
|
|
92
|
+
* Missing robots.txt
|
|
93
|
+
* `Disallow: /` (blocks everything)
|
|
94
|
+
* Googlebot-specific blocks
|
|
95
|
+
* Missing `Sitemap:` directive
|
|
96
|
+
|
|
97
|
+
### Sitemap
|
|
98
|
+
|
|
99
|
+
* Missing or malformed `sitemap.xml`
|
|
100
|
+
* Sitemap redirects (some crawlers won’t follow)
|
|
101
|
+
* Empty sitemap / broken URLs (samples up to 10)
|
|
102
|
+
* Sitemap index support
|
|
103
|
+
* Cross-check with robots.txt `Sitemap:` directive
|
|
104
|
+
|
|
105
|
+
### Metadata
|
|
106
|
+
|
|
107
|
+
* Canonical URL presence & mismatch
|
|
108
|
+
* `noindex` via meta tags **and** `X-Robots-Tag` header
|
|
109
|
+
* Missing `title`, `description`, `charset`, `viewport`
|
|
110
|
+
* Open Graph basics: `og:title`, `og:description`, `og:image`
|
|
111
|
+
|
|
112
|
+
### Favicon
|
|
113
|
+
|
|
114
|
+
* Missing favicon entirely
|
|
115
|
+
* `/favicon.ico` exists but no `<link>` tag
|
|
116
|
+
* Conflicting favicon declarations (multiple icons)
|
|
117
|
+
|
|
118
|
+
### Next.js / Vercel
|
|
119
|
+
|
|
120
|
+
* Detect Vercel deployment
|
|
121
|
+
* Detect Next.js trailing slash redirect behavior
|
|
122
|
+
* Middleware rewrite/redirect headers (best-effort)
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Severity & exit codes
|
|
127
|
+
|
|
128
|
+
Findings are categorized by severity:
|
|
129
|
+
|
|
130
|
+
| Icon | Severity | Meaning |
|
|
131
|
+
| ---- | ----------- | ---------------------------------- |
|
|
132
|
+
| `✖` | **error** | Actively hurting SEO — fix now |
|
|
133
|
+
| `⚠` | **warning** | Likely causing problems — fix soon |
|
|
134
|
+
| `ℹ` | **info** | Useful context |
|
|
135
|
+
| `✔` | **pass** | Looks good |
|
|
136
|
+
|
|
137
|
+
Exit codes:
|
|
138
|
+
|
|
139
|
+
| Code | Meaning |
|
|
140
|
+
| ---- | ------------------------------------------ |
|
|
141
|
+
| `0` | No errors found (warnings/info don’t fail) |
|
|
142
|
+
| `1` | One or more errors found |
|
|
143
|
+
| `2` | Crash / invalid input |
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## CI / GitHub Actions
|
|
148
|
+
|
|
149
|
+
Fail the build only when **errors** exist:
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
name: SEO Audit
|
|
153
|
+
on:
|
|
154
|
+
workflow_dispatch:
|
|
155
|
+
push:
|
|
156
|
+
branches: [main]
|
|
157
|
+
|
|
158
|
+
jobs:
|
|
159
|
+
audit:
|
|
160
|
+
runs-on: ubuntu-latest
|
|
161
|
+
steps:
|
|
162
|
+
- uses: actions/checkout@v4
|
|
163
|
+
- uses: actions/setup-node@v4
|
|
164
|
+
with:
|
|
165
|
+
node-version: 20
|
|
166
|
+
- run: npx vercel-seo-audit https://yoursite.com --json
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Tip: If you want warnings to fail CI too, add a `--strict` flag in roadmap (see below).
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Roadmap
|
|
174
|
+
|
|
175
|
+
* [ ] `--strict` (warnings fail with exit code 1)
|
|
176
|
+
* [ ] `--pages` to customize sampled paths (`/about,/pricing`)
|
|
177
|
+
* [ ] `--user-agent` presets (`googlebot`, `bingbot`)
|
|
178
|
+
* [ ] `--report` to write `report.json` / `report.md`
|
|
179
|
+
* [ ] GitHub Action marketplace wrapper
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## FAQ
|
|
184
|
+
|
|
185
|
+
**Does this replace Google Search Console?**
|
|
186
|
+
No — it explains & verifies the things Search Console often reports vaguely.
|
|
187
|
+
|
|
188
|
+
**Will it scan my entire site?**
|
|
189
|
+
No. It checks critical endpoints + samples common pages to stay fast.
|
|
190
|
+
|
|
191
|
+
**Does it work on non-Next.js sites?**
|
|
192
|
+
Yes for most checks (redirects/robots/sitemap/metadata). Some checks are Next.js/Vercel-specific.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Contributing
|
|
197
|
+
|
|
198
|
+
PRs welcome. If you’re fixing a false positive, include:
|
|
199
|
+
|
|
200
|
+
* the URL (or a reproducible HTML sample)
|
|
201
|
+
* expected behavior
|
|
202
|
+
* actual output
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"favicon.d.ts","sourceRoot":"","sources":["../../src/audit/favicon.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,CA6E7E"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { DEFAULT_PATHS } from '../constants.js';
|
|
2
|
+
import { fetchHead, fetchPage } from '../utils/http.js';
|
|
3
|
+
import { getFaviconLinks } from '../utils/html-parser.js';
|
|
4
|
+
import { getOrigin } from '../utils/url.js';
|
|
5
|
+
export async function auditFavicon(ctx) {
|
|
6
|
+
const findings = [];
|
|
7
|
+
const origin = getOrigin(ctx.normalizedUrl);
|
|
8
|
+
const faviconUrl = `${origin}${DEFAULT_PATHS.faviconIco}`;
|
|
9
|
+
// 1. Check /favicon.ico directly
|
|
10
|
+
let faviconIcoExists = false;
|
|
11
|
+
try {
|
|
12
|
+
const { status } = await fetchHead(faviconUrl, ctx.fetchOptions);
|
|
13
|
+
faviconIcoExists = status === 200;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// Ignore fetch errors
|
|
17
|
+
}
|
|
18
|
+
// 2. Check HTML link tags for favicons
|
|
19
|
+
let html = ctx.html;
|
|
20
|
+
if (!html) {
|
|
21
|
+
try {
|
|
22
|
+
const page = await fetchPage(ctx.normalizedUrl, ctx.fetchOptions);
|
|
23
|
+
html = page.body;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return findings;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const htmlFavicons = getFaviconLinks(html);
|
|
30
|
+
const hasHtmlFavicon = htmlFavicons.length > 0;
|
|
31
|
+
// 3. Report findings
|
|
32
|
+
if (!faviconIcoExists && !hasHtmlFavicon) {
|
|
33
|
+
findings.push({
|
|
34
|
+
code: 'FAVICON_MISSING',
|
|
35
|
+
severity: 'warning',
|
|
36
|
+
category: 'favicon',
|
|
37
|
+
message: 'No favicon found',
|
|
38
|
+
explanation: 'A missing favicon causes 404 errors in server logs and looks unprofessional in browser tabs and bookmarks.',
|
|
39
|
+
suggestion: 'Add a favicon.ico at the root of your site or declare one via <link rel="icon"> in your HTML.',
|
|
40
|
+
url: faviconUrl,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
else if (!hasHtmlFavicon && faviconIcoExists) {
|
|
44
|
+
findings.push({
|
|
45
|
+
code: 'FAVICON_HTML_MISSING',
|
|
46
|
+
severity: 'info',
|
|
47
|
+
category: 'favicon',
|
|
48
|
+
message: 'Favicon exists at /favicon.ico but no HTML link tag declares it',
|
|
49
|
+
explanation: 'While browsers will find /favicon.ico by convention, explicitly declaring it in HTML ensures compatibility and allows specifying multiple sizes.',
|
|
50
|
+
suggestion: 'Add <link rel="icon" href="/favicon.ico"> to your HTML <head>.',
|
|
51
|
+
url: faviconUrl,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else if (hasHtmlFavicon && faviconIcoExists) {
|
|
55
|
+
// Check for potential conflicts
|
|
56
|
+
const icoLinks = htmlFavicons.filter((f) => f.href.endsWith('.ico') || f.href.includes('favicon.ico'));
|
|
57
|
+
const nonIcoLinks = htmlFavicons.filter((f) => !f.href.endsWith('.ico') && !f.href.includes('favicon.ico'));
|
|
58
|
+
if (icoLinks.length > 0 && nonIcoLinks.length > 0) {
|
|
59
|
+
findings.push({
|
|
60
|
+
code: 'FAVICON_CONFLICT',
|
|
61
|
+
severity: 'info',
|
|
62
|
+
category: 'favicon',
|
|
63
|
+
message: `Multiple favicon formats declared (${htmlFavicons.length} links)`,
|
|
64
|
+
explanation: 'Multiple favicon declarations are normal for supporting different devices, but verify they all resolve.',
|
|
65
|
+
suggestion: 'Ensure all declared favicon URLs are accessible.',
|
|
66
|
+
details: { favicons: htmlFavicons },
|
|
67
|
+
url: ctx.normalizedUrl,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return findings;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=favicon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"favicon.js","sourceRoot":"","sources":["../../src/audit/favicon.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { auditRedirects } from './redirects.js';
|
|
2
|
+
export { auditRobots } from './robots.js';
|
|
3
|
+
export { auditSitemap } from './sitemap.js';
|
|
4
|
+
export { auditMetadata } from './metadata.js';
|
|
5
|
+
export { auditFavicon } from './favicon.js';
|
|
6
|
+
export { auditNextjs } from './nextjs.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/audit/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { auditRedirects } from './redirects.js';
|
|
2
|
+
export { auditRobots } from './robots.js';
|
|
3
|
+
export { auditSitemap } from './sitemap.js';
|
|
4
|
+
export { auditMetadata } from './metadata.js';
|
|
5
|
+
export { auditFavicon } from './favicon.js';
|
|
6
|
+
export { auditNextjs } from './nextjs.js';
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/audit/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../src/audit/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAY9D,wBAAsB,aAAa,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAgN9E"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { fetchPage } from '../utils/http.js';
|
|
2
|
+
import { getCanonicalUrl, getNoindexDirective, getCharset, getViewport, getTitle, getMetaTag, } from '../utils/html-parser.js';
|
|
3
|
+
import { isSameOrigin } from '../utils/url.js';
|
|
4
|
+
export async function auditMetadata(ctx) {
|
|
5
|
+
const findings = [];
|
|
6
|
+
const { normalizedUrl, fetchOptions } = ctx;
|
|
7
|
+
let html;
|
|
8
|
+
let headers;
|
|
9
|
+
let finalUrl;
|
|
10
|
+
try {
|
|
11
|
+
const page = await fetchPage(normalizedUrl, fetchOptions);
|
|
12
|
+
html = page.body;
|
|
13
|
+
headers = page.headers;
|
|
14
|
+
finalUrl = page.finalUrl;
|
|
15
|
+
ctx.html = html;
|
|
16
|
+
ctx.headers = Object.fromEntries(headers.entries());
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return findings;
|
|
20
|
+
}
|
|
21
|
+
// 1. Noindex check — meta tag
|
|
22
|
+
if (getNoindexDirective(html)) {
|
|
23
|
+
findings.push({
|
|
24
|
+
code: 'NOINDEX_DETECTED',
|
|
25
|
+
severity: 'error',
|
|
26
|
+
category: 'indexing',
|
|
27
|
+
message: 'noindex meta tag detected on homepage',
|
|
28
|
+
explanation: 'A noindex directive tells search engines not to include this page in search results.',
|
|
29
|
+
suggestion: 'Remove the noindex directive unless you intentionally want to prevent indexing.',
|
|
30
|
+
url: normalizedUrl,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
// 2. Noindex check — X-Robots-Tag header
|
|
34
|
+
const xRobots = headers.get('x-robots-tag') ?? '';
|
|
35
|
+
if (xRobots.toLowerCase().includes('noindex')) {
|
|
36
|
+
findings.push({
|
|
37
|
+
code: 'X_ROBOTS_NOINDEX',
|
|
38
|
+
severity: 'error',
|
|
39
|
+
category: 'indexing',
|
|
40
|
+
message: 'X-Robots-Tag: noindex header detected',
|
|
41
|
+
explanation: 'The X-Robots-Tag header with noindex prevents search engines from indexing this page.',
|
|
42
|
+
suggestion: 'Remove the X-Robots-Tag noindex header from your server configuration or middleware.',
|
|
43
|
+
details: { header: xRobots },
|
|
44
|
+
url: normalizedUrl,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// 3. Canonical URL checks
|
|
48
|
+
const canonical = getCanonicalUrl(html);
|
|
49
|
+
if (!canonical) {
|
|
50
|
+
findings.push({
|
|
51
|
+
code: 'CANONICAL_MISSING',
|
|
52
|
+
severity: 'warning',
|
|
53
|
+
category: 'metadata',
|
|
54
|
+
message: 'No canonical URL found on homepage',
|
|
55
|
+
explanation: 'Without a canonical tag, search engines may index duplicate versions of your page.',
|
|
56
|
+
suggestion: 'Add a <link rel="canonical"> tag pointing to the preferred URL of this page.',
|
|
57
|
+
url: normalizedUrl,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
try {
|
|
62
|
+
const canonicalFull = new URL(canonical, finalUrl).href;
|
|
63
|
+
if (canonicalFull !== finalUrl && canonicalFull !== normalizedUrl) {
|
|
64
|
+
findings.push({
|
|
65
|
+
code: 'CANONICAL_MISMATCH',
|
|
66
|
+
severity: 'warning',
|
|
67
|
+
category: 'metadata',
|
|
68
|
+
message: 'Canonical URL does not match the page URL',
|
|
69
|
+
explanation: 'A mismatched canonical signals to search engines that this page is a duplicate of another.',
|
|
70
|
+
suggestion: 'Ensure the canonical URL matches the page URL, or verify the mismatch is intentional.',
|
|
71
|
+
details: { canonical: canonicalFull, pageUrl: finalUrl },
|
|
72
|
+
url: normalizedUrl,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (!isSameOrigin(canonicalFull, normalizedUrl)) {
|
|
76
|
+
findings.push({
|
|
77
|
+
code: 'CANONICAL_EXTERNAL',
|
|
78
|
+
severity: 'info',
|
|
79
|
+
category: 'metadata',
|
|
80
|
+
message: 'Canonical URL points to a different domain',
|
|
81
|
+
explanation: 'An external canonical tells search engines that this content originates on another domain.',
|
|
82
|
+
suggestion: 'Verify this is intentional. External canonicals transfer ranking signals to the other domain.',
|
|
83
|
+
details: { canonical: canonicalFull },
|
|
84
|
+
url: normalizedUrl,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Invalid canonical URL
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// 4. Charset
|
|
93
|
+
const charset = getCharset(html);
|
|
94
|
+
if (!charset) {
|
|
95
|
+
findings.push({
|
|
96
|
+
code: 'CHARSET_MISSING',
|
|
97
|
+
severity: 'info',
|
|
98
|
+
category: 'metadata',
|
|
99
|
+
message: 'No charset declaration found',
|
|
100
|
+
explanation: 'Without a charset declaration, browsers may misinterpret special characters on your page.',
|
|
101
|
+
suggestion: 'Add <meta charset="utf-8"> to the <head> of your document.',
|
|
102
|
+
url: normalizedUrl,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
// 5. Viewport
|
|
106
|
+
const viewport = getViewport(html);
|
|
107
|
+
if (!viewport) {
|
|
108
|
+
findings.push({
|
|
109
|
+
code: 'VIEWPORT_MISSING',
|
|
110
|
+
severity: 'warning',
|
|
111
|
+
category: 'metadata',
|
|
112
|
+
message: 'No viewport meta tag found',
|
|
113
|
+
explanation: 'Without a viewport tag, mobile devices may render the page at desktop width, harming mobile SEO.',
|
|
114
|
+
suggestion: 'Add <meta name="viewport" content="width=device-width, initial-scale=1">.',
|
|
115
|
+
url: normalizedUrl,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
// 6. Title
|
|
119
|
+
const title = getTitle(html);
|
|
120
|
+
if (!title) {
|
|
121
|
+
findings.push({
|
|
122
|
+
code: 'TITLE_MISSING',
|
|
123
|
+
severity: 'warning',
|
|
124
|
+
category: 'metadata',
|
|
125
|
+
message: 'No <title> tag found',
|
|
126
|
+
explanation: 'The title tag is one of the most important on-page SEO elements. Missing titles hurt rankings.',
|
|
127
|
+
suggestion: 'Add a unique, descriptive <title> tag to your page.',
|
|
128
|
+
url: normalizedUrl,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// 7. Meta description
|
|
132
|
+
const description = getMetaTag(html, 'description');
|
|
133
|
+
if (!description) {
|
|
134
|
+
findings.push({
|
|
135
|
+
code: 'DESCRIPTION_MISSING',
|
|
136
|
+
severity: 'info',
|
|
137
|
+
category: 'metadata',
|
|
138
|
+
message: 'No meta description found',
|
|
139
|
+
explanation: 'Meta descriptions appear in search results and can improve click-through rates.',
|
|
140
|
+
suggestion: 'Add a <meta name="description"> tag with a compelling summary of your page.',
|
|
141
|
+
url: normalizedUrl,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
// 8. Open Graph tags
|
|
145
|
+
const ogTitle = getMetaTag(html, 'og:title');
|
|
146
|
+
const ogDescription = getMetaTag(html, 'og:description');
|
|
147
|
+
const ogImage = getMetaTag(html, 'og:image');
|
|
148
|
+
if (!ogTitle) {
|
|
149
|
+
findings.push({
|
|
150
|
+
code: 'OG_TITLE_MISSING',
|
|
151
|
+
severity: 'info',
|
|
152
|
+
category: 'metadata',
|
|
153
|
+
message: 'No og:title meta tag found',
|
|
154
|
+
explanation: 'Open Graph tags control how your page appears when shared on social media platforms.',
|
|
155
|
+
suggestion: 'Add <meta property="og:title"> for better social sharing previews.',
|
|
156
|
+
url: normalizedUrl,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (!ogDescription) {
|
|
160
|
+
findings.push({
|
|
161
|
+
code: 'OG_DESCRIPTION_MISSING',
|
|
162
|
+
severity: 'info',
|
|
163
|
+
category: 'metadata',
|
|
164
|
+
message: 'No og:description meta tag found',
|
|
165
|
+
explanation: 'og:description controls the description shown in social media previews.',
|
|
166
|
+
suggestion: 'Add <meta property="og:description"> for better social sharing.',
|
|
167
|
+
url: normalizedUrl,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (!ogImage) {
|
|
171
|
+
findings.push({
|
|
172
|
+
code: 'OG_IMAGE_MISSING',
|
|
173
|
+
severity: 'info',
|
|
174
|
+
category: 'metadata',
|
|
175
|
+
message: 'No og:image meta tag found',
|
|
176
|
+
explanation: 'Social media platforms display a default placeholder when no og:image is set.',
|
|
177
|
+
suggestion: 'Add <meta property="og:image"> with a representative image URL.',
|
|
178
|
+
url: normalizedUrl,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return findings;
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../../src/audit/metadata.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nextjs.d.ts","sourceRoot":"","sources":["../../src/audit/nextjs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI9D,wBAAsB,WAAW,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAmH5E"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { fetchPage, fetchWithoutRedirect } from '../utils/http.js';
|
|
2
|
+
import { addTrailingSlash, removeTrailingSlash, hasTrailingSlash } from '../utils/url.js';
|
|
3
|
+
export async function auditNextjs(ctx) {
|
|
4
|
+
const findings = [];
|
|
5
|
+
const { normalizedUrl, fetchOptions } = ctx;
|
|
6
|
+
// 1. Fetch the page to get headers
|
|
7
|
+
let headers;
|
|
8
|
+
try {
|
|
9
|
+
if (ctx.headers) {
|
|
10
|
+
headers = ctx.headers;
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
const page = await fetchPage(normalizedUrl, fetchOptions);
|
|
14
|
+
headers = Object.fromEntries(page.headers.entries());
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return findings;
|
|
19
|
+
}
|
|
20
|
+
// 2. Vercel detection
|
|
21
|
+
const server = headers['server'] ?? '';
|
|
22
|
+
const xVercelId = headers['x-vercel-id'] ?? '';
|
|
23
|
+
const xPoweredBy = headers['x-powered-by'] ?? '';
|
|
24
|
+
const isVercel = server.toLowerCase().includes('vercel') || !!xVercelId;
|
|
25
|
+
const isNextjs = xPoweredBy.toLowerCase().includes('next.js');
|
|
26
|
+
if (isVercel) {
|
|
27
|
+
findings.push({
|
|
28
|
+
code: 'VERCEL_DETECTED',
|
|
29
|
+
severity: 'info',
|
|
30
|
+
category: 'nextjs',
|
|
31
|
+
message: `Vercel deployment detected${isNextjs ? ' (Next.js)' : ''}`,
|
|
32
|
+
explanation: 'Vercel-specific optimizations and checks are being applied to this audit.',
|
|
33
|
+
suggestion: 'No action needed. Vercel-specific checks are enabled.',
|
|
34
|
+
details: { server, xVercelId, xPoweredBy },
|
|
35
|
+
url: normalizedUrl,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// 3. Next.js trailing slash 308 behavior
|
|
39
|
+
try {
|
|
40
|
+
const testUrl = hasTrailingSlash(normalizedUrl)
|
|
41
|
+
? removeTrailingSlash(normalizedUrl)
|
|
42
|
+
: addTrailingSlash(normalizedUrl);
|
|
43
|
+
const res = await fetchWithoutRedirect(testUrl, fetchOptions);
|
|
44
|
+
if (res.status === 308) {
|
|
45
|
+
findings.push({
|
|
46
|
+
code: 'NEXTJS_TRAILING_SLASH_308',
|
|
47
|
+
severity: 'info',
|
|
48
|
+
category: 'nextjs',
|
|
49
|
+
message: 'Next.js 308 permanent redirect for trailing slash normalization',
|
|
50
|
+
explanation: 'Next.js uses 308 (Permanent Redirect) to enforce its trailingSlash configuration. This is normal behavior but ensure it matches your intended URL structure.',
|
|
51
|
+
suggestion: 'If this is unexpected, check next.config.js trailingSlash setting. 308 redirects are cached by browsers.',
|
|
52
|
+
details: {
|
|
53
|
+
testedUrl: testUrl,
|
|
54
|
+
status: 308,
|
|
55
|
+
location: res.headers.get('location'),
|
|
56
|
+
},
|
|
57
|
+
url: testUrl,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Not critical
|
|
63
|
+
}
|
|
64
|
+
// 4. Middleware redirect headers
|
|
65
|
+
const xMiddlewareRewrite = headers['x-middleware-rewrite'] ?? '';
|
|
66
|
+
const xMiddlewareRedirect = headers['x-middleware-redirect'] ?? '';
|
|
67
|
+
const xMiddlewareNext = headers['x-middleware-next'] ?? '';
|
|
68
|
+
if (xMiddlewareRewrite || xMiddlewareRedirect) {
|
|
69
|
+
findings.push({
|
|
70
|
+
code: 'MIDDLEWARE_REDIRECT',
|
|
71
|
+
severity: 'info',
|
|
72
|
+
category: 'nextjs',
|
|
73
|
+
message: 'Next.js middleware is modifying the request',
|
|
74
|
+
explanation: 'Middleware rewrites or redirects can affect how search engines see your pages. Ensure middleware is not unintentionally altering SEO-critical pages.',
|
|
75
|
+
suggestion: 'Review your middleware.ts to ensure it does not redirect or rewrite SEO-critical URLs.',
|
|
76
|
+
details: {
|
|
77
|
+
rewrite: xMiddlewareRewrite || undefined,
|
|
78
|
+
redirect: xMiddlewareRedirect || undefined,
|
|
79
|
+
next: xMiddlewareNext || undefined,
|
|
80
|
+
},
|
|
81
|
+
url: normalizedUrl,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// 5. App Router metadata check (presence of Next.js metadata headers)
|
|
85
|
+
const html = ctx.html;
|
|
86
|
+
if (html) {
|
|
87
|
+
const hasNextMetadata = html.includes('next-head-count') ||
|
|
88
|
+
html.includes('__next') ||
|
|
89
|
+
html.includes('_next/static');
|
|
90
|
+
if (isNextjs && !hasNextMetadata) {
|
|
91
|
+
findings.push({
|
|
92
|
+
code: 'APP_ROUTER_METADATA',
|
|
93
|
+
severity: 'info',
|
|
94
|
+
category: 'nextjs',
|
|
95
|
+
message: 'Next.js detected but standard Next.js markers not found in HTML',
|
|
96
|
+
explanation: 'This may indicate a custom rendering setup or edge runtime that could affect metadata generation.',
|
|
97
|
+
suggestion: 'Ensure your Next.js App Router pages export proper metadata using the Metadata API.',
|
|
98
|
+
url: normalizedUrl,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return findings;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=nextjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nextjs.js","sourceRoot":"","sources":["../../src/audit/nextjs.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirects.d.ts","sourceRoot":"","sources":["../../src/audit/redirects.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAiB9D,wBAAsB,cAAc,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA8I/E"}
|