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

FieldSoft targetHard ceilingSource
title10–60 display width> 70 triggers keyword-stuffing detectionGoogle SERP truncates ~60, Bing ~65
description50–160 display width> 200 triggers counter-productive penaltyGoogle SERP ~155, social platforms ~200
keywords≤10 items, ≤200 charsModern SEO ignores itStuffing 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:

Languagetitle soft targetdescription soft target
English10–60 chars50–160 chars
Chinese / Traditional5–30 chars25–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 title of Login is more direct than User 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).

ModeUse caseSEO impact
isr (ISR)Listings, detail pages, homepage✅ Cache-friendly, SEO first-class
ssrUser-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 Disallow the same private path in robots.txt: blocking the crawl means the crawler never sees the page's noindex, so the URL may still be indexed (without a snippet). To deindex, keep the page crawlable + noindex; use robots.txt Disallow only 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>
ElementPurpose
<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") — use class to 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.


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.

  • 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)
<!-- 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.

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

TypeUseSwift Web implementation
WebSiteSite-wide (one per site)useSeo automatic
WebPagePer-pageuseSeo automatic
Article / NewsArticle / BlogPostingArticle / news / bloguseSeo({ type: 'article' })
BreadcrumbListBreadcrumb navigationTBD (suggested at component level)
FAQFAQ pagesBusiness-side: call schema-org/vue directly
ProductProducts / appsBusiness-side
OrganizationOrg infoBusiness-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

MetricGoodNeeds ImprovementPoor
LCP (Largest Contentful Paint)≤ 2.5s2.5–4.0s> 4.0s
INP (Interaction to Next Paint)≤ 200ms200–500ms> 500ms
CLS (Cumulative Layout Shift)≤ 0.10.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 requestIdleCallback to 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.exclude appends 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 crawlers
  • Disallow: /api/ — block API endpoint paths
  • Disallow: /*?utm_* — block tracking-parameter URLs
  • Sitemap: — points to sitemap.xml (multiple allowed)

⚠️ Don't stack Disallow and meta noindex on the same path: a robots.txt block stops the crawl, so the crawler can't see noindex → the URL may be indexed without a snippet. Pick one by goal: to deindex / exclude a private page use page meta robots: 'private' (keep it crawlable); to purely block crawling (/api/, utm params) use robots.txt Disallow.

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


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 (/about is 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 detects no_prefix with locales ≥ 2, suggesting prefix_except_default.
  • no_prefix is 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/**/*.vue and app/error.vue have a useSeo() call? (ESLint swift-web/require-useSeo-in-pages enforces; 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 use robots: 'private' + silenceLengthWarnings: true instead)
  • 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 (use data?.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' + the article fields?
  • Is the render strategy reasonable (defineRouteRules mandatory, 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> with alt + ratio; above-the-fold images with preload + fetchpriority="high"?
  • Do <input> elements have associated <label>s?
  • Only one <h1> per page? Heading hierarchy h1→h2→h3 without 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/123 not /detail?id=123)?
  • Color contrast meets WCAG (body ≥4.5:1, large text ≥3:1)?

Comment Templates

Must-fix / important: use GitLab Discussion (with position to anchor to a code line)
Minor: mention in the review summary, no Discussion


Appendix B: Authoritative References

Google

Bing

Schema.org

W3C / WCAG

Tooling

Swift Web Internal