不用买服务器、不用备案、也不用会写后端,用 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 步:准备本地环境

其实只要装两样东西:

  1. Node.js——装 LTS 版就行,去官网下个安装包
  2. 一个代码编辑器——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,自动部署

  1. 先把项目推到 GitHub(私有仓库就行;还没有仓库的话,先在 GitHub 新建一个空的,再 git push 上去)。

  2. 打开 Cloudflare Dashboard,进 Workers & Pages(新版界面可能写作 Workers,入口在侧边栏)→ Create → Pages → Connect to Git。

  3. 选中你的仓库,构建配置填这几项:

    • Framework preset:Astro
    • Build command:npm run build
    • Build output directory:dist
    • (可选)环境变量 NODE_VERSION 设成 24
  4. 点 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 文档。

参考文献

评论

评论加载中…