SEO khi triển khai SvelteKit — bảng tra nhanh

SvelteKit đã kết xuất các trang của bạn trên máy chủ — phần đó đã được xử lý. Trang này nói về quyết định *tiếp theo*: trang web của bạn được xây dựng và triển khai như thế nào. Một **adapter** đóng gói ứng dụng SvelteKit cho một máy chủ lưu trữ (máy chủ tệp tĩnh, máy chủ Node hoặc một dịch vụ như Vercel hay Cloudflare), còn thiết lập **prerender** theo từng trang quyết định trang đó được chuyển thành tệp HTML thuần từ trước hay được kết xuất mới ở mỗi lượt truy cập. Hai lựa chọn này quyết định trình thu thập dữ liệu nhận HTML nhanh đến đâu — và SvelteKit sẽ không tạo sitemap hay robots.txt cho bạn, nên bạn phải tự bổ sung chúng.

Xuất bản lần đầu: 3 thg 7, 2026 · Cập nhật lần cuối: 24 thg 8, 2026 · Nâng cao

SvelteKit đã kết xuất các trang của bạn trên máy chủ — phần đó đã được xử lý. Trang này nói về quyết định *tiếp theo*: trang web của bạn được xây dựng và triển khai như thế nào. Một **adapter** đóng gói ứng dụng SvelteKit cho một máy chủ lưu trữ (máy chủ tệp tĩnh, máy chủ Node hoặc một dịch vụ như Vercel hay Cloudflare), còn thiết lập **prerender** theo từng trang quyết định trang đó được chuyển thành tệp HTML thuần từ trước hay được kết xuất mới ở mỗi lượt truy cập. Hai lựa chọn này quyết định trình thu thập dữ liệu nhận HTML nhanh đến đâu — và SvelteKit sẽ không tạo sitemap hay robots.txt cho bạn, nên bạn phải tự bổ sung chúng.

TL;DR — Adapter không thay đổi SvelteKit kết xuất nội dung gì — nó thay đổi ở đâukhi nào: dạng tĩnh tại thời điểm build (adapter-static), theo yêu cầu trên máy chủ do bạn vận hành (adapter-node), hoặc theo yêu cầu trên các hàm serverless/edge (adapter-vercel/-netlify/-cloudflare). prerender = true theo từng route tạo HTML tĩnh và loại route khỏi manifest động; prerender = 'auto' vừa kết xuất trước vừa giữ route trong manifest — công cụ dành cho các trang hỗn hợp /blog/[slug]. Runtime edge chạy trên các isolate V8: không có fs của Node, và cold start làm TTFB xấu đi, từ đó ảnh hưởng đến LCP và (theo tài liệu của Google về crawl budget) khả năng thu thập dữ liệu. SvelteKit không tạo sitemap.xml hay robots.txt — hãy xây dựng chúng dưới dạng endpoint +server.js, đồng thời lưu ý rằng chiến lược phụ thuộc vào adapter. Đây là bài viết đồng hành có phạm vi hẹp hơn, tập trung vào triển khai, với bài kiến thức nền tảng về SEO cho SvelteKit trong phần này; tôi giả định bạn đã biết SvelteKit mặc định dùng SSR và sẽ không tranh luận lại điều đó ở đây.

Một ý tưởng giúp mọi thứ trở nên sáng tỏ

Adapter không thay đổi nội dung được kết xuất. Nó thay đổi ở đâukhi nào. Chỉ có vậy. Tài liệu SvelteKit diễn đạt rất chính xác: adapter “nhận ứng dụng đã build làm đầu vào và tạo đầu ra để triển khai.” Bằng chứng cho nhận định này SvelteKit adapters take the built application as input and generate deployment-specific output. Phạm vi: Deployment output; adapter choice can still constrain supported runtime features. Độ tin cậy: cao · Đã xác minh: SvelteKit: Adapters Các component, hàm load và metadata <svelte:head> của bạn — đều giống hệt nhau trên mọi adapter. Điểm khác biệt là:

  • HTML được tạo ra khi nào: tại thời điểm build (tĩnh/được kết xuất trước) hoặc tại thời điểm yêu cầu (SSR trên máy chủ, hàm serverless hoặc hàm edge).
  • HTML được tạo ra ở đâu: trên một máy chủ gốc duy nhất, một hàm serverless theo khu vực hoặc một mạng edge gần khách truy cập.

Mọi nội dung bên dưới đều là hệ quả của hai trục này.

Vì sao lựa chọn triển khai cũng là lựa chọn SEO

Chuỗi quan hệ này ngắn và có tài liệu rõ ràng: TTFB → LCP → khả năng thu thập dữ liệu.

Time to first byte là khoảng thời gian máy chủ lưu trữ cần để bắt đầu gửi phản hồi. Một tệp được kết xuất trước và phục vụ từ bộ nhớ đệm CDN có TTFB gần bằng không. Một máy chủ phải kết xuất trang có TTFB cao hơn. Một hàm serverless hoặc edge phải cold start có thể có TTFB cao hơn nhiều ở lượt truy cập đầu tiên. TTFB là đầu vào trực tiếp của Largest Contentful Paint — bạn không thể hiển thị thứ mình chưa nhận được — và LCP là một tín hiệu Core Web Vitals.

Về khía cạnh thu thập dữ liệu, Google đưa ra tuyên bố rõ ràng nhất. Theo tài liệu về crawl budget: “Nếu trang web phản hồi nhanh trong một thời gian, giới hạn sẽ tăng lên, nghĩa là có thể sử dụng nhiều kết nối hơn để thu thập dữ liệu. Nếu trang web chậm lại hoặc phản hồi bằng lỗi máy chủ, giới hạn sẽ giảm xuống và Google thu thập dữ liệu ít hơn.” Và dòng hướng dẫn thực hành tốt nhất: “Hãy làm cho các trang của bạn tải hiệu quả. Nếu Google có thể tải và kết xuất các trang nhanh hơn, chúng tôi có thể đọc được nhiều nội dung hơn từ trang web của bạn.” Một hàm edge cold start phản hồi chậm chịu tác động theo cùng cơ chế với một máy chủ gốc chậm.

Một lưu ý thẳng thắn ngay từ đầu: Google không công bố hướng dẫn dành riêng cho SvelteKit. Không có tài liệu hay tập Search Off the Record nào nêu tên adapter SvelteKit, prerender = 'auto' hoặc cold start ở edge. Điều tôi làm ở đây là áp dụng hướng dẫn chung của Google về kết xuất và crawl budget vào cơ chế cụ thể của SvelteKit — không phải trích lời một đại diện từng bình luận về SvelteKit, vì chưa có ai làm vậy. Nhận định của Google rằng “kết xuất phía máy chủ hoặc kết xuất trước vẫn là một ý tưởng tuyệt vời vì nó giúp trang web nhanh hơn cho người dùng và trình thu thập dữ liệu, đồng thời không phải bot nào cũng có thể chạy JavaScript” là cơ sở chính thức gần nhất và không phụ thuộc framework.

Chọn adapter để đạt kết quả SEO mong muốn

adapter-auto — lựa chọn mặc định không cần cấu hình và giới hạn của nó

Các dự án SvelteKit mới đi kèm adapter-auto. Nó phát hiện nền tảng — Vercel, Netlify, Cloudflare Pages, Azure, AWS — và cài đặt adapter tương ứng tại thời điểm build. Đây là điểm khởi đầu ổn, nhưng có một giới hạn cứng đáng biết: adapter-auto không nhận bất kỳ tùy chọn nào. Ngay khi cần { edge: true }, binding Cloudflare, Vercel ISR hoặc bất kỳ cấu hình riêng cho nền tảng nào, bạn phải cài trực tiếp adapter nền tảng (adapter-vercel, adapter-cloudflare, v.v.). Hãy xem auto là bộ khung ban đầu, không phải quyết định cho môi trường production.

adapter-static — SSG toàn phần, dành cho trang web ưu tiên nội dung

adapter-static kết xuất trước toàn bộ trang web thành các tệp tĩnh tại thời điểm build. Không có máy chủ nào chạy; máy chủ lưu trữ phục vụ HTML phẳng. Bằng chứng cho nhận định này adapter-static prerenders a SvelteKit site as static files. Phạm vi: Routes must be prerenderable; performance outcomes depend on hosting and page design. Độ tin cậy: cao · Đã xác minh: SvelteKit: Static site generation Với một trang web ưu tiên nội dung, đây là cấu hình SEO mạnh nhất bạn có thể có — TTFB thấp nhất, không có cold start, không có gì để ngừng hoạt động. Yêu cầu duy nhất chính là cái bẫy được trình bày kỹ trong bài kiến thức nền tảng: SSR phải được bật trong quá trình build, nếu không bạn sẽ nhận được các vỏ rỗng thay vì HTML đã kết xuất. Ngoài việc lưu ý điểm đó, tôi sẽ không giải thích lại ở đây.

Điểm đánh đổi là sự cứng nhắc. Bất kỳ thứ gì thực sự cần logic máy chủ theo từng yêu cầu (tìm kiếm thực, nội dung theo từng người dùng, xử lý biểu mẫu mà không có endpoint bên thứ ba) đều không thể tồn tại trong một bản dựng hoàn toàn tĩnh — và đó chính là mục đích của các adapter tiếp theo.

adapter-node — máy chủ do bạn kiểm soát

adapter-node tạo ra một máy chủ Node.js độc lập. Bạn vận hành nó, mở rộng quy mô cho nó và chịu trách nhiệm về TTFB. Đây là lựa chọn linh hoạt nhất và ít gây bất ngờ nhất về runtime — có đầy đủ API Node, bao gồm fs. Nó phù hợp khi bạn đã có hạ tầng, cần các thư viện Node mà runtime edge không chạy được hoặc muốn thời gian phản hồi có thể dự đoán từ một máy chủ đang ấm (không phải cold start). Đánh đổi nằm ở vận hành: bạn đang chạy một máy chủ, và tốc độ cùng thời gian hoạt động của nó giờ trở thành khả năng thu thập dữ liệu của bạn.

adapter-vercel — serverless, edge và ISR

Theo mặc định, adapter-vercel triển khai lên các hàm serverless của Vercel, với một số công cụ điều chỉnh liên quan đến SEO được thiết lập theo từng route qua export const config:

  • runtime: 'edge' chuyển route đó sang runtime edge của Vercel (xem thêm bên dưới).
  • regions kiểm soát nơi các hàm serverless chạy — gần người dùng (hoặc cơ sở dữ liệu) hơn nghĩa là độ trễ thấp hơn.
  • isr bật Incremental Static Regeneration: isr: { expiration: 60 } phục vụ một tài nguyên tĩnh đã được lưu trong bộ nhớ đệm và tái tạo nó sau khoảng thời gian đó, mang lại “lợi thế về hiệu năng và chi phí của nội dung được kết xuất trước cùng tính linh hoạt của nội dung được kết xuất động.” ISR là con đường thứ tư thực sự giữa hoàn toàn tĩnh và hoàn toàn SSR — nhưng hãy lưu ý cảnh báo trong chính tài liệu: “Việc dùng ISR trên một route có export const prerender = true sẽ không có tác dụng, vì route đó đã được kết xuất trước tại thời điểm build.” ISR và prerender là các phương án thay thế, không thể xếp chồng.

adapter-cloudflare — Workers/Pages, edge toàn cầu

adapter-cloudflare nhắm đến Cloudflare Workers và Pages — SSR trên một mạng edge toàn cầu, thường cho TTFB thấp nhất với nhóm người dùng phân bố rộng về địa lý. Hạn chế quan trọng là runtime: Workers chạy trên các isolate V8, không phải Node. Theo tài liệu: “Bạn không thể dùng fs trong Cloudflare Workers.” Một số API Node chỉ hoạt động khi bật cờ tương thích nodejs_compat, và ngay cả khi đó mức hỗ trợ cũng không tương đương hoàn toàn. Nếu bạn đang đọc tệp tại thời điểm yêu cầu (bản đồ chuyển hướng, tệp dữ liệu, đầu vào ảnh OG tùy chỉnh), đoạn mã đó cần được thiết kế lại — phần về edge bên dưới sẽ trình bày vấn đề này.

(adapter-cloudflare-workers cũ đã ngừng được khuyến nghị; dự án mới dùng adapter-cloudflare, hỗ trợ cả Workers lẫn Pages. Nếu bạn vẫn dùng adapter cũ, chuyển đổi sang adapter mới là hướng được khuyến nghị.)

adapter-netlify — functions hoặc Edge Functions (Deno)

Theo mặc định, adapter-netlify triển khai lên các hàm dựa trên Node của Netlify, hoặc lên Edge Functions dựa trên Deno với edge: true. Mô hình giống Vercel: mặc định là serverless và có thể chọn dùng edge. Một lưu ý riêng cho SvelteKit — Netlify Forms yêu cầu trang chứa biểu mẫu phải được kết xuất trước để Netlify có thể phát hiện markup của biểu mẫu tại thời điểm triển khai; đây là một yêu cầu nhỏ “hãy kết xuất trước route này” bổ sung trên lựa chọn adapter.

Quyết định, mỗi lựa chọn trong một dòng

  • Trang web thuần nội dungadapter-static, kết xuất trước mọi thứ.
  • Trang web nội dung có các phần độngadapter-node/-vercel/-cloudflare, dùng prerender = true cho nội dung và false/'auto' cho các route động.
  • Ứng dụng/bảng điều khiển có cá nhân hóa → ưu tiên SSR (node hoặc edge), chỉ kết xuất trước phần khung tĩnh (tiếp thị, đăng nhập).
  • Nhóm người dùng toàn cầu, TTFB có tính quyết định → dùng adapter edge cho các route động, đồng thời chấp nhận hạn chế của API Node và thực tế về cold start.

(Tab Decision Tree trình bày quyết định này dưới dạng một luồng phân nhánh.)

Chiến lược kết xuất trước cho các trang web hỗn hợp

true / false / 'auto' thực sự làm gì

export const prerender là một tùy chọn trang theo từng route (hoặc từng layout), và ba giá trị này không chỉ đơn giản là bật/tắt:

  • true — tạo HTML tĩnh cho route này tại thời điểm build. Điểm quan trọng là route đó “bị loại khỏi các manifest dùng cho SSR động, khiến máy chủ (hoặc các hàm serverless/edge) nhỏ hơn.” Sau khi được kết xuất trước, route không thể quay về kết xuất động — nó là tĩnh, hoàn toàn như vậy.
  • false — luôn kết xuất theo yêu cầu. Không có tệp tĩnh.
  • 'auto' — công cụ cho trang web hỗn hợp. Nó kết xuất trước route giữ route trong manifest máy chủ động, vì vậy cùng một route có thể được phục vụ dưới dạng tĩnh cho các đường dẫn đã biết và được kết xuất phía máy chủ cho phần còn lại. Cơ chế này được xây dựng chính xác cho trường hợp mà tài liệu mô tả: một route như /blog/[slug], “nơi bạn muốn kết xuất trước nội dung mới nhất/phổ biến nhất nhưng kết xuất phía máy chủ phần đuôi dài.”

Vì các route được kết xuất trước làm giảm kích thước gói máy chủ, một trang web phần lớn được kết xuất trước với vài route 'auto'/false sẽ triển khai một hàm nhỏ hơn, rẻ hơn và nhanh hơn — một lợi ích hiệu quả độc lập với SEO.

Route động cần một hàm entries

Trình thu thập dữ liệu của quá trình prerender khám phá trang bằng cách đi theo các liên kết <a> từ những điểm đầu vào. Cách này hiệu quả với route tĩnh, nhưng một route động như /blog/[slug] không có URL cố định để trình thu thập dữ liệu tìm thấy. Nếu không có gì liên kết đến một slug cụ thể, SvelteKit sẽ không biết slug đó tồn tại — và bạn sẽ gặp lỗi build kinh điển rằng các route “được đánh dấu là có thể kết xuất trước nhưng chưa được kết xuất trước.”

Cách khắc phục là dùng một hàm entries rõ ràng (hoặc config.kit.prerender.entries) để liệt kê các giá trị tham số:

// src/routes/blog/[slug]/+page.server.js
export const prerender = true;

export function entries() {
  return [
    { slug: 'hello-world' },
    { slug: 'sveltekit-deployment-seo' },
  ];
}

Trong thực tế, bạn tạo danh sách đó từ CMS hoặc thư mục nội dung. Nếu không có danh sách này, quá trình prerender chỉ bao phủ các slug mà trình thu thập liên kết tình cờ tìm thấy.

Mẫu /blog/[slug] trong thực tế

Kết hợp hai phần lại, bạn có cấu hình chuẩn cho trang web hỗn hợp: prerender = 'auto' cùng một hàm entries trả về các bài viết mới và phổ biến. Những bài đó nhận HTML tĩnh tại thời điểm build; bất kỳ bài nào không có trong danh sách sẽ chuyển sang SSR theo yêu cầu. Bài viết mới được kết xuất động cho đến khi bản build tiếp theo kết xuất trước chúng. Đây là điểm cân bằng thực dụng giữa “kết xuất trước toàn bộ 40.000 bài viết ở mỗi lần build” và “kết xuất mọi bài viết ở mọi yêu cầu”.

Các hạn chế của runtime edge ảnh hưởng đến SEO

config.runtime = 'edge' áp dụng theo từng route (trên Vercel)

Edge không phải là một công tắc tất-cả-hoặc-không-gì. Trên Vercel, đây là một tùy chọn trang theo từng route:

// +page.server.js or +server.js
export const config = { runtime: 'edge' };

Điều đó có nghĩa là bạn có thể đẩy các route có lưu lượng cao, có thể lưu trong bộ nhớ đệm ra edge để có TTFB thấp, trong khi vẫn giữ các route phụ thuộc Node trên runtime serverless (Node) tiêu chuẩn trong cùng một lần triển khai. Hãy kết hợp có chủ đích.

Không có fs, không có các API Node tùy ý

Các runtime edge — Cloudflare Workers, Vercel Edge Functions, Netlify Deno Edge Functions — không cung cấp fs của Node. Tài liệu Cloudflare nói: “Bạn không thể dùng fs trong Cloudflare Workers.” Tài liệu Vercel nói: “Bạn không thể dùng fs trong các hàm edge.” Cả hai đều chỉ ra hai lối thoát giống nhau: dùng trợ giúp read từ $app/server để truy cập tài nguyên được đóng gói, hoặc “kết xuất trước các route liên quan” để việc truy cập tệp diễn ra tại thời điểm build thay vì thời điểm yêu cầu.

Các trường hợp liên quan gián tiếp đến SEO mà hạn chế này gây vướng mắc gồm: tạo ảnh OG động có đọc tệp phông chữ hoặc mẫu, bản đồ chuyển hướng dựa trên tệp, hoặc endpoint sitemap đọc nội dung từ đĩa. Mỗi trường hợp phải chuyển sang read() của $app/server hoặc chuyển sang thời điểm prerender/build. Đây không phải rào cản — mà là hạn chế “cần biết trước khi chọn edge”.

Cold start và TTFB — khi nào edge hữu ích và khi nào không

Các hàm edge vẫn có cold start. Một hàm edge lạnh ở yêu cầu đầu tiên có thể chậm hơn máy chủ Node đã ấm, và chậm hơn đáng kể so với tệp được kết xuất trước phục vụ từ bộ nhớ đệm. Edge phát huy lợi thế khi hàm luôn ấm hoặc khi được kết hợp với chiến lược lưu đệm mạnh để phần lớn yêu cầu không bao giờ chạm đến hàm. Nó không tự động là lựa chọn nhanh nhất — “triển khai ra edge” không đồng nghĩa với “nhanh hơn”. Với trang web nội dung, đầu ra tĩnh được kết xuất trước luôn thắng edge SSR về TTFB, vì không có hàm nào cần khởi động.

Tạo sitemap.xml và robots.txt (SvelteKit sẽ không làm việc này)

Đây là khoảng trống mà hầu hết hướng dẫn SvelteKit bỏ qua và hầu hết cuộc kiểm tra phát hiện ra. SvelteKit không tự động tạo sitemap.xml và cũng không tự động tạo robots.txt — bất kể adapter nào, bất kể bạn kết xuất trước bao nhiêu trang. Một trang web hoàn toàn tĩnh với hàng nghìn trang được kết xuất trước vẫn không có sitemap nếu bạn không tự xây dựng nó.

Mẫu endpoint +server.js

Cách viết sitemap theo đúng lối SvelteKit là một endpoint route trả về XML với Content-Type phù hợp:

// src/routes/sitemap.xml/+server.js
export const prerender = true; // needed on adapter-static

export async function GET() {
  const urls = await getAllUrls(); // from your CMS/content
  const body = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls.map((u) => `  <url><loc>${u}</loc></url>`).join('\n')}
</urlset>`;

  return new Response(body, {
    headers: { 'Content-Type': 'application/xml' },
  });
}

Chiến lược phụ thuộc vào adapter của bạn

Đây là phần liên kết toàn bộ bài viết: chiến lược sitemap của bạn là hệ quả phía sau của lựa chọn adapter.

  • Trên adapter-static, endpoint sitemap cần export const prerender = true để được đưa vào đầu ra tĩnh — không có máy chủ lúc runtime để tạo sitemap theo yêu cầu. Sitemap được tạo sẵn tại thời điểm build, nghĩa là độ mới của nó chỉ bằng lần build gần nhất.
  • Trên adapter Node/serverless/edge, cùng endpoint đó có thể tạo sitemap động theo từng yêu cầu từ CMS hoặc cơ sở dữ liệu — luôn cập nhật, không cần build lại. (Trên adapter edge, hãy nhớ hạn chế fs: lấy URL từ API hoặc binding, không đọc từ đĩa.)

Vì vậy, câu hỏi “sitemap của tôi nên là tĩnh hay động?” không phải một quyết định riêng — câu trả lời phát sinh từ adapter bạn đã chọn.

robots.txt: tệp tĩnh hay endpoint

Có hai lựa chọn. Đặt một tệp robots.txt thuần trong thư mục static/ (tự động được phục vụ tại /robots.txt); đây là lựa chọn đơn giản nhất và phù hợp với hầu hết trang web. Hoặc tạo nó từ endpoint src/routes/robots.txt/+server.js khi bạn cần nội dung khác nhau theo môi trường (chẳng hạn chặn trình thu thập dữ liệu trên staging và cho phép chúng trên production). Dù theo cách nào, đừng chặn gói /_app/ hoặc CSS — điều đó làm hỏng việc kết xuất đối với các công cụ tìm kiếm có thực hiện kết xuất.

Nếu bạn tiếp cận vấn đề này từ góc nhìn rộng hơn về framework hoặc JavaScript SEO, logic “việc kết xuất diễn ra ở đâu và khi nào” tại đây cũng là logic chi phối JavaScript SEO nói chung; bài kiến thức nền tảng SvelteKit trong phần này trình bày các chế độ kết xuất và mẫu metadata mà bài viết này xây dựng dựa trên.

Thêm ghi chú chuyên gia

Ghim trích dẫn chuyên gia

Người mới? Hãy tạo hồ sơ chưa có người xác nhận cho họ tại /admin/experts/ → Ghim trích dẫn chuyên gia trước.