<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Yuumi</title><description>悠米的开发笔记</description><link>https://yuumiblog.com/</link><language>zh_CN</language><item><title>从找最小值到旅行商问题：模拟退火算法的 Java 实现</title><link>https://yuumiblog.com/posts/dailyknowledge/dailyknowledge01/</link><guid isPermaLink="true">https://yuumiblog.com/posts/dailyknowledge/dailyknowledge01/</guid><description>模拟退火算法的 Java 实现：从多峰函数求最值到二维旅行商问题，涵盖原理推导、代码示例与参数调优。</description><pubDate>Tue, 16 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 什么是模拟退火算法？&lt;/h2&gt;
&lt;p&gt;WIKI定义：&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E6%A8%A1%E6%8B%9F%E9%80%80%E7%81%AB&quot;&gt;模拟退火&lt;/a&gt; ( &lt;a href=&quot;https://en.wikipedia.org/wiki/Simulated_annealing&quot;&gt;Simulated annealing&lt;/a&gt; )&lt;/p&gt;
&lt;h3&gt;冶金学中的退火&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://img.yuumiblog.com/80b0a074-300d-42bd-b461-1223151e08cc.png&quot; alt=&quot;什么是退火&quot; /&gt;
模拟退火的退火源自冶金学术语退火。金属在加热后，原子能量变高，会离开原来的位置随机的在其他位置移动，退火时冷却速度较慢，使得原子有更多可能找到内能比原来位置更低的位置。&lt;/p&gt;
&lt;h3&gt;由此衍生的模拟退火算法&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://img.yuumiblog.com/Hill_Climbing_with_Simulated_Annealing.gif&quot; alt=&quot;图片源自Wiki百科&quot; /&gt;
模拟退火算法的原理与金属退火相似，将待搜索空间内的每一点类比为一个金属原子，定义一个温度类比原子当前的能量，随机的选取一个邻居，计算原子会移动到当前邻居的概率，同时原子的温度会逐渐降低。随着温度降低到临界值，原子也会趋于一个全局的能量最低点。&lt;/p&gt;
&lt;p&gt;在模拟退火算法中，需要进行如下的定义：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;状态 &amp;amp; 邻态选择： 模拟退火在每一步都需要基于当前的状态选取一个随机的邻态，并考虑是否要转移到这个邻态。&lt;/li&gt;
&lt;li&gt;温度： 模拟退火定义了一个温度变量，类比冶金退火中的当前温度。随着退火历程逐渐降低。&lt;/li&gt;
&lt;li&gt;接收概率： 是否从当前状态转移到邻态需要由接收概率定义。&amp;lt;br/&amp;gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当邻态更优于当前状态时，自然会选择转移；而当邻态劣于当前状态时，由于原子自身具备的能量，还是有概率离开当前状态转移到邻态的。&amp;lt;br/&amp;gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这使得在高能量时，原子能更广泛的检索，而不是被困在当前的局部最优解，这也是模拟退火算法在爬山算法基础上的改进之处。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;2. 什么时候用到模拟退火算法？&lt;/h2&gt;
&lt;p&gt;当问题满足下面的条件时，可以考虑使用模拟退火算法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;解空间特别大：
100个任务分配给10台机器；20个城市的旅行商问题；范围内的函数最值问题
无法暴力枚举的问题&lt;/li&gt;
&lt;li&gt;可以定义评分函数：
可以明确判断两个方案哪个好
任务分配问题可以通过总耗时判断；旅行商问题可以根据路线的总时间判断；函数最值问题可以通过比较函数在不同点的值判读&lt;/li&gt;
&lt;li&gt;可以生成邻居解：
交换随机两个任务执行的机器；随机交换旅行商路线中的两个城市；向左或向右移动随机的位置取值
均可以生成一个邻居，进而比较邻居解和当前方案&lt;/li&gt;
&lt;li&gt;存在局部最优方案：
例如旅行商问题，当城市很多时，使用贪心算法容易陷入局部最优，而无法找到其他更优秀方案
函数最值问题，函数图像可能有多个波峰，存在局部最优&lt;/li&gt;
&lt;li&gt;允许得到近似最优解：
模拟退火算法不保证全局最优，但能在可接受时间内得到足够好的近似解，适合问题规模大到无法求精确解的场景。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. 模拟退火算法求解多峰函数最值&lt;/h2&gt;
&lt;p&gt;给定一个函数：
$$ f(x) = \sin(x) + \sin(3x) + 0.1x^2 $$
，求这个函数在 [0, 5]区间内的最大值。&lt;/p&gt;
&lt;p&gt;函数图像如下：
&lt;img src=&quot;https://img.yuumiblog.com/9ef0217d-1efb-45b2-9cbf-44351e291316.png&quot; alt=&quot;image-20260616143234870&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package simulated_annealing;

import java.util.Random;

public class SimulatedAnnealingDemo {

    static Random random = new Random();

    // 定义函数（即条件2中的评分函数）
    static double target(double x){
        return Math.sin(x) + Math.sin(3 * x) + 0.1 * x * x;
    }

    // 定义扰动（即条件3中的获取邻居的方法）
    static double mutate(double curr){
    
        // 通过Math.min和Math.max把生成的邻居的范围限制在[0, 5]
        return Math.max(0, Math.min(curr + (random.nextDouble() - 0.5), 5));
    }


    // SA主逻辑
    public static void main(String[] args) {
    
        // 生成初始状态为当前状态，将当前状态的(x, y)设置为最佳状态
        double currX = random.nextDouble() * 6;
        double currY = target(currX);

        double bestX = currX;
        double bestY = currY;

        // 定义温度，最低温度阈值，冷却系数
        double temperature = 1000;
        double minTemperature = 0.0001;
        double coolingRate = 0.995;

        // 退火过程
        while (temperature &amp;gt; minTemperature) {
        
            // 调用扰动，获取当前状态的邻居态，并求解邻居态的值
            double nextX = mutate(currX);
            double nextY = target(nextX);

            double delta = nextY - currY;

            // 比较当前状态和邻居态的优劣
            if (delta &amp;gt; 0) {
            
                // 因为要求最大值，因此如果邻居态比当前状态更优，则接受
                currX = nextX;
                currY = nextY;
            } else {
            
                // SA的核心
                // 如果邻居态比当前状态更劣，则概率接受
                // 接收概率计算方法为：Math.exp(delta / temperature)
                double probability = Math.exp(delta / temperature);
                if (random.nextDouble() &amp;lt; probability) {
                    currX = nextX;
                    currY = nextY;
                }
            }

            // 如果新状态比最佳状态更好，则最佳状态替换为新的当前状态
            if (currY &amp;gt; bestY) {
                bestY = currY;
                bestX = currX;
            }

            // 温度降低
            temperature *= coolingRate;
            
            // 输出过程状态
            System.out.println(currX + &quot; ==&amp;gt; &quot; + currY);
        }
        
        // 输出最终结果
        System.out.printf(&quot;最优解: x = %.6f, f(x) = %.6f%n&quot;, bestX, bestY);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;5.0 ==&amp;gt; 2.1913635654939783
4.663310255727348 ==&amp;gt; 2.1650306949291216
4.932227738744374 ==&amp;gt; 2.247043141298705
4.8347081857352325 ==&amp;gt; 2.2783351972660553
4.609863083298898 ==&amp;gt; 2.083404663625435
4.795880597591812 ==&amp;gt; 2.2723253037868276
4.835759295604256 ==&amp;gt; 2.2783444866281934
5.0 ==&amp;gt; 2.1913635654939783
4.825175791571457 ==&amp;gt; 2.277886007069018
4.625396083392352 ==&amp;gt; 2.1093483021343054
... 此处省略很多行
0.6346640055673788 ==&amp;gt; 1.5781887948289557
0.6346640055673788 ==&amp;gt; 1.5781887948289557
0.6346640055673788 ==&amp;gt; 1.5781887948289557
0.6346640055673788 ==&amp;gt; 1.5781887948289557
最优解: x = 4.836988, f(x) = 2.278345
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
其实我们可以观察到最后结果收敛到了(0.6346640055673788, 1.5781887948289557)，通过函数图像我们观察到这并不是全局最优解，而是一个局部最优解 &amp;lt;br/&amp;gt;
退火末期 currX 卡在某个局部最优附近随机抖动是正常现象，但这不影响最终结果，因为算法全程都在用 bestX/bestY 跟踪目前见过的最优解，二者要分开看，不能拿 currX 最后停留的位置去判断算法有没有找到全局最优。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;4. 模拟退火算法求解二维旅行商问题&lt;/h2&gt;
&lt;p&gt;简单描述下旅行商问题：&amp;lt;br/&amp;gt;
给定一系列城市的位置（有时候是城市间距离）, 请找出一条最短的路线，要求经过每条城市恰好一次，最终回到起点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package simulated_annealing;

import java.util.Arrays;
import java.util.Random;

public class TravelingSalesmanProblemDemo {

    static Random random = new Random();

    // 定义城市位置
    static int[][] cities = {
            {0, 0},
            {3, 4},
            {6, 1},
            {7, 6},
            {2, 7}
    };

    // 计算城市间距离
    static double distance(int a, int b){
        int dx = cities[a][0] - cities[b][0];
        int dy = cities[a][1] - cities[b][1];
        return Math.sqrt(dx * dx + dy * dy);
    }

    // 定义函数（即条件2中的评分函数）
    static double totalDistance(int[] route){
        double total = 0;
        for (int i = 0; i &amp;lt; route.length - 1; i++) {
            total += distance(route[i], route[i + 1]);
        }
        total += distance(route[route.length - 1], route[0]);
        return total;
    }

    // 定义扰动（即条件3中的获取邻居的方法）
    // 此处扰动的方式是将当前路线中随机两个城市交换，生成新的路线
    static int[] mutate(int[] route){
        int[] next = route.clone();
        
        // 这里选择范围为random.nextInt(route.length - 1) + 1，因为 TSP 的路线本身是一个环路，起点选哪个城市不影响总距离，所以可以固定
        int i = random.nextInt(route.length - 1) + 1;
        int j = random.nextInt(route.length - 1) + 1;
        int tmp = next[i];
        next[i] = next[j];
        next[j] = tmp;
        return next;
    }

    // SA主逻辑
    public static void main(String[] args) {
    
        // 生成初始路线为当前状态，将当前路线和当前路线的总距离设置为最佳状态
        int[] currRoute = { 0, 1, 2, 3, 4 };
        double currDist = totalDistance(currRoute);

        int[] bestRoute = currRoute.clone();
        double bestDist = currDist;

        // 定义温度，最低温度阈值，冷却系数
        double temperature = 1000;
        double minTemperature = 0.0001;
        double coolingRate = 0.995;

        // 退火过程
        while (minTemperature &amp;lt; temperature){
        
            // 调用扰动，根据当前路线生成新的的邻居态路线，并求解邻居态的总路程
            int[] nextRoute = mutate(currRoute);
            double nextDist = totalDistance(nextRoute);

            double delta = nextDist - currDist;
            
            // 比较当前状态和邻居态的优劣
            if (delta &amp;lt; 0){
            
                // 因为要求最小值，因此如果邻居态的总距离比当前状态更小则是优，则接受
                currRoute = nextRoute;
                currDist = nextDist;
            } else {
            
                // SA的核心
                // 如果邻居态比当前状态更劣，则概率接受
                // 接收概率计算方法为：Math.exp(-delta / temperature)
                double probability = Math.exp(-delta / temperature);
                if (random.nextDouble() &amp;lt; probability){
                    currRoute = nextRoute;
                    currDist = nextDist;
                }
            }

            // 如果新状态比最佳状态更好，则最佳状态替换为新的当前状态
            if (currDist &amp;lt; bestDist){
                bestRoute = currRoute.clone();
                bestDist = currDist;
            }

            // 温度降低
            temperature *= coolingRate;
        }

        // 输出最终结果
        System.out.println(&quot;最优路线: &quot; + Arrays.toString(bestRoute));
        System.out.println(&quot;最短距离: &quot; + bestDist);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;最优路线: [0, 2, 3, 4, 1]
最短距离: 24.44307921765217
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 模拟退火算法的思路&lt;/h2&gt;
&lt;p&gt;总结上面两个例子，我们可以发现模拟退火算法的大致思路是相通的，转换为伪代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Demo{

    static T 扰动方法(T 当前状态){
        T 邻居态 = 随机扰动(当前状态);
        return 邻居态;
    }

    static 评分 评分方法(T 状态){
        return 评分方法的具体逻辑(状态);
    }

    public static void main(String[] args) {
        定义初始状态currX, currY = 评分方法(currX)
        定义最佳状态bestX = currX, 最佳评分bestY = currY

        定义温度T，最低温度阈值TMin，冷却系数coolingRate

        while(T &amp;gt; TMin){
            nextX = 扰动方法(currX);
            nextY = 评分方法(nextX);

            delta = nextY - currY;
            if (nextX 优于 currX){
                currX = nextX;
                currY = nextY;
            } else if {
                double probability = Math.exp(-delta / temperature);
                // 这里是取delta还是-delta取决于求较优的比较方向

                if (random.nextDouble() &amp;lt; probability){
                    currX = nextX;
                    currY = nextY;
                }
            }

            // 这里的大于小于也是取决于求较优的比较方向
            if (currY &amp;gt; bestY) {
                bestY = currY;
                bestX = currX;
            }
        }

        // 输出最终结果
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. 参数调优&lt;/h2&gt;
&lt;p&gt;模拟退火的效果很大程度上取决于三个参数：初始温度、冷却系数、终止温度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 定义温度，最低温度阈值，冷却系数
double temperature = 1000;
double minTemperature = 0.0001;
double coolingRate = 0.995;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1. 初始温度$$ T $$&lt;/h3&gt;
&lt;p&gt;根据接收概率公式：$$ P = e^{-\frac{\Delta}{T}} $$， 我们不难发现：
如果改变值 $$\Delta$$ 比当前温度 $$ T $$ 相比小几个量级时，$$ -\frac{\Delta}{T} $$ 趋近 0，总的$$ {P} $$ 趋近
1，即几乎所有变差的邻居都会被接受。&amp;lt;br/&amp;gt;
而如果初始温度 $$ T $$ 设置的和 $$\Delta$$ 差不多，甚至比 $$\Delta$$ 小，则会导致算法在刚起步时就几乎只接受比当前状态更好的邻居态，退化为普通的爬山算法。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
总结：&amp;lt;br/&amp;gt;
当初始温度$$ T $$设置的较大时，算法的早期会浪费时间在随机的游走上，虽然收敛变慢但是不会影响最终求解的质量。&amp;lt;br/&amp;gt;
当初始温度$$ T $$设置的较小时，算法会在一开始很难跳出局部最优解，效率快但是可能忽略全局最优解。&amp;lt;br/&amp;gt;
实践中可以这样估算：先粗略计算一下评分函数在邻居之间的典型差值大小（比如 TSP 里交换两个城市导致的总距离变化量级），让初始温度比这个量级大上一到两个量级。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2. 冷却系数&lt;/h3&gt;
&lt;p&gt;冷却系数控制温度下降的速度，越接近 1 降温越慢。&amp;lt;br/&amp;gt;
是否需要调整冷却系数，取决于问题规模。例子中的 TSP 只有 5 个城市，解空间很小，0.995&amp;lt;br/&amp;gt;
这个保守的降温速度完全够用；但如果城市数量上升到几十甚至上百个，解空间呈指数级增长，可能需要把冷却系数调得更接近 1（比如 0.999)，或者配合增加每个温度下的迭代次数，否则算法会过早收敛到一个较差的解。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
总结：&amp;lt;br/&amp;gt;
当冷却系数设置的较大时，温度很快跌到很低，算法还没来得及探索完解空间就早早收敛，容易陷入局部最优。&amp;lt;br/&amp;gt;
当冷却系数设置的较小时，探索更充分，更可能找到全局最优，但需要更多迭代次数才能让温度降到终止阈值，运行时间显著增加。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3. 终止温度&lt;/h3&gt;
&lt;p&gt;终止温度只是一个工程上的截断阈值，用来决定循环什么时候停止，本身没有特别的物理意义。&amp;lt;br/&amp;gt;
判断终止温度是否合理，可以看对应的接受概率: &amp;lt;br/&amp;gt;
当$$ T $$降到终止温度时， 即使$$\Delta$$很小， $$ P = e^{-\frac{\Delta}{T}} $$也应该已经趋近于 0， 这意味着此时算法基本只接受变好的邻居，跟普通爬山算法没有区别。如果终止温度设得过高，会导致算法在还有一定随机性的时候就提前停止，浪费了继续优化的机会。&lt;/p&gt;
&lt;h2&gt;7. 总结&lt;/h2&gt;
&lt;p&gt;回到文章标题——从找最小值到旅行商问题，这两个例子表面上差异很大：一个在连续的实数区间里找极值，一个在离散的排列组合里找最短路径。但如果把代码并排放在一起看，会发现两者用的是完全相同的退火框架，即##5中的伪代码。&amp;lt;br/&amp;gt;
两个例子也分别暴露了 SA 在两类问题上的不同表现。函数最值问题里，在运行结果那里其实说明了一个容易被忽略的点：算法运行过程中的&quot;当前状态&quot;和最终输出的&quot;最佳状态&quot;是两回事，温度降到很低后当前状态会卡在某个点上随机抖动，但真正有意义的答案始终是过程中记录的 bestX/bestY。TSP 问题里，由于解空间是离散的排列组合，邻居的定义（交换两个城市）直接决定了搜索能到达哪些状态，这也是为什么不同问题中&quot;如何定义邻居&quot;往往比调参数更影响最终效果。&amp;lt;br/&amp;gt;
当然，模拟退火不是万能的。它不保证找到全局最优解，只能保证大概率找到一个足够好的近似解；它对参数（初始温度、冷却系数、终止温度）比较敏感，调得不好效果可能还不如简单的贪心算法；遇到城市数量上百的 TSP 这类大规模问题，单纯依赖一次退火过程也往往不够，通常需要结合多次重启、自适应降温，或者和遗传算法、禁忌搜索等其他启发式方法配合使用。但对于中小规模、能够明确定义评分函数和邻居关系的优化问题，模拟退火胜在实现简单、思路直观，是一个性价比很高的起点。&lt;/p&gt;
</content:encoded></item><item><title>Astro 博客添加 Waline 评论系统，Waline 的 docker 部署</title><link>https://yuumiblog.com/posts/blogbuild/blogbuildnote04/</link><guid isPermaLink="true">https://yuumiblog.com/posts/blogbuild/blogbuildnote04/</guid><description>记录在 Astro 博客中接入 Waline 评论系统，并使用 Docker 部署 Waline 与 MongoDB</description><pubDate>Fri, 20 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Waline是一款轻量的，快速的，带后端的评论系统，官方提供了多种部署方案以及不同的数据存储可供选择&lt;/p&gt;
&lt;p&gt;本文选择使用 MongoDB 作为数据存储服务， MongoDB 以及 Waline 均使用Docker部署&lt;/p&gt;
&lt;p&gt;其他部署方案以及数据存储服务可以参考官方文档：&lt;a href=&quot;https://waline.js.org/advanced/intro.html&quot;&gt;Waline介绍&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;架构图如下
&lt;img src=&quot;http://img.yuumiblog.com/0fc96d41-4bea-4462-9b23-fc751a2a6e66.png&quot; alt=&quot;架构图&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;0. 准备工作&lt;/h2&gt;
&lt;p&gt;你需要准备：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;docker环境, 用来部署 Waline 以及对应数据库&lt;/li&gt;
&lt;li&gt;一个可运行的 astro 项目（可以参考 &lt;a href=&quot;https://selibert.xyz/posts/blogbuildnote02/&quot;&gt;Astro项目搭建部署&lt;/a&gt; 系列搭建一个 astro 博客项目）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1. Mongo 以及 Waline 的 Docker 部署&lt;/h2&gt;
&lt;h3&gt;1. 创建 Docker 网络&lt;/h3&gt;
&lt;p&gt;用于让 Waline 通过容器名访问 MongoDB&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker network create waline-net
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Docker 部署 MongoDB&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;docker run -d \
  --name mongo \
  --network waline-net \
  -e MONGO_INITDB_ROOT_USERNAME=DemoUsername \
  -e MONGO_INITDB_ROOT_PASSWORD=DemoPassword \
  -v /var/lib/mongo:/data/db \
  --restart unless-stopped
  mongo:6
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]&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;--name mongo&lt;/td&gt;
&lt;td&gt;容器名称为mongo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--network waline-net&lt;/td&gt;
&lt;td&gt;接入第一步创建的waline-net网络&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-e MONGO_INITDB_ROOT_USERNAME=DemoUsername&lt;/td&gt;
&lt;td&gt;设定初始用户的用户名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-e MONGO_INITDB_ROOT_PASSWORD=DemoPassword&lt;/td&gt;
&lt;td&gt;设定初始用户的密码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-v ${pwd}/data:/data/db&lt;/td&gt;
&lt;td&gt;挂载本地的data文件夹到容器的/data/db&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mongo:6&lt;/td&gt;
&lt;td&gt;选择镜像版本为mongo6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3. Docker 部署 Waline&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;docker run -d \
  --name waline \
  --network waline-net \
  -p 8360:8360 \
  -e MONGO_DB=waline \
  -e MONGO_USER=DemoUsername \
  -e MONGO_PASSWORD=DemoPassword \
  -e MONGO_HOST=mongo \
  -e MONGO_PORT=27017 \
  -e MONGO_AUTHSOURCE=admin \
  -e JWT_SECRET=testsecret \
  --restart unless-stopped
  lizheming/waline
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]&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;--name waline&lt;/td&gt;
&lt;td&gt;容器名称为waline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;--network waline-net&lt;/td&gt;
&lt;td&gt;接入第一步创建的waline-net网络&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-p 8360:8360&lt;/td&gt;
&lt;td&gt;指定端口为8360&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-e MONGO_DB=waline&lt;/td&gt;
&lt;td&gt;指定Waline使用MongoDB的数据库名为waline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-e MONGO_USER=DemoUsername&lt;/td&gt;
&lt;td&gt;连接MongoDB的用户名，可以用2中设定的初始用户用户名，生产环境建议创建单独数据库用户&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-e MONGO_PASSWORD=DemoPassword&lt;/td&gt;
&lt;td&gt;连接MongoDB的密码，可以用2中设定的初始用户密码，生产环境建议创建单独数据库用户&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-e MONGO_HOST=mongo&lt;/td&gt;
&lt;td&gt;连接MongoDB的地址，因为接入了Docker网络因此使用MongoDB的容器名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-e MONGO_PORT=27017&lt;/td&gt;
&lt;td&gt;连接MongoDB的端口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-e MONGO_AUTHSOURCE=admin&lt;/td&gt;
&lt;td&gt;指定在MongoDB的admin数据库验证身份&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-e JWT_SECRET=testsecret&lt;/td&gt;
&lt;td&gt;一个随机长字符串，用于登录Waline时生成Token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lizheming/waline&lt;/td&gt;
&lt;td&gt;选择Waline的镜像&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/blockquote&gt;
&lt;h2&gt;2. astro 项目接入 waline&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
本部分主要参考了这篇文章：&lt;a href=&quot;https://kasuha.com/posts/fuwari-waline-comments/&quot;&gt;Fuwari 接入 Waline 评论系统 ~ 可选是否开启评论&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;1. Pnpm安装waline&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pnpm add @waline/client
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 在 src/components/comment 下新建文件 Waline.astro&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;---
import { commentConfig } from &quot;../../config&quot;;

interface Props {
	path: string;
}

const config = {
	...commentConfig.waline,
	el: &quot;#waline&quot;,
	path: Astro.props.path,
	dark: &quot;html.dark&quot;,
	wordLimit: [&quot;2&quot;, &quot;300&quot;],
};
---
&amp;lt;!-- Waline --&amp;gt;
&amp;lt;div class=&quot;relative w-full&quot;&amp;gt;
    &amp;lt;div id=&quot;waline&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://unpkg.com/@waline/client@v3/dist/waline.css&quot; /&amp;gt;
    &amp;lt;script type=&quot;module&quot; is:inline define:vars={{ config }}&amp;gt;
        import { init } from &apos;https://unpkg.com/@waline/client@v3/dist/waline.js&apos;;
        init(config);
    &amp;lt;/script&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
path: Astro.props.path
这一行的作用是指明Waline使用文章的path区分不同文章下的评论，不会把不同文章的评论混淆&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;3. 在 src/components/comment 下新建文件 index.astro&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;---
import type { CollectionEntry } from &apos;astro:content&apos;
import { commentConfig } from &apos;@/config&apos;
import Waline from &apos;./Waline.astro&apos;
interface Props {
    post: CollectionEntry&amp;lt;&apos;posts&apos;&amp;gt;
}

const { id, data, slug } = Astro.props.post

const path = `/posts/${slug}`
const url = `${Astro.site?.href}${path}`
const comments = data.comments

let commentService = &apos;&apos;
if (commentConfig?.waline) {
    commentService = &apos;waline&apos;
}
---
{ comments == true &amp;amp;&amp;amp;
    (
    &amp;lt;div class=&quot;card-base p-6 mb-5&quot;&amp;gt;
        {commentService === &apos;waline&apos; &amp;amp;&amp;amp; &amp;lt;Waline path={path} /&amp;gt;}
        {commentService === &apos;&apos; &amp;amp;&amp;amp; null}
    &amp;lt;/div&amp;gt;
        )
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 修改 src/content/config.ts 中的类型约束&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const postsCollection = defineCollection({
	schema: z.object({
		title: z.string(),
		published: z.date(),
		updated: z.date().optional(),
		draft: z.boolean().optional().default(false),
		comments: z.boolean().optional().default(true),
		description: z.string().optional().default(&quot;&quot;),
		image: z.string().optional().default(&quot;&quot;),
		tags: z.array(z.string()).optional().default([]),
		category: z.string().optional().nullable().default(&quot;&quot;),
		lang: z.string().optional().default(&quot;&quot;),

		/* For internal use */
		prevTitle: z.string().default(&quot;&quot;),
		prevSlug: z.string().default(&quot;&quot;),
		nextTitle: z.string().default(&quot;&quot;),
		nextSlug: z.string().default(&quot;&quot;),
	}),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. src/types/config.ts 中添加CommentConfig和WalineConfig&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;export type BlogPostData = {
	body: string;
	title: string;
	published: Date;
	description: string;
	tags: string[];
	draft?: boolean;
	image?: string;
	category?: string;
	prevTitle?: string;
	prevSlug?: string;
	nextTitle?: string;
	nextSlug?: string;
	comments?: boolean;
};

export type ExpressiveCodeConfig = {
	theme: string;
};

export type CommentConfig = {
	waline?: WalineConfig;
};

type WalineConfig = {
	serverURL: string;
	login?: string;
};

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6. src/config.ts中添加commentConfig&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import type {
	ExpressiveCodeConfig,
	LicenseConfig,
	NavBarConfig,
	ProfileConfig,
	SiteConfig,
	CommentConfig
} from &quot;./types/config&quot;;

export const commentConfig: CommentConfig = {
	waline: {
	    //这里填入#1 Docker宿主机的IP以及Docker中Waline容器的端口
		serverURL: &apos;http://localhost:8360&apos;, 
		login: &apos;enable&apos;
	}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7. src/pages/posts/[...slug].astro中引入评论模块&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;---
import path from &quot;node:path&quot;;
import License from &quot;@components/misc/License.astro&quot;;
import Markdown from &quot;@components/misc/Markdown.astro&quot;;
import I18nKey from &quot;@i18n/i18nKey&quot;;
import { i18n } from &quot;@i18n/translation&quot;;
import MainGridLayout from &quot;@layouts/MainGridLayout.astro&quot;;
import { getSortedPosts } from &quot;@utils/content-utils&quot;;
import { getDir, getPostUrlBySlug } from &quot;@utils/url-utils&quot;;
import { Icon } from &quot;astro-icon/components&quot;;
import { licenseConfig } from &quot;src/config&quot;;
import ImageWrapper from &quot;../../components/misc/ImageWrapper.astro&quot;;
import PostMetadata from &quot;../../components/PostMeta.astro&quot;;
import { profileConfig, siteConfig } from &quot;../../config&quot;;
import { formatDateToYYYYMMDD } from &quot;../../utils/date-utils&quot;;
import Comment from &quot;@components/comments/index.astro&quot;;

export async function getStaticPaths() {
	const blogEntries = await getSortedPosts();
	return blogEntries.map((entry) =&amp;gt; ({
		params: { slug: entry.slug },
		props: { entry },
	}));
}

const { entry } = Astro.props;
const { Content, headings } = await entry.render();

const { remarkPluginFrontmatter } = await entry.render();

const jsonLd = {
	&quot;@context&quot;: &quot;https://schema.org&quot;,
	&quot;@type&quot;: &quot;BlogPosting&quot;,
	headline: entry.data.title,
	description: entry.data.description || entry.data.title,
	keywords: entry.data.tags,
	author: {
		&quot;@type&quot;: &quot;Person&quot;,
		name: profileConfig.name,
		url: Astro.site,
	},
	datePublished: formatDateToYYYYMMDD(entry.data.published),
	inLanguage: entry.data.lang
		? entry.data.lang.replace(&quot;_&quot;, &quot;-&quot;)
		: siteConfig.lang.replace(&quot;_&quot;, &quot;-&quot;),
	// TODO include cover image here
};
---
&amp;lt;MainGridLayout banner={entry.data.image} title={entry.data.title} description={entry.data.description} lang={entry.data.lang} setOGTypeArticle={true} headings={headings}&amp;gt;
    &amp;lt;script is:inline slot=&quot;head&quot; type=&quot;application/ld+json&quot; set:html={JSON.stringify(jsonLd)}&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;div class=&quot;flex w-full rounded-[var(--radius-large)] overflow-hidden relative mb-4&quot;&amp;gt;
        &amp;lt;div id=&quot;post-container&quot; class:list={[&quot;card-base z-10 px-6 md:px-9 pt-6 pb-4 relative w-full &quot;,
            {}
        ]}&amp;gt;
            &amp;lt;!-- word count and reading time --&amp;gt;
            &amp;lt;div class=&quot;flex flex-row text-black/30 dark:text-white/30 gap-5 mb-3 transition onload-animation&quot;&amp;gt;
                &amp;lt;div class=&quot;flex flex-row items-center&quot;&amp;gt;
                    &amp;lt;div class=&quot;transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/10 text-black/50 dark:text-white/50 flex items-center justify-center mr-2&quot;&amp;gt;
                        &amp;lt;Icon name=&quot;material-symbols:notes-rounded&quot;&amp;gt;&amp;lt;/Icon&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;div class=&quot;text-sm&quot;&amp;gt;{remarkPluginFrontmatter.words} {&quot; &quot; + i18n(I18nKey.wordsCount)}&amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
                &amp;lt;div class=&quot;flex flex-row items-center&quot;&amp;gt;
                    &amp;lt;div class=&quot;transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/10 text-black/50 dark:text-white/50 flex items-center justify-center mr-2&quot;&amp;gt;
                        &amp;lt;Icon name=&quot;material-symbols:schedule-outline-rounded&quot;&amp;gt;&amp;lt;/Icon&amp;gt;
                    &amp;lt;/div&amp;gt;
                    &amp;lt;div class=&quot;text-sm&quot;&amp;gt;
                        {remarkPluginFrontmatter.minutes} {&quot; &quot; + i18n(remarkPluginFrontmatter.minutes === 1 ? I18nKey.minuteCount : I18nKey.minutesCount)}
                    &amp;lt;/div&amp;gt;
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;!-- title --&amp;gt;
            &amp;lt;div class=&quot;relative onload-animation&quot;&amp;gt;
                &amp;lt;div
                    data-pagefind-body data-pagefind-weight=&quot;10&quot; data-pagefind-meta=&quot;title&quot;
                    class=&quot;transition w-full block font-bold mb-3
                    text-3xl md:text-[2.25rem]/[2.75rem]
                    text-black/90 dark:text-white/90
                    md:before:w-1 before:h-5 before:rounded-md before:bg-[var(--primary)]
                    before:absolute before:top-[0.75rem] before:left-[-1.125rem]
                &quot;&amp;gt;
                    {entry.data.title}
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;

            &amp;lt;!-- metadata --&amp;gt;
            &amp;lt;div class=&quot;onload-animation&quot;&amp;gt;
                &amp;lt;PostMetadata
                        class=&quot;mb-5&quot;
                        published={entry.data.published}
                        updated={entry.data.updated}
                        tags={entry.data.tags}
                        category={entry.data.category}
                &amp;gt;&amp;lt;/PostMetadata&amp;gt;
                {!entry.data.image &amp;amp;&amp;amp; &amp;lt;div class=&quot;border-[var(--line-divider)] border-dashed border-b-[1px] mb-5&quot;&amp;gt;&amp;lt;/div&amp;gt;}
            &amp;lt;/div&amp;gt;

            &amp;lt;!-- always show cover as long as it has one --&amp;gt;

            {entry.data.image &amp;amp;&amp;amp;
                &amp;lt;ImageWrapper id=&quot;post-cover&quot; src={entry.data.image} basePath={path.join(&quot;content/posts/&quot;, getDir(entry.id))} class=&quot;mb-8 rounded-xl banner-container onload-animation&quot;/&amp;gt;
            }


            &amp;lt;Markdown class=&quot;mb-6 markdown-content onload-animation&quot;&amp;gt;
                &amp;lt;Content /&amp;gt;
            &amp;lt;/Markdown&amp;gt;

            {licenseConfig.enable &amp;amp;&amp;amp; &amp;lt;License title={entry.data.title} slug={entry.slug} pubDate={entry.data.published} class=&quot;mb-6 rounded-xl license-container onload-animation&quot;&amp;gt;&amp;lt;/License&amp;gt;}

        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;div class=&quot;flex flex-col md:flex-row justify-between mb-4 gap-4 overflow-hidden w-full&quot;&amp;gt;
        &amp;lt;a href={entry.data.nextSlug ? getPostUrlBySlug(entry.data.nextSlug) : &quot;#&quot;}
           class:list={[&quot;w-full font-bold overflow-hidden active:scale-95&quot;, {&quot;pointer-events-none&quot;: !entry.data.nextSlug}]}&amp;gt;
            {entry.data.nextSlug &amp;amp;&amp;amp; &amp;lt;div class=&quot;btn-card rounded-2xl w-full h-[3.75rem] max-w-full px-4 flex items-center !justify-start gap-4&quot; &amp;gt;
                &amp;lt;Icon name=&quot;material-symbols:chevron-left-rounded&quot; class=&quot;text-[2rem] text-[var(--primary)]&quot; /&amp;gt;
                &amp;lt;div class=&quot;overflow-hidden transition overflow-ellipsis whitespace-nowrap max-w-[calc(100%_-_3rem)] text-black/75 dark:text-white/75&quot;&amp;gt;
                    {entry.data.nextTitle}
                &amp;lt;/div&amp;gt;
            &amp;lt;/div&amp;gt;}
        &amp;lt;/a&amp;gt;

        &amp;lt;a href={entry.data.prevSlug ? getPostUrlBySlug(entry.data.prevSlug) : &quot;#&quot;}
           class:list={[&quot;w-full font-bold overflow-hidden active:scale-95&quot;, {&quot;pointer-events-none&quot;: !entry.data.prevSlug}]}&amp;gt;
            {entry.data.prevSlug &amp;amp;&amp;amp; &amp;lt;div class=&quot;btn-card rounded-2xl w-full h-[3.75rem] max-w-full px-4 flex items-center !justify-end gap-4&quot;&amp;gt;
                &amp;lt;div class=&quot;overflow-hidden transition overflow-ellipsis whitespace-nowrap max-w-[calc(100%_-_3rem)] text-black/75 dark:text-white/75&quot;&amp;gt;
                    {entry.data.prevTitle}
                &amp;lt;/div&amp;gt;
                &amp;lt;Icon name=&quot;material-symbols:chevron-right-rounded&quot; class=&quot;text-[2rem] text-[var(--primary)]&quot; /&amp;gt;
            &amp;lt;/div&amp;gt;}
        &amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;

    &amp;lt;Comment post={entry}&amp;gt;&amp;lt;/Comment&amp;gt;

&amp;lt;/MainGridLayout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3. 注册为管理员账号&lt;/h2&gt;
&lt;p&gt;浏览器访问:http://localhost:8360/ui/register
第一个注册并登录Waline的账号会被设置为管理员账号，使用该账号可以管理评论及账号&lt;/p&gt;
&lt;h2&gt;4. 其他的优化项&lt;/h2&gt;
&lt;h3&gt;1. 在src/config.ts中，目前的写法宝暴露了IP端口，可以用Nginx反代的方式隐藏&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;在DNS解析添加子域名解析
&lt;img src=&quot;http://img.yuumiblog.com/b6c5c1db-f0e4-4f46-9bdf-c55ef41d97fd.png&quot; alt=&quot;DNS解析&quot; /&gt;&lt;/li&gt;
&lt;li&gt;使用CertBot添加Https证书
&amp;lt;br&amp;gt;
安装CertBot&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update
sudo apt install certbot -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;申请证书&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo certbot certonly --standalone -d comments.selibert.xyz
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;在Nginx添加对应的server块&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# ------------------------------
# Waline 评论系统（HTTPS）
# ------------------------------
server {
    listen 443 ssl;
    server_name comments.selibert.xyz;

    ssl_certificate /etc/letsencrypt/live/comments.selibert.xyz/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/comments.selibert.xyz/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location / {
        proxy_pass http://localhost:8360;  # Waline的IP端口
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_http_version 1.1;
        proxy_set_header Connection &quot;&quot;;
        proxy_set_header X-Forwarded-Proto https;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. （可选）可以默认隐藏登录按钮，让用户默认以匿名方式评论&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;---
import { commentConfig } from &quot;../../config&quot;;

interface Props {
	path: string;
}

const config = {
	...commentConfig.waline,
	el: &quot;#waline&quot;,
	path: Astro.props.path,
	dark: &quot;html.dark&quot;,
	wordLimit: [&quot;2&quot;, &quot;300&quot;],
};
---
&amp;lt;!-- Waline --&amp;gt;
&amp;lt;div class=&quot;relative w-full&quot;&amp;gt;
    &amp;lt;div id=&quot;waline&quot;&amp;gt;&amp;lt;/div&amp;gt;
    &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://unpkg.com/@waline/client@v3/dist/waline.css&quot; /&amp;gt;
    &amp;lt;script type=&quot;module&quot; is:inline define:vars={{ config }}&amp;gt;
        import { init } from &apos;https://unpkg.com/@waline/client@v3/dist/waline.js&apos;;
        init(config);
    &amp;lt;/script&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;style is:inline&amp;gt;
.wl-btn {
    border-color: transparent;
    color: transparent;
    background-color: transparent;
    cursor: default;
}
&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Astro 博客 CI/CD 自动部署教程：使用 GitHub Actions 实现服务器自动发布</title><link>https://yuumiblog.com/posts/blogbuild/blogbuildnote03/</link><guid isPermaLink="true">https://yuumiblog.com/posts/blogbuild/blogbuildnote03/</guid><description>本文实现通过配置Github的CICD实现代码提交后自动在指定服务器上部署Astro服务</description><pubDate>Wed, 15 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;0. 准备工作&lt;/h2&gt;
&lt;p&gt;你需要准备&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一台有&lt;code&gt;公网IP&lt;/code&gt;的&lt;code&gt;Linux&lt;/code&gt;服务器&lt;/li&gt;
&lt;li&gt;一个可以运行的&lt;code&gt;Astro&lt;/code&gt;项目&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1. Linux上的部署&lt;/h2&gt;
&lt;p&gt;在Linux服务器上依次执行如下步骤：&lt;/p&gt;
&lt;h3&gt;1. 创建角色&lt;/h3&gt;
&lt;p&gt;创建Github部署时登录的角色&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
推荐创建一个用户专门用于Github的CI/CD，不建议使用root角色进行CI/CD；&amp;lt;br&amp;gt;
如果使用root需要配置Root的远程登录&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;sudo adduser github-ci
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 创建文件夹&lt;/h3&gt;
&lt;p&gt;创建放置部署文件的文件夹并给CI角色赋权&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir /var/www/releases
chown -R github-ci:github-ci /var/www/releases
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 安装openssh-server服务&lt;/h3&gt;
&lt;p&gt;先检查服务器上是否安装了ssh server&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;which ssh
which sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果显示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@:~/.ssh# which ssh
/usr/bin/ssh
root@:~/.ssh# which sshd
/usr/sbin/sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表示已经安装了SSH服务，请跳转到；&amp;lt;br&amp;gt;
如果未显示一个路径地址，则表示没有安装，需要安装，步骤如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Ubuntu / Debian&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装服务
sudo apt update
sudo apt install -y openssh-server
# 设置自启
sudo systemctl enable ssh
# 启动服务
sudo systemctl start ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CentOS / RHEL&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装服务
sudo yum install -y openssh-server
# 设置自启
sudo systemctl enable sshd
# 启动服务
sudo systemctl start sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 权限配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 先切换到CI角色
su - github-ci
# 生成SSH的公钥和私钥
chmod 700 ~/.ssh
ssh-keygen -t ed25519
# 查看~/.ssh文件夹下有没有authorized_keys文件，如果没有，则创建authorized_keys并赋权
touch ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
# 将公钥（~/.ssh/id_ed25519.pub）的内容添加到 ~/.ssh/authorized_keys
grep &quot;$(cat ~/.ssh/id_ed25519.pub)&quot; ~/.ssh/authorized_keys || cat ~/.ssh/id_ed25519.pub &amp;gt;&amp;gt; ~/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 安装Nginx服务&lt;/h3&gt;
&lt;p&gt;先查看服务器有没有安装Nginx&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nginx -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果提示：&lt;code&gt;command not found&lt;/code&gt;，则表示未安装Nginx，安装步骤如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ubuntu / Debian&lt;pre&gt;&lt;code&gt;# 安装服务
sudo apt update
sudo apt install -y nginx
# 设置自启
sudo systemctl enable nginx
# 启动服务
sudo systemctl start nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;CentOS / RHEL&lt;pre&gt;&lt;code&gt;# 安装服务
sudo yum install -y nginx
# 设置自启
sudo systemctl enable nginx
# 启动服务
sudo systemctl start nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;验证安装情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl status nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果显示下列内容，则表示已经安装并启动了Nginx：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;github-ci@:~/.ssh$ systemctl status nginx
● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/lib/systemd/system/nginx.service; disabled; vendor preset: enabled)
     Active: active (running) since Mon 2026-03-23 15:58:11 CST; 1 week 2 days ago
       Docs: man:nginx(8)
    Process: 2379804 ExecReload=/usr/sbin/nginx -g daemon on; master_process on; -s reload (code=exited, status=0/SUCCESS)
   Main PID: 2330136 (nginx)
      Tasks: 3 (limit: 1834)
     Memory: 10.2M
        CPU: 18.277s
     CGroup: /system.slice/nginx.service
             ├─2330136 &quot;nginx: master process /usr/sbin/nginx -g daemon on; master_process on;&quot;
             ├─2379805 &quot;nginx: worker process&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot;
             └─2379806 &quot;nginx: worker process&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot; &quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6. 配置Nginx&lt;/h3&gt;
&lt;p&gt;修改Nginx的配置文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;user www-data;
worker_processes auto;

events {
    worker_connections 1024;
}

http {
    server {
        listen 80;
        server_name _;
    
        root /var/www/current/dist;
        index index.html;
    
        location / {
            try_files $uri $uri/ /index.html;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改完成后，运行检查：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo nginx -t
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果显示下列内容，则显示配置文件没有问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，需要为CI用户添加配置允许无密码执行 nginx reload：&amp;lt;br&amp;gt;
执行：&lt;code&gt;sudo visudo&lt;/code&gt;
在文件尾部添加如下内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;github-ci ALL=(ALL) NOPASSWD:/bin/systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加完成后键入：&lt;code&gt;ctrl&lt;/code&gt;+&lt;code&gt;o&lt;/code&gt;&amp;lt;br&amp;gt;
显示 &lt;code&gt;File Name to Write: /etc/sudoers&lt;/code&gt;，然后按&lt;code&gt;Enter&lt;/code&gt;保存&lt;/p&gt;
&lt;h2&gt;2. Github上的配置：&lt;/h2&gt;
&lt;p&gt;根据如下路径配置Github远程登录服务器相关内容：
&lt;img src=&quot;https://img.yuumiblog.com/bf51e55282c14e9c82d887a15a2c4b7d.png&quot; alt=&quot;GitHub Actions Secrets 配置界面&quot; /&gt;&lt;/p&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;HOST&lt;/td&gt;
&lt;td&gt;服务器的公网IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;USER&lt;/td&gt;
&lt;td&gt;1-1中创建的角色名，示例为github-ci&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSH_KEY&lt;/td&gt;
&lt;td&gt;1-4中生成的SSH私钥&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;3. 编写deploy.yml&lt;/h2&gt;
&lt;p&gt;在项目的根目录下，在&lt;code&gt;.github/workflows/&lt;/code&gt;下创建&lt;code&gt;deploy.yml&lt;/code&gt;，如果没有则创建。&amp;lt;br&amp;gt;
&lt;code&gt;deploy.yml&lt;/code&gt;文件内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: Astro deploy
on:
  push:
    branches:
      - main # 这里是分支名称
jobs:
  build:
    runs-on: ubuntu-latest # 编译运行的平台
    steps: # 编译运行的步骤
      - name: 拉代码
        uses: actions/checkout@v4
      - name: Node环境
        uses: actions/setup-node@v4
        with:
          node-version: 20
      - name: 安装 pnpm
        uses: pnpm/action-setup@v3
        with:
          version: 9
          run_install: false
      - name: 安装依赖
        run: pnpm install --no-frozen-lockfile
      - name: 构建
        run: pnpm build
      - name: 上传文件
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USER }}
          key: ${{ secrets.SSH_KEY }}
          source: &quot;dist/&quot;
          target: &quot;/var/www/releases/${{ github.run_id }}&quot;
      - name: 部署
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USER }}
          key: ${{ secrets.SSH_KEY }}
          script: |
            mkdir -p /var/www/releases/${{ github.run_id }}
            ln -sfn /var/www/releases/${{ github.run_id }} /var/www/current
            systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. 编写博文&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;src/content/post&lt;/code&gt;文件夹下，创建一个新的Markdown文件，或者在控制台运行下列命令生成新的文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm new-post &amp;lt;filename&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在其中编写你的博文内容后，将markdown文件commit并push到Github后，在Github中查看编译运行情况：
&lt;img src=&quot;https://img.yuumiblog.com/21a14d57519d4a58afce013c98e00f20.png&quot; alt=&quot;如何在Action页面查看每次CI/CD&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;左侧的Astro Deploy是deploy.yml中的name键的值 &amp;lt;br&amp;gt;
点击右侧的Commit Message可以查看这次Push对应的Action详情&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https://img.yuumiblog.com/f9f67ca71acc43c197908f79d3a9aff5.png&quot; alt=&quot;如何在Action页面查看某次CI/CD&quot; /&gt;
这里可以看到每一步操作的日志信息，如果出现错误则进行对应的处理后重新部署&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
如果你修改了服务器配置或者Github相关配置，没有修改代码但是又需要重新触发CI/CD，你有如下两种方案&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;在Action左侧选择Astro Deploy，在右侧单击最后一次Push，然后在右侧点选&lt;code&gt;Re-run all jobs&lt;/code&gt;&amp;lt;br&amp;gt;
&lt;img src=&quot;https://img.yuumiblog.com/ea8056ade5c94ece81c2280d4c24688c.png&quot; alt=&quot;如何重新触发CI/CD&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;你可以通过Commit并Push一次空的提交来触发CI/CD &amp;lt;br&amp;gt;
命令为：&lt;code&gt;git commit --allow-empty -m &amp;lt;commit message&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;5. 回滚版本&lt;/h2&gt;
&lt;p&gt;你可以通过如下的命令将发布的版本回退到指定的版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ln -sfn /var/www/releases/旧版本ID /var/www/current
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个旧版本ID为github执行CI/CD的run-id &amp;lt;br&amp;gt;
github.run_id 是 GitHub Actions 为每次 workflow run 自动生成的唯一整数 ID，用于区分不同部署版本。 &amp;lt;br&amp;gt;
这一项是在&lt;code&gt;deploy.yml&lt;/code&gt;中写到的：&lt;code&gt;mkdir -p /var/www/releases/${{ github.run_id }}&lt;/code&gt; &amp;lt;br&amp;gt;
在Action中，找到你需要回退到的那一次Push，在详情页面的URL可以找到这次CI/CD对应的run-id &amp;lt;br&amp;gt;
&lt;img src=&quot;https://img.yuumiblog.com/a75398a786c147bebfd14a9f5a0a421a.png&quot; alt=&quot;服务器上的run-id&quot; /&gt;
&lt;img src=&quot;https://img.yuumiblog.com/3b11f0558b1344fdb597f4b085d787d5.png&quot; alt=&quot;github中的run-id&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Astro 博客搭建教程：框架选择与项目初始化完整指南</title><link>https://yuumiblog.com/posts/blogbuild/blogbuildnote02/</link><guid isPermaLink="true">https://yuumiblog.com/posts/blogbuild/blogbuildnote02/</guid><description>记录Astro博客框架的选择、主题推荐及项目构建步骤</description><pubDate>Fri, 10 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. 技术选型&lt;/h2&gt;
&lt;p&gt;个人博客框架大致分为两类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;静态博客&lt;/strong&gt;：例如 &lt;a href=&quot;https://astro.build/&quot;&gt;Astro&lt;/a&gt;、&lt;a href=&quot;https://gohugo.io/&quot;&gt;Hugo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态博客&lt;/strong&gt;：例如 &lt;a href=&quot;https://wordpress.com/zh-cn/&quot;&gt;WordPress&lt;/a&gt;，或者自己编写 &lt;a href=&quot;https://spring.io/projects/spring-boot&quot;&gt;Spring Boot&lt;/a&gt; + &lt;a href=&quot;https://cn.vuejs.org/&quot;&gt;Vue&lt;/a&gt; / &lt;a href=&quot;https://react.dev/&quot;&gt;React&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当然也可以在CSDN、稀土掘金之类的平台上直接上传文档。&lt;/p&gt;
&lt;p&gt;静态博客和动态博客的运行方式完全不同：静态博客在写完文章后，通过构建生成静态HTML页面；动态博客则在每次访问时实时从服务器或数据库获取内容并渲染页面。以下是静态博客和动态博客的简单比较&lt;/p&gt;
&lt;h3&gt;静态博客和动态博客的优缺点&lt;/h3&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;/td&gt;
&lt;td&gt;❌ 每次请求需查询数据库和渲染，速度可能较慢&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;成本&lt;/td&gt;
&lt;td&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;/td&gt;
&lt;td&gt;⚠️ 若配置不当，可能存在 SQL 注入或其他安全风险&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;维护&lt;/td&gt;
&lt;td&gt;✅ 更新文章只需上传文件，可结合 Git 自动构建&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;/td&gt;
&lt;td&gt;✅ 功能更丰富，可实现用户交互&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;基于个人网站的需求和机器性能的限制，本文选择使用Astro作为博客框架&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;2. 选择主题&lt;/h2&gt;
&lt;p&gt;Astro 官网的&lt;a href=&quot;https://astro.js.cn/themes/&quot;&gt;“资源-主题”页签&lt;/a&gt;页面提供大量免费和付费主题。&amp;lt;br&amp;gt;
当然你也可以在其他推荐博文寻找合适主题，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.moewah.com/posts/5-best-astro-blog-themes-recommended/&quot;&gt;5个最好用的Astro博客主题推荐 - 喵斯基部落&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://eastondev.com/blog/zh/posts/dev/20251202-best-astro-blog-themes/&quot;&gt;5个最好用的Astro博客主题，附详细安装配置教程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rzzz.net/daily-notes/2024/2024-10-16.html&quot;&gt;推荐10个Astro简洁适用于个人网站的博客主题&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大部分主题的 GitHub 仓库都会提供部署好的 Demo 页面，方便预览效果。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;本站使用的主题是&lt;a href=&quot;https://github.com/saicaca/fuwari&quot;&gt;Fuwari&lt;/a&gt;&lt;/strong&gt;
::github{repo=&quot;saicaca/fuwari&quot;}&lt;/p&gt;
&lt;h2&gt;3. 构建项目&lt;/h2&gt;
&lt;h3&gt;第一步：fork上述的Fuwari项目&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://img.yuumiblog.com/46ce7cb6920d40c19445b779f88910d2.png&quot; alt=&quot;image-20260331162836865&quot; /&gt;
&lt;img src=&quot;https://img.yuumiblog.com/e818ff8f4f624c72b7a5996deefa78db.png&quot; alt=&quot;image-20260331163013789&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;第二步：使用VSCode或者Webstorm将仓库克隆到本地&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/your-username/fuwari.git
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;第三步：安装依赖&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 安装pnpm，如果已安装 pnpm 可跳过此步骤
npm install -g pnpm

# 安装依赖
pnpm install
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;第四步：启动项目&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pnpm dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://img.yuumiblog.com/6f0154c825da434b973c7750433cebb1.png&quot; alt=&quot;image-20260331164355107&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个时候就可以打开 &lt;a href=&quot;http://localhost:4321/&quot;&gt;http://localhost:4321/&lt;/a&gt;查看初试页面效果了&lt;/p&gt;
&lt;h2&gt;4. 修改配置&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import type {
	ExpressiveCodeConfig,
	LicenseConfig,
	NavBarConfig,
	ProfileConfig,
	SiteConfig,
} from &quot;./types/config&quot;;
import { LinkPreset } from &quot;./types/config&quot;;

// 网站基本信息配置
export const siteConfig: SiteConfig = {
	title: &quot;Fuwari&quot;, // 网站的标题
	subtitle: &quot;Demo Site&quot;, // 网站的二级标题
	lang: &quot;en&quot;, // 网站的使用语言
	themeColor: {
		hue: 250, // 默认主题色
		fixed: false, // fixed为true则隐藏调整主题色的选择器
	},
	
	// 主页图片相关
	banner: {
		enable: false, // 是否启用主页图片
		src: &quot;assets/images/demo-banner.png&quot;, // 主页图文件路径，基于src文件夹
		position: &quot;center&quot;, // 显示方式，可选项为：&apos;top&apos;, &apos;center&apos;, &apos;bottom&apos;。&apos;center&apos;为默认选项
		credit: {
			enable: false, // 显示图片的版权信息
			text: &quot;&quot;, // 版权信息文本
			url: &quot;&quot;, // 点击版权信息文本后跳转到的地址
		},
	},
	toc: {
		enable: true, // 博文右侧是否显示目录
		depth: 2, // 目录层级
	},
	
	// 网站图标
	favicon: [
		// 置空则显示默认图标
		// {
		//   src: &apos;/favicon/icon.png&apos;,    // 网站图标图文件路径，基于public文件夹
		//   theme: &apos;light&apos;,              // (可选项) 可选项：&apos;light&apos; or &apos;dark&apos;, 仅在需要在亮色和暗色模式显示不同图标时配置
		//   sizes: &apos;32x32&apos;,              // (可选项) 网站图标尺寸
		// }
	],
};

// 导航栏配置
export const navBarConfig: NavBarConfig = {
	links: [
		LinkPreset.Home,
		LinkPreset.Archive,
		LinkPreset.About,
		// 链接
		{
			name: &quot;GitHub&quot;,
			url: &quot;https://github.com/saicaca/fuwari&quot;,
			external: true, // 外部链接or内部链接
		},
	],
};

// 个人信息配置
export const profileConfig: ProfileConfig = {
	avatar: &quot;assets/images/demo-avatar.png&quot;, // 头像图片地址。基于src文件夹
	name: &quot;Lorem Ipsum&quot;, // 昵称
	bio: &quot;Lorem ipsum dolor sit amet, consectetur adipiscing elit.&quot;, // 个人简介
	
	// 配置个人信息下方显示的链接
	links: [
		{
			name: &quot;Twitter&quot;,            // 链接名称
			icon: &quot;fa6-brands:twitter&quot;, // 链接图标，可以在https://icones.js.org找到更多的icon
			url: &quot;https://twitter.com&quot;, // 链接地址
		},
		{
			name: &quot;Steam&quot;,
			icon: &quot;fa6-brands:steam&quot;,
			url: &quot;https://store.steampowered.com&quot;,
		},
		{
			name: &quot;GitHub&quot;,
			icon: &quot;fa6-brands:github&quot;,
			url: &quot;https://github.com/saicaca/fuwari&quot;,
		},
	],
};

// 博文下方的许可协议配置
export const licenseConfig: LicenseConfig = {
	enable: true,
	name: &quot;CC BY-NC-SA 4.0&quot;,
	url: &quot;https://creativecommons.org/licenses/by-nc-sa/4.0/&quot;,
};

// markdown代码框主题配置
export const expressiveCodeConfig: ExpressiveCodeConfig = {
	theme: &quot;github-dark&quot;,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启项目后即可查看修改的内容&lt;/p&gt;
</content:encoded></item><item><title>Intel Mac Mini 安装 Ubuntu（Intel 版）详细教程</title><link>https://yuumiblog.com/posts/blogbuild/blogbuildnote01/</link><guid isPermaLink="true">https://yuumiblog.com/posts/blogbuild/blogbuildnote01/</guid><description>本文实现在一台Intel芯片的MacMini上安装Linux系统</description><pubDate>Wed, 08 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;0.准备工作&lt;/h2&gt;
&lt;p&gt;你需要准备:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一台MacMini(intel芯片)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;因为Apple Silicon的Mac需要使用特定的Linux发行版本，而本人购入的是二手的MacMini(Late 2014)，型号为A1347，CPU为4代i7，&amp;lt;strong&amp;gt;因此本文不适用于M芯片的MacMini&amp;lt;/strong&amp;gt;
在MacOS下，点击左上角苹果标志-关于本机 即可查看CPU版本&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;一个U盘，建议16G及以上&lt;/li&gt;
&lt;li&gt;Ubuntu系统镜像&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1.数据备份&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;使用MacOS时间机器备份数据:&lt;a href=&quot;https://support.apple.com/zh-cn/104984&quot;&gt;使用“时间机器”备份你的 Mac&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;如果计划抹除MacOS使用Linux单系统，请先使用当前MacOS创建系统盘:&lt;a href=&quot;https://support.apple.com/zh-cn/101578&quot;&gt;创建可引导的 macOS 安装器&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;手动备份重要的数据到其他存储介质&lt;/li&gt;
&lt;li&gt;如果是双系统，则需要提前使用磁盘工具创建空闲分区用来安装Linux&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2.制作Linux启动盘&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;下载Linux系统镜像文件，本文选择的Ubuntu:&lt;a href=&quot;https://releases.ubuntu.com/22.04&quot;&gt;下载页面&lt;/a&gt;，&lt;a href=&quot;https://releases.ubuntu.com/22.04/ubuntu-22.04.5-desktop-amd64.ISO&quot;&gt;下载链接&lt;/a&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;受限于网络，可以选择其他镜像站下载，如 &lt;a href=&quot;https://launchpad.net/ubuntu/+mirror/mirrors.qlu.edu.cn-release&quot;&gt;齐鲁工业大学镜像站&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;你可以访问 &lt;a href=&quot;https://launchpad.net/ubuntu/+cdmirrors&quot;&gt;Official CD Mirrors for Ubuntu&lt;/a&gt;，在China页签下找到其他的镜像站&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;下载启动盘工具，本文选择Ventoy:&lt;a href=&quot;https://github.com/ventoy/Ventoy/releases&quot;&gt;Github-Ventoy&lt;/a&gt;，&lt;a href=&quot;https://www.ventoy.net/cn/download.html&quot;&gt;官方下载页面&lt;/a&gt;，按照操作系统下载对应版本
&lt;img src=&quot;https://img.yuumiblog.com/bb6058dc50ae45cf95e4c75439bea60b.png&quot; alt=&quot;image-20260325150056106&quot; /&gt;&lt;/li&gt;
&lt;li&gt;解压Ventoy，双击exe文件启动&lt;/li&gt;
&lt;li&gt;插入用来做启动盘的U盘，在Ventoy中选择对应的U盘，点击安装
&lt;blockquote&gt;
&lt;p&gt;安装前后
&lt;img src=&quot;https://img.yuumiblog.com/adce64d613a749dd9f67a46fd2c52bcd.png&quot; alt=&quot;官网截图&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;将1中下载的ISO文件放在U盘内&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3.从启动盘安装Linux&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;重启MacMini，开机时按住Option(⌥)键，选择U盘启动&lt;/li&gt;
&lt;li&gt;在Ventoy页面，上下选择上一步放入的ISO文件&lt;/li&gt;
&lt;li&gt;磁盘分配：
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;首先，&amp;lt;strong&amp;gt;不可以删除和重建EFI分区（约 200MB，FAT32）！&amp;lt;/strong&amp;gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;u&amp;gt;&amp;lt;strong&amp;gt;如果你想要抹除MacOS，使用单Linux系统：&amp;lt;/strong&amp;gt;&amp;lt;/u&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;删除除 EFI 分区以外的所有分区&lt;/li&gt;
&lt;li&gt;保留 EFI 分区，并设置挂载点为：/boot/efi；不要勾选格式化&lt;/li&gt;
&lt;li&gt;新建主分区，并设置挂载点为：/，设置文件系统为：ext4，勾选格式化&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;u&amp;gt;&amp;lt;strong&amp;gt;但是，如果你想要使用MacOS+Linux双系统:&amp;lt;/strong&amp;gt;&amp;lt;/u&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;不要删除 macOS 分区&lt;/li&gt;
&lt;li&gt;选择空闲空间，新建主分区，并设置挂载点为：/，设置文件系统为：ext4，勾选格式化&lt;/li&gt;
&lt;li&gt;选择已有 EFI 分区，并设置挂载点为：/boot/efi，不要格式化&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;Ubuntu安装程序，按照安装程序设置地区、键盘布局、用户名密码等，等待安装完成&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;4.完成&lt;/h2&gt;
&lt;p&gt;至此系统安装完毕，可以使用你的Linux系统了
&lt;img src=&quot;https://img.yuumiblog.com/b8c4ffa78f93459f91281c2b8fada3ef.png&quot; alt=&quot;39f03bf24a72b10229e2aa43eb821a72&quot; /&gt;&lt;/p&gt;
</content:encoded></item></channel></rss>