<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Junius 的博客</title><description>Junius 的全站订阅——技术文章 / 配方 / 摄影（可凭分类标签筛选板块）</description><link>https://blog.junius.top/</link><language>zh-cn</language><item><title>经典蛋黄莲蓉月饼</title><link>https://blog.junius.top/recipes/lotus-paste-mooncake/</link><guid isPermaLink="true">https://blog.junius.top/recipes/lotus-paste-mooncake/</guid><description>经典蛋黄莲蓉月饼方子——转化糖浆 + 花生油 + 碱水 + 中筋粉做柔软饼皮，裹白莲蓉与咸蛋黄，165°C 烤 20 分钟，密封回油 2-3 天。</description><pubDate>Tue, 08 Sep 2026 11:33:00 GMT</pubDate><content:encoded>&lt;p&gt;非常经典的广式月饼——蛋黄莲蓉月饼。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;转化糖浆 + 花生油 + 碱水 + 中筋粉做柔软饼皮，裹白莲蓉和咸蛋黄。皮馅 3:7，165°C 烤 20 分钟，密封回油 2-3 天后吃，饼皮回软、蛋黄流油。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;饼皮&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;原料&lt;/th&gt;
&lt;th&gt;用量&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;转化糖浆&lt;/td&gt;
&lt;td&gt;110g&lt;/td&gt;
&lt;td&gt;广式月饼核心糖浆，提供回油&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;花生油&lt;/td&gt;
&lt;td&gt;40g&lt;/td&gt;
&lt;td&gt;无味植物油，忌菜籽油/芝麻油等重味油&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;碱水&lt;/td&gt;
&lt;td&gt;2g&lt;/td&gt;
&lt;td&gt;约 1/4 小勺；家用：食用碱 1 : 水 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;奶粉&lt;/td&gt;
&lt;td&gt;7g&lt;/td&gt;
&lt;td&gt;一大勺约 15ml；&lt;strong&gt;无奶粉可等量换中筋面粉&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中筋面粉&lt;/td&gt;
&lt;td&gt;150g&lt;/td&gt;
&lt;td&gt;过筛使用&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;馅料&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;原料&lt;/th&gt;
&lt;th&gt;用量&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;白莲蓉&lt;/td&gt;
&lt;td&gt;500g&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;咸蛋黄&lt;/td&gt;
&lt;td&gt;20 个&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;真空咸蛋黄把不规则边剪掉可变圆。可白酒清洗去腥，180°C 烤约 5 分钟到&lt;strong&gt;微微出油&lt;/strong&gt;就停；烤到发白就过了，包时易碎。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;做法&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;饼皮：碱水倒入糖浆+花生油混合液，搅至完全乳化（先分离后均匀发白）→ 加奶粉拌匀 → 筛入中筋粉，切拌成团（勿过度出筋）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;松弛&lt;/strong&gt;：装保鲜袋常温 30 分钟 ~ 1 小时&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;馅料&lt;/strong&gt;：咸蛋黄 180°C 烤 5 分钟微微出油；白莲蓉揉匀&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;包&lt;/strong&gt;：皮馅比 3:7（皮 15g + 馅 35g/50g 月饼）；蛋黄&lt;strong&gt;横放&lt;/strong&gt;（竖放饼皮易破）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;压模&lt;/strong&gt;：顶部拍面粉防粘；花纹简单易脱模&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;烤&lt;/strong&gt;：165°C 约 20 分钟，中途第 5 分钟取刷薄全蛋液&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回油&lt;/strong&gt;：出炉色浅质硬；密封 2-3 天回油变深变软、皮馅蛋黄味相融&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;重量换算速查&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;成品&lt;/th&gt;
&lt;th&gt;皮&lt;/th&gt;
&lt;th&gt;馅+蛋黄&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;50g&lt;/td&gt;
&lt;td&gt;15g&lt;/td&gt;
&lt;td&gt;35g&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;75g&lt;/td&gt;
&lt;td&gt;23g&lt;/td&gt;
&lt;td&gt;52g&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100g&lt;/td&gt;
&lt;td&gt;30g&lt;/td&gt;
&lt;td&gt;70g&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;保存&lt;/h2&gt;
&lt;p&gt;阴凉密封 7-10 天；&lt;strong&gt;勿放冰箱&lt;/strong&gt;（冷凝水回潮变软）；回油后再吃风味最佳。&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul class=&quot;plain&quot;&gt;
  &lt;li&gt;🥮 &lt;strong&gt;不要自己炒莲蓉馅&lt;/strong&gt;——新手炒馅含水量难控、失败率高，用市售白莲蓉&lt;/li&gt;
  &lt;li&gt;🥮 烘烤时间按自家烤箱调整（18/20/25 分钟）&lt;/li&gt;
  &lt;li&gt;🥮 蛋液要薄刷，厚了糊花纹&lt;/li&gt;
&lt;/ul&gt;</content:encoded><lastmod>2026-09-08T11:33:00.000Z</lastmod><category>配方</category><category>月饼</category><category>中式点心</category><category>中秋</category></item><item><title>零成本自建网站访问统计（Umami + NAS）</title><link>https://blog.junius.top/blog/2026/09/08/2/</link><guid isPermaLink="true">https://blog.junius.top/blog/2026/09/08/2/</guid><description>想给博客看访问数据、又在乎数据自主？用 Umami 跑在 NAS 上，数据落本地硬盘，后台面板现成。实际部署 + 页面底部显示访问数。</description><pubDate>Tue, 08 Sep 2026 10:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;这套东西能给你什么&lt;/h2&gt;
&lt;p&gt;装完之后你会得到两样：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;后台分析面板&lt;/strong&gt;：PV/UV、来源、国家、设备、实时在线，都有现成图表&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前台数字&lt;/strong&gt;：在自己网站底部显示「本站总浏览量 X 次」这种计数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据全部存在你自己的设备上，不经第三方。Umami 不用 cookie，所以也不用给网站加隐私弹窗。&lt;/p&gt;
&lt;p&gt;一个 Umami 实例能管多个网站，每个站分一个 ID，各贴一段 JS 就行。&lt;/p&gt;
&lt;p&gt;做完之后，网站底部会显示这样一行实时计数（数字到秒在跳）：&lt;/p&gt;
&lt;img src=&quot;https://img.junius.top/blog-posts/umami-footer.webp&quot; alt=&quot;页脚访问统计&quot; width=&quot;900&quot; height=&quot;143&quot; /&gt;
&lt;h2&gt;整体结构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;访客浏览器
   ↓ 加载 Umami 统计脚本
统计域名（比如 stats.你的域名.com）
   ↓ Cloudflare Tunnel 反代
NAS：Umami 容器 + PostgreSQL（数据在硬盘上）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cloudflare 在这里只当网络通道，不碰你的数据内容。&lt;/p&gt;
&lt;h2&gt;第 1 步：在 NAS 上跑起 Umami&lt;/h2&gt;
&lt;p&gt;我用 Docker Compose 一次起两个容器：Umami 本体 + PostgreSQL 数据库。&lt;/p&gt;
&lt;p&gt;新建一个 &lt;code&gt;docker-compose.yml&lt;/code&gt;，内容类似这样（按你的实际情况改端口和密码）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  umami:
    image: ghcr.io/umami-software/umami:postgresql-latest
    ports:
      - &amp;quot;36439:3000&amp;quot;   # 左边是宿主机（NAS）端口，右边是容器内端口
    environment:
      DATABASE_URL: postgresql://umami:你的密码@db:5432/umami
      DATABASE_TYPE: postgresql
      APP_SECRET: 换成随机字符串
    depends_on:
      db:
        condition: service_healthy   # 等数据库就绪再启动 Umami
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: umami
      POSTGRES_USER: umami
      POSTGRES_PASSWORD: 你的密码
    volumes:
      - ./data:/var/lib/postgresql/data
    healthcheck:
      test: [&amp;quot;CMD-SHELL&amp;quot;, &amp;quot;pg_isready -U umami&amp;quot;]
      interval: 10s
      timeout: 5s
      retries: 5
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;healthcheck&lt;/code&gt; 那段建议留着。不加的话，PostgreSQL 还没完全启动 Umami 就去连，容易启动失败或者连上又断——重启 NAS 后尤其容易遇到。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 NAS 的 Docker 管理界面里粘贴这段（各家 NAS 都有&amp;quot;项目 / Compose&amp;quot;这种入口），启动。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;端口别记混&lt;/strong&gt;：上面把容器的 &lt;code&gt;3000&lt;/code&gt; 映射成了 &lt;code&gt;36439&lt;/code&gt;，所以内网访问要开 &lt;code&gt;http://NAS的IP:36439&lt;/code&gt;。直接按 3000 打开是打不开的，那是容器内部的端口。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;启动后浏览器打开这个地址，用默认账号登录（Umami 默认 &lt;code&gt;admin&lt;/code&gt; / &lt;code&gt;umami&lt;/code&gt;），&lt;strong&gt;进去第一件事是改密码&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;第 2 步：让它能从外网访问&lt;/h2&gt;
&lt;p&gt;家里或内网的 NAS 没有公网 IP，用 Cloudflare Tunnel 穿出来：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;进 Cloudflare Zero Trust 后台，创建一个 Tunnel，会拿到一串 Token&lt;/li&gt;
&lt;li&gt;在 NAS 上跑 &lt;code&gt;cloudflared&lt;/code&gt;（Docker 或装二进制都行），把 Token 配上&lt;/li&gt;
&lt;li&gt;给这个 Tunnel 加一条 Public Hostname，比如 &lt;code&gt;stats.你的域名.com&lt;/code&gt;，回源指向 &lt;code&gt;http://localhost:36439&lt;/code&gt;（或容器名 &lt;code&gt;http://umami:3000&lt;/code&gt;）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;配好之后，你从任何地方打开 &lt;code&gt;stats.你的域名.com&lt;/code&gt; 就能进后台了，而且是 HTTPS，不用自己申请证书。&lt;/p&gt;
&lt;h2&gt;第 3 步：给网站加上统计脚本&lt;/h2&gt;
&lt;p&gt;在 Umami 后台添加你的网站，会生成一段 tracking 代码和一个 &lt;strong&gt;website ID&lt;/strong&gt;。把这段脚本贴到网站的 &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; 里：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;script defer src=&amp;quot;https://stats.你的域名.com/script.js&amp;quot; data-website-id=&amp;quot;你的websiteID&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;静态博客一般贴在公共布局文件里（我的是 &lt;code&gt;BaseLayout.astro&lt;/code&gt;），全站页面就都带上了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;脚本文件名有两种：新版本是 &lt;code&gt;script.js&lt;/code&gt;，老一点的版本叫 &lt;code&gt;umami.js&lt;/code&gt;，功能一样。不确定就直接复制后台给你生成的那段代码，别自己拼。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;等几分钟回后台看，有访客进来就能看到数据了。&lt;/p&gt;
&lt;h2&gt;第 4 步：在页面底部显示访问数&lt;/h2&gt;
&lt;p&gt;后台有了，前台也想显示个总数。Umami 支持&amp;quot;公开分享&amp;quot;——在后台给网站开一个分享链接，会得到一个 &lt;strong&gt;share ID&lt;/strong&gt;，靠它就能调接口拿数字。&lt;/p&gt;
&lt;p&gt;流程是两步：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① 用 share ID 换一个临时 token&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;GET https://stats.你的域名.com/api/share/{shareId}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回里有个 &lt;code&gt;token&lt;/code&gt; 和 &lt;code&gt;websiteId&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 带这个 token 去查统计&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;GET https://stats.你的域名.com/api/websites/{websiteId}/stats?startAt=0&amp;amp;endAt=当前时间戳
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请求头带上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x-umami-share-token: 刚才换到的 token
x-umami-share-context: share
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回的是访问量和访客数，前端渲染成「本站总浏览量 X 次」就行。&lt;/p&gt;
&lt;p&gt;我博客上是个 Astro 组件做这件事：进页面时先显示上次缓存在 localStorage 的数字（不等接口），后台同时去拉最新值，拉到再更新。这样访客不会看到空白或者等半天才出数字。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;分享 ID 本来就是只读的，拿它只能查统计，改不了数据也进不了后台，所以放在前端代码里没问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;第 5 步：记得备份&lt;/h2&gt;
&lt;p&gt;数据都在 PostgreSQL 里，定期导出一下比较稳妥。我是每天跑一次 &lt;code&gt;pg_dump&lt;/code&gt; 并保留 7 天：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pg_dump -U umami umami &amp;gt; backup_$(date +%F).sql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;丢进定时任务里就行。NAS 一般都有&amp;quot;计划任务&amp;quot;功能，配个每天凌晨执行。&lt;/p&gt;
&lt;h2&gt;关于速度&lt;/h2&gt;
&lt;p&gt;这套架构下，访客的请求路径是：浏览器 → Cloudflare 边缘 → Tunnel → 你的 NAS。国内访问因为要绕一圈，加载统计脚本会比纯 CDN 慢一些，但对统计这种后台数据来说影响不大（它不阻塞页面显示）。&lt;/p&gt;
&lt;p&gt;如果你特别在意国内打开速度，可以再租个国内轻量云做反代，或者用 DNS 分线路解析——不过那是后话了，先用起来再说。&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;写在最后&lt;/h2&gt;
&lt;p&gt;自己搭统计最有价值的地方不是省多少钱，而是&lt;strong&gt;数据在自己手上&lt;/strong&gt;：想查什么自己写 SQL，想导出去随时能导，也不用担心哪天第三方服务关停就没了。&lt;/p&gt;
&lt;p&gt;装好之后我每天会瞄一眼后台的实时在线，看到有人正在读自己写的东西，那种感觉还挺实在的。你要是也想给自己的站点加一套，可以照着这个流程走一遍。&lt;/p&gt;</content:encoded><lastmod>2026-09-08T10:00:00.000Z</lastmod><category>博客</category><category>Umami</category><category>访问统计</category><category>NAS</category><category>自托管</category></item><item><title>零成本给静态博客加评论区（Twikoo + Cloudflare）</title><link>https://blog.junius.top/blog/2026/09/08/1/</link><guid isPermaLink="true">https://blog.junius.top/blog/2026/09/08/1/</guid><description>想给静态博客加评论区又不想养服务器？用 Twikoo + Cloudflare（Worker + D1）给 Astro 站加一套免费、免登录、数据自管的评论区。实际部署步骤 + 踩坑。</description><pubDate>Tue, 08 Sep 2026 04:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;为什么选 Twikoo&lt;/h2&gt;
&lt;p&gt;静态站加评论，说白了就是借个后端存数据，页面上嵌个前端组件。我筛过一轮，最后留下 Twikoo 的原因很实际：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;访客不用注册&lt;/strong&gt;。填昵称 + 邮箱就能评，不会劝退读者（Giscus 和 utterances 都要 GitHub 账号，对普通读者太重）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;和我的博客在同一张网&lt;/strong&gt;。博客跑在 Cloudflare Pages，评论后端也在 Cloudflare，不会出现页面秒开但评论区一直转圈&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据在手&lt;/strong&gt;。评论存在自己的 D1 库里，想查想导都方便&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他方案各有各的问题：Waline 放 NAS 要绕 Cloudflare Tunnel，慢；Disqus 在大陆基本打不开。这篇不打算做选型对比，只讲怎么落地上线。&lt;/p&gt;
&lt;h2&gt;整体结构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;博客页面（Cloudflare Pages）
        │  嵌一段前端脚本
        ▼
   Twikoo 前端组件
        │  发请求
        ▼
 Cloudflare Worker（Twikoo 后端）
        │
        ▼
   D1 数据库（存评论）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;域名这块有个坑，得单独说。&lt;/p&gt;
&lt;p&gt;先看一眼做完的效果。文章底部会出现评论区，访客填昵称邮箱就能留言：&lt;/p&gt;
&lt;img src=&quot;https://img.junius.top/blog-posts/twikoo-comments.webp&quot; alt=&quot;评论区实际效果&quot; width=&quot;900&quot; height=&quot;428&quot; /&gt;
&lt;h2&gt;第 1 步：部署后端&lt;/h2&gt;
&lt;p&gt;Twikoo 官方有现成的 Cloudflare 模板，拉下来就能用，全程大概二十分钟。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① 装 wrangler 并登录&lt;/strong&gt;（Cloudflare 的命令行工具）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install -g wrangler
npx wrangler login   # 会弹浏览器让你授权
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;② 克隆官方模板 + 装依赖&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;git clone https://github.com/twikoojs/twikoo-cloudflare.git
cd twikoo-cloudflare &amp;amp;&amp;amp; npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;③ 清掉 3 个不兼容的包&lt;/strong&gt;。CF 免费版单个 Worker 代码上限 1MiB，这几个包在 Worker 里用不上（Node 兼容问题），清空能压到限制内：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;echo &amp;quot;&amp;quot; &amp;gt; node_modules/jsdom/lib/api.js
echo &amp;quot;&amp;quot; &amp;gt; node_modules/tencentcloud-sdk-nodejs/tencentcloud/index.js
echo &amp;quot;&amp;quot; &amp;gt; node_modules/nodemailer/lib/nodemailer.js
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;第一次部署我忘了这步，bundle 超限直接失败。后面 D1 和 R2 的命令都依赖它，别漏。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;④ 建数据库（存评论）&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx wrangler d1 create twikoo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出里有个 &lt;code&gt;database_id&lt;/code&gt;，填进 &lt;code&gt;wrangler.toml&lt;/code&gt; 替换占位值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;⑤ 建表 + 建存储&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx wrangler d1 execute twikoo --remote --file=./schema.sql
npx wrangler r2 bucket create twikoo   # R2 存评论里的图片，暂时不需要传图也可以跳过
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;建 R2 桶报 &lt;code&gt;code 10042&lt;/code&gt;，是账号还没开通 R2，去后台点一下就行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;⑥ 部署&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx wrangler deploy --minify
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功后会给你一个 &lt;code&gt;https://twikoo-cloudflare.&amp;lt;你的用户名&amp;gt;.workers.dev&lt;/code&gt;。浏览器打开，看到 &lt;code&gt;{&amp;quot;code&amp;quot;:100,&amp;quot;message&amp;quot;:&amp;quot;Twikoo 云函数运行正常...&amp;quot;}&lt;/code&gt; 就是后端活了。这个地址就是前端要用的 &lt;code&gt;envId&lt;/code&gt;——但别急着用，先做第 2 步。&lt;/p&gt;
&lt;h2&gt;第 2 步：绑自定义域名（别跳过）&lt;/h2&gt;
&lt;p&gt;我被这个坑卡了最久，单独拎出来讲。&lt;/p&gt;
&lt;p&gt;CF 给的 &lt;code&gt;*.workers.dev&lt;/code&gt; 域名&lt;strong&gt;在大陆时通时不通&lt;/strong&gt;。我的症状是：评论时有时无，头像一片空白，刷新几次又好了。一开始以为是头像配置错了，折腾半天才反应过来——评论数据根本没拉回来，那个域名不通。&lt;/p&gt;
&lt;p&gt;解法是给 Worker 绑你自己的域名。&lt;code&gt;wrangler.toml&lt;/code&gt; 加一行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;routes = [
  { pattern = &amp;quot;评论.你的域名.com&amp;quot;, custom_domain = true }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再 &lt;code&gt;npx wrangler deploy&lt;/code&gt; 一次，workers.dev 的路由会自动停用，之后前端 &lt;code&gt;envId&lt;/code&gt; 就用你自己的域名。绑完之后再没出过加载问题。&lt;/p&gt;
&lt;h2&gt;第 3 步：接入前端&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;① 装 Twikoo 前端&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install twikoo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;② 建 &lt;code&gt;Comments.astro&lt;/code&gt; 组件&lt;/strong&gt;（我博客用的 Astro，其他框架思路一样：放个容器 + 调 &lt;code&gt;twikoo.init&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
const path = Astro.url.pathname;
---
&amp;lt;section class=&amp;quot;comments&amp;quot; id=&amp;quot;comments&amp;quot;&amp;gt;
  &amp;lt;h2&amp;gt;评论&amp;lt;/h2&amp;gt;
  &amp;lt;div id=&amp;quot;tcomment&amp;quot; data-path={path}&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/section&amp;gt;

&amp;lt;script&amp;gt;
  import twikoo from &apos;twikoo&apos;;
  const container = document.getElementById(&apos;tcomment&apos;);
  if (container) {
    const path = container.dataset.path || location.pathname;
    twikoo.init({
      envId: &apos;https://评论.你的域名.com&apos;, // 填第 2 步绑的域名
      el: &apos;#tcomment&apos;,
      path, // 每篇传自己的 URL，评论就按篇分开了
    });
  }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;View Transitions 的坑&lt;/strong&gt;：博客如果开了 Astro 的站内切页动画，切到新文章时评论区不会自己刷新（第一篇能显示，点别的文章就空白）。要监听 &lt;code&gt;astro:page-load&lt;/code&gt;，每次先把容器清空再重新 init，不然会重复渲染。没开的话不管它，init 一次就行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;③ 挂到文章页&lt;/strong&gt;：在文章详情页 &lt;code&gt;&amp;lt;Content/&amp;gt;&lt;/code&gt; 后面引入，放上 &lt;code&gt;&amp;lt;Comments /&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;④ 样式贴合主题&lt;/strong&gt;：给 &lt;code&gt;.comments&lt;/code&gt; 补点间距、用上站点的强调色。评论区底部那排 Markdown 提示图标和 “Powered by Twikoo” 页脚，我直接用 CSS 藏了，看着干净些。&lt;/p&gt;
&lt;h2&gt;第 4 步：管理评论&lt;/h2&gt;
&lt;p&gt;Twikoo 没有独立后台，管理入口就在评论窗口右下角那个&lt;strong&gt;小齿轮图标&lt;/strong&gt;（不是点头像，我一开始也找错地方）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第一次设密码&lt;/strong&gt;：点小齿轮，会让你设管理员密码，设完存到后端&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日常管理&lt;/strong&gt;：再点小齿轮输密码，能置顶、加精、删除、把评论标成博主&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;想藏得更深，可以用 &lt;code&gt;HIDE_ADMIN_CRYPT&lt;/code&gt; 设个暗号：之后只有在昵称栏输入暗号的人，点齿轮才看得到管理入口。不过暗号只是藏界面，真正挡人的还是密码，两个别设成一样的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;踩过的坑&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;头像空白、评论时有时无&lt;/strong&gt;：先查 workers.dev 通不通。大多是那个域名的问题，按第 2 步绑域名解决，别像我一样先去折腾头像配置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同一篇文章的评论分成两处&lt;/strong&gt;：CF 版不自动归一化 URL 尾斜杠，&lt;code&gt;/post/1/&lt;/code&gt; 和 &lt;code&gt;/post/1&lt;/code&gt; 会被当成两篇。前端 &lt;code&gt;path&lt;/code&gt; 统一格式就行，别直接裸用 &lt;code&gt;location.pathname&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认头像服务是坏的&lt;/strong&gt;：Twikoo 默认的 weavatar.com 已经 500 了。换的时候注意 cravatar.cn 会无视 &lt;code&gt;d=&lt;/code&gt; 参数，一直返回它家那个卡皮巴拉吉祥物，想要按邮箱生成几何图案得换尊重参数的镜像（我用的 sdn.geekzu.org）。管理面板里配 &lt;code&gt;GRAVATAR_CDN&lt;/code&gt; 和 &lt;code&gt;DEFAULT_GRAVATAR&lt;/code&gt; 两项。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;邮箱格式要写全&lt;/strong&gt;：QQ 邮箱只认 &lt;code&gt;@qq.com&lt;/code&gt;，少个 &lt;code&gt;.com&lt;/code&gt; 会被当普通邮箱，头像就查不对。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管理密码是双重 md5&lt;/strong&gt;：前端先 md5 一次发给后端，后端再 md5 一次比对。要是直接改库重置密码，存 &lt;code&gt;md5(md5(明文))&lt;/code&gt;，只存单层会一直提示密码错误。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置改完要点保存&lt;/strong&gt;：管理面板填的配置存数据库，每项改完得点保存按钮才落库。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前端锁版本&lt;/strong&gt;：用 CDN 引 Twikoo 建议锁死版本号（比如 &lt;code&gt;twikoo@1.7.22&lt;/code&gt;），国内访问可以走 npmmirror 或 zstatic 镜像，避免升级带出不兼容变更。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;实际用下来&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;评论功能该有的都有：发、读、回复、嵌套、Markdown、表情&lt;/li&gt;
&lt;li&gt;评论区内容自动做 XSS 过滤（CF 版用 xss 替代不兼容的 dompurify），不用自己配&lt;/li&gt;
&lt;li&gt;成本为零，CF 免费额度对个人博客的评论量来说绰绰有余&lt;/li&gt;
&lt;li&gt;唯一受限的是邮件通知：常规 SMTP 用不了（nodemailer 不兼容 Worker），要接 SendGrid 之类的 API。我没配，不影响使用&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;写在最后&lt;/h2&gt;
&lt;p&gt;这套东西真正省心的地方是后端几乎不用管——模板官方在维护，评论躺在我自己的库里随时能查。要说有什么非记不可的，就一条：&lt;strong&gt;一定绑自己的域名&lt;/strong&gt;，别直接用 workers.dev。&lt;/p&gt;
&lt;p&gt;你要是也在用静态框架搭博客、还没想好评论区怎么做，可以试试这个组合。这篇底下就是现成的评论区，能留言就说明它真的跑通了。&lt;/p&gt;</content:encoded><lastmod>2026-09-08T04:00:00.000Z</lastmod><category>博客</category><category>Astro</category><category>Twikoo</category><category>评论系统</category><category>Cloudflare Worker</category></item><item><title>零成本搭建个人静态网站（Astro + Pagefind）</title><link>https://blog.junius.top/blog/2026/09/04/1/</link><guid isPermaLink="true">https://blog.junius.top/blog/2026/09/04/1/</guid><description>不用买服务器、不用备案、不用会写后端，用 Astro + Pagefind + Cloudflare Pages 就能搭出带全文搜索的个人网站。本文从零讲起，一步步带你上线。</description><pubDate>Fri, 04 Sep 2026 07:30:00 GMT</pubDate><content:encoded>&lt;h2&gt;这套方案好在哪&lt;/h2&gt;
&lt;p&gt;很多人一听“搭个人网站”就觉得得买服务器、搞备案、写后端，其实&lt;strong&gt;纯静态网站&lt;/strong&gt;这些都不需要。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不用服务器&lt;/strong&gt;：页面都是提前生成好的 HTML，丢到免费托管平台就能跑，没有后端程序要养。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不用花钱&lt;/strong&gt;：框架是开源的，托管平台有免费额度，还会自动送你一个 &lt;code&gt;&amp;lt;项目名&amp;gt;.pages.dev&lt;/code&gt; 子域名，&lt;strong&gt;域名都不用买就能先上线&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不用给访客背 JS 包袱&lt;/strong&gt;：默认只下发纯 HTML，打开快，流量省。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自带全文搜索&lt;/strong&gt;：用 Pagefind，构建的时候顺手把全站文字做成索引，访客在浏览器里就能搜，不靠任何搜索服务器。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这套东西适合&lt;strong&gt;个人博客、作品集、简历页、知识库&lt;/strong&gt;这类“展示为主、内容不大动”的站。反过来，如果你要的是登录、评论、实时数据这种，那静态站就不合适了。&lt;/p&gt;
&lt;p&gt;做完大概长这样——深色主题（默认）：&lt;/p&gt;
&lt;img src=&quot;https://img.junius.top/blog-posts/hero-dark.webp&quot; alt=&quot;博客首页深色主题&quot; width=&quot;900&quot; height=&quot;593&quot; /&gt;
&lt;p&gt;点右上角的按钮还能一键切成浅色主题：&lt;/p&gt;
&lt;img src=&quot;https://img.junius.top/blog-posts/hero-light.webp&quot; alt=&quot;博客首页浅色主题&quot; width=&quot;900&quot; height=&quot;593&quot; /&gt;
&lt;h2&gt;技术选型（为什么是这几样）&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;组件&lt;/th&gt;
&lt;th&gt;选择&lt;/th&gt;
&lt;th&gt;为什么&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;框架&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Astro&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;用 Markdown 写文章很顺手；内容集合帮你把文章的标题、日期这些元信息管起来；默认产出的就是零 JS 静态页面&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;搜索&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Pagefind&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;纯静态的全文搜索：构建时生成索引，浏览器端检索，没有服务器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;托管&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cloudflare Pages&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;免费额度够用、自带全球 CDN、还能接 Git 自动部署；国内打开速度一般，但免费站已经很值了&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;样式&lt;/td&gt;
&lt;td&gt;手写 CSS&lt;/td&gt;
&lt;td&gt;静态站的样式不复杂，用几个 CSS 变量当“设计 token”就够，不必上重型框架&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;提前提醒一个坑：&lt;strong&gt;纯静态站别装 Cloudflare 的 Astro 适配器&lt;/strong&gt;（&lt;code&gt;@astrojs/cloudflare&lt;/code&gt;），原因见文末「常见坑 1」。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;第 1 步：准备本地环境&lt;/h2&gt;
&lt;p&gt;其实只要装两样东西：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Node.js&lt;/strong&gt;——装 LTS 版就行，去官网下个安装包&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一个代码编辑器&lt;/strong&gt;——VS Code 挺好，你用别的顺手的也完全没问题&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;装完在终端里敲两下，确认没装错：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;node -v   # 能打印出版本号就是装好了
npm -v    # npm 会跟着 Node 一起装
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第 2 步：创建项目&lt;/h2&gt;
&lt;p&gt;用 Astro 官方的脚手架来建。&lt;strong&gt;先 &lt;code&gt;cd&lt;/code&gt; 进一个你想放项目的空文件夹&lt;/strong&gt;，然后跑（把 &lt;code&gt;my-site&lt;/code&gt; 换成你想起的名字）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm create astro@latest my-site
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;中途会问几个问题，新手照下面选就行（没列到的直接回车用默认）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;How would you like to start?&lt;/strong&gt; → &lt;code&gt;Empty&lt;/code&gt;（空白模板，最干净）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Install dependencies?&lt;/strong&gt; → &lt;code&gt;Yes&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Initialize a git repository?&lt;/strong&gt; → &lt;code&gt;Yes&lt;/code&gt;（后面接自动部署要用）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TypeScript&lt;/strong&gt; → &lt;code&gt;Strict&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;这些提问的文案会随 Astro 版本变，你只要对上大意就行，不用抠字眼。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;接着进到项目里，把本地预览跑起来：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd my-site
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;浏览器打开 &lt;code&gt;http://localhost:4321&lt;/code&gt;，能看到页面、也没有报错，说明环境没问题，可以继续了。&lt;/p&gt;
&lt;h2&gt;第 3 步：先看懂项目结构&lt;/h2&gt;
&lt;p&gt;先认识下这个项目&lt;strong&gt;最后会长成什么样&lt;/strong&gt;（&lt;code&gt;src/&lt;/code&gt; 下的 &lt;code&gt;content/&lt;/code&gt;、&lt;code&gt;layouts/&lt;/code&gt;、&lt;code&gt;styles/&lt;/code&gt; 这些子目录，空白模板一开始是没有的，是你后面几步自己加出来的）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my-site/
├── src/
│   ├── content/          # 文章放这（博客/项目等分目录）
│   ├── content.config.ts # 内容集合的格式定义（schema，见第 4 步）
│   ├── layouts/          # 页面外壳（头部/页脚/主题）
│   ├── pages/            # 一个 .astro 文件 = 一个页面
│   │   ├── index.astro   # 首页
│   │   └── blog/         # 博客相关页面
│   └── styles/           # 全局样式
├── astro.config.mjs      # Astro 配置（站点地址等）
└── package.json          # 依赖与命令
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;心里记着一条就行：&lt;strong&gt;&lt;code&gt;pages/&lt;/code&gt; 管“有哪些页面”，&lt;code&gt;content/&lt;/code&gt; 管“文章放哪”&lt;/strong&gt;。把 Markdown 丢进 &lt;code&gt;content/blog/&lt;/code&gt;，它就成了能写的文章。&lt;/p&gt;
&lt;h2&gt;第 4 步：告诉 Astro 你的文章长什么样&lt;/h2&gt;
&lt;p&gt;去 &lt;code&gt;src/content.config.ts&lt;/code&gt; 里定义一下文章格式（这玩意儿叫 schema）。拿博客举例，规定每篇文章得有标题、日期，描述和标签可有可无：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { defineCollection, z } from &apos;astro:content&apos;;
import { glob } from &apos;astro/loaders&apos;;

// 博客文章集合：扫描 src/content/blog/ 下所有 .md
const blog = defineCollection({
  loader: glob({ pattern: &apos;**/*.md&apos;, base: &apos;./src/content/blog&apos; }),
  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 };  // 用到的集合都要在这登记
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再写一篇测试文章试试，&lt;code&gt;src/content/blog/hello.md&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-md&quot;&gt;---
title: 你好，世界
description: 我的第一篇文章
date: 2026-09-04
tags: [随笔]
---

这是我的第一篇文章。欢迎来到我的网站。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第 5 步：建列表页和详情页&lt;/h2&gt;
&lt;p&gt;要让文章能被点开，需要两个页面：一个&lt;strong&gt;列表页&lt;/strong&gt;（罗列文章标题），一个&lt;strong&gt;详情页&lt;/strong&gt;（点进去看正文）。&lt;/p&gt;
&lt;p&gt;下面贴的是&lt;strong&gt;核心逻辑&lt;/strong&gt;（取数据 + 循环渲染）。一个完整页面还要有站点自己的头部、页脚这些外壳，套进你项目里现成的布局就行，这里先省略，免得干扰你理解重点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;列表页&lt;/strong&gt; &lt;code&gt;src/pages/blog/index.astro&lt;/code&gt; 的核心：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
// 取出所有非草稿文章，按日期倒序
import { getCollection } from &apos;astro:content&apos;;

const posts = (await getCollection(&apos;blog&apos;, ({ data }) =&amp;gt; !data.draft))
  .sort((a, b) =&amp;gt; b.data.date - a.data.date);
---

{posts.length &amp;gt; 0 ? (
  &amp;lt;ul&amp;gt;
    {posts.map((post) =&amp;gt; (
      &amp;lt;li&amp;gt;&amp;lt;a href={`/blog/${post.id}/`}&amp;gt;{post.data.title}&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
    ))}
  &amp;lt;/ul&amp;gt;
) : (
  &amp;lt;p&amp;gt;还没有文章。&amp;lt;/p&amp;gt;
)}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;详情页&lt;/strong&gt; &lt;code&gt;src/pages/blog/[slug].astro&lt;/code&gt; 的核心——&lt;code&gt;[slug]&lt;/code&gt; 是 Astro 的动态路由，一篇 Markdown 就对应一个网址：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-astro&quot;&gt;---
import { getCollection, render } from &apos;astro:content&apos;;

// 为每一篇文章生成一个静态详情页
export async function getStaticPaths() {
  const posts = await getCollection(&apos;blog&apos;, ({ data }) =&amp;gt; !data.draft);
  return posts.map((post) =&amp;gt; ({ params: { slug: post.id }, props: { post } }));
}

const { post } = Astro.props;
const { Content } = await render(post);
---

&amp;lt;h1&amp;gt;{post.data.title}&amp;lt;/h1&amp;gt;
&amp;lt;time&amp;gt;{post.data.date.toISOString().slice(0, 10)}&amp;lt;/time&amp;gt;
{post.data.tags.map((t) =&amp;gt; &amp;lt;span&amp;gt;{t}&amp;lt;/span&amp;gt;)}

&amp;lt;Content /&amp;gt;  {/* 文章正文会自动渲染到这里 */}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;存盘后 &lt;code&gt;npm run dev&lt;/code&gt; 刷新一下，&lt;code&gt;/blog/&lt;/code&gt; 就能看到文章列表，点进去是正文。&lt;/p&gt;
&lt;h2&gt;第 6 步：加全文搜索（Pagefind）&lt;/h2&gt;
&lt;p&gt;Pagefind 的思路是：&lt;strong&gt;构建的时候&lt;/strong&gt;扫一遍生成好的 HTML，把文字建成索引，访客在浏览器里搜，全程不碰后端。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;装依赖：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install pagefind
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;再往 package.json 里加一条 postbuild 命令&lt;/strong&gt;——这样每次 &lt;code&gt;npm run build&lt;/code&gt; 建完站，会自动顺手跑 Pagefind 生成索引：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&amp;quot;scripts&amp;quot;: {
  &amp;quot;build&amp;quot;: &amp;quot;astro build&amp;quot;,
  &amp;quot;postbuild&amp;quot;: &amp;quot;pagefind --site dist&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这里容易踩坑，重点看：&lt;code&gt;dist&lt;/code&gt; 是 Astro 默认的构建输出目录，Pagefind 就是扫它、把索引生成到 &lt;code&gt;dist/pagefind/&lt;/code&gt; 里。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;接着往页面里放搜索框&lt;/strong&gt;。以官方默认 UI 为例，在你想放搜索的那个页面（比如 &lt;code&gt;src/pages/search.astro&lt;/code&gt;）里加：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;link href=&amp;quot;/pagefind/pagefind-ui.css&amp;quot; rel=&amp;quot;stylesheet&amp;quot; /&amp;gt;
&amp;lt;script src=&amp;quot;/pagefind/pagefind-ui.js&amp;quot; defer&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;div id=&amp;quot;search&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;script&amp;gt;
  window.addEventListener(&apos;DOMContentLoaded&apos;, () =&amp;gt; {
    new PagefindUI({ element: &apos;#search&apos; });
  });
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;再控制一下什么内容进搜索（这步很关键）。&lt;/strong&gt; Pagefind 默认把整张页面都收进索引，导航、页脚这些每页都重复的文字也会混进去，搜出来一堆没用的。正确做法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;给&lt;strong&gt;正文容器&lt;/strong&gt;打个 &lt;code&gt;data-pagefind-body&lt;/code&gt;，告诉 Pagefind“只索引这一块”。&lt;/li&gt;
&lt;li&gt;只要&lt;strong&gt;全站有任意一个&lt;/strong&gt; &lt;code&gt;data-pagefind-body&lt;/code&gt;，Pagefind 就会&lt;strong&gt;自动跳过&lt;/strong&gt;没打标记的页面——首页、列表页、搜索页这些导航类页面就都被排除了。&lt;/li&gt;
&lt;li&gt;正文里还有个别不想收的局部（比如“返回列表”的链接、日期那行 meta），单独打 &lt;code&gt;data-pagefind-ignore&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;article data-pagefind-body&amp;gt;   &amp;lt;!-- 只索引正文 --&amp;gt;
  &amp;lt;h1&amp;gt;文章标题&amp;lt;/h1&amp;gt;
  &amp;lt;div class=&amp;quot;meta&amp;quot; data-pagefind-ignore&amp;gt;2026-09-04&amp;lt;/div&amp;gt;  &amp;lt;!-- 排除 meta --&amp;gt;
  正文内容……
&amp;lt;/article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;怎么在本地试搜索&lt;/strong&gt;：Pagefind 的索引和搜索框资源，只有 &lt;code&gt;npm run build&lt;/code&gt; 之后才生成（落在 &lt;code&gt;dist/pagefind/&lt;/code&gt;），而 &lt;code&gt;npm run dev&lt;/code&gt; 的开发服务器并不服务 &lt;code&gt;dist/&lt;/code&gt;，所以&lt;strong&gt;开发模式里搜不到是正常的，别慌&lt;/strong&gt;。想本地看效果，先 &lt;code&gt;npm run build&lt;/code&gt; 再 &lt;code&gt;npm run preview&lt;/code&gt;（preview 服务的是构建产物），打开预览地址就能搜了。上线之后一切正常。&lt;/p&gt;
&lt;p&gt;上线后的搜索页长这样（输入关键词，纯浏览器端实时检索）：&lt;/p&gt;
&lt;img src=&quot;https://img.junius.top/blog-posts/search-dark.webp&quot; alt=&quot;博客全文搜索&quot; width=&quot;900&quot; height=&quot;571&quot; /&gt;
&lt;h2&gt;第 7 步：部署上线（Cloudflare Pages）&lt;/h2&gt;
&lt;p&gt;两种方式任选，推荐第一种，省心、一次配好。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方式 A：接 Git，自动部署&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;先把项目推到 GitHub（私有仓库就行；还没有仓库的话，先在 GitHub 新建一个空的，再 &lt;code&gt;git push&lt;/code&gt; 上去）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;打开 &lt;a href=&quot;https://dash.cloudflare.com&quot;&gt;Cloudflare Dashboard&lt;/a&gt;，进 &lt;strong&gt;Workers &amp;amp; Pages&lt;/strong&gt;（新版界面可能写作 &lt;strong&gt;Workers&lt;/strong&gt;，入口在侧边栏）→ &lt;strong&gt;Create&lt;/strong&gt; → &lt;strong&gt;Pages&lt;/strong&gt; → &lt;strong&gt;Connect to Git&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;选中你的仓库，构建配置填这几项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Framework preset&lt;/strong&gt;：Astro&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build command&lt;/strong&gt;：&lt;code&gt;npm run build&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build output directory&lt;/strong&gt;：&lt;code&gt;dist&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;（可选）环境变量 &lt;code&gt;NODE_VERSION&lt;/code&gt; 设成 &lt;code&gt;24&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;点 &lt;strong&gt;Save and Deploy&lt;/strong&gt;。等一两分钟，Cloudflare 会给你一个 &lt;code&gt;https://你的项目名.pages.dev&lt;/code&gt; 的免费子域名，打开就是你的站了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以后每次 &lt;code&gt;git push&lt;/code&gt;，Cloudflare 都会自动重新构建部署——&lt;strong&gt;写完文章 → push → 自动上线&lt;/strong&gt;，全程不花一分钱。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;方式 B：命令行直接传（不接 Git）&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npx wrangler pages deploy dist --project-name=my-site
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;适合你本地已经 build 好、想直接传上去、不想折腾 Git 的情况。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;想绑自己的域名（可选）&lt;/strong&gt;：去 Pages 项目设置里加自定义域，按提示在 DNS 加一条 CNAME 指到 &lt;code&gt;&amp;lt;项目名&amp;gt;.pages.dev&lt;/code&gt; 就行。不绑也没关系，Cloudflare 送的那个免费地址已经能用了。&lt;/p&gt;
&lt;h2&gt;常见坑（都是我踩过的）&lt;/h2&gt;
&lt;h3&gt;1. 纯静态站别装 Cloudflare 的 Astro 适配器&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@astrojs/cloudflare&lt;/code&gt; 是给“在 Workers 上跑动态逻辑”准备的。纯静态项目装上反而会拖进来一堆多余的本地模拟，构建变慢还可能报些莫名其妙的错。&lt;strong&gt;纯静态根本用不着它&lt;/strong&gt;，把构建产物直接托管给 Pages 就行。&lt;/p&gt;
&lt;h3&gt;2. Astro 的样式是有作用域范围的&lt;/h3&gt;
&lt;p&gt;Astro 每个 &lt;code&gt;.astro&lt;/code&gt; 文件里的 &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; 默认&lt;strong&gt;只作用于当前文件&lt;/strong&gt;（它会自动加随机属性做隔离）。所以用普通写法，是匹配不到 JS 动态生成的内容的（比如 Pagefind 搜出来的结果 DOM）。要让样式管到动态内容，得用 &lt;code&gt;:global()&lt;/code&gt; 把选择器包起来。&lt;/p&gt;
&lt;h3&gt;3. 记得限定搜索范围&lt;/h3&gt;
&lt;p&gt;不标 &lt;code&gt;data-pagefind-body&lt;/code&gt; 的话，Pagefind 会把每页的导航、页脚全收进索引。记得给正文打标记（见第 6 步）。另外注意：&lt;strong&gt;当全站还没有任何内容页时&lt;/strong&gt;（比如刚建站、一篇文章都还没写），Pagefind 会退回去索引整页——等你写了第一篇带 &lt;code&gt;data-pagefind-body&lt;/code&gt; 的文章，导航页自然就被排除掉了。&lt;/p&gt;
&lt;h3&gt;4. 列表没内容时记得兜底&lt;/h3&gt;
&lt;p&gt;文章还没写、集合是空的时候，直接去遍历会渲染成一片空白。用条件渲染判断一下再输出就行（写法看第 5 步列表页里那个 &lt;code&gt;posts.length &amp;gt; 0 ? … : …&lt;/code&gt;）。&lt;/p&gt;
&lt;h3&gt;5. Pagefind 的中文搜索，能用但别指望完美&lt;/h3&gt;
&lt;p&gt;Pagefind 对中文的支持不如英文：它不做词根折叠（搜“部署”不会顺带匹配到“部署中”），个别词还可能因为浏览器分词和建索引时分词对不上而搜不到。这是 Pagefind 的老毛病，&lt;strong&gt;不是你配错了&lt;/strong&gt;。英文内容它表现很好；要是你的中文站特别看重模糊搜索，就得自己再接一套中文分词方案。一般个人博客，能做到“按标题、按关键词搜到主要文章”就够用了。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;到这儿，你就白捡了一个能搜全文的个人网站：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内容&lt;/strong&gt;：写 Markdown，丢进 &lt;code&gt;src/content/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;搜索&lt;/strong&gt;：构建时自动生成索引，访客在浏览器里就能搜&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;部署&lt;/strong&gt;：push 一下自动上线，全程零成本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扩展&lt;/strong&gt;：框架的内容集合是开放的，以后想加作品集、摄影、知识库，照葫芦画瓢再开几个集合就行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;先别想太远，把它当成“一个带全文搜索的静态博客”，往里填东西就对了。碰到问题，多翻翻 &lt;a href=&quot;https://docs.astro.build&quot;&gt;Astro 文档&lt;/a&gt;、&lt;a href=&quot;https://pagefind.app&quot;&gt;Pagefind 文档&lt;/a&gt; 和 &lt;a href=&quot;https://developers.cloudflare.com/pages&quot;&gt;Cloudflare Pages 文档&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;参考文献&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build&quot;&gt;Astro 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pagefind.app&quot;&gt;Pagefind 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/pages&quot;&gt;Cloudflare Pages 文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><lastmod>2026-09-04T09:00:00.000Z</lastmod><category>博客</category><category>Astro</category><category>Pagefind</category><category>Cloudflare Pages</category><category>建站教程</category></item></channel></rss>