博客搭建教程【懒人版】 前言:
本来是没想着把这篇放博客上的,但是想着下学期可能要给新生讲学长课堂就还是去找了一下之前的聊天窗口,让ai总结了一下全过程。网上的视频教程很多,但本人非常爱偷懒,所以自己另外弄了一个图床来自动上传md文件里的图片,还有一个一键上传博客的脚本。不然得自己每次上传都把图片放文件夹里,还要自己敲一堆命令行。
【还是那句话,计算机岂是如此不便之物?】
学习网络安全方向,拥有一个自己的个人技术博客,不仅是记录技术、沉淀知识的极佳阵地,也是展示个人实力的“第二张身份证”。
很多新手会选择购买昂贵的云服务器(VPS)来搭建 WordPress 等动态网站。但从网络安全和运维的角度来看,动态网站存在较多的数据库交互和后台入口,容易产生 SQL 注入、XSS、弱口令爆破等安全隐患 。同时,服务器维护、备案、续费也是一笔不小的开销。
因此,本文将带你采用现代极客最推崇的 静态博客方案 —— Hexo 框架 + Butterfly 主题 + 腾讯云 COS 黄金图床 + Vercel 全球部署 + 自动化中控脚本 。
这套方案拥有三个核心优势:
零成本 :服务器与 CDN 节点由全球大厂提供,除十来块钱的域名费外,其余完全免费白嫖。
零攻击面 :全站纯静态 HTML 页面,无数据库,无后端交互,天然免疫 99% 的 Web 攻击。
极佳写作体验 :配合 Typora 与自动化脚本,本地一键排版、一键公网发布。
一、 总体结构 在开始搭建前,我们先来理清整套博客系统的流量流向与数据传输链路:
1 2 3 4 5 6 7 8 9 10 11 [本地 Typora 写作] ➔ [一键中控台.bat 自动格式化] │ ▼ (Git 极速推送) [GitHub 私有仓库] │ ▼ (自动触发 Hook) [Vercel 云端部署] ➔ [绑定自定义域名 (全国秒开)] │ (图片自动异步上传) ▼ [腾讯云 COS 存储桶]
下面我们一步步打通这条流水线。
二、 第一阶段:本地环境准备与 Hexo 初始化 Hexo 是基于 Node.js 运行的静态网页生成引擎。我们需要在本地电脑安装环境:
1. 安装 Node.js 与 Git
2. 全局安装 Hexo 并初始化项目 为了加快下载速度,建议先将 npm 切换至国内阿里镜像源,随后挑选一个非系统盘(如 D 盘)初始化博客:
1 2 3 4 5 6 7 8 9 10 11 # 切换阿里镜像源 npm config set registry https://registry.npmmirror.com # 全局安装 Hexo 命令行工具 npm install -g hexo-cli # 初始化你的博客主文件夹(以 D:\myblog 为例) D: hexo init myblog cd myblog npm install
三、 第二阶段:Butterfly 主题与汉化美化 Butterfly 是目前 Hexo 圈子里最流行、颜值最高的响应式主题之一。
1. 下载主题与渲染引擎 在博客根目录(myblog)下执行:
1 2 3 4 5 6 7 8 # 安装主题包 npm install hexo-theme-butterfly --save # 安装 Pug 和 Stylus 渲染器 npm install hexo-renderer-pug hexo-renderer-stylus --save # 备份主题默认配置文件至根目录(便于独立配置,此为 Windows 命令) copy .\node_modules\hexo-theme-butterfly\_config.yml _config.butterfly.yml
2. 修改配置启用主题
四、 第三阶段:腾讯云 COS 黄金图床(摆脱图片裂开痛点) Markdown 文件的痛点是本地图片上传到公网容易裂开。传统的 GitHub 图床在大陆常受网络限制,我们采用 腾讯云对象存储(COS) 方案。
1. 腾讯云端配置
登录腾讯云控制台,搜索进入 “对象存储(COS)” ,创建存储桶(Bucket)。
访问权限(核心重点) :必须选择 公有读私有写 。
安全提示 :若选“私有”,读者看不了图;若选“公有读写”,黑客可任意上传恶意文件导致欠费。
获取 API 密钥 :在控制台右上角头像 ➔ API密钥管理 ➔ 新建一对 SecretId 和 SecretKey。
2. 客户端 Typora + PicGo 终极配置
下载开源上传工具 PicGo 。
在 PicGo 图床设置 ➔ 选择 “腾讯云COS” ,填入你的 SecretId、SecretKey、存储区域(如 ap-chengdu)、存储桶名(格式为 xxx-123456789,带数字),AppId 留空,点击确定并设为默认。
Typora 绑定 :强烈推荐使用 Typora 0.9.98 最后的免费 Beta 绿色版。你可以从 GitHub 干净的备份仓库中下载: 👉 点击这里:前往 GitHub 下载 Typora 0.9.98 绿色版 下载后直接解压运行即可,打开 偏好设置 ➔ 图像 ➔ 插入图片时选择 无特殊操作 或 复制到本地,服务选择 PicGo (app),并填入你本地 PicGo.exe 的绝对路径。
测试成功后,以后粘贴图片,链接会自动替换为公网直链!
五、 第四阶段:极客的“全自动博客中控台” 每次更新博客都要反复敲 Git 命令,实在枯燥,所以搞了一套中控脚本:建文章、补标签、提交、推送全包了。
先说清楚为什么是两个文件。
早期版本我只写了一个纯 .bat,能跑,但很脆。踩过的坑大致三类:批处理是按系统代码页逐字节 读自己的,chcp 65001 的切换时机不可靠,中文经常被按 GBK 解码成乱码,而乱码又会反过来被当成命令执行;批处理的 goto 和标签靠字节偏移定位,文件换行符一乱解析就错位,直接跑到文件末尾把窗口关掉。
所以现在把逻辑搬进了 PowerShell,.bat 只留一个纯 ASCII 的启动器 ——整个文件里一个中文字符都没有,从根上就没有编码问题。
把下面两个文件放在同一个文件夹 里(建议直接放桌面),以后双击 一键更新博客.bat 就行。
1. 一键更新博客.bat(启动器) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @echo off setlocal set "TOOL=%~dp0blog-tool.ps1"if not exist "%TOOL% " ( echo [ERROR] Missing file: echo %TOOL% echo blog-tool.ps1 must stay in the same folder as this .bat pause exit /b 1 ) where pwsh >nul 2 >&1 if %errorlevel% ==0 ( pwsh -NoProfile -ExecutionPolicy Bypass -File "%TOOL% " ) else ( powershell -NoProfile -ExecutionPolicy Bypass -File "%TOOL% " ) endlocal
这个文件是纯 ASCII 的,用记事本直接保存即可,不用管编码。
2. blog-tool.ps1(主程序) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 param ( [string ]$BlogDir = 'D:\myblog' ) $ErrorActionPreference = 'Continue' $script:Branch = 'main' $script:Remote = 'origin' $script:Editor = 'notepad' $script:PostsDir = Join-Path $BlogDir 'source\_posts' function Show-Header { param ([string ]$Text , [ConsoleColor ]$Color = 'Green' ) $line = '=' * 50 Write-Host $line -ForegroundColor $Color Write-Host " $Text " -ForegroundColor $Color Write-Host $line -ForegroundColor $Color } function Show-Step { param ([string ]$Text , [ConsoleColor ]$Color = 'Green' ) $line = '=' * 36 Write-Host '' Write-Host $line -ForegroundColor $Color Write-Host " $Text " -ForegroundColor $Color Write-Host $line -ForegroundColor $Color } function Write-Utf8NoBom { param ([string ]$Path , [string ]$Text ) $enc = New-Object System.Text.UTF8Encoding($false ) [IO.File ]::WriteAllText([IO.Path ]::GetFullPath($Path ), $Text , $enc ) } function Get-Stamp { return (Get-Date ).ToString('yyyy-MM-dd HH:mm:ss' ) } function Invoke-Git { param ([string []]$GitArgs ) $raw = & git -C $BlogDir @GitArgs 2 >&1 return [pscustomobject ]@ { ExitCode = $LASTEXITCODE Output = (($raw | Out-String ).Trim()) } } function Get-MissingCommits { $r = Invoke-Git @ ('log' , "$ ($script:Remote )/$ ($script:Branch )..HEAD" , '--oneline' ) return $r .Output } function Push-WithRetry { param ([int ]$MaxRetry = 3 ) for ($i = 1 ; $i -le $MaxRetry ; $i ++) { Write-Host " [推送尝试 $i /$MaxRetry ]" -ForegroundColor Gray $r = Invoke-Git @ ('push' , $script:Remote , $script:Branch ) if ($r .ExitCode -eq 0 ) { Write-Host ' [成功] 推送完成。' -ForegroundColor Green return $true } if ($i -eq 1 ) { Write-Host ' [提示] 推送被拒绝,先尝试同步远程的新提交...' -ForegroundColor Yellow $pr = Invoke-Git @ ('-c' , 'core.editor=true' , 'pull' , '--rebase' , $script:Remote , $script:Branch ) if ($pr .ExitCode -ne 0 ) { Write-Host ' [警告] 自动同步远程失败,可能需要在命令行手动处理。' -ForegroundColor Yellow $firstLines = ($pr .Output -split "`r?`n" | Where-Object { $_ } | Select-Object -First 3 ) -join ' | ' if ($firstLines ) { Write-Host " $firstLines " -ForegroundColor DarkGray } } } if ($i -lt $MaxRetry ) { Write-Host ' [失败] 5 秒后重试...' -ForegroundColor Yellow Start-Sleep -Seconds 5 } } return $false } function New-FrontMatterText { param ([string ]$Title , [string ]$Body , [string ]$Category , [string ]$Tag ) $lines = New-Object System.Collections.Generic.List[string ] $lines .Add('---' ) $lines .Add("title: $Title " ) $lines .Add("date: $ (Get-Stamp)" ) if ($Category ) { $lines .Add('categories:' ) $lines .Add(" - $Category " ) } if ($Tag ) { $lines .Add('tags:' ) $lines .Add(" - $Tag " ) } $lines .Add('---' ) $lines .Add('' ) if ($Body ) { $lines .Add($Body .TrimStart("`r" , "`n" )) } return (($lines -join "`r`n" ) + "`r`n" ) } function New-Post { Write-Host '' $title = (Read-Host '请输入文章标题' ).Trim() if (-not $title ) { Write-Host '[错误] 标题不能为空。' -ForegroundColor Red Read-Host '按回车返回菜单' return } if ($title .IndexOfAny([IO.Path ]::GetInvalidFileNameChars()) -ge 0 ) { Write-Host '[错误] 标题里含有不能用作文件名的符号: \ / : * ? " < > |' -ForegroundColor Red Read-Host '按回车返回菜单' return } $path = Join-Path $script:PostsDir ($title + '.md' ) if (Test-Path -LiteralPath $path ) { Write-Host '' Write-Host '[提示] 已存在同名文章,直接打开,不覆盖原有内容:' -ForegroundColor Yellow Write-Host " $path " -ForegroundColor DarkGray Start-Process -FilePath $script:Editor -ArgumentList @ ($path ) return } $category = (Read-Host '请输入分类 (如: Web安全攻防)' ).Trim() $tag = (Read-Host '请输入标签 (如: SQL注入)' ).Trim() $text = New-FrontMatterText -Title $title -Body '请在这里开始写你的正文...' -Category $category -Tag $tag Write-Utf8NoBom -Path $path -Text $text Write-Host '' Write-Host "[成功] 文章已创建: $path " -ForegroundColor Green Write-Host ' 正在为你打开...' -ForegroundColor DarkGray Start-Process -FilePath $script:Editor -ArgumentList @ ($path ) } function Sync-Blog { if (-not (Test-Path -LiteralPath (Join-Path $BlogDir '.git' ))) { Write-Host "[错误] 这里不是 git 仓库: $BlogDir " -ForegroundColor Red Write-Host ' 请检查脚本顶部的 BlogDir 设置。' -ForegroundColor Yellow Read-Host '按回车返回菜单' return } Show-Step '正在获取远程状态...' $f = Invoke-Git @ ('fetch' , $script:Remote ) if ($f .ExitCode -ne 0 ) { Write-Host '[警告] 无法连接 GitHub,推送时可能会失败。' -ForegroundColor Yellow Write-Host ' 你的改动仍然保存在本地,不会丢失。' -ForegroundColor DarkGray } if (Get-MissingCommits ) { Show-Step '发现上次未推送的提交,正在补推...' if (-not (Push-WithRetry )) { Write-Host '' Write-Host '[错误] 补推失败。你的文章已保存在本地,不会丢失。' -ForegroundColor Red Write-Host ' 请检查网络或 SSH 权限后重新运行。' -ForegroundColor Yellow Read-Host '按回车返回菜单' return } } Show-Step '正在扫描外部直接拖入的裸文章...' $bareFiles = @ () if (Test-Path -LiteralPath $script:PostsDir ) { foreach ($file in (Get-ChildItem -LiteralPath $script:PostsDir -Filter '*.md' -File -ErrorAction SilentlyContinue)) { $firstLine = Get-Content -LiteralPath $file .FullName -TotalCount 1 -Encoding UTF8 -ErrorAction SilentlyContinue if ($firstLine -ne '---' ) { $bareFiles += $file } } } if ($bareFiles .Count -eq 0 ) { Write-Host ' 没有发现裸文件,全部文章都有 front-matter。' -ForegroundColor DarkGray } else { foreach ($file in $bareFiles ) { Write-Host '' Write-Host "[发现裸文章]: $ ($file .BaseName)" -ForegroundColor Yellow $category = (Read-Host ' 请输入分类 (直接回车可跳过)' ).Trim() $tag = (Read-Host ' 请输入标签 (直接回车可跳过)' ).Trim() $body = [IO.File ]::ReadAllText($file .FullName, [Text.Encoding ]::UTF8) $text = New-FrontMatterText -Title $file .BaseName -Body $body -Category $category -Tag $tag Write-Utf8NoBom -Path $file .FullName -Text $text Write-Host ' 已自动补全 front-matter。' -ForegroundColor Green } } Show-Step '正在打包所有修改...' Invoke-Git @ ('add' , '-A' ) | Out-Null $commit = Invoke-Git @ ('commit' , '-m' , "Auto Sync: $ (Get-Stamp)" ) if ($commit .ExitCode -ne 0 ) { Write-Host '[提示] 没有新的文件需要提交。' -ForegroundColor Yellow if (Get-MissingCommits ) { Write-Host ' 但本地有未推送的提交,正在推送...' -ForegroundColor Yellow if (Push-WithRetry ) { Write-Host '[成功] 已推送!Vercel 正在后台编译。' -ForegroundColor Green } else { Write-Host '[错误] 推送失败,下次运行本工具会自动重试。' -ForegroundColor Red } } else { Write-Host ' 本地和线上已经完全同步。' -ForegroundColor Green } Read-Host '按回车返回菜单' return } Write-Host '[成功] 已提交修改。' -ForegroundColor Green Show-Step '正在推送到 GitHub...' if (-not (Push-WithRetry )) { Write-Host '' Write-Host '[错误] 推送失败!你的文章已保存在本地提交中,不会丢失。' -ForegroundColor Red Write-Host ' 请检查网络或 SSH 权限,然后重新运行本工具,' -ForegroundColor Yellow Write-Host ' 它会自动检测并补推未推送的提交。' -ForegroundColor Yellow Read-Host '按回车返回菜单' return } Show-Step '同步成功!Vercel 正在后台编译!' Write-Host ' 通常 1-2 分钟后线上生效。' -ForegroundColor Green Read-Host '按回车返回菜单' } while ($true ) { Clear-Host Show-Header '个人网安博客控制台' Write-Host ' [1] 构思新文章 (自动生成名片并打开文件)' Write-Host ' [2] 一键同步公网 (扫描裸文件 + 提交所有修改)' Write-Host ' [3] 退出控制台' Write-Host ('=' * 50 ) -ForegroundColor Green $choice = (Read-Host '请输入你的选择 (1/2/3)' ).Trim() try { switch ($choice ) { '1' { New-Post } '2' { Sync-Blog } '3' { Write-Host '' Write-Host '已退出。' -ForegroundColor Green return } default { Write-Host '' Write-Host '[提示] 请输入 1、2 或 3。' -ForegroundColor Yellow Start-Sleep -Seconds 1 } } } catch { Write-Host '' Write-Host "[意外错误] $ ($_ .Exception.Message)" -ForegroundColor Red Write-Host ' 工具不会退出,按回车返回菜单。' -ForegroundColor DarkGray Read-Host } }
注意:这个文件的保存编码很关键,别偷懒。
它必须存成 UTF-8 带 BOM 。Windows PowerShell 5.1 在没有 BOM 时会按 GBK 去读文件,里面的中文全变乱码,脚本直接打不开。
记事本另存为时编码选 UTF-8 with BOM;VS Code 是右下角编码 → Save with Encoding → UTF-8 with BOM。验证方法:双击 .bat,菜单里的中文显示正常就对了。
3. 需要改成你自己的地方 要确认的一共四处,都在 blog-tool.ps1 开头,我在代码里也标了「硬编码 1/4」到「硬编码 4/4」:
变量
默认值
说明
$BlogDir
'D:\myblog'
必改 。你的博客仓库根目录,也就是有 .git 和 source 文件夹的那一层。
$script:Branch
'main'
主分支名。GitHub 新建的仓库默认叫 main,老仓库可能叫 master。
$script:Remote
'origin'
远程仓库名,一般不用动。
$script:Editor
'notepad'
新建文章后用什么打开。装了 VS Code 可以改成 'code'。
.bat 里没有任何需要改的地方,它用 %~dp0 自动找同目录的 .ps1。
4. 用法 双击 一键更新博客.bat:
按 1 新建文章,自动写好 front-matter(标题、日期、分类、标签)并打开编辑器
按 2 一键同步:先补推上次没推上去的提交,再扫描 source\_posts 里没写 front-matter 的裸文件(比如直接从别处拖进来的笔记),问你分类和标签后自动补全,然后提交、推送
按 3 退出
几个省心的地方:推送失败会自动重试 3 次,第一次还会先试着 git pull --rebase 同步远程;万一推失败但已经提交了,改动也留在本地,下次按 2 会自动补推;建文章遇到重名会直接打开旧文件,不会覆盖掉你已经写好的内容。
六、 第五阶段:Vercel 云部署与域名绑定(免备案秒开) 1. 代码托管 GitHub 在 GitHub 上创建一个 Private(私有) 仓库。本地配置好 SSH 密钥后,将代码推送至私有仓库保护隐私。
2. Vercel 自动部署 登录 Vercel 导入你的 GitHub 私有仓库。Vercel 会自动识别 Hexo 并部署,但其默认域名在国内存在污染。
3. 绑定自定义域名 在腾讯云/阿里云购买一个便宜的域名(如 .top 后缀,首年十几元,续费三十元左右):
由于服务器在海外 Vercel,域名直接解析 完全无需工信部 ICP 备案 !
在 Vercel 控制台 ➔ Settings ➔ Domains 添加你的域名。
获取 Vercel 分配的 IP。
去云服务商的 DNS 解析处,添加 A 记录 (指向 76.76.21.21,以 Vercel 显示为准)和 CNAME 记录 (指向 cname.vercel-dns.com)。
静待 5 分钟,你的博客即可实现全国免挂梯子极速秒开!