|
1
|
# SEO Audit Methodology |
|
2
|
|
|
3
|
This is the shared, audience-neutral SEO audit methodology. It applies whether |
|
4
|
you are auditing an external, third-party site (the `seo-auditor` skill) or the |
|
5
|
user's own Replit app (the SEO Agent scanner mode). Where the two differ, the |
|
6
|
distinction is called out inline. |
|
7
|
|
|
8
|
## Critical First Step: SPA vs SSR Reality Check |
|
9
|
|
|
10
|
Before anything else, determine whether the site is a React/Vue/Angular SPA or server-side rendered. **This is the single most important distinction in a modern SEO audit.** |
|
11
|
|
|
12
|
- **SPA (React, Vue, Angular):** Googlebot cannot reliably execute JavaScript. Any page that requires the JS bundle to render is **effectively invisible** to search engines. The `<title>` and `<meta>` in `index.html` are all Google sees. |
|
13
|
- **SSR (Express, Next.js, Nuxt, SvelteKit):** Full HTML is returned to the crawler. Everything is indexable. |
|
14
|
- **Hybrid:** Many apps are SPA for authenticated pages but SSR for public/marketing pages. Identify which routes are which. |
|
15
|
|
|
16
|
**How to detect (external sites, live URL available):** `curl -s https://domain.com/some-page | grep "<h1"` — if no H1 is in the curl output but is visible in the browser, the page is SPA-rendered and invisible to Googlebot. |
|
17
|
|
|
18
|
**How to detect (own-app scan, source only):** read the framework config and the public HTML entry (`index.html`, `app/layout.tsx`, `pages/_document.tsx`, `src/app.html`, `src/layouts/*.astro`) and classify each public route as SPA / SSR / pre-rendered from the source — do not rely on hitting a live URL. |
|
19
|
|
|
20
|
**For SPA+SSR hybrids:** Audit only the SSR pages deeply. Note which routes are SPA (and therefore invisible) without alarming the user — authenticated dashboards being SPA is expected and fine. |
|
21
|
|
|
22
|
## Methodology |
|
23
|
|
|
24
|
### Audit Priority Order |
|
25
|
|
|
26
|
1. **Crawlability & Indexation** (can Google find and index it?) |
|
27
|
2. **Technical Foundations** (is the site fast and functional?) |
|
28
|
3. **On-Page Optimization** (is content optimized?) |
|
29
|
4. **Content Quality** (does it deserve to rank?) |
|
30
|
5. **Authority & Links** (does it have credibility?) |
|
31
|
|
|
32
|
### Step 1: Crawlability & Indexation |
|
33
|
|
|
34
|
#### Robots.txt |
|
35
|
|
|
36
|
- Check for unintentional blocks |
|
37
|
- Verify important pages allowed |
|
38
|
- Check sitemap reference |
|
39
|
- **⚠️ Verify actual content, not just HTTP status.** A React SPA returns 200 for any URL — including `/robots.txt` if no Express route handles it. Always `curl -s /robots.txt | head -5` and confirm the output is `User-agent:` plain text, not `<!DOCTYPE html>`. |
|
40
|
|
|
41
|
##### XML Sitemap |
|
42
|
|
|
43
|
- Exists and accessible |
|
44
|
- Contains only canonical, indexable URLs |
|
45
|
- Updated regularly |
|
46
|
- **⚠️ Same SPA trap applies to `/sitemap.xml`.** `curl /sitemap.xml` and confirm the first line is `<?xml version=`. If it returns `<!DOCTYPE html>`, the sitemap route is missing and the SPA is serving as fallback. |
|
47
|
- For Express/Node backends with pSEO content: implement sitemap as a dynamic Express route that maps over data arrays (e.g., `BANK_GUIDES`, `GLOSSARY_TERMS`). This way, adding new entries to the arrays automatically updates the sitemap — no manual maintenance needed. |
|
48
|
|
|
49
|
###### Site Architecture |
|
50
|
|
|
51
|
- Important pages within 3 clicks of homepage |
|
52
|
- Logical hierarchy |
|
53
|
- No orphan pages (pages with no internal links) |
|
54
|
|
|
55
|
###### Index Status |
|
56
|
|
|
57
|
- site:domain.com check |
|
58
|
- Compare indexed vs. expected page count |
|
59
|
|
|
60
|
###### Indexation Issues |
|
61
|
|
|
62
|
- Noindex tags on important pages |
|
63
|
- Canonicals pointing wrong direction |
|
64
|
- Redirect chains/loops |
|
65
|
- Soft 404s |
|
66
|
- Duplicate content without canonicals |
|
67
|
|
|
68
|
###### Canonicalization |
|
69
|
|
|
70
|
- All pages have canonical tags |
|
71
|
- HTTP → HTTPS canonicals |
|
72
|
- www vs. non-www consistency |
|
73
|
- Trailing slash consistency |
|
74
|
|
|
75
|
### Step 2: Technical Foundations |
|
76
|
|
|
77
|
#### Core Web Vitals (2025-2026) |
|
78
|
|
|
79
|
- **LCP** (Largest Contentful Paint): < 2.5s |
|
80
|
- **INP** (Interaction to Next Paint): < 200ms — replaced FID in 2025 as the responsiveness metric |
|
81
|
- **CLS** (Cumulative Layout Shift): < 0.1 |
|
82
|
|
|
83
|
##### Speed Factors |
|
84
|
|
|
85
|
- Server response time (TTFB) |
|
86
|
- Image optimization and modern formats (WebP) |
|
87
|
- JavaScript execution and bundle size |
|
88
|
- CSS delivery |
|
89
|
- Caching headers and CDN usage |
|
90
|
- Font loading strategy |
|
91
|
|
|
92
|
###### Font Bundle Audit — Common Performance Killer |
|
93
|
|
|
94
|
Check `client/index.html` (the actual source file, not just what Googlebot sees) for bloated Google Fonts requests. A single `<link href="fonts.googleapis.com/css2?family=Inter&family=Roboto&family=Poppins&...">` loading 10+ font families is a render-blocking LCP killer. |
|
95
|
|
|
96
|
###### Correct async font loading pattern |
|
97
|
|
|
98
|
```html |
|
99
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
100
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
101
|
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap"> |
|
102
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" media="print" onload="this.media='all'"> |
|
103
|
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap"></noscript> |
|
104
|
``` |
|
105
|
|
|
106
|
Apply this pattern to both `client/index.html` (SPA) and any SSR shared shell functions. |
|
107
|
|
|
108
|
###### Mobile-Friendliness |
|
109
|
|
|
110
|
- Responsive design (not separate m. site) |
|
111
|
- Tap target sizes |
|
112
|
- Viewport configured |
|
113
|
- No horizontal scroll |
|
114
|
- Mobile-first indexing readiness |
|
115
|
|
|
116
|
###### Security |
|
117
|
|
|
118
|
- HTTPS across entire site |
|
119
|
- Valid SSL certificate |
|
120
|
- No mixed content |
|
121
|
- HSTS header |
|
122
|
|
|
123
|
###### URL Structure |
|
124
|
|
|
125
|
- Readable, descriptive URLs |
|
126
|
- Keywords where natural |
|
127
|
- Consistent structure (lowercase, hyphen-separated) |
|
128
|
- No unnecessary parameters |
|
129
|
|
|
130
|
**Note:** Google now excludes pages returning non-200 status codes (4xx, 5xx) from the rendering queue entirely — critical for SPAs. |
|
131
|
|
|
132
|
### Step 3: On-Page SEO |
|
133
|
|
|
134
|
#### Title Tags |
|
135
|
|
|
136
|
- Unique per page, 50-65 characters |
|
137
|
- Primary keyword near beginning |
|
138
|
- Compelling and click-worthy |
|
139
|
- Brand name at end with `|` separator (not `-` or `—`) |
|
140
|
- Common issues: duplicates, too long/short, keyword stuffing, missing |
|
141
|
- **Watch for Unicode symbols in H1s** (e.g., `↔`, `→`, `©`). These look odd in SERPs and can be keyword-unfriendly. Use plain text equivalents. |
|
142
|
|
|
143
|
##### Meta Descriptions |
|
144
|
|
|
145
|
- Unique per page, 150-160 characters |
|
146
|
- Includes primary keyword |
|
147
|
- Clear value proposition with CTA |
|
148
|
- Common issues: duplicates, auto-generated, no compelling reason to click |
|
149
|
|
|
150
|
###### Heading Structure |
|
151
|
|
|
152
|
- One H1 per page containing primary keyword |
|
153
|
- Logical hierarchy (H1 → H2 → H3, no skipping) |
|
154
|
- Headings describe content, not used just for styling |
|
155
|
|
|
156
|
###### Content Optimization |
|
157
|
|
|
158
|
- Keyword in first 100 words |
|
159
|
- Related keywords naturally used |
|
160
|
- Sufficient depth for topic |
|
161
|
- Answers search intent |
|
162
|
- Better than current top-ranking competitors |
|
163
|
|
|
164
|
###### Image Optimization |
|
165
|
|
|
166
|
- Descriptive file names and alt text |
|
167
|
- Compressed file sizes, modern formats (WebP) |
|
168
|
- Lazy loading, responsive images |
|
169
|
|
|
170
|
###### Internal Linking |
|
171
|
|
|
172
|
- Important pages well-linked with descriptive anchor text |
|
173
|
- No broken internal links |
|
174
|
- No orphan pages |
|
175
|
|
|
176
|
###### Keyword Targeting (per page) |
|
177
|
|
|
178
|
- Clear primary keyword target |
|
179
|
- Title, H1, URL aligned with keyword |
|
180
|
- Content satisfies search intent |
|
181
|
- Not competing with other pages (cannibalization) |
|
182
|
|
|
183
|
### Step 4: Content Quality — E-E-A-T Signals |
|
184
|
|
|
185
|
**Experience:** First-hand experience demonstrated, original insights/data, real examples |
|
186
|
|
|
187
|
**Expertise:** Author credentials visible, accurate and detailed information, properly sourced claims |
|
188
|
|
|
189
|
**Authoritativeness:** Recognized in the space, cited by others, industry credentials |
|
190
|
|
|
191
|
**Trustworthiness:** Accurate information, transparent about business, contact info available, privacy policy, HTTPS |
|
192
|
|
|
193
|
### Step 5: Structured Data & Social Signals |
|
194
|
|
|
195
|
#### Schema Markup |
|
196
|
|
|
197
|
Every SSR page should have the appropriate schema type. Use an array-based approach in your shared shell function so each schema is its own `<script type="application/ld+json">` block — never concatenate JSON strings and inject them inside an existing `<script>` tag, as this creates malformed HTML with nested script tags. |
|
198
|
|
|
199
|
##### Correct pattern (Express/Node SSR shell function) |
|
200
|
|
|
201
|
```typescript |
|
202
|
// Accept an array of schema JSON strings |
|
203
|
schemaJsons?: string[]; |
|
204
|
|
|
205
|
// Render each as its own script block |
|
206
|
allSchemas.map(s => `<script type="application/ld+json">${s}</script>`).join("\n") |
|
207
|
``` |
|
208
|
|
|
209
|
###### Always-present schemas (add to shared shell) |
|
210
|
|
|
211
|
- `WebSite` + `Organization` `@graph` — entity identity for Google's Knowledge Graph, sitelinks signals |
|
212
|
- `BreadcrumbList` — add to every page that has visible breadcrumb navigation, even if the page already has another schema type (HowTo, DefinedTerm, etc.) |
|
213
|
|
|
214
|
###### Page-specific schemas |
|
215
|
|
|
216
|
- HowTo — step-by-step guides |
|
217
|
- FAQPage — Q&A content (excellent for AI Overviews / featured snippets) |
|
218
|
- DefinedTerm + DefinedTermSet — glossary terms |
|
219
|
- ItemList — hub/index pages listing multiple items |
|
220
|
- Article / BlogPosting — editorial content (include `datePublished`, `dateModified`) |
|
221
|
|
|
222
|
**Always include `datePublished` and `dateModified`** in schema — freshness signals help maintain rankings on competitive queries. |
|
223
|
|
|
224
|
**OG Image requirement:** `og:image` requires an actual hosted image URL (1200×630px). Base64-embedded images cannot be OG images. If no hosted image exists, note it as a blocker for social sharing and skip the `og:image` tag rather than pointing to a non-existent URL. |
|
225
|
|
|
226
|
###### Minimum social meta set (every page) |
|
227
|
|
|
228
|
```html |
|
229
|
<meta property="og:title" content="..." /> |
|
230
|
<meta property="og:description" content="..." /> |
|
231
|
<meta property="og:url" content="..." /> |
|
232
|
<meta property="og:type" content="website" /> |
|
233
|
<meta property="og:locale" content="..." /> <!-- e.g., es_DO, en_US --> |
|
234
|
<meta property="og:site_name" content="..." /> |
|
235
|
<meta name="twitter:card" content="summary_large_image" /> |
|
236
|
<meta name="twitter:title" content="..." /> |
|
237
|
<meta name="twitter:description" content="..." /> |
|
238
|
<meta name="twitter:site" content="@handle" /> |
|
239
|
``` |
|
240
|
|
|
241
|
**Schema Markup Detection Warning:** `webFetch` and `curl` cannot reliably detect structured data — many CMS plugins inject JSON-LD via client-side JavaScript. Never report "no schema found" based solely on webFetch. Recommend using Google Rich Results Test or browser DevTools for accurate schema verification. For SSR pages, curl is reliable. |
|
242
|
|
|
243
|
### Step 6: Bot Governance & AI Readiness |
|
244
|
|
|
245
|
- Review robots.txt to differentiate between beneficial retrieval agents (OAI-SearchBot, Googlebot) and non-beneficial training scrapers |
|
246
|
- Use structured data (schema.org) as the language of LLMs |
|
247
|
- Use "BLUF" (Bottom Line Up Front) formatting to help content get cited in AI Overviews |
|
248
|
|
|
249
|
### Step 7: Competitor SEO Comparison |
|
250
|
|
|
251
|
- Search for target keywords and analyze top-ranking pages |
|
252
|
- Identify content gaps and opportunities |
|
253
|
- Compare meta tags, content depth, structure, and E-E-A-T signals |
|
254
|
|
|
255
|
## SSR Shared Shell Optimization Pattern |
|
256
|
|
|
257
|
When a site uses a **shared HTML shell function** (common in Express/Node SSR setups), a single change to that function fixes all pages simultaneously. This is the highest-leverage opportunity in an SSR SEO audit. |
|
258
|
|
|
259
|
### Audit the shell function for these — fixing once applies to all pages |
|
260
|
|
|
261
|
1. `og:locale`, `og:site_name` — often missing |
|
262
|
2. Twitter Card tags — almost always missing |
|
263
|
3. `<meta name="theme-color">` — small trust/UX signal |
|
264
|
4. Async font loading — often render-blocking |
|
265
|
5. `WebSite` + `Organization` JSON-LD — missing from most sites |
|
266
|
6. Canonical tag structure — verify it's using the correct canonical per page |
|
267
|
|
|
268
|
#### Per-page additions (must be done individually) |
|
269
|
|
|
270
|
- `BreadcrumbList` schema — specific to each page's breadcrumb path |
|
271
|
- `datePublished` / `dateModified` — specific to each content type's schema |
|
272
|
|
|
273
|
## Common Issues by Site Type |
|
274
|
|
|
275
|
**SaaS/Product Sites:** Product pages lack content depth, blog not integrated with product pages, missing comparison/alternative pages, thin feature pages |
|
276
|
|
|
277
|
### SPA + SSR Hybrid (React/Node, Next.js, etc.) |
|
278
|
|
|
279
|
- robots.txt and sitemap.xml not handled by Express — SPA returns 200 with HTML instead of proper plain text / XML |
|
280
|
- Massive font bundles in `index.html` affecting landing page LCP |
|
281
|
- Missing OG/Twitter tags in `index.html` (since that's all social crawlers see for SPA pages) |
|
282
|
- SSR pages missing BreadcrumbList schema despite having breadcrumb HTML |
|
283
|
- Shared shell function missing Twitter Card, og:locale, og:site_name — fixing once would help all pages |
|
284
|
|
|
285
|
**E-commerce:** Thin category pages, duplicate product descriptions, missing product schema, faceted navigation creating duplicates |
|
286
|
|
|
287
|
**Content/Blog Sites:** Outdated content not refreshed, keyword cannibalization, no topical clustering, poor internal linking |
|
288
|
|
|
289
|
**Local Business:** Inconsistent NAP, missing local schema, no Google Business Profile optimization |
|
290
|
|
|
291
|
## Output Format |
|
292
|
|
|
293
|
### SEO Audit Report Structure |
|
294
|
|
|
295
|
```text |
|
296
|
# SEO Audit Report: [Website] |
|
297
|
|
|
298
|
## Executive Summary |
|
299
|
- Overall health assessment |
|
300
|
- Top 3-5 priority issues |
|
301
|
- Quick wins identified |
|
302
|
|
|
303
|
## Critical Issues (Fix Immediately) |
|
304
|
| Issue | Page | Impact | Evidence | Fix | |
|
305
|
|-------|------|--------|----------|-----| |
|
306
|
|
|
307
|
## High-Impact Improvements |
|
308
|
| Issue | Page | Impact | Evidence | Fix | |
|
309
|
|-------|------|--------|----------|-----| |
|
310
|
|
|
311
|
## Quick Wins |
|
312
|
| Opportunity | Page | Potential Impact | |
|
313
|
|------------|------|-----------------| |
|
314
|
|
|
315
|
## Page-by-Page Analysis |
|
316
|
|
|
317
|
### [Page URL] |
|
318
|
- **Title**: Current | Recommended |
|
319
|
- **Meta Description**: Current | Recommended |
|
320
|
- **H1**: Current | Recommended |
|
321
|
- **Content Score**: X/10 |
|
322
|
- **Issues**: [list] |
|
323
|
|
|
324
|
## Prioritized Action Plan |
|
325
|
1. Critical fixes (blocking indexation/ranking) |
|
326
|
2. High-impact improvements (SSR shell function — fix once, applies to all pages) |
|
327
|
3. Quick wins (easy, immediate benefit) |
|
328
|
4. Long-term recommendations (OG image creation, Privacy/Terms SSR, etc.) |
|
329
|
``` |
|
330
|
|
|
331
|
## Tools |
|
332
|
|
|
333
|
**Free:** Google Search Console, Google PageSpeed Insights, Rich Results Test (use for schema validation — it renders JavaScript), Mobile-Friendly Test, Schema Validator |
|
334
|
|
|
335
|
**Paid (if available):** Screaming Frog, Ahrefs / Semrush, Sitebulb |
|
336
|
|
|
337
|
## Best Practices |
|
338
|
|
|
339
|
1. **Prioritize by impact** — fix critical issues before optimizing nice-to-haves |
|
340
|
2. **Write for humans first** — keyword-stuffed content hurts rankings |
|
341
|
3. **Check actual SERPs** — search for target keywords to understand what Google currently rewards |
|
342
|
4. **Focus on search intent** — match content type to what users actually want |
|
343
|
5. **Monitor competitors** — see what top-ranking pages do well and identify gaps |
|
344
|
6. **Always curl the URL, read the body** — HTTP 200 status means nothing if the SPA is returning HTML for `/robots.txt` or `/sitemap.xml`. Confirm content type and first few lines |
|
345
|
7. **Find the shared shell function** — in Express/Node SSR apps, a shared HTML shell function is a force multiplier. One change fixes all pages |
|
346
|
8. **Dynamic sitemaps > static sitemaps** — for apps with content arrays (guides, terms, products), generate the sitemap dynamically from those arrays so it stays current automatically |
|
347
|
|
|
348
|
## Limitations |
|
349
|
|
|
350
|
- Cannot access Google Search Console or Analytics data |
|
351
|
- Cannot measure actual page speed (use Google Lighthouse separately) |
|
352
|
- Cannot check backlink profiles (recommend Ahrefs, Semrush, or Moz) |
|
353
|
- Cannot run full site crawls (recommend Screaming Frog or Sitebulb) |
|
354
|
- Cannot guarantee ranking improvements — SEO involves many factors |
|
355
|
- Cannot access pages behind authentication or paywalls |