Chuyển tới nội dung chính

Đa ngôn ngữ, đường dẫn và SEO

Ngôn ngữ của trang web​

Mỗi trang web có một ngôn ngữ chính (primaryLocale) và danh sách ngôn ngữ đã bật (locales), lấy từ /site. API chỉ phục vụ các ngôn ngữ đã bật:

  • locale ngoài sáu mã vi, en, es, pt, fr, it → 400.
  • locale hợp lệ nhưng trang web chưa bật → 404 (giống hệt trang web không tồn tại).

Hãy dựng bộ chuyển ngôn ngữ từ locales, đừng viết cứng.

Cái gì có dự phòng, cái gì không​

Nội dungKhi thiếu bản dịch ở locale bạn hỏi
Khối info của /siteDùng bản của ngôn ngữ chính. Trường locale trong phản hồi cho biết bản nào thực sự được trả về. Nếu nhân sự chưa từng lưu thông tin, info là null.
Tên danh mục, tên thẻTừng tên tự dự phòng về ngôn ngữ chính, cuối cùng là slug. Trường locale không nói gì về tên.
Nhãn menu, url của mục menuTừng mục tự dự phòng về ngôn ngữ chính.
Bài viết và trangKhông có dự phòng. Bài không có bản dịch ở locale thì không nằm trong danh sách, và mở theo slug sẽ nhận 404.
Giá trị trường tùy chỉnh theo ngôn ngữKhông có dự phòng: khóa đó vắng mặt trong meta.

Hệ quả cho trang web của bạn:

  • Danh sách tiếng Anh có thể ít bài hơn danh sách tiếng Việt. Đó là hành vi đúng.
  • API không cho biết một entry có những bản dịch nào. Khi người đọc chuyển ngôn ngữ ngay trên một bài và nhận 404, hãy hiện một thông báo kiểu «Bài này chưa có bản tiếng Anh» kèm liên kết về bản ngôn ngữ chính, thay vì một trang lỗi trống.
async function loadEntry(slug: string, locale: string, primary: string) {
const res = await fetch(`${API}/entries/${encodeURIComponent(slug)}?locale=${locale}`, { credentials: 'omit' })
if (res.ok) return { entry: (await res.json()).data, missingTranslation: false }
if (res.status === 404 && locale !== primary) {
const fallback = await fetch(`${API}/entries/${encodeURIComponent(slug)}?locale=${primary}`, { credentials: 'omit' })
if (fallback.ok) return { entry: (await fallback.json()).data, missingTranslation: true }
}
if (res.status === 404) return null // render your "not found" page
throw new Error(`CMS ${res.status}`)
}

Slug và cấu trúc đường dẫn​

  • Slug của một entry dùng chung cho mọi ngôn ngữ: bài le-bon-mang có cùng slug ở vi và en. Ngôn ngữ nằm trong đường dẫn của bạn (ví dụ /tin-tuc/le-bon-mang và /en/news/le-bon-mang), không nằm trong slug.
  • Slug chỉ gồm chữ thường không dấu, số và dấu - đơn (ví dụ bai-3-bay-phep-bi-tich). Bạn có thể đặt slug thẳng vào URL mà không lo ký tự đặc biệt; vẫn nên encodeURIComponent cho chắc.
  • Slug danh mục và slug thẻ có thể trùng nhau (một danh mục gioi-tre và một thẻ gioi-tre), nên hãy giữ chúng ở hai dạng đường dẫn khác nhau, ví dụ ?category= và ?tag=.

Slug cũ (đổi tên đường dẫn)​

Khi nhân sự đổi slug của một entry, slug cũ không trả 404 và không trả mã chuyển hướng 3xx. API trả 200 với entry hiện tại, và trường slug trong phản hồi là slug mới. Cách nhận biết: so sánh slug bạn hỏi với data.slug.

  • Trang chạy trên trình duyệt (SPA): thay URL bằng slug mới không thêm lịch sử (history.replaceState hoặc navigate(…, { replace: true })), để nút Quay lại không đưa người đọc về URL cũ.
  • Trang dựng phía máy chủ: trả 301 tới URL có slug mới.
  • Trang tĩnh (SSG): bạn chỉ biết slug hiện tại khi dựng; slug cũ được giải quyết lúc chạy nếu trang của bạn có nhánh dự phòng phía trình duyệt cho slug chưa dựng sẵn.
const { data: entry } = await res.json()
if (entry.slug !== requestedSlug) {
const url = location.pathname.replace(/[^/]+\/?$/, entry.slug) + location.search + location.hash
history.replaceState(null, '', url)
}

Tín hiệu lượt xem gửi với slug cũ vẫn được tính cho entry hiện tại.

SEO: API cho bạn những gì​

CầnLấy từ
<title>, <meta name="description">seo.title, seo.description của entry (máy chủ tự tạo nếu nhân sự để trống; khi đó seo.auto là true).
Ảnh chia sẻ (og:image)cover.url, cover.alt.
Ngày đăng (article:published_time, JSON-LD)publishedAt (ISO 8601, UTC).
Tên, địa chỉ, điện thoại của đơn vị (JSON-LD tổ chức)info của /site.

API không cung cấp: URL chuẩn (canonical), sitemap, ngày sửa gần nhất, danh sách bản dịch của từng entry, hay danh sách các trang tĩnh. Bạn tự dựng URL chuẩn theo cấu trúc trang web của mình.

Dựng sitemap:

  • Bài viết: duyệt /entries?locale=…&page=N&limit=100 cho từng ngôn ngữ đến hết pageCount. Nếu trang web có loại nội dung «không liệt kê», bài của loại đó không nằm trong danh sách chung: duyệt thêm danh sách theo từng danh mục cấp 1 (danh mục cha bao gồm bài của danh mục con).
  • Trang tĩnh: không có đường dẫn liệt kê trang. Lấy từ các mục menu có kind: "entry" và entryType: "page", cộng với các slug trang mà bạn và nhân sự đã thống nhất (ví dụ gioi-thieu, lien-he).
  • Đi chậm khi duyệt: xem giới hạn tần suất.

Bài hẹn giờ​

Bài được hẹn giờ đăng chưa xuất hiện ở bất kỳ đâu trước publishedAt. Sau giờ đó, bài xuất hiện trong vòng tối đa khoảng 60 giây (thời gian sống của bộ đệm). Nếu trang của bạn là trang tĩnh, bài chỉ hiện ra sau lần dựng lại kế tiếp.