---
title: "Làm sao để Google thấy đủ site hai ngôn ngữ của bạn (Next.js)"
url: https://phatdev.net/vi/blog/technical-seo-nextjs
lang: vi
updated: 2026-10-21
alternate:
  en: https://phatdev.net/en/blog/technical-seo-nextjs
markdown: https://phatdev.net/vi/blog/technical-seo-nextjs.md
---

# Làm sao để Google thấy đủ site hai ngôn ngữ của bạn (Next.js)

> Site hai ngôn ngữ vẫn có thể vô hình với Google nếu thiếu vài dòng nhỏ. Cách khai báo địa chỉ chính, cặp ngôn ngữ, sơ đồ trang và dữ liệu có cấu trúc, cùng lỗi trang gốc tự chuyển ngôn ngữ.

Chuyên mục: SEO | Đăng: 2026-10-19 | Cấp độ: Trung cấp | 8 phút đọc

Tags: seo, nextjs, hreflang, sitemap, json-ld, canonical

Bạn làm xong một site hai ngôn ngữ, đẹp, chạy nhanh. Vài tuần sau gõ tên mình lên Google thì chỉ thấy bản tiếng Anh, hoặc không thấy gì. Lỗi thường không nằm ở nội dung. Nó nằm ở những dòng nhỏ trong phần đầu của trang mà người xem không bao giờ nhìn thấy, như thiếu thẻ nói "đây là địa chỉ chính", hai bản ngôn ngữ không trỏ vào nhau, hay trang chủ tự đẩy mọi người sang một ngôn ngữ.

Bài này là danh sách việc mình làm cho một site song ngữ vi/en viết bằng Next.js (App Router). Ví dụ dùng Next.js 16.4 trong một dự án tạm, nhưng các tính năng metadata này chạy tương tự từ Next 14. Khác biệt chính là ở bản mới `params` là Promise nên phải `await`.

## Google nhìn site của bạn thế nào

Con robot của Google (Googlebot) tải HTML về, đọc phần `<head>`, đi theo các liên kết, rồi quyết định trang nào được lưu và trang nào là bản "chính". Nó không có tài khoản, không có cookie, và không gửi thông tin ngôn ngữ trình duyệt như người thật. Vì vậy mọi thứ quan trọng phải nằm ngay trong HTML mà máy chủ trả về cho một người lạ vô danh.

## Địa chỉ chính của từng trang (canonical)

Mỗi trang nên tự khai báo địa chỉ chính của nó, viết đầy đủ từ `https://`. Như vậy dù người ta vào bằng nhiều đường (có hay không có dấu `/`, có tham số theo dõi), Google vẫn biết gộp về một. Trong Next.js, đặt `metadataBase` ở layout gốc, rồi khai báo `generateMetadata` cho mỗi trang.

```tsx
// app/[lang]/blog/[slug]/page.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { lang, slug } = await params;          // Next 15+: params is a Promise
  const post = posts.find((p) => p.slug === slug);
  if (!isLocale(lang) || !post) return {};
  const path = `/blog/${slug}`;
  return {
    title: post.title[lang],
    description: post.description[lang],
    alternates: {
      canonical: url(lang, path),               // self-referencing canonical
      languages: languageAlternates(path),      // vi + en + x-default
    },
    openGraph: { type: "article", url: url(lang, path), locale: lang === "vi" ? "vi_VN" : "en_US",
                 publishedTime: post.published, modifiedTime: post.modified },
  };
}
```

Mẹo nhỏ: gom việc dựng địa chỉ vào một hàm (`url()`, `languageAlternates()`) và dùng chung cho metadata, dữ liệu có cấu trúc lẫn sơ đồ trang. Chỉ cần lệch một dấu `/` giữa hai nơi là hai bản ngôn ngữ không còn khớp nhau.

## Nói cho Google biết bản tiếng Việt và bản tiếng Anh là một cặp (hreflang)

Thẻ này (gọi là hreflang) nói với Google: "trang này có bản tương đương bằng ngôn ngữ khác ở đây". Theo tài liệu của Google có bốn quy tắc. Phải chỉ hai chiều: A trỏ tới B mà B không trỏ lại A thì cả hai bị bỏ qua. Mỗi bản phải có cả chính nó trong danh sách. Địa chỉ phải đầy đủ, gồm cả giao thức. Và `x-default` là trang dự phòng cho người không khớp ngôn ngữ nào, nên hợp nhất là một trang chọn ngôn ngữ.

Mình làm một trang chọn ngôn ngữ tĩnh ở `/` (chỉ là trang bình thường có hai liên kết) và trỏ `x-default` vào đó.

```ts
export function languageAlternates(path = "") {
  return {
    vi: url("vi", path),
    en: url("en", path),
    "x-default": `${SITE_URL}/`, // the language-neutral chooser page
  };
}
```

![Một khối gốc rẽ thành hai cổng mở rõ ràng, một xanh một hổ phách, nối nhau bằng đường chấm, và một đầu dò tròn quét cả hai](https://phatdev.net/uploads/blog/technical-seo-nextjs/language-fork-1920.webp)

## Bản đồ trang cho robot (sitemap)

Sitemap là danh sách mọi địa chỉ bạn muốn Google biết. Với site vài chục tới vài nghìn trang, mình dùng một sitemap tổng (index) trỏ tới các sitemap con; mỗi file con chứa tối đa 50.000 địa chỉ. Mình viết bằng route handler để tự kiểm soát các dòng khai báo ngôn ngữ.

```ts
// app/sitemap.xml/route.ts  (the index)
export const dynamic = "force-static";
export function GET() {
  const lastmod = new Date().toISOString().slice(0, 10);
  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <sitemap><loc>${SITE_URL}/sitemap-blog.xml</loc><lastmod>${lastmod}</lastmod></sitemap>
</sitemapindex>`;
  return new Response(xml, { headers: { "Content-Type": "application/xml; charset=utf-8" } });
}
```

Trong sitemap con, mỗi địa chỉ phải kèm đủ bộ liên kết ngôn ngữ của nó (vi, en, x-default), và thẻ `<urlset>` phải khai báo `xmlns:xhtml="http://www.w3.org/1999/xhtml"`. Nhớ thêm dòng `Sitemap:` vào `robots.txt`, rồi gửi `sitemap.xml` trong Search Console. Còn `robots.txt` chỉ nên dùng để chỉ đường cho robot. Đừng dùng nó để giấu trang khỏi Google: địa chỉ bị chặn vẫn có thể hiện trong kết quả mà không có nội dung. Muốn giấu thật thì dùng thẻ `noindex` hoặc yêu cầu đăng nhập.

![Cây trang web phân cấp isometric, một đầu quét tròn bên trái quét qua cây và vài ô được đánh dấu hổ phách](https://phatdev.net/uploads/blog/technical-seo-nextjs/crawl-map-1920.webp)

## Giới thiệu bản thân cho Google bằng dữ liệu có cấu trúc (JSON-LD)

Đây là một đoạn dữ liệu nhỏ ghi rõ "trang này là bài viết, tác giả là ai, ngày đăng nào". Next.js khuyên đặt nó trong thẻ `<script type="application/ld+json">`, và thay ký tự `<` bằng `<` để nếu dữ liệu có chứa `</script>` thì cũng không phá được trang.

```tsx
export function JsonLd({ data }: { data: Record<string, unknown> }) {
  return (
    <script type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(data).replace(/</g, "\\u003c") }} />
  );
}
```

Mình dùng ba loại. `Person` ở trang chủ (tên, địa chỉ, các hồ sơ chính thức của bạn). `BlogPosting` ở mỗi bài (tiêu đề, ngày đăng, ngày sửa, ảnh, tác giả, ngôn ngữ). `BreadcrumbList` cho đường dẫn kiểu "Trang chủ > Blog > Bài này". Mọi địa chỉ trong đó phải có thật: lúc đầu mình để một mục `/blog` chưa tồn tại và phải bỏ đi.

## Xác minh Search Console và favicon

Có hai cách xác minh bạn sở hữu site. Thẻ meta ở trang chủ hợp với kiểu thuộc tính theo địa chỉ. Bản ghi DNS là cách duy nhất cho thuộc tính "Domain", bao phủ mọi subdomain lẫn http và https. Xác minh xong đừng xoá thẻ hay bản ghi, kẻo mất quyền.

Để Google hiện được favicon (biểu tượng nhỏ cạnh kết quả), ảnh phải vuông tỉ lệ 1:1, nên lớn hơn 48x48 px, địa chỉ ổn định, và không bị `robots.txt` chặn. Google nói việc hiển thị không được đảm bảo và có thể mất vài ngày tới vài tuần.

## Lỗi kinh điển: trang gốc tự chuyển ngôn ngữ

Rất nhiều site song ngữ làm thế này: ai vào `/` thì đọc ngôn ngữ trình duyệt rồi chuyển tới `/vi` hoặc `/en`. Nghe thân thiện, nhưng Googlebot không gửi thông tin ngôn ngữ và đến từ một địa chỉ có vẻ ở Mỹ, nên lần nào nó cũng bị đưa về cùng một ngôn ngữ. Nó không bao giờ tới bản kia. Thêm nữa, `/` trở thành một lần chuyển hướng tạm (mã 307), không phải trang bạn dùng làm `x-default` được.

Mình tái hiện bằng cách thêm một middleware kiểu đó vào dự án thử. Với ngôn ngữ `vi-VN`, `curl` nhận `307` về `/vi`. Với `en-US`, về `/en`. Không có thông tin ngôn ngữ cũng về `/en`, kể cả khi giả User-Agent của Googlebot. Gỡ middleware đi và để `/` là trang chọn ngôn ngữ tĩnh thì mọi yêu cầu đều nhận `200`.

Cách an toàn: `/` luôn là trang cố định, mỗi ngôn ngữ có địa chỉ riêng mở được không cần cookie. Muốn gợi ý ngôn ngữ cho khách, hiện một dòng "Chuyển sang tiếng Việt?" bằng JavaScript, đừng ép chuyển ở phía máy chủ.

## Mình đã kiểm gì

Hôm 21/10/2026 mình build lại dự án thử rồi đọc HTML thật. Trang `/vi/blog/hello-seo` có canonical đúng, đủ ba thẻ ngôn ngữ (vi, en, x-default), `og:locale` là `vi_VN`, và `<html lang="vi">` lấy từ địa chỉ chứ không từ yêu cầu. Các khối dữ liệu có cấu trúc đọc ra được bằng `JSON.parse`, hai file sitemap là XML hợp lệ. Mình chưa chạy được Rich Results Test hay Search Console vì cần địa chỉ công khai và tài khoản, và cũng không kiểm được việc Google có hiện favicon hay không.

## Làm gì tiếp theo

Việc đáng làm đầu tiên là gỡ bất cứ chỗ nào đoán ngôn ngữ ở trang gốc. Sau đó mở HTML của một bài bất kỳ bằng "Xem nguồn trang" và đếm: có canonical tuyệt đối, có đủ ba thẻ ngôn ngữ, ngôn ngữ trong thẻ `<html>` đúng không. Làm được đủ chừng đó là đã hơn phần lớn site hai ngôn ngữ mình từng gặp. Khi phần chỉ mục ổn, bước tiếp theo là tốc độ, mình viết ở [Từ 95 lên 99 điểm tốc độ mobile: mình đã làm gì với một site Next.js](https://phatdev.net/vi/blog/core-web-vitals-99-case-study).

## Nguồn tham khảo

- [Next.js docs: generateMetadata](https://nextjs.org/docs/app/api-reference/functions/generate-metadata)
- [Next.js docs: JSON-LD guide](https://nextjs.org/docs/app/guides/json-ld)
- [Google Search Central: Localized versions (hreflang)](https://developers.google.com/search/docs/specialty/international/localized-versions)
- [Google Search Central: Locale-adaptive pages](https://developers.google.com/search/docs/specialty/international/locale-adaptive-pages)
- [Google Search Central: Consolidate duplicate URLs (canonical)](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls)
- [Google Search Central: Large sitemaps and sitemap index](https://developers.google.com/search/docs/crawling-indexing/sitemaps/large-sitemaps)
- [Google Search Central: Favicon in Search](https://developers.google.com/search/docs/appearance/favicon-in-search)
- [Google Search Central: Article structured data](https://developers.google.com/search/docs/appearance/structured-data/article)
- [Search Console Help: Verify site ownership](https://support.google.com/webmasters/answer/9008080)

## Bài liên quan

- [Từ 95 lên 99 điểm tốc độ mobile: mình đã làm gì với một site Next.js](https://phatdev.net/vi/blog/core-web-vitals-99-case-study.md)
- [SEO cơ bản cho web bán hàng: việc chủ shop tự làm và việc cần nhờ dev](https://phatdev.net/vi/blog/seo-basics-online-shop.md)
- [Làm web cho AI đọc được năm 2026: cái nào là chuẩn chính thức, cái nào mới là đề xuất](https://phatdev.net/vi/blog/website-ready-for-ai-agents.md)
