<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Nskdfh 的博客</title><description>个人博客</description><link>https://56076077.xyz/</link><language>zh_CN</language><item><title>关于SpringBoot中拦截器和过滤器的关系</title><link>https://56076077.xyz/posts/sb-if/</link><guid isPermaLink="true">https://56076077.xyz/posts/sb-if/</guid><pubDate>Mon, 05 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Filter&lt;/h1&gt;
&lt;p&gt;首先是过滤器（Filter），是Servlet中定义的一个规范，也就是说依赖tomcat等容器，只能在web程序中使用，用来拦截和处理http的请求，过滤器可以对请求进行预处理，也可以对响应进行后处理。过滤器的主要作用是在请求到达Controller之前或响应离开Controller之后，对请求和响应进行处理。&lt;/p&gt;
&lt;h4&gt;使用方法&lt;/h4&gt;
&lt;p&gt;首先导入servlet库，因为需要我们来实现当前这个接口，会重写3个主要方法，&lt;/p&gt;
&lt;p&gt;示例代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import javax.servlet.*;
import java.io.IOException;
public class MyFilter implements Filter {
   @Override
   public void init(FilterConfig filterConfig) throws ServletException {
       System.out.println(&quot;MyFilter初始化&quot;);
   }
   @Override
   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
       System.out.println(&quot;MyFilter拦截请求&quot;);
       chain.doFilter(request, response); // 继续执行后续过滤器或Controller方法
       System.out.println(&quot;MyFilter处理响应&quot;);
   }
   @Override
   public void destroy() {
       System.out.println(&quot;MyFilter销毁&quot;);
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Interceptor&lt;/h1&gt;
&lt;p&gt;拦截器是Spring中的一个组件，有spring容器来管理，不用依赖tomcat，用于拦截和处理HTTP请求。拦截器可以对请求进行预处理，也可以对响应进行后处理。拦截器的主要作用是在请求到达Controller之前或响应离开Controller之后，对请求和响应进行处理。&lt;/p&gt;
&lt;h4&gt;使用方法&lt;/h4&gt;
&lt;p&gt;这次是实现&lt;/p&gt;
&lt;p&gt;示例代码如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyInterceptor implements HandlerInterceptor {
   @Override
   public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
       System.out.println(&quot;MyInterceptor拦截请求&quot;);
       return true; // 返回true表示放行，返回false表示拦截请求并终止后续处理流程
   }
   @Override
   public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
       System.out.println(&quot;MyInterceptor处理响应&quot;);
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Filter 与 Interceptor 区别&lt;/h1&gt;
&lt;p&gt;1.接口规范不同：过滤器需要实现Filter接口，而拦截器需要实现HandlerInterceptor接口。&lt;/p&gt;
&lt;p&gt;2.拦截范围不同：过滤器Filter会拦截所有的资源，而Interceptor只会拦截Spring环境中的资源。&lt;/p&gt;
</content:encoded></item><item><title>卡了我半天的那些坑（顺便聊聊 vibe coding）</title><link>https://56076077.xyz/posts/blog-pitfalls/</link><guid isPermaLink="true">https://56076077.xyz/posts/blog-pitfalls/</guid><description>前两篇讲的是&quot;怎么做对的&quot;，这篇讲&quot;怎么错的&quot;：一个字母抄错导致登录 404、无刷新跳转把页面卡在半截、bfcache 让关掉的面板又活了、还有一次我自己的测试把我自己骗了。最后聊聊 vibe coding 的趋势，以及它的硬性要求。</description><pubDate>Sat, 19 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;这是「搭建这个博客」系列的第 &lt;strong&gt;3&lt;/strong&gt; 篇。前两篇是 &lt;a href=&quot;/posts/blog-setup/&quot;&gt;1 从零搭一个博客&lt;/a&gt; 和 &lt;a href=&quot;/posts/blog-admin/&quot;&gt;2 给静态博客接一个能登录的后台&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;前两篇讲的是&quot;怎么做对的&quot;。这篇讲&quot;怎么错的&quot;——而且我保证，&lt;strong&gt;这一篇比前两篇有用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;顺便说一句：整个博客是我和 AI 协作做的（也就是现在说的 vibe coding）。下面这些坑，有一半是它帮我找到的，也有一半是它先带偏、后来才被我拽回来的。最后一节专门聊这件事。&lt;/p&gt;
&lt;h2&gt;坑 1：一个字符抄错，GitHub 直接给我 404&lt;/h2&gt;
&lt;p&gt;配 OAuth 的时候，我从 GitHub 页面上把 &lt;code&gt;Client ID&lt;/code&gt; &lt;strong&gt;手动敲&lt;/strong&gt;进了 Cloudflare 的环境变量。&lt;/p&gt;
&lt;p&gt;然后点登录：打开 GitHub 授权页 → &lt;strong&gt;404&lt;/strong&gt;，一只沙漠里的猫告诉我&quot;This is not the web page you are looking for&quot;。&lt;/p&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; GitHub，打开那个授权地址，它会&lt;strong&gt;正常跳转到登录页&lt;/strong&gt;——看起来&quot;授权页是通的&quot;！&lt;/li&gt;
&lt;li&gt;只有&lt;strong&gt;登录之后&lt;/strong&gt;，GitHub 才真正处理这个授权请求，这时才发现找不到这个应用 → 404&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是接下来的一个多小时，我依次怀疑并排查了：网络被墙、浏览器缓存、油猴脚本、代理软件、DNS、hosts……&lt;strong&gt;一个都不是&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;真正的原因是：我把第 5 位的小写字母 &lt;code&gt;l&lt;/code&gt; 敲成了数字 &lt;code&gt;1&lt;/code&gt;，还漏掉了一个字符——&lt;strong&gt;19 位，而正确的是 20 位&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;教训：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Client ID&lt;/code&gt; / &lt;code&gt;Client Secret&lt;/code&gt; 这类东西，&lt;strong&gt;永远用复制按钮&lt;/strong&gt;，不要手打。它们的字符集里全是 &lt;code&gt;l / 1 / I&lt;/code&gt;、&lt;code&gt;O / 0&lt;/code&gt; 这种长得一模一样的东西。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自检方法&lt;/strong&gt;：这类 client ID 通常是 20 位。长度不对，基本就是抄错了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;排查顺序&lt;/strong&gt;：先怀疑自己刚输入的东西，再怀疑环境。我是反着来的，所以白花了一个多小时。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;坑 2：无刷新跳转，把页面卡在了半截&lt;/h2&gt;
&lt;p&gt;Fuwari 用 &lt;strong&gt;Swup&lt;/strong&gt; 做无刷新页面切换——点站内链接时不整页刷新，只把 &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt; 里的内容换掉。体验确实好，但有个前提：&lt;strong&gt;目标页面得有 &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt; 容器&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;而后台页面（CMS）是独立的单页应用，&lt;strong&gt;压根没有 &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;于是点导航里指向后台的链接时，浏览器控制台里出现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[swup] Container missing in incoming document: main
[swup] Container mismatch, aborting
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Swup 一看容器对不上 → &lt;strong&gt;把整个跳转中止了&lt;/strong&gt; → 页面就停在&quot;切了一半&quot;的状态，导航栏上打开的浮层也跟着挂在那儿不动。&lt;/p&gt;
&lt;p&gt;这也是我一开始判断错的地方：我以为是&quot;面板关闭逻辑写错了&quot;，改了好几轮都没用。&lt;strong&gt;因为问题根本不在关闭逻辑，而在&quot;跳转失败了&quot;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：给指向后台的链接加一个 &lt;code&gt;data-no-swup&lt;/code&gt; 属性（这是 Swup 的忽略标记），让它老老实实整页加载。后台本来就是独立应用，整页加载才是对的。&lt;/p&gt;
&lt;h2&gt;坑 3：bfcache 让&quot;已经关掉&quot;的面板又活了&lt;/h2&gt;
&lt;p&gt;我做了个导航栏浮层面板，逻辑是&quot;打开时关掉别人，离开页面时关掉自己&quot;。听起来没毛病。&lt;/p&gt;
&lt;p&gt;但&lt;strong&gt;浏览器前进后退&lt;/strong&gt;的时候出问题了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;你点了链接离开 → pagehide 触发 → 代码把状态设成「关闭」
                                  ↓ 但框架是异步更新 DOM 的
                          浏览器立刻把页面冻结进 bfcache
                          （DOM 上还是「打开」的样子）
                                  ↓ 你按了后退，页面从缓存里被恢复
                  状态已经是「关闭」→ 框架认为没变化 → 永远不再渲染
                                  ↓
                  面板就永久卡在打开的样子，只有刷新能救
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键点在于：&lt;strong&gt;bfcache 冻结页面时，JS 状态和 DOM 会一起被原样保存，而且 pagehide 之后留给你的时间非常短&lt;/strong&gt;。你改了状态，但框架还没把 DOM 更新出来，页面就已经被冻住了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：不要依赖框架重渲染——&lt;strong&gt;直接操作 DOM 类名&lt;/strong&gt;（&lt;code&gt;classList.add(...)&lt;/code&gt;），并且在 &lt;code&gt;pageshow&lt;/code&gt; 时再对齐一次。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;普遍启示&lt;/strong&gt;：任何&quot;离开页面时改状态&quot;的逻辑，都要考虑 bfcache。这不是框架的 bug，是浏览器的正常行为。&lt;/p&gt;
&lt;h2&gt;坑 4：改了环境变量，必须重新部署&lt;/h2&gt;
&lt;p&gt;这个纯属不知道就会踩：Cloudflare &lt;strong&gt;不会&lt;/strong&gt;把新填的环境变量自动应用到已经跑着的 Functions 上。&lt;/p&gt;
&lt;p&gt;现象是：变量明明填对了，接口却一直报&quot;未配置&quot;。&lt;strong&gt;去部署页面点一次&quot;重新部署&quot;就好了。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;坑 5：&quot;代码没生效&quot;十有八九是浏览器在跑旧代码&lt;/h2&gt;
&lt;p&gt;调试期间反复出现&quot;我明明改了，怎么还是老样子&quot;。后来发现是两类原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;开发服务器的热更新（HMR）能更新模板和状态，但不重新绑定已经挂载的组件的事件逻辑&lt;/strong&gt;。你改了事件处理，页面不整页刷新就还是旧的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;浏览器前进后退会恢复缓存页面&lt;/strong&gt;，看起来就像&quot;我刚改的东西消失了&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：我在组件里加了一行版本标记——&lt;code&gt;window.__adminMenuV = &quot;v6&quot;&lt;/code&gt;。不确定当前页面跑的是哪一版，控制台敲一下就知道。就这一行，省了后面大量的&quot;到底生效了没&quot;的扯皮。&lt;/p&gt;
&lt;h2&gt;坑 6：我自己的测试，把我自己骗了&lt;/h2&gt;
&lt;p&gt;这条最值得说，因为它跟代码无关，跟&quot;怎么确认问题真的解决了&quot;有关。&lt;/p&gt;
&lt;p&gt;修完 bfcache 那个 bug 之后，我写了个自动化测试来验证。&lt;strong&gt;测试通过了。但线上问题还在。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;原因是我用&lt;strong&gt;脚本合成&lt;/strong&gt;的 &lt;code&gt;pagehide&lt;/code&gt; / &lt;code&gt;pageshow&lt;/code&gt; 事件去模拟&quot;前进后退&quot;。而合成事件&lt;strong&gt;根本不会触发真正的页面冻结&lt;/strong&gt;——于是 DOM 更新有充足的时间跑完，那个 bug 自然复现不出来。测试给了我一个绿色的假象。&lt;/p&gt;
&lt;p&gt;后来我把测试改成两条硬要求：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;用真实的导航&lt;/strong&gt;：&lt;code&gt;location.href&lt;/code&gt; 真的跳走，再 &lt;code&gt;history.back()&lt;/code&gt; 真的回来&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;断言&quot;这次确实走了 bfcache&quot;&lt;/strong&gt;：在 &lt;code&gt;pageshow&lt;/code&gt; 里记录 &lt;code&gt;event.persisted&lt;/code&gt;，如果它没变成 &lt;code&gt;true&lt;/code&gt;，&lt;strong&gt;这次测试就不算数&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;改完之后测试才真正有价值——它才真的在测那条会出问题的路径。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：&lt;strong&gt;测试通过 ≠ 问题解决&lt;/strong&gt;，尤其是当测试自己构造了运行环境的时候。多问一句：&quot;我这么测，有没有可能绕过了真实路径？&quot;&lt;/p&gt;
&lt;h2&gt;关于 vibe coding：它改变了什么，又要求什么&lt;/h2&gt;
&lt;p&gt;这个博客是我和 AI 协作做的。整个过程里我几乎没手写代码，但&lt;strong&gt;每一步都在做判断&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;它确实很能打&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;一个下午搞定整个站点的汉化、配置、若干交互组件&lt;/li&gt;
&lt;li&gt;几十行写出并部署了 OAuth 代理，还顺手做了 CSRF、白名单、最小权限&lt;/li&gt;
&lt;li&gt;排查问题时能快速给出&lt;strong&gt;可验证的假设&lt;/strong&gt;（例如&quot;直连返回 302、走代理返回 404，所以问题在本机代理&quot;——这个假设完全正确，一下省了我半小时）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;放在以前，这些事情的时间成本大概是这里的几倍。&lt;/p&gt;
&lt;h3&gt;但它有硬性要求&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. 验证权必须留在人手里。&lt;/strong&gt;
它会非常自信地给结论，包括错的结论。这一轮里它先后把问题归因于&quot;缓存&quot;&quot;浏览器扩展&quot;&quot;本机代理&quot;，全都不是。&lt;strong&gt;关键结论自己复现一遍&lt;/strong&gt;，这是底线。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 凭据永远不进仓库、不进聊天。&lt;/strong&gt;
Client Secret、访问令牌这类东西只能待在部署平台的环境变量里。我中途把 secret 截图发出去过，事后只能作废重发。&lt;strong&gt;能不发就不发。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 小步提交，保持可回滚。&lt;/strong&gt;
每次改动独立成一次提交，写清&quot;改了什么、为什么&quot;。出问题时能一眼定位到是哪一步引入的——这一条救过我不止一次。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. 让它承认&quot;我不确定&quot;。&lt;/strong&gt;
比&quot;答错了&quot;更危险的是&quot;答得很顺但你没法验证&quot;。所以我会强制它给依据：日志、状态码、测试结果。&lt;strong&gt;没有证据的结论，一律当假设。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. 关键代码你得看得懂。&lt;/strong&gt;
不用逐行懂，但&lt;strong&gt;安全相关的必须懂&lt;/strong&gt;：谁能拿到 token、白名单在哪校验、secret 存在哪。看不懂的地方，就不该让它上线。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. 文档要写&quot;为什么&quot;。&lt;/strong&gt;
我把踩过的坑和排查方法都写进仓库的 &lt;code&gt;docs/&lt;/code&gt; 了。三个月后的我，或者下一个接手的人，能少走一遍。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;7. 接受&quot;它会自信地犯错&quot;，并据此设计流程。&lt;/strong&gt;
所以要有版本标记、要有可复现的测试、要有能回滚的提交历史。&lt;strong&gt;这些不是仪式感，是保险。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;我的结论&lt;/h3&gt;
&lt;p&gt;vibe coding 没有让&quot;懂技术&quot;变得不重要。恰恰相反，它把人的角色从&quot;实现者&quot;推到了&quot;审查者&quot;——而审查需要的判断力一点不比写代码少：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;看得出方案哪里不对&lt;/li&gt;
&lt;li&gt;读得懂报错在说什么&lt;/li&gt;
&lt;li&gt;问得出&quot;这个测试有没有绕过真实路径&quot;&lt;/li&gt;
&lt;li&gt;守得住&quot;凭据不进仓库&quot;这种底线&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;它能极大压缩&quot;实现&quot;的时间，但压缩不了&quot;判断&quot;的责任。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;这三篇写下来，其实我想说的就一句：&lt;strong&gt;搭博客这件事，现在真的不难了&lt;/strong&gt;。难的是那些&quot;看起来好了其实没好&quot;的时刻——而对付它们，靠的不是更聪明的工具，是更笨的验证。&lt;/p&gt;
&lt;p&gt;如果你也在搭，希望这几篇能帮你少花一个多小时。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;系列导航&lt;/strong&gt;：&lt;a href=&quot;/posts/blog-setup/&quot;&gt;1 从零搭一个博客&lt;/a&gt; · &lt;a href=&quot;/posts/blog-admin/&quot;&gt;2 给静态博客接一个能登录的后台&lt;/a&gt; · &lt;a href=&quot;/posts/blog-pitfalls/&quot;&gt;3 卡了我半天的那些坑&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>给静态博客接一个能登录的后台</title><link>https://56076077.xyz/posts/blog-admin/</link><guid isPermaLink="true">https://56076077.xyz/posts/blog-admin/</guid><description>静态博客唯一的缺点是写作体验。这篇讲怎么用 Sveltia CMS 加一个自写的 GitHub OAuth 代理，把「改文件 + push」变成「打开网页就能写」，顺便聊聊登录安全是怎么设计的。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;这是「搭建这个博客」系列的第 &lt;strong&gt;2&lt;/strong&gt; 篇。另外两篇是 &lt;a href=&quot;/posts/blog-setup/&quot;&gt;1 从零搭一个博客&lt;/a&gt; 和 &lt;a href=&quot;/posts/blog-pitfalls/&quot;&gt;3 卡了我半天的那些坑&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;静态博客唯一不爽的地方&lt;/h2&gt;
&lt;p&gt;上一篇说到，我选了静态博客，好处一大堆。但它有个很实在的问题：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;想写篇文章，得打开编辑器、新建 Markdown、写完、然后 push。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在自己电脑上还好，但我经常在别的设备上（手机、公司电脑），那就很难受了——总不能在手机上装个 Git 客户端写文章吧。&lt;/p&gt;
&lt;p&gt;所以我想给它补一个后台：&lt;strong&gt;打开网页，登录，写，保存&lt;/strong&gt;。写完自动就是一次提交，Cloudflare 自动部署。对，就是 WordPress 那种体验，但不想要 WordPress 那一整套服务器。&lt;/p&gt;
&lt;h2&gt;思路：基于 Git 的前端 CMS&lt;/h2&gt;
&lt;p&gt;这类工具不少，统称&quot;Git-based CMS&quot;（以前的 Netlify CMS、Decap CMS、现在的 Sveltia CMS 都属于这一类）。&lt;/p&gt;
&lt;p&gt;它的原理其实特别简单：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它本身&lt;strong&gt;就是几个静态文件&lt;/strong&gt;，挂在 &lt;code&gt;/admin/&lt;/code&gt; 路径下（不需要任何服务器）&lt;/li&gt;
&lt;li&gt;配置写在 &lt;code&gt;public/admin/config.yml&lt;/code&gt;：你的仓库、分支、有哪些内容类型、每个字段叫什么&lt;/li&gt;
&lt;li&gt;登录之后，它&lt;strong&gt;直接调 GitHub API 往你的仓库提交文件&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以**&quot;保存&quot; = 一次 commit = 一次自动部署**。跟手动 push 的结果一模一样，只是把过程变成了点按钮。&lt;/p&gt;
&lt;p&gt;我选的是 &lt;strong&gt;Sveltia CMS&lt;/strong&gt;：界面比老牌的 Decap 现代很多，中文也正常，作者还在活跃维护。版本我是&lt;strong&gt;锁死&lt;/strong&gt;的（不写 &lt;code&gt;latest&lt;/code&gt;），免得上游更新把我这边弄挂。&lt;/p&gt;
&lt;h2&gt;文章在仓库里长什么样&lt;/h2&gt;
&lt;p&gt;后台里每篇文章，对应仓库里一个&lt;strong&gt;文件夹&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/content/posts/
└── blog-admin/
    ├── index.md      ← 正文 + frontmatter
    └── cover.webp    ← 封面图（可选，放同一层）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好处是&lt;strong&gt;每篇文章的图片跟着文章走&lt;/strong&gt;，不会越堆越乱。后台里拖一张封面图进去，它自动就存到这篇文章自己的文件夹里。&lt;/p&gt;
&lt;h2&gt;登录：两种方式，我都要&lt;/h2&gt;
&lt;p&gt;Sveltia 支持两种登录：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一种：访问令牌。&lt;/strong&gt; 去 GitHub 手动创建一个 40 位的 token，粘贴进后台。优点是&lt;strong&gt;不需要任何后端&lt;/strong&gt;，缺点是麻烦（要手动建、到期要换）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二种：GitHub 授权登录。&lt;/strong&gt; 点一下按钮就进来了。但它有个前提：&lt;strong&gt;需要一个能安全保存 &lt;code&gt;client secret&lt;/code&gt; 的地方&lt;/strong&gt;——因为拿授权码换 token 这一步必须带 secret，而 secret &lt;strong&gt;绝对不能放在前端&lt;/strong&gt;（放前端等于公开）。&lt;/p&gt;
&lt;p&gt;问题就来了：我这是纯静态站，&lt;strong&gt;没有后端&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;于是用 Cloudflare 写了个小代理&lt;/h2&gt;
&lt;p&gt;好在站点已经部署在 Cloudflare Pages 上了，它支持 &lt;strong&gt;Functions&lt;/strong&gt;：在仓库里建一个 &lt;code&gt;functions/&lt;/code&gt; 目录，里面的文件会自动变成服务端接口。&lt;/p&gt;
&lt;p&gt;于是整个登录流程被我拆成两个接口（一共几十行代码）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/api/auth      → 生成一个随机 state，种下 CSRF cookie，302 跳转到 GitHub 授权页
/api/callback  → 校验 state 和 cookie → 用 code 换 token（secret 只在服务端）
               → 用 token 查一下用户名 → 和白名单比对 → 通过才把 token 交给后台页面
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么中间要绕一圈，不直接把 token 给前端？因为我要&lt;strong&gt;先确认&quot;是谁&quot;&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;在 callback 里，函数拿着 token 去问一次 GitHub&quot;这是谁&quot;，拿到用户名，和我配置的&lt;strong&gt;白名单&lt;/strong&gt;比对。&lt;strong&gt;不是我本人，token 根本不会发出去&lt;/strong&gt;——这样即使别人知道了后台地址、也完成了授权，他什么也拿不到。&lt;/p&gt;
&lt;p&gt;安全上具体做了四件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;state + CSRF cookie 双重校验&lt;/strong&gt;：防止有人伪造回调地址来骗 token&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户名白名单&lt;/strong&gt;：只允许指定的 GitHub 账号登录&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;权限最小化&lt;/strong&gt;：申请的 scope 只有 &lt;code&gt;public_repo&lt;/code&gt;（我的仓库是公开的，这就够了），授权页上明确显示&quot;仅公开仓库&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;secret 只存在 Cloudflare 的环境变量里&lt;/strong&gt;：不进仓库、不进浏览器、不写进任何文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;顺便说，函数在失败时会返回明确的错误码和中文提示，后台界面直接把原因显示出来——排查的时候特别省事（这一点在第 3 篇里救了我不止一次）。&lt;/p&gt;
&lt;h2&gt;Cloudflare 上要配的东西&lt;/h2&gt;
&lt;p&gt;就四个环境变量：&lt;/p&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;code&gt;GITHUB_CLIENT_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;你 OAuth 应用的 Client ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GITHUB_CLIENT_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;对应的 Secret&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ALLOWED_GITHUB_LOGIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;白名单：允许登录的 GitHub 用户名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ALLOWED_DOMAINS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;只允许你自己的域名使用这个代理&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;有个特别容易踩的坑&lt;/strong&gt;：Cloudflare &lt;strong&gt;不会&lt;/strong&gt;把新填的环境变量自动应用到已经跑着的函数上。填完必须去&quot;部署&quot;页面点一次&lt;strong&gt;重新部署&lt;/strong&gt;，否则函数读到的还是旧值——现象就是&quot;明明填了变量，接口却一直报未配置&quot;。&lt;/p&gt;
&lt;p&gt;（另外：Client ID 和 Secret &lt;strong&gt;一定要用复制按钮&lt;/strong&gt;，别手打。我手打的后果，第 3 篇里详细写了。）&lt;/p&gt;
&lt;h2&gt;现在的写作流程&lt;/h2&gt;
&lt;p&gt;打开 &lt;code&gt;你的域名/admin/&lt;/code&gt; → 点&quot;使用 GitHub 登录&quot; → 授权 → 写 → 保存。&lt;/p&gt;
&lt;p&gt;然后就没了。后台会提交、Cloudflare 会自动部署，一分钟左右文章就上线了。&lt;/p&gt;
&lt;p&gt;手机上也能这么写——这大概是我最满意的一点。&lt;/p&gt;
&lt;p&gt;下一篇是这个系列里最&quot;血泪&quot;的一篇：几个把我卡了半天的坑，以及我怎么被自己的测试骗了一次。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;系列导航&lt;/strong&gt;：&lt;a href=&quot;/posts/blog-setup/&quot;&gt;1 从零搭一个博客&lt;/a&gt; · &lt;a href=&quot;/posts/blog-admin/&quot;&gt;2 给静态博客接一个能登录的后台&lt;/a&gt; · &lt;a href=&quot;/posts/blog-pitfalls/&quot;&gt;3 卡了我半天的那些坑&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>从零搭一个博客：为什么我最后选了 Astro</title><link>https://56076077.xyz/posts/blog-setup/</link><guid isPermaLink="true">https://56076077.xyz/posts/blog-setup/</guid><description>想有个自己的地方写字，于是从零搭了这个博客。这篇讲选型思路（以及为什么放弃了 WordPress），从本地跑起来、改成自己的、汉化、再到免费部署上线的完整步骤。</description><pubDate>Thu, 17 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;这是「搭建这个博客」系列的第 &lt;strong&gt;1&lt;/strong&gt; 篇。另外两篇是 &lt;a href=&quot;/posts/blog-admin/&quot;&gt;2 给静态博客接一个能登录的后台&lt;/a&gt; 和 &lt;a href=&quot;/posts/blog-pitfalls/&quot;&gt;3 卡了我半天的那些坑&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;起因&lt;/h2&gt;
&lt;p&gt;说简单点：我想要一个自己的地方写字。&lt;/p&gt;
&lt;p&gt;不想发在别人的平台上——排版不能改、数据不在自己手里、说不定哪天就没了。所以决定自己搭一个。&lt;/p&gt;
&lt;p&gt;我不是前端工程师，平时也就写写脚本。所以我给自己定的要求很低：&lt;strong&gt;能跑起来、好看、以后写文章不折腾&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;先把 WordPress 排除掉&lt;/h2&gt;
&lt;p&gt;第一反应当然是 WordPress，毕竟&quot;建站&quot;两个字搜索引擎第一个推的就是它。&lt;/p&gt;
&lt;p&gt;但想了下我的实际需求：我就写点技术笔记，一年可能几十篇。为了这个去租服务器、装 PHP、配数据库、装插件、还得记着升级和备份——&lt;strong&gt;服务器一旦不维护，就是被挂马的重灾区&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;相比之下，静态博客的好处太明显了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;页面是提前生成好的纯静态文件，&lt;strong&gt;快&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;扔到 CDN 上就行，&lt;strong&gt;免费&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;没有数据库、没有后台入口，&lt;strong&gt;几乎没什么可攻击的&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;文章就是 Markdown 文件，&lt;strong&gt;在 Git 里，永远不会丢&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它唯一的问题就是：写文章要改文件、还要 push。这个后面我专门写了一篇解决它（就是本系列第 2 篇）。&lt;/p&gt;
&lt;h2&gt;为什么是 Astro + Fuwari&lt;/h2&gt;
&lt;p&gt;框架我看了 Hugo、Hexo、Astro。最后选 Astro 的原因很朴素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它默认就是&lt;strong&gt;静态生成&lt;/strong&gt;，不用我去理解什么渲染模式&lt;/li&gt;
&lt;li&gt;生态好，图片优化、代码高亮、搜索、站点地图都有现成集成&lt;/li&gt;
&lt;li&gt;想加点交互组件也不难（它支持 Svelte / React 这些）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;主题选了 &lt;strong&gt;Fuwari&lt;/strong&gt;（开源，MIT）。原因更朴素：&lt;strong&gt;一眼看过去顺眼&lt;/strong&gt;——左边个人卡、右边文章列表，二次元风格的封面图，暗色模式也好看。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;选主题这事我建议别纠结太久。好看、能改，就够了。反正代码在自己手里，不满意随时换。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;第 1 步：本地先跑起来&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git clone &amp;lt;主题仓库&amp;gt; 我的博客
cd 我的博客
pnpm install
pnpm dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要 Node 20+ 和 pnpm。跑起来后打开 &lt;code&gt;http://localhost:4321&lt;/code&gt; 就能看到默认样子。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这里有个小建议&lt;/strong&gt;：先别急着改东西，先跑一遍 &lt;code&gt;pnpm build&lt;/code&gt; 和 &lt;code&gt;pnpm preview&lt;/code&gt;，确认&quot;原样是能构建的&quot;。不然一会儿改坏了，你分不清是环境问题还是自己改错了。&lt;/p&gt;
&lt;h2&gt;第 2 步：把它变成&quot;我的&quot;博客&lt;/h2&gt;
&lt;p&gt;Fuwari 把所有能改的东西都集中在一个文件里：&lt;strong&gt;&lt;code&gt;src/config.ts&lt;/code&gt;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;站点标题、副标题、语言（我改成 &lt;code&gt;zh_CN&lt;/code&gt;）、主题色、banner 大图、头像、导航栏、社交链接、每页显示几篇文章、音乐播放器……全在这儿。改这一个文件，站点的&quot;身份&quot;就变了。&lt;/p&gt;
&lt;p&gt;我的做法是：&lt;strong&gt;先把这里改到自己满意，再动别的&lt;/strong&gt;。因为后面改组件的时候，你会需要一个&quot;确定没问题&quot;的基线。&lt;/p&gt;
&lt;h2&gt;第 3 步：汉化（有个小坑）&lt;/h2&gt;
&lt;p&gt;主题自带多语言，中文文案在 &lt;code&gt;src/i18n/languages/zh_CN.ts&lt;/code&gt;。改文案很直接。&lt;/p&gt;
&lt;p&gt;但如果你要&lt;strong&gt;新增&lt;/strong&gt;一个词条，注意了：不是只改中文文件就行。得先在 &lt;code&gt;i18nKey.ts&lt;/code&gt; 里加 key，然后把&lt;strong&gt;全部 10 个语言文件&lt;/strong&gt;都补上——少一个就是构建报错（类型检查过不去）。&lt;/p&gt;
&lt;p&gt;我第一次就是这么踩的：改完中文，一构建，红字一片。&lt;/p&gt;
&lt;h2&gt;第 4 步：部署上线（免费）&lt;/h2&gt;
&lt;p&gt;代码推到自己 GitHub 仓库后：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Cloudflare Dashboard → Workers &amp;amp; Pages → 创建 Pages 项目 → &lt;strong&gt;连接这个仓库&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;构建命令填 &lt;code&gt;pnpm run build&lt;/code&gt;，输出目录填 &lt;code&gt;dist&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;绑定自己的域名，HTTPS 证书 Cloudflare 自动签&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;然后你会发现一件很爽的事：&lt;strong&gt;以后每次 &lt;code&gt;git push&lt;/code&gt;，它自动重新构建部署&lt;/strong&gt;，一分钟左右就上线。&lt;/p&gt;
&lt;p&gt;这个&quot;提交即上线&quot;的闭环特别重要——它让后面加后台变成可能（后台写完文章 = 一次提交 = 自动部署）。&lt;/p&gt;
&lt;h2&gt;现在的样子&lt;/h2&gt;
&lt;p&gt;搭完之后陆陆续续又加了些东西：侧栏加了文章/分类/标签的统计、补了两个总览页、右下角加了回到顶部、侧栏塞了个音乐播放器。都是小东西，但用起来舒服很多。&lt;/p&gt;
&lt;p&gt;整个过程最大的感受是：&lt;strong&gt;别追求一次到位&lt;/strong&gt;。先上线一个能用的，再慢慢加。&lt;/p&gt;
&lt;p&gt;下一篇讲怎么给这个纯静态的站接一个能登录的后台，让它变得&quot;能写&quot;。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;系列导航&lt;/strong&gt;：&lt;a href=&quot;/posts/blog-setup/&quot;&gt;1 从零搭一个博客&lt;/a&gt; · &lt;a href=&quot;/posts/blog-admin/&quot;&gt;2 给静态博客接一个能登录的后台&lt;/a&gt; · &lt;a href=&quot;/posts/blog-pitfalls/&quot;&gt;3 卡了我半天的那些坑&lt;/a&gt;&lt;/p&gt;
</content:encoded></item></channel></rss>