不用买服务器、不用备案、也不用会写后端,用 Astro + Pagefind + Cloudflare Pages 就能搭出带全文搜索的个人网站,一分钱不花。这篇从零讲起,跟着一步步来就行。
这套方案好在哪
很多人一听“搭个人网站”就觉得得买服务器、搞备案、写后端,其实纯静态网站这些都不需要。
- 不用服务器:页面都是提前生成好的 HTML,丢到免费托管平台就能跑,没有后端程序要养。
- 不用花钱:框架是开源的,托管平台有免费额度,还会自动送你一个
<项目名>.pages.dev子域名,域名都不用买就能先上线。 - 不用给访客背 JS 包袱:默认只下发纯 HTML,打开快,流量省。
- 自带全文搜索:用 Pagefind,构建的时候顺手把全站文字做成索引,访客在浏览器里就能搜,不靠任何搜索服务器。
这套东西适合个人博客、作品集、简历页、知识库这类“展示为主、内容不大动”的站。反过来,如果你要的是登录、评论、实时数据这种,那静态站就不合适了。
做完大概长这样——深色主题(默认):
点右上角的按钮还能一键切成浅色主题:
技术选型(为什么是这几样)
| 组件 | 选择 | 为什么 |
|---|---|---|
| 框架 | Astro | 用 Markdown 写文章很顺手;内容集合帮你把文章的标题、日期这些元信息管起来;默认产出的就是零 JS 静态页面 |
| 搜索 | Pagefind | 纯静态的全文搜索:构建时生成索引,浏览器端检索,没有服务器 |
| 托管 | Cloudflare Pages | 免费额度够用、自带全球 CDN、还能接 Git 自动部署;国内打开速度一般,但免费站已经很值了 |
| 样式 | 手写 CSS | 静态站的样式不复杂,用几个 CSS 变量当“设计 token”就够,不必上重型框架 |
提前提醒一个坑:纯静态站别装 Cloudflare 的 Astro 适配器(
@astrojs/cloudflare),原因见文末「常见坑 1」。
第 1 步:准备本地环境
其实只要装两样东西:
- Node.js——装 LTS 版就行,去官网下个安装包
- 一个代码编辑器——VS Code 挺好,你用别的顺手的也完全没问题
装完在终端里敲两下,确认没装错:
node -v # 能打印出版本号就是装好了
npm -v # npm 会跟着 Node 一起装
第 2 步:创建项目
用 Astro 官方的脚手架来建。先 cd 进一个你想放项目的空文件夹,然后跑(把 my-site 换成你想起的名字):
npm create astro@latest my-site
中途会问几个问题,新手照下面选就行(没列到的直接回车用默认):
- How would you like to start? →
Empty(空白模板,最干净) - Install dependencies? →
Yes - Initialize a git repository? →
Yes(后面接自动部署要用) - TypeScript →
Strict
这些提问的文案会随 Astro 版本变,你只要对上大意就行,不用抠字眼。
接着进到项目里,把本地预览跑起来:
cd my-site
npm run dev
浏览器打开 http://localhost:4321,能看到页面、也没有报错,说明环境没问题,可以继续了。
第 3 步:先看懂项目结构
先认识下这个项目最后会长成什么样(src/ 下的 content/、layouts/、styles/ 这些子目录,空白模板一开始是没有的,是你后面几步自己加出来的):
my-site/
├── src/
│ ├── content/ # 文章放这(博客/项目等分目录)
│ ├── content.config.ts # 内容集合的格式定义(schema,见第 4 步)
│ ├── layouts/ # 页面外壳(头部/页脚/主题)
│ ├── pages/ # 一个 .astro 文件 = 一个页面
│ │ ├── index.astro # 首页
│ │ └── blog/ # 博客相关页面
│ └── styles/ # 全局样式
├── astro.config.mjs # Astro 配置(站点地址等)
└── package.json # 依赖与命令
心里记着一条就行:pages/ 管“有哪些页面”,content/ 管“文章放哪”。把 Markdown 丢进 content/blog/,它就成了能写的文章。
第 4 步:告诉 Astro 你的文章长什么样
去 src/content.config.ts 里定义一下文章格式(这玩意儿叫 schema)。拿博客举例,规定每篇文章得有标题、日期,描述和标签可有可无:
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
// 博客文章集合:扫描 src/content/blog/ 下所有 .md
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
schema: z.object({
title: z.string(), // 标题(必填)
description: z.string().optional(), // 摘要(可选)
date: z.coerce.date(), // 发布日期(必填)
updated: z.coerce.date().optional(), // 更新日期(可选)
tags: z.array(z.string()).default([]), // 标签(默认空)
draft: z.boolean().default(false), // 草稿标记(默认发布)
}),
});
export const collections = { blog }; // 用到的集合都要在这登记
再写一篇测试文章试试,src/content/blog/hello.md:
---
title: 你好,世界
description: 我的第一篇文章
date: 2026-09-04
tags: [随笔]
---
这是我的第一篇文章。欢迎来到我的网站。
第 5 步:建列表页和详情页
要让文章能被点开,需要两个页面:一个列表页(罗列文章标题),一个详情页(点进去看正文)。
下面贴的是核心逻辑(取数据 + 循环渲染)。一个完整页面还要有站点自己的头部、页脚这些外壳,套进你项目里现成的布局就行,这里先省略,免得干扰你理解重点。
列表页 src/pages/blog/index.astro 的核心:
---
// 取出所有非草稿文章,按日期倒序
import { getCollection } from 'astro:content';
const posts = (await getCollection('blog', ({ data }) => !data.draft))
.sort((a, b) => b.data.date - a.data.date);
---
{posts.length > 0 ? (
<ul>
{posts.map((post) => (
<li><a href={`/blog/${post.id}/`}>{post.data.title}</a></li>
))}
</ul>
) : (
<p>还没有文章。</p>
)}
详情页 src/pages/blog/[slug].astro 的核心——[slug] 是 Astro 的动态路由,一篇 Markdown 就对应一个网址:
---
import { getCollection, render } from 'astro:content';
// 为每一篇文章生成一个静态详情页
export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => !data.draft);
return posts.map((post) => ({ params: { slug: post.id }, props: { post } }));
}
const { post } = Astro.props;
const { Content } = await render(post);
---
<h1>{post.data.title}</h1>
<time>{post.data.date.toISOString().slice(0, 10)}</time>
{post.data.tags.map((t) => <span>{t}</span>)}
<Content /> {/* 文章正文会自动渲染到这里 */}
存盘后 npm run dev 刷新一下,/blog/ 就能看到文章列表,点进去是正文。
第 6 步:加全文搜索(Pagefind)
Pagefind 的思路是:构建的时候扫一遍生成好的 HTML,把文字建成索引,访客在浏览器里搜,全程不碰后端。
装依赖:
npm install pagefind
再往 package.json 里加一条 postbuild 命令——这样每次 npm run build 建完站,会自动顺手跑 Pagefind 生成索引:
"scripts": {
"build": "astro build",
"postbuild": "pagefind --site dist"
}
这里容易踩坑,重点看:
dist是 Astro 默认的构建输出目录,Pagefind 就是扫它、把索引生成到dist/pagefind/里。
接着往页面里放搜索框。以官方默认 UI 为例,在你想放搜索的那个页面(比如 src/pages/search.astro)里加:
<link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
<script src="/pagefind/pagefind-ui.js" defer></script>
<div id="search"></div>
<script>
window.addEventListener('DOMContentLoaded', () => {
new PagefindUI({ element: '#search' });
});
</script>
再控制一下什么内容进搜索(这步很关键)。 Pagefind 默认把整张页面都收进索引,导航、页脚这些每页都重复的文字也会混进去,搜出来一堆没用的。正确做法:
- 给正文容器打个
data-pagefind-body,告诉 Pagefind“只索引这一块”。 - 只要全站有任意一个
data-pagefind-body,Pagefind 就会自动跳过没打标记的页面——首页、列表页、搜索页这些导航类页面就都被排除了。 - 正文里还有个别不想收的局部(比如“返回列表”的链接、日期那行 meta),单独打
data-pagefind-ignore。
<article data-pagefind-body> <!-- 只索引正文 -->
<h1>文章标题</h1>
<div class="meta" data-pagefind-ignore>2026-09-04</div> <!-- 排除 meta -->
正文内容……
</article>
怎么在本地试搜索:Pagefind 的索引和搜索框资源,只有 npm run build 之后才生成(落在 dist/pagefind/),而 npm run dev 的开发服务器并不服务 dist/,所以开发模式里搜不到是正常的,别慌。想本地看效果,先 npm run build 再 npm run preview(preview 服务的是构建产物),打开预览地址就能搜了。上线之后一切正常。
上线后的搜索页长这样(输入关键词,纯浏览器端实时检索):
第 7 步:部署上线(Cloudflare Pages)
两种方式任选,推荐第一种,省心、一次配好。
方式 A:接 Git,自动部署
-
先把项目推到 GitHub(私有仓库就行;还没有仓库的话,先在 GitHub 新建一个空的,再
git push上去)。 -
打开 Cloudflare Dashboard,进 Workers & Pages(新版界面可能写作 Workers,入口在侧边栏)→ Create → Pages → Connect to Git。
-
选中你的仓库,构建配置填这几项:
- Framework preset:Astro
- Build command:
npm run build - Build output directory:
dist - (可选)环境变量
NODE_VERSION设成24
-
点 Save and Deploy。等一两分钟,Cloudflare 会给你一个
https://你的项目名.pages.dev的免费子域名,打开就是你的站了。
以后每次 git push,Cloudflare 都会自动重新构建部署——写完文章 → push → 自动上线,全程不花一分钱。
方式 B:命令行直接传(不接 Git)
npx wrangler pages deploy dist --project-name=my-site
适合你本地已经 build 好、想直接传上去、不想折腾 Git 的情况。
想绑自己的域名(可选):去 Pages 项目设置里加自定义域,按提示在 DNS 加一条 CNAME 指到 <项目名>.pages.dev 就行。不绑也没关系,Cloudflare 送的那个免费地址已经能用了。
常见坑(都是我踩过的)
1. 纯静态站别装 Cloudflare 的 Astro 适配器
@astrojs/cloudflare 是给“在 Workers 上跑动态逻辑”准备的。纯静态项目装上反而会拖进来一堆多余的本地模拟,构建变慢还可能报些莫名其妙的错。纯静态根本用不着它,把构建产物直接托管给 Pages 就行。
2. Astro 的样式是有作用域范围的
Astro 每个 .astro 文件里的 <style> 默认只作用于当前文件(它会自动加随机属性做隔离)。所以用普通写法,是匹配不到 JS 动态生成的内容的(比如 Pagefind 搜出来的结果 DOM)。要让样式管到动态内容,得用 :global() 把选择器包起来。
3. 记得限定搜索范围
不标 data-pagefind-body 的话,Pagefind 会把每页的导航、页脚全收进索引。记得给正文打标记(见第 6 步)。另外注意:当全站还没有任何内容页时(比如刚建站、一篇文章都还没写),Pagefind 会退回去索引整页——等你写了第一篇带 data-pagefind-body 的文章,导航页自然就被排除掉了。
4. 列表没内容时记得兜底
文章还没写、集合是空的时候,直接去遍历会渲染成一片空白。用条件渲染判断一下再输出就行(写法看第 5 步列表页里那个 posts.length > 0 ? … : …)。
5. Pagefind 的中文搜索,能用但别指望完美
Pagefind 对中文的支持不如英文:它不做词根折叠(搜“部署”不会顺带匹配到“部署中”),个别词还可能因为浏览器分词和建索引时分词对不上而搜不到。这是 Pagefind 的老毛病,不是你配错了。英文内容它表现很好;要是你的中文站特别看重模糊搜索,就得自己再接一套中文分词方案。一般个人博客,能做到“按标题、按关键词搜到主要文章”就够用了。
小结
到这儿,你就白捡了一个能搜全文的个人网站:
- 内容:写 Markdown,丢进
src/content/ - 搜索:构建时自动生成索引,访客在浏览器里就能搜
- 部署:push 一下自动上线,全程零成本
- 扩展:框架的内容集合是开放的,以后想加作品集、摄影、知识库,照葫芦画瓢再开几个集合就行
先别想太远,把它当成“一个带全文搜索的静态博客”,往里填东西就对了。碰到问题,多翻翻 Astro 文档、Pagefind 文档 和 Cloudflare Pages 文档。
评论