SEO Best Practices
Swift Web is a content-driven business; organic search traffic is the lifeblood of our ad revenue. SEO is not an add-on, it's a first-class product concern. This guide walks every Swift Web developer through the full picture, from useSeo length warnings all the way to Core Web Vitals and GEO, with framework-internal best practices baked in.
Introduction
Why we treat SEO as a hard requirement:
- The business depends on website content to attract users and earn ad revenue from organic search traffic
- Good SEO vs. bad SEO can yield a 5–10x difference in organic traffic on the same page
- Modern search engines (Google / Bing) and AI answer engines (ChatGPT Search, Perplexity, Bing Copilot) keep raising their bar on content quality, structure, performance, and accessibility
This guide covers 12 chapters + 2 appendices. Chapter 1 is directly tied to the dev console warnings emitted by useSeo (the chapter you land on first when clicking the link from console). Subsequent chapters serve as a "developer-friendly SEO crash course" — to make you realize "wait, there's actually this much to SEO."
Authoritative sources: Google Search Central / Bing Webmaster Guidelines / Schema.org / web.dev (Core Web Vitals) / W3C WCAG.
1. TDK Fields: title / description / keywords
This chapter directly addresses the dev-time console warnings emitted by
useSeo.
1.1 Where the Length Recommendations Come From
| Field | Soft target | Hard ceiling | Source |
|---|---|---|---|
| title | 10–60 display width | > 70 triggers keyword-stuffing detection | Google SERP truncates ~60, Bing ~65 |
| description | 50–160 display width | > 200 triggers counter-productive penalty | Google SERP ~155, social platforms ~200 |
| keywords | ≤10 items, ≤200 chars | Modern SEO ignores it | Stuffing reverse-triggers anti-spam |
title / description get truncated visually in SERPs (search engine result pages) past these thresholds; too short hurts CTR (click-through rate). The keywords meta no longer carries ranking weight in modern engines, but stuffing it still trips anti-cheat heuristics.
::: tip Unit is "display width", not character count
The numbers in the table are measured as display width (visual width), not JS .length:
- ASCII characters (Latin letters / digits / half-width punctuation) count as 1
- CJK characters (Chinese / Traditional Chinese / fullwidth symbols / CJK punctuation) count as 2
The threshold numbers are based on English-character SERP norms; CJK target lengths must be halved:
| Language | title soft target | description soft target |
|---|---|---|
| English | 10–60 chars | 50–160 chars |
| Chinese / Traditional | 5–30 chars | 25–80 chars |
Why: CJK characters take roughly 2× the visual width of English in SERP snippets. A flat
.length check would let CJK copy "look fine" while actually being too long, and would
flag short English copy as too short. useSeo uses a displayWidth
algorithm internally (exported for unit tests), and warning messages explicitly print
(显示宽度 N) so developers see the unit.
:::
1.2 Short Isn't Always Bad (When to Use silenceLengthWarnings)
Tool / private pages (login, register, account center, redirect bridges, demo) are inherently short:
- A
titleofLoginis more direct thanUser Login Entry - Swift Web - Same with
description— padding it to 50 chars dilutes signal density
These pages typically already carry robots: 'noindex' | 'private' and don't participate in search indexing — length warnings are meaningless on them.
Solution: pass silenceLengthWarnings: true to silence length-class warnings in one shot.
useSeo({
title: 'Login',
description: 'Sign in to your account',
robots: 'private',
silenceLengthWarnings: true,
})
1.3 But "Too Long" Is Different — That's a Counter-Productive Warning
Warnings prefixed with ⚠️ (title > 70, description > 200, keywords > 10 items) are genuinely harmful: modern search engines flag them as keyword stuffing and demote rankings, sometimes triggering manual actions.
silenceLengthWarnings: true will suppress these messages too, but the underlying behavior is still something to avoid — silence doesn't mean "all clear."
1.4 For Public Pages, Fix It — Don't Cheap Out with Silence
For indexable public pages (homepage, detail pages, listing pages), a too-short description is a real SEO opportunity. Don't use silenceLengthWarnings to brush it off — write more content (clear value prop + intent-matching keywords).
// Before (24 chars, triggers "too short")
useSeo({
title: 'App Center - Hidden Manga Heaven',
description: 'Hidden Manga Heaven app center — curated gambling, entertainment, card-game apps',
})
// After (~80 chars, covers search intent)
useSeo({
title: 'App Center - Hidden Manga Heaven',
description: 'Hidden Manga Heaven app center curates premium gambling, entertainment, and card-game apps — secure, no-download, with instant login and cross-device sync. Mobile-game recommendations updated regularly.',
})
For detail pages, use the backend SEO node fallback chain:
useSeo({
title: data?.seo?.title ?? data?.name,
description: data?.seo?.description ?? data?.summary,
image: data?.seo?.image ?? data?.cover,
type: 'article',
})
2. Content Quality: E-E-A-T and GEO (the AI Search Era)
2.1 The E-E-A-T Framework
Google's 2026 Search Quality Rater Guidelines emphasize E-E-A-T:
- Experience — first-hand experience, having actually used / tested the thing
- Expertise — domain knowledge, academic credentials, industry experience
- Authoritativeness — recognized authority, citations from peers / press
- Trustworthiness — credibility, fact-checkable, source-traceable
E-E-A-T isn't a direct ranking factor, but Google's algorithm updates ("helpful content update", "core update") fold it into the score. Business takeaway: ad-product pages should clearly state "why our app is safe / legitimate / popular"; editorial content (reviews, walkthroughs) should be bylined and show author qualifications.
2.2 AI-Generated Content: Allowed, But Add Human Value
Google's 2026 stance: "AI-generated content is fine, as long as it provides real value to users."
- ✅ AI-drafted + human-revised, fact-checked, augmented with first-hand experience and expert judgment
- ❌ Mass low-quality AI content for SEO churn (
scaled content abuse) → algorithmic demotion or removal - ❌ Acquiring expired domains and slapping spam content on them (
expired domain abuse) - ❌ Borrowing a high-authority site to publish junk subdomains / subdirectories (
site reputation abuse)
2.3 GEO: Generative Engine Optimization
Bing's 2026 webmaster guidelines officially defined GEO: optimizing your content so that AI answer engines (Copilot / ChatGPT Search / Perplexity) "ground" (cite) it as a source.
In practice:
- NOARCHIVE meta prevents content from being cited by AI (e.g., internal drafts, paywalled content)
- Structured data (see Chapter 8) lets AI extract facts more reliably
- Clear heading hierarchy + paragraph topic sentences make content easier to summarize
- Verifiable factual claims (with links, dates, data sources)
<!-- Refuse AI grounding: serve human visitors only -->
<meta name="robots" content="noarchive">
3. Rendering Strategy and Crawlability
3.1 Render Mode Decision Tree
Nuxt 4 uses defineRouteRules() to declare each page's render strategy explicitly. Swift Web makes this mandatory for every page (ESLint enforces).
| Mode | Use case | SEO impact |
|---|---|---|
| isr (ISR) | Listings, detail pages, homepage | ✅ Cache-friendly, SEO first-class |
| ssr | User-state-heavy pages (cart, checkout) | ✅ Full SEO, but server-heavy |
csr (ssr: false) | Account, admin, logged-in only pages | ❌ No SEO, only when you don't need indexing |
| prerender (SSG) | Truly static pages (about, terms) — but isr: 3600 is the recommended default, see below | ✅ Fastest; not for dynamic [id].vue routes |
Note: prerender: true on [id].vue causes a combinatorial explosion (one static file per id).
3.2 The Cost of Losing SEO
ssr: false hands the page off to client-side JS. Crawlers see an empty HTML skeleton — no title, description, or main content. Google does support JS rendering ("two-wave indexing"), but it's slow and unreliable; do not use it for public-facing content pages.
csr is acceptable only when:
- The page is strictly logged-in only (account center, orders, wallet)
- Temporary campaign landing pages that don't need indexing
3.3 robots: noindex / private Shorthands
Swift Web's useSeo exposes three robots shorthands:
useSeo({ robots: 'noindex' }) // = 'noindex, follow', for redirect / search-result pages
useSeo({ robots: 'private' }) // = 'noindex, nofollow', for login / account / admin
useSeo({ robots: 'noindex, nofollow, noarchive' }) // raw string (advanced)
⚠️ Don't also
Disallowthe same private path inrobots.txt: blocking the crawl means the crawler never sees the page'snoindex, so the URL may still be indexed (without a snippet). To deindex, keep the page crawlable +noindex; use robots.txtDisallowonly for paths you simply don't want crawled and don't care about indexing (/api/etc.). See framework SEO guide.
4. Semantic HTML: Half of SEO Is Picking the Right Tag
4.1 HTML5 Landmark Elements
W3C WCAG H101 requires semantic elements to demarcate page regions.
<body>
<header>
<nav>...</nav>
</header>
<main>
<article>
<h1>Page main title</h1>
<section>
<h2>Section heading</h2>
</section>
</article>
<aside>Sidebar</aside>
</main>
<footer>...</footer>
</body>
| Element | Purpose |
|---|---|
<header> | Page / section top (logo, nav) |
<nav> | Primary navigation links |
<main> | Main content area (one per page) |
<article> | Standalone, complete content (an article, a product) |
<section> | Topically grouped content (with a heading) |
<aside> | Content related to but separable from main content (sidebar) |
<footer> | Page / section bottom |
Both crawlers and screen readers rely on these landmarks to understand page structure. Stacking unsemantic <div> everywhere makes them both blind.
4.2 H1–H6 Heading Hierarchy
Iron rule: only one <h1> per page (the page's main title), and it should be the most visually and semantically important heading.
Hierarchy must be clean and not skip levels:
- ✅
h1→h2→h3→h2→h3 - ❌
h1→h3→h2(skipped h2) - ❌ Using
<h3>for visual styling ("I want smaller text") — useclassto control visual size
Practical guidance:
- Listing page:
<h1>is the category name (e.g., "Anime"); list-item cards use<h2>/<h3> - Detail page:
<h1>is the work title; chapters use<h2> - Homepage:
<h1>can be the site name or main slogan
4.3 Don't Drown in <div>
<!-- ❌ -->
<div class="header">
<div class="nav">
<div class="link">Home</div>
</div>
</div>
<!-- ✅ -->
<header>
<nav>
<NuxtLink to="/">Home</NuxtLink>
</nav>
</header>
Tooling: axe DevTools browser extension scans for semantic issues.
5. Links: ahref Is the Crawler's Lifeline
5.1 NuxtLink / SwLink, Not onClick + router.push
Crawlers only follow <a href="...">. A link triggered via @click + router.push() is completely invisible to search engines.
<!-- ❌ Crawlers can't see this link -->
<button @click="$router.push('/movie/123')">Detail</button>
<!-- ✅ Crawler-reachable, supports prefetch / middle-click / right-click copy URL -->
<NuxtLink to="/movie/123">Detail</NuxtLink>
<!-- ✅ Swift Web preferred: SwLink — built-in crawler semantics + visual style -->
<SwLink to="/movie/123">Detail</SwLink>
Exception: pure UI actions (expand/collapse, tab switching) should use button + onClick. Any route navigation must be an <a> tag.
5.2 Anchor Text Should Be Descriptive
<!-- ❌ -->
<NuxtLink to="/article/seo">Click here</NuxtLink>
<NuxtLink to="/article/seo">Learn more</NuxtLink>
<!-- ✅ -->
<NuxtLink to="/article/seo">SEO optimization tips in detail</NuxtLink>
Crawlers use anchor text to understand the destination's topic. "Click here" wastes a ranking signal.
5.3 Internal Links = Site Map
- Important pages should have multiple entry points (home, category, related-recommendations, breadcrumb all link to it)
- Secondary pages (private / utility) shouldn't appear in main traffic paths
- Clean up dead / 404 links (wasted crawler budget)
5.4 External Links: rel Attributes
<!-- Don't endorse outbound links (avoid being dragged down by low-quality sites) -->
<a href="https://other.com" rel="nofollow">Reference</a>
<!-- User-generated outbound links: sponsored / ugc -->
<a href="https://ad.com" rel="sponsored">Sponsored</a>
<a href="https://example.com" rel="ugc">User-shared link</a>
<!-- Outbound links opening in new tab: noopener prevents tab hijacking + noreferrer hides referrer -->
<a href="https://other.com" target="_blank" rel="noopener noreferrer">External site</a>
6. Accessibility (WCAG 2.2) = SEO Bonus
W3C WCAG 2.2 is the practical standard for 2026. Accessibility and SEO share the same foundation: screen readers and search-engine crawlers both consume the same semantic markup.
6.1 Image alt (Required)
<!-- ❌ -->
<img src="/cover.jpg">
<!-- ✅ -->
<img src="/cover.jpg" alt="Cover of episode 1 of [Some Anime], protagonist holding a sword">
<!-- ✅ Swift Web preferred: SwImage requires alt -->
<SwImage src="/cover.jpg" alt="..." :ratio="16/9" />
alt describes the image's content / semantics, not the filename. For decorative images use alt="" to indicate "ignore me" — don't omit the attribute entirely.
6.2 Form Label Association
<!-- ❌ placeholder cannot replace label -->
<input type="email" placeholder="Email">
<!-- ✅ -->
<label for="email">Email</label>
<input id="email" type="email" placeholder="user@example.com">
<!-- ✅ Wrapping form -->
<label>
Email
<input type="email">
</label>
6.3 ARIA: Patch What HTML Can't Express
ARIA is not a replacement for semantic HTML elements — it's a supplement when no native tag fits.
<!-- ✅ Toggle button state -->
<button aria-expanded="true" aria-controls="menu">Menu</button>
<ul id="menu">...</ul>
<!-- ✅ Custom dialog -->
<div role="dialog" aria-labelledby="dlg-title" aria-modal="true">
<h2 id="dlg-title">Confirm deletion</h2>
...
</div>
6.4 Keyboard Navigation + Touch Target
WCAG 2.2 additions:
- Visible focus indicator (don't hide focus behind hover only)
- Touch target ≥ 24×24 px (mobile) — practical recommendation ≥ 44×44 px
- Redundant entry (don't ask users to re-enter the same info)
- Accessible authentication (don't force memory-based puzzles)
6.5 Color Contrast
Body text contrast ≥ 4.5:1; large text (≥18px or ≥14px bold) ≥ 3:1. Tool: WebAIM Contrast Checker.
7. URL / Routing / canonical
7.1 Semantic Paths
✅ /movie/spirited-away
✅ /movie/123-spirited-away (id + slug, both)
❌ /detail?id=123 (query params aren't semantic)
❌ /m/123 (too cryptic, no info)
Keywords in URL paths are a lightweight ranking signal. Both crawlers and users can infer the page's topic from the URL.
7.2 Important Content in path, Filters in query
/movie/list Main resource (gets indexed)
/movie/list?genre=action Filter parameter (not every combo gets indexed)
/movie/list?page=2 Pagination (with rel="next"/"prev")
Crawl budget is finite; meaningless query combinations waste it. Pair Disallow: /*?utm_* in robots.txt to block tracking-parameter URLs from being indexed.
7.3 canonical: Normalizing Duplicate Content
The same content under multiple URLs (multi-domain mirrors / case differences / trailing slash / tracking params) gets flagged as duplicate content. <link rel="canonical"> tells the search engine "use this URL as the source of truth."
// useSeo writes canonical automatically
useSeo({
title: '...',
description: '...',
// Default canonical = `${siteUrl}${currentPathname}`
canonicalPath: '/movie/123', // explicit override (rare)
siteUrl: 'https://main.example.com', // explicit override for multi-domain
})
7.4 Avoid Deeply Nested Dynamic Parameters
❌ /shop/cat/sub/sub/sub/item/123 too deep, dilutes weight
✅ /shop/item/123 flat structure
8. Structured Data: Schema.org / JSON-LD
Schema.org is the structured-vocabulary standard co-built by Google / Bing / Yandex. As of 2026 it has 823 types / 1,529 properties. Letting search engines and AI answer engines "understand" your page's structure → triggers rich results and AI Overview citations.
8.1 JSON-LD Is Google's Recommended Format
Three formats: JSON-LD / Microdata / RDFa. Google explicitly recommends JSON-LD:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "SEO Optimization Tips",
"author": { "@type": "Person", "name": "Jane Doe" },
"datePublished": "2026-04-28"
}
</script>
JSON-LD is decoupled from the DOM, easier to template and easier for AI to scrape.
8.2 useSeo Outputs Automatically
Swift Web's useSeo outputs WebSite + WebPage schema nodes by default:
useSeo({
title: '...',
description: '...',
})
// Equivalent output:
// {
// "@context": "https://schema.org",
// "@graph": [
// { "@type": "WebSite", "name": "...", "url": "..." },
// { "@type": "WebPage", "name": "...", "description": "..." }
// ]
// }
For detail pages, passing type: 'article' outputs the Article schema (the article fields are optional, but without datePublished the schema is incomplete and rich snippets won't pick it up — always pass them together):
useSeo({
title: data.value?.name,
description: data.value?.summary,
image: data.value?.cover,
type: 'article',
article: {
author: data.value?.author,
datePublished: data.value?.created_at,
dateModified: data.value?.updated_at,
},
})
8.3 Common Schema Types
| Type | Use | Swift Web implementation |
|---|---|---|
| WebSite | Site-wide (one per site) | useSeo automatic |
| WebPage | Per-page | useSeo automatic |
| Article / NewsArticle / BlogPosting | Article / news / blog | useSeo({ type: 'article' }) |
| BreadcrumbList | Breadcrumb navigation | TBD (suggested at component level) |
| FAQ | FAQ pages | Business-side: call schema-org/vue directly |
| Product | Products / apps | Business-side |
| Organization | Org info | Business-side (homepage) |
8.4 Triggering Rich Results
Google rich-result display is algorithm-determined (not guaranteed), but necessary conditions:
- Required fields complete (varies by type — verify with Rich Results Test)
- Data matches visible content (no hiding facts)
- Page is indexed
8.5 GEO Angle: Get Cited by AI
JSON-LD is the primary data source AI answer engines extract facts from. The clearer your structure, the higher the grounding probability.
9. Core Web Vitals 2026 (Most Impactful Ranking Signal)
Core Web Vitals is Google's openly published user-experience ranking signal. Google Search uses 75th-percentile real-user data (not lab data) to evaluate.
9.1 Three Metrics + Thresholds
| Metric | Good | Needs Improvement | Poor |
|---|---|---|---|
| LCP (Largest Contentful Paint) | ≤ 2.5s | 2.5–4.0s | > 4.0s |
| INP (Interaction to Next Paint) | ≤ 200ms | 200–500ms | > 500ms |
| CLS (Cumulative Layout Shift) | ≤ 0.1 | 0.1–0.25 | > 0.25 |
INP replaced FID in 2024 — it more comprehensively reflects interaction smoothness. All click / tap / keypress events have their next-frame delay measured; the p98 across a session becomes the INP.
9.2 LCP Optimization (Largest Contentful Paint)
The LCP element is usually the above-the-fold large image or large text block. Optimization paths:
<!-- ✅ Swift Web practice: above-the-fold hero image -->
<SwImage
src="/hero.jpg"
:ratio="16/9"
alt="..."
preload <!-- adds <link rel="preload"> -->
fetchpriority="high" <!-- raises priority -->
loading="eager" <!-- no lazy loading -->
/>
Server side:
- Use ISR / SSR instead of CSR (HTML pre-render lowers LCP)
- CDN cache static assets
- Subset fonts +
font-display: swap
9.3 INP Optimization (Interaction Latency)
<!-- ✅ Swift Web practice: SwButton + :loading state to prevent double-submit -->
<SwButton :loading="submitting" @click="handleSubmit">Submit</SwButton>
<!-- :loading state managed by parent disables button during async -->
Code level:
- Avoid long main-thread tasks (> 50ms of JS computation) — offload heavy compute to a
Web Worker - Use
requestIdleCallbackto schedule non-urgent work - Reduce sync re-renders (use precise reactivity)
9.4 CLS Optimization (Cumulative Layout Shift)
Layout shift sources:
- Images / videos / ads loading without placeholder space, then expanding
- Font swap shifting text width/height (FOIT/FOUT)
- Async-injected content (toasts, modals) pushing main content
<!-- ✅ Swift Web practice: SwImage requires ratio -->
<SwImage src="..." :ratio="16/9" />
<!-- Placeholder reserves 16:9 container; no shift when image loads -->
Fonts:
/* ✅ font-display: swap + size-adjust fine-tuning */
@font-face {
font-family: 'BrandFont';
src: url('/font.woff2');
font-display: swap;
size-adjust: 100%;
ascent-override: 90%;
}
9.5 Monitoring
- Search Console Core Web Vitals report (real-user data, p75)
- PageSpeed Insights (lab + real-user)
- CrUX Dashboard (Chrome User Experience Report)
- Business-side RUM (Real User Monitoring): optional, self-built
10. sitemap.xml / robots.txt
10.1 sitemap.xml: Telling Crawlers What Pages Exist
Swift Web uses @nuxtjs/sitemap (enabled by default in framework) to auto-generate /sitemap.xml:
- Lists all indexable URLs (
swiftConfig.seo.sitemap.excludeappends private paths to skip; framework already excludes common private paths like/login,/mine/**) - Includes
<lastmod>timestamps for incremental crawling - Multilingual hreflang cross-links (
<xhtml:link rel="alternate" hreflang="...">) are planned (framework#88, to start once a real prefix-strategy applier exists)
Submit the sitemap: in Google Search Console and Bing Webmaster Tools, each one once (set-and-forget).
10.2 robots.txt: The Crawler's First Stop
# /public/robots.txt (or generated by @nuxtjs/robots)
User-agent: *
Disallow: /api/
Disallow: /*?utm_*
Allow: /
Sitemap: https://example.com/sitemap.xml
Field meanings:
User-agent: *— applies to all crawlersDisallow: /api/— block API endpoint pathsDisallow: /*?utm_*— block tracking-parameter URLsSitemap:— points to sitemap.xml (multiple allowed)
⚠️ Don't stack
Disallowand metanoindexon the same path: a robots.txt block stops the crawl, so the crawler can't seenoindex→ the URL may be indexed without a snippet. Pick one by goal: to deindex / exclude a private page use page metarobots: 'private'(keep it crawlable); to purely block crawling (/api/, utm params) use robots.txtDisallow.
10.3 AI Crawler Policy
Since 2024, AI crawlers have proliferated: GPTBot / ClaudeBot / Google-Extended / PerplexityBot, etc. Swift Web's seo.aiBotPolicy option controls them:
// nuxt.config.ts → @nuxtjs/robots
swiftConfig: {
seo: { aiBotPolicy: 'allow' | 'disallow' }
}
// disallow: adds User-agent: GPTBot/ClaudeBot/... + Disallow: / to robots.txt
Business decision:
- Want content cited by AI (more exposure) → allow
- Paid / internal content → disallow + meta noarchive
11. Open Graph / Twitter Card / Social Sharing
11.1 og:* for Beautiful Shares
Facebook / LinkedIn / WeChat / Slack / Discord all share the Open Graph protocol. Twitter uses Twitter Cards (semantically similar).
// useSeo writes the full og:* and twitter:* set automatically
useSeo({
title: '...', // → og:title, twitter:title
description: '...', // → og:description, twitter:description
image: '/cover.jpg', // → og:image, twitter:image
type: 'article', // → og:type
locale: 'en_US', // → og:locale
})
// Auto-added: og:url, twitter:card='summary_large_image'
// Site name does NOT go to og:site_name — it goes into the WebSite JSON-LD (runtimeConfig.public.siteName)
// og:image:width=1200, og:image:height=630
11.2 og:image Standard Size
1200 × 630 px (1.91:1) is the universal standard for Facebook / Twitter / LinkedIn; minimum 200 × 200.
Swift Web fallback chain: input.image → /og-image.png (project-level prebuild ensures it exists; since #89, no longer reads backend header.og_image / header.img).
11.3 Debugging Tools
- Facebook Sharing Debugger
- Twitter Card Validator
- LinkedIn Post Inspector
- WeChat: built-in OG inspector in WeChat Developer Tools
12. Internationalization / hreflang
Multilingual sites (zh-CN / en / zh-TW) use hreflang to tell search engines "I have other-language versions of this page", avoiding duplicate-content penalties.
12.1 hreflang Annotation
<link rel="alternate" hreflang="zh-CN" href="https://example.com/zh/movie/1">
<link rel="alternate" hreflang="en" href="https://example.com/en/movie/1">
<link rel="alternate" hreflang="zh-TW" href="https://example.com/tw/movie/1">
<link rel="alternate" hreflang="x-default" href="https://example.com/movie/1">
x-default is the "fallback for unmatched languages."
12.2 Swift Web Practice
@nuxtjs/i18n + useSeo handle this automatically:
// nuxt.config.ts
swiftConfig: {
i18n: {
enabled: true,
defaultLocale: 'zh-CN',
locales: ['zh-CN', 'en', 'zh-TW'],
},
}
// → automatic hreflang configuration is planned (framework#88, once a real prefix-strategy applier exists)
// → og:locale requires an explicit useSeo({ locale }) — it is NOT taken from the current i18n locale
12.3 i18n strategy required, no_prefix forbidden (hard rule)
When @nuxtjs/i18n is enabled you must use strategy: 'prefix' or 'prefix_except_default' — 'no_prefix' is forbidden:
'no_prefix'makes all locales share one URL (/aboutis both zh-CN and en) → sitemap.xml can't emit<xhtml:link rel="alternate" hreflang>cross-links → search engines flag duplicate content → multi-language SEO is gone.- The framework
console.warns in dev when it detectsno_prefixwith locales ≥ 2, suggestingprefix_except_default. no_prefixis only acceptable when the business explicitly doesn't care about multi-language SEO (internal systems / authenticated back-office).
i18n: {
strategy: 'prefix_except_default', // default locale has no prefix; others get /en/, /zh-TW/ prefixes
locales: [
{ code: 'zh-CN', file: 'zh-CN.json' },
{ code: 'zh-TW', file: 'zh-TW.json' },
{ code: 'en', file: 'en.json' },
],
}
12.4 Caveats
- Each language version must reference all the others (including a self-referential link)
- canonical does not cross languages — each locale's canonical points to itself
- URL strategy: path prefix (
/zh/...) > query (?lang=zh) > subdomain (zh.example.com) - Avoid machine-translation SEO disasters: low-quality MT gets flagged by algorithms as low-value content
Appendix A: MR Review SEO Self-Check Checklist
Before submitting a page / component PR, developers should self-check; reviewers should focus on:
Must-fix
- Do
app/pages/**/*.vueandapp/error.vuehave auseSeo()call? (ESLintswift-web/require-useSeo-in-pagesenforces; covers error.vue since fw#240) - TDK lengths on target? description 50-160 chars, title 10-60 chars in display width (literals are statically pre-checked by ESLint
swift-web/seo-tdk-length; utility/private pages should userobots: 'private'+silenceLengthWarnings: trueinstead) - Are private pages (login / register / mine / profile/* / admin / demo / creator) marked
robots: 'private'? - Are redirect / search-result pages marked
robots: 'noindex'? - Are private pages also paired with
silenceLengthWarnings: true(suppresses dev console noise)? - Do detail pages pass
image(usedata?.cover, not the site default placeholder)? - Do detail pages use the backend SEO fallback chain (
data?.seo?.title ?? data?.name)? - Do detail pages set
type: 'article'+ thearticlefields? - Is the render strategy reasonable (
defineRouteRulesmandatory, detail pages → isr, content pages don't use csr)?
Important (discussion)
- Are route navigations using
<NuxtLink>/<SwLink>, not@click + router.push? - Are images using
<SwImage>withalt+ratio; above-the-fold images withpreload+fetchpriority="high"? - Do
<input>elements have associated<label>s? - Only one
<h1>per page? Heading hierarchyh1→h2→h3without skipping? - Using semantic elements (
<header>/<nav>/<main>/<article>/<section>/<footer>) instead of<div>soup? - External links with
rel="nofollow"/"noopener noreferrer"?
Minor (mentions)
- Is the anchor text descriptive (not "click here")?
- Are URL paths semantic (
/movie/123not/detail?id=123)? - Color contrast meets WCAG (body ≥4.5:1, large text ≥3:1)?
Comment Templates
Must-fix / important: use GitLab Discussion (with
positionto anchor to a code line)
Minor: mention in the review summary, no Discussion
Appendix B: Authoritative References
- Google Search Central docs home
- SEO Starter Guide
- Search Quality Rater Guidelines (PDF)
- Core Web Vitals / LCP / INP / CLS
- Rich Results Test
- PageSpeed Insights
Bing
Schema.org
W3C / WCAG
- WCAG 2.2 — Current accessibility standard
- H101 Semantic-elements technique
- WAI-ARIA 1.3
Tooling
- axe DevTools — accessibility scanner
- WebAIM Contrast Checker — color contrast checker
- Facebook Sharing Debugger
- Twitter Card Validator
Swift Web Internal
framework/CLAUDE.mdSEO chapter- standards repo
rulesSEO review rules - useSeo.ts JSDoc