Cách thêm tính năng tìm kiếm vào Astro với Pagefind
Tích hợp full-text search không cần server vào bất kỳ Astro static site nào bằng Pagefind 1.5: cài đặt, Astro integration, Component UI, data attributes, CSP, và deploy.
Bởi Ethan
2.122 từ · 11 phút đọc
Pagefind là lựa chọn phù hợp cho tính năng tìm kiếm trên Astro static site. Nó tạo ra một index được phân mảnh và tải theo nhu cầu ngay trong thư mục dist/ — không cần cấu hình server, không cần tài khoản SaaS, không phát sinh chi phí theo lượt truy vấn. Bạn chỉ cần chạy thêm một lệnh lúc build; Cloudflare Pages, Netlify, và Vercel sẽ phục vụ các file index như static asset thông thường.
Bài viết này hướng dẫn cài Pagefind vào một Astro project từ đầu đến cuối. Nếu bạn đang dùng Lunr hoặc Fuse.js và đang cân nhắc chuyển sang, hãy nhảy đến phần so sánh các giải pháp thay thế, đọc một đoạn, rồi quay lại các bước cài đặt.
Dành cho ai
Các developer dùng Astro và deploy lên Cloudflare Pages, Netlify, hoặc Vercel (hoặc bất kỳ CDN nào phục vụ static file). Cách tiếp cận này phù hợp cho blog, trang tài liệu, và trang marketing. Yêu cầu: Node ≥ 18 và một Astro build đang hoạt động (astro build tạo ra thư mục dist/).
Nếu bạn đang dùng Astro 2 hoặc cũ hơn, lưu ý rằng Component UI bên dưới yêu cầu Astro 3+; cách dùng PagefindUI theo class vẫn hoạt động với các phiên bản cũ.
Môi trường thử nghiệm
| Package | Phiên bản |
|---|---|
| astro | 6.3.7 (2026-05-21) |
| pagefind | 1.5.2 (2026-04-12) |
| @pagefind/component-ui | 1.5.2 |
Node 22.x, pnpm 9.x. Mục tiêu deploy: Cloudflare Pages (static output, không dùng adapter).
Tại sao chọn Pagefind thay vì các giải pháp khác
Ba giải pháp thường được nhắc đến cho static-site search:
Lunr.js / Fuse.js — tìm kiếm phía trình duyệt. Toàn bộ index được tải ngay khi load trang. Với site nhỏ (dưới ~200 trang) cả hai đều ổn. Với site lớn hơn, bạn đang gửi hàng megabyte JSON đến mọi người dùng ghé qua trang có widget tìm kiếm. Kích thước index tỉ lệ thuận với lượng nội dung.
Algolia — dịch vụ hosted. Nhanh, UI tốt, trải nghiệm developer đáng khen. Miễn phí chỉ dành cho dự án open-source (DocSearch). Gói trả phí tính tiền theo thao tác và yêu cầu tài khoản, API key, và đồng bộ index mỗi lần deploy.
Pagefind — static, phân mảnh, chạy bằng WASM. Index nằm trong dist/pagefind/. Khi truy vấn, trình duyệt chỉ tải các chunk khớp với vài ký tự đầu tiên của từ khóa. Một site 10.000 trang không làm nó chậm lại như cách một index tải toàn bộ từ đầu. Không cần tài khoản; các file chỉ đơn giản được deploy cùng với HTML của bạn.
Chọn Pagefind nếu bạn muốn tìm kiếm mở rộng được tới vài trăm trang mà không cần lo đến hạ tầng. Phần còn lại của bài viết này hướng dẫn cách làm.
Bước 1: Cài đặt Pagefind
pnpm add -D pagefind @pagefind/component-ui
pagefind là CLI dùng để build index. @pagefind/component-ui cung cấp các custom element <pagefind-modal-trigger> và <pagefind-modal> được giới thiệu trong Pagefind 1.5. Nếu bạn vẫn dùng Pagefind < 1.5, bỏ qua package thứ hai và dùng class PagefindUI phiên bản cũ.
Lỗi thường gặp: pnpm add sẽ đặt cả hai vào devDependencies. Đừng chuyển chúng sang dependencies — index builder là công cụ build-time; nó không có runtime trong production.
Bước 2: Kết nối Pagefind vào Astro build
Bạn có hai cách. Chọn một.
Cách A: npm script (đơn giản hơn)
Cập nhật package.json:
{
"scripts": {
"build": "astro build && npx pagefind --site dist"
}
}
Lệnh này chạy Pagefind sau khi Astro ghi xong dist/. Cờ --site dist cho Pagefind biết nơi tìm HTML để đánh index. Nó sẽ ghi index vào dist/pagefind/.
Lỗi thường gặp: nếu thư mục output của bạn không phải dist/, hãy điều chỉnh --site cho phù hợp. Kiểm tra astro.config.mjs xem có override outDir không.
Cách B: Astro integration (kiểm soát nhiều hơn)
Dùng Node API của Pagefind trong hook astro:build:done. Cách này giúp bạn lấy thư mục build lúc runtime và tránh hardcode dist/:
// astro.config.ts
import { defineConfig } from 'astro/config';
import type { AstroIntegration } from 'astro';
function pagefindIntegration(): AstroIntegration {
return {
name: 'pagefind',
hooks: {
'astro:build:done': async ({ dir }) => {
const { createIndex, writeFiles } = await import('pagefind');
const { index } = await createIndex({});
await index!.addDirectory({ path: dir.pathname });
await writeFiles(index!, { outputPath: new URL('pagefind/', dir).pathname });
},
},
};
}
export default defineConfig({
integrations: [pagefindIntegration()],
});
Lỗi thường gặp: dir.pathname trên Windows tạo ra đường dẫn bắt đầu bằng /C:/.... Pagefind xử lý được điều này, nhưng nếu bạn gặp lỗi phân giải đường dẫn trên Windows, hãy dùng fileURLToPath(dir) từ 'node:url'.
Bước 3: Giới hạn phạm vi Pagefind đánh index
Mặc định Pagefind đánh index tất cả text node tìm thấy trong HTML — bao gồm nav, footer, sidebar, banner cookie, tức mọi thứ bạn không muốn xuất hiện trong kết quả tìm kiếm.
Thêm data-pagefind-body vào element nội dung chính để giới hạn chỉ đánh index trong cây con đó:
<!-- src/layouts/ArticleLayout.astro -->
<article data-pagefind-body>
<slot />
</article>
Những element nằm ngoài data-pagefind-body sẽ bị bỏ qua. Những element bên trong mà bạn vẫn muốn loại trừ có thể được đánh dấu bằng data-pagefind-ignore:
<aside data-pagefind-ignore>
Sidebar bài viết liên quan — không xuất hiện trong tìm kiếm
</aside>
Nếu không tìm thấy element nào có data-pagefind-body trong một trang, Pagefind sẽ fallback về việc đánh index toàn bộ <body>. Nên opt-in tường minh.
Lỗi thường gặp: nếu kết quả tìm kiếm hiển thị cả text của nav hoặc footer, bạn chưa thêm data-pagefind-body. Kiểm tra HTML layout bằng View Source — nếu element bọc <slot /> chưa có attribute này, hãy thêm vào.
Bước 4: Thêm giao diện tìm kiếm
Pagefind 1.5 đi kèm <pagefind-modal-trigger> và <pagefind-modal> dưới dạng custom element. Đặt chúng vào layout:
<!-- src/layouts/BaseLayout.astro -->
---
import '@pagefind/component-ui';
---
<html>
<head>
<meta charset="utf-8" />
<title>Trang của bạn</title>
</head>
<body>
<header>
<nav><!-- các link nav --></nav>
<pagefind-modal-trigger>Tìm kiếm</pagefind-modal-trigger>
<pagefind-modal></pagefind-modal>
</header>
<slot />
</body>
</html>
<pagefind-modal-trigger> render một button mở modal khi click. <pagefind-modal> là overlay tìm kiếm. Cả hai element được định nghĩa bởi script custom element đi kèm với @pagefind/component-ui.
Tùy chỉnh giao diện: component UI dùng shadow DOM, nên CSS toàn cục sẽ không ảnh hưởng vào bên trong modal. Dùng các CSS custom property --pagefind-ui-* để tùy chỉnh:
:root {
--pagefind-ui-scale: 1;
--pagefind-ui-primary: #1d4ed8;
--pagefind-ui-text: #1e293b;
--pagefind-ui-background: #ffffff;
--pagefind-ui-border: #e2e8f0;
--pagefind-ui-border-radius: 0.5rem;
--pagefind-ui-font: inherit;
}
Phiên bản cũ (Pagefind < 1.5): nếu bạn chưa sẵn sàng nâng cấp lên Component UI, class PagefindUI vẫn hoạt động. Load bundle và khởi tạo:
<link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
<script src="/pagefind/pagefind-ui.js"></script>
<div id="search"></div>
<script>
window.addEventListener('DOMContentLoaded', () => {
new PagefindUI({ element: '#search', showSubResults: true });
});
</script>
Bước 5: Kiểm tra ở local
Build và preview:
pnpm build # chạy astro build + pagefind index
pnpm preview # hoặc: npx pagefind --site dist --serve
astro preview phục vụ dist/ dưới dạng static, bao gồm cả thư mục dist/pagefind/ mà Pagefind đã tạo. Mở trang web, click nút tìm kiếm, nhập một từ xuất hiện trong bài viết. Kết quả sẽ hiện ra trong khoảng ~150ms.
Nếu bạn muốn kiểm tra indexing riêng biệt trước khi kết nối UI:
npx pagefind --site dist --serve
Lệnh này khởi động một HTTP server đơn giản tại http://localhost:1414 phục vụ output đã build. Mở trình duyệt vào đó và xác nhận widget tìm kiếm load được.
Lỗi thường gặp: nếu modal mở nhưng không có kết quả, kiểm tra tab Network của trình duyệt xem có request nào đến /pagefind/pagefind-index-*.pf_meta không. Nếu các file đó 404, nghĩa là index chưa được ghi vào dist/pagefind/ — khả năng cao là script build chưa chạy Pagefind sau astro build. Kiểm tra lại script build trong package.json xem có lệnh && để nối không.
Bước 6: Deploy
Không cần cấu hình thêm. Output của Pagefind là các static file thông thường — HTML, JSON, và WASM module trong dist/pagefind/. CDN của bạn phục vụ chúng như bình thường.
Cloudflare Pages: build command pnpm build, output directory dist. Thư mục con dist/pagefind/ được deploy tự động cùng với HTML.
Netlify: tương tự pnpm build + dist. Netlify copy toàn bộ dist/ lên CDN.
Vercel: Framework preset Astro, build command pnpm build, output directory dist. Không cần thay đổi gì.
Nếu bạn chưa chọn nền tảng deploy, xem so sánh nền tảng deploy cho static site để so sánh chi tiết Cloudflare Pages, Netlify và Vercel về bandwidth, số lượng build miễn phí và phạm vi CDN.
Lỗi thường gặp: nếu tìm kiếm hoạt động ở local nhưng 404 trên production, kiểm tra lại xem lệnh build trong CI/CD có chạy Pagefind không (chứ không chỉ astro build). Một số nền tảng cho phép override build command trong vercel.json, netlify.toml, hoặc wrangler.jsonc — hãy đảm bảo phần override khớp với script build trong package.json ở local.
Lưu ý
i18n và nhiều ngôn ngữ
Mặc định Pagefind tạo ra một index duy nhất. Các trang thuộc các ngôn ngữ khác nhau dùng chung index đó trừ khi bạn cấu hình build riêng cho từng ngôn ngữ.
Để tách index, chạy Pagefind hai lần với cờ --language:
npx pagefind --site dist --glob "en/**/*.html" --language en
npx pagefind --site dist --glob "vi/**/*.html" --language vi
Component UI nhận biết attribute lang trên thẻ <html> và tải đúng index. Đặt nó trong layout của bạn:
<html lang={locale}>
Mức độ hỗ trợ ngôn ngữ: ~60% ngôn ngữ có bản dịch UI đầy đủ trong catalog của Pagefind. Tiếng Việt đã được hỗ trợ. Các ngôn ngữ CJK (Trung, Nhật, Hàn) yêu cầu binary Pagefind mở rộng (pagefind-extended) để phân tách từ đúng cách — binary tiêu chuẩn xử lý text CJK như một token duy nhất.
Nếu bạn đang xây dựng site song ngữ EN+VI, xem hướng dẫn xây dựng Astro site đa ngôn ngữ để tìm hiểu toàn bộ locale routing, language switcher và hreflang — phối hợp tốt với index riêng theo ngôn ngữ của Pagefind.
CSP headers
Pagefind tải một WASM module và một web worker lúc runtime. Thêm các CSP directive sau vào bất kỳ trang nào có widget tìm kiếm:
Content-Security-Policy: script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:;
wasm-unsafe-eval cho phép WASM của Pagefind compile. worker-src blob: cho phép search worker được spawn từ blob URL. Nếu thiếu những directive này, Pagefind sẽ thất bại trong môi trường có CSP chặt mà không báo lỗi gì.
Trên Cloudflare Pages, thêm header vào public/_headers:
/*
Content-Security-Policy: script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:;
Các issue đang mở cần theo dõi
- #1157 — Lỗi compile WASM trên Safari trong một số môi trường. Có vẻ phụ thuộc vào cấu hình cụ thể; hầu hết deployment không bị ảnh hưởng. Xem thread đó nếu tìm kiếm bị lỗi trên Safari nhưng bình thường ở các trình duyệt khác.
- #1126 — Component UI bị duplicate instance khi re-render trong các SPA-like transition. Mức độ thấp với static site; đáng chú ý nếu bạn dùng
<ViewTransitions />của Astro. Kiểm tra trạng thái trước khi đưa lên production.
Khi Pagefind không còn đủ dùng
Pagefind mở rộng tốt đến 10.000+ trang nhờ cơ chế phân mảnh index. Giới hạn nằm ở độ phức tạp của truy vấn: Pagefind là keyword index, không phải semantic search engine. Fuzzy matching và tìm kiếm substring hoạt động tốt; ranking dựa trên ML thì không.
Đến lúc đó, Algolia DocSearch (miễn phí cho dự án open-source) là bước tiếp theo hợp lý. Xem so sánh Search-as-a-Service để đánh giá đầy đủ các phương án hosted và tự host bao gồm Typesense và Meilisearch.
Tài liệu tham khảo
- Tài liệu Pagefind — tham chiếu cấu hình, Node API, custom attributes
- Pagefind Component UI — các tùy chọn
<pagefind-modal>và<pagefind-modal-trigger>, CSS custom properties - Pagefind Node API —
createIndex,addDirectory,writeFiles - Pagefind changelog — v1.5.0 — release notes của Component UI
- Hướng dẫn tích hợp Astro — hook
astro:build:done, tham sốdir - Pagefind i18n — index theo ngôn ngữ, cờ
--language, binary CJK mở rộng - Pagefind GitHub #1157 — theo dõi lỗi Safari WASM
- Pagefind GitHub #1126 — duplicate instance của Component UI khi transition