Le Duy Khuong

Chuỗi: seo-technical · Phần 3

Năng suất & công cụ dev

Canonical URLs & Duplicate Content

Canonical, hreflang, edge cases

2026-03-205 phút đọcVI

Canonical URLs & Duplicate Content

Mở đầu

leduykhuong.com có mỗi bài blog ở 2 URLs: /vi/blog/slug/en/blog/slug. Google thấy 2 pages cùng content → duplicate content issue. Nếu không xử lý, Google phải tự chọn version nào index — và có thể chọn sai, hoặc split ranking signals giữa 2 versions.

Canonical URL giải quyết bằng cách nói rõ: "URL chính thức cho content này là X. Các URL khác là alternate versions."

Mục tiêu: Hiểu duplicate content problem, canonical URL implementation trên leduykhuong.com, hreflang cho multilingual, và edge cases.


Duplicate Content — Vấn đề gì?

Google không "phạt" duplicate content (trừ spam). Nhưng nó tạo vấn đề:

  1. Ranking signal dilution — Backlinks tới /vi/blog/slug/en/blog/slug bị split. Thay vì 1 URL có 10 backlinks, có 2 URLs mỗi URL 5 backlinks.

  2. Wrong version indexed — Google có thể chọn index /en/blog/slug cho Vietnamese users nếu Google coi en version là "better."

  3. Crawl waste — Google crawl cả 2 versions → sử dụng crawl budget cho duplicate content.

Sources of Duplicates trên leduykhuong.com

SourceURLsDuplicate?
Locale variants/vi/blog/slug + /en/blog/slugCó (cùng content, khác locale)
With/without trailing slash/blog/ + /blogCloudflare Pages handles
HTTP vs HTTPShttp:// + https://Cloudflare auto-redirect
www vs non-wwwwww.leduykhuong.com + leduykhuong.comDNS config

Canonical URL — Solution

<link rel="canonical" href="https://leduykhuong.com/vi/blog/learning-in-public" />

Nói Google: "URL chính thức cho page này là https://leduykhuong.com/vi/blog/learning-in-public."

Implementation trên leduykhuong.com

// app/[locale]/blog/[slug]/page.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { locale: localeParam, slug } = await params;
  const locale = parseLocale(localeParam);
 
  return {
    alternates: {
      canonical: `${BASE_URL}/${locale}/blog/${slug}`,
      languages: {
        vi: `${BASE_URL}/vi/blog/${slug}`,
        en: `${BASE_URL}/en/blog/${slug}`,
        "x-default": `${BASE_URL}/vi/blog/${slug}`,
      },
    },
  };
}

HTML Output

Cho /vi/blog/learning-in-public:

<link rel="canonical" href="https://leduykhuong.com/vi/blog/learning-in-public" />
<link rel="alternate" hreflang="vi" href="https://leduykhuong.com/vi/blog/learning-in-public" />
<link rel="alternate" hreflang="en" href="https://leduykhuong.com/en/blog/learning-in-public" />
<link rel="alternate" hreflang="x-default" href="https://leduykhuong.com/vi/blog/learning-in-public" />

Cho /en/blog/learning-in-public:

<link rel="canonical" href="https://leduykhuong.com/en/blog/learning-in-public" />
<link rel="alternate" hreflang="vi" href="https://leduykhuong.com/vi/blog/learning-in-public" />
<link rel="alternate" hreflang="en" href="https://leduykhuong.com/en/blog/learning-in-public" />
<link rel="alternate" hreflang="x-default" href="https://leduykhuong.com/vi/blog/learning-in-public" />

Key points:

  • Mỗi locale version có canonical tự trỏ tới chính nó (self-referencing canonical)
  • hreflang links bidirectional — vi page link tới en, en page link tới vi
  • x-default trỏ tới vi version (default language)

hreflang — Giải thích cho Google

hreflang nói Google: "Page này có version khác cho locale khác."

Format

hreflang="vi"       → Vietnamese version
hreflang="en"       → English version
hreflang="x-default" → Default/fallback version

Google behavior

Khi Vietnamese user search:

  1. Google thấy hreflang="vi" → ưu tiên hiển thị /vi/blog/slug
  2. Ranking signals từ cả vi và en consolidated

Khi English user search:

  1. Google thấy hreflang="en" → ưu tiên hiển thị /en/blog/slug

x-default

x-default = fallback cho locales không được list. Nếu French user search → Google dùng x-default version (vi trong trường hợp leduykhuong.com).


Self-Referencing Canonical — Tại sao?

Tại sao vi page có canonical trỏ tới chính nó?

<!-- /vi/blog/slug → canonical = /vi/blog/slug -->
<link rel="canonical" href="https://leduykhuong.com/vi/blog/learning-in-public" />

3 lý do:

  1. Explicit declaration — Google biết chắc anh chủ đích muốn URL này là canonical. Không đoán.

  2. Prevent parameter issues — Nếu ai link tới ?utm_source=twitter, canonical nói rõ version sạch.

  3. Google best practice — Google khuyến nghị mọi page có self-referencing canonical.


Edge Cases & Pitfalls

1. Canonical conflict với hreflang

<!-- ❌ SAI: canonical trỏ tới vi, nhưng đây là en page -->
<!-- /en/blog/slug -->
<link rel="canonical" href="https://leduykhuong.com/vi/blog/learning-in-public" />

Nếu en page có canonical trỏ tới vi page → Google coi en page là duplicate → KHÔNG index en version → English users không tìm thấy.

Correct: Mỗi locale version canonical trỏ tới chính nó.

2. Missing hreflang on one side

<!-- /vi/blog/slug có hreflang tới en -->
<link rel="alternate" hreflang="en" href=".../en/blog/slug" />
 
<!-- NHƯNG /en/blog/slug KHÔNG có hreflang tới vi -->

Google cần bidirectional confirmation — cả 2 pages phải link tới nhau. Missing one direction → Google ignore hreflang.

leduykhuong.com: generateMetadata() chạy cho CẢ HAI locales → luôn bidirectional. ✅

3. Canonical 404

<!-- Canonical URL trả về 404 -->
<link rel="canonical" href="https://leduykhuong.com/vi/blog/deleted-post" />

Google fetch canonical → 404 → confusion. Always verify canonical URL tồn tại.


getCanonicalUrl() — Helper function

// lib/seo.ts
export function getCanonicalUrl(path: string): string {
  const clean = path.replace(/^\/(en|vi)/, "");
  return `${SITE_CONFIG.baseUrl}${clean || "/"}`;
}

Lưu ý: Function này strip locale prefix. Nó được dùng cho JSON-LD url field (locale-independent canonical). Blog post generateMetadata() dùng locale-specific canonical (${BASE_URL}/${locale}/blog/${slug}).

2 canonical strategies đang coexist:

  • HTML <link rel="canonical"> → locale-specific (mỗi locale tự trỏ)
  • JSON-LD url → locale-independent (strip locale)

Điều này acceptable vì Google dùng HTML canonical cho indexing decisions, JSON-LD url cho structured data.


Verification Workflow

Sau deploy, verify canonical setup:

# 1. Check vi page canonical
curl -s https://leduykhuong.com/vi/blog/learning-in-public | \
  grep 'rel="canonical"'
 
# 2. Check en page canonical (should be different)
curl -s https://leduykhuong.com/en/blog/learning-in-public | \
  grep 'rel="canonical"'
 
# 3. Check hreflang bidirectional
curl -s https://leduykhuong.com/vi/blog/learning-in-public | \
  grep 'hreflang'
 
# 4. GSC URL Inspection → both URLs should be "Indexed"

Thực hành

Bài tập 1: Verify canonicals

cd ACE-component/ACE-leduykhuong-site && npm run build
 
# Compare vi vs en canonical
grep 'rel="canonical"' out/vi/blog/learning-in-public.html
grep 'rel="canonical"' out/en/blog/learning-in-public.html

Câu hỏi: Canonical khác nhau? Mỗi locale tự trỏ?

Bài tập 2: Check hreflang bidirectional

# vi page hreflang
grep 'hreflang' out/vi/blog/learning-in-public.html
 
# en page hreflang
grep 'hreflang' out/en/blog/learning-in-public.html

Câu hỏi: Cả 2 có hreflang tới nhau? x-default trỏ đúng?

Bài tập 3: GSC duplicate check

GSC → Pages → filter "Duplicate":

  • Có pages nào bị "Duplicate, Google chose different canonical"?
  • Nếu có, canonical nào Google chọn? Match với intent?

Tóm tắt

  • Duplicate content — Multilingual sites có inherent risk. Không bị phạt nhưng dilute ranking.
  • Canonical URL — Self-referencing per locale. Mỗi locale page trỏ canonical tới chính nó.
  • hreflang — Bidirectional relationship. Cả 2 locales phải link tới nhau.
  • x-default — Fallback cho unmatched locales. leduykhuong.com → vi.
  • Pitfalls: Canonical cross-locale (sai), missing bidirectional (invalid), canonical 404 (broken).
  • Verify sau deploy bằng curl + GSC URL Inspection.

Bài tiếp theo

Kết thúc series Technical SEO. Series cuối: Cloudflare Pages — Bài 20 sẽ giới thiệu Cloudflare Pages architecture, tại sao leduykhuong.com chọn CF Pages, và cách nó ảnh hưởng tới SEO/analytics.

LDK

Le Duy Khuong

AI Transformation & Digital Strategy. Writing about agentic systems, engineering leadership, and building in public.