把自己写的东西放到一个属于自己的网址上,是一件比想象中简单的事。这篇文章记录我从零搭起这个博客、并给它绑上一个免费短域名的完整过程,包括所有踩过的坑。

本站就是这套方案的产物:源码用 Hexo,托管在 GitHub Pages,短域名 crui.is-a.dev 通过 is-a.dev 免费申请。整个过程零服务器、零费用、不需要备案。

一、先想清楚要什么

在动手之前,先明确三个问题,它们决定了技术选型:

问题 本方案的选择 为什么
内容会频繁变动吗? 不会,主要是写文章 静态站点足够,不需要数据库
有服务器吗? 没有 用 GitHub Pages 免费托管
需要动态功能吗? 不需要 纯静态最省心,也最快

静态博客 + 免费托管的组合,是目前个人技术博客性价比最高的方案:

  • 零成本:GitHub Pages 免费,自定义域名也可以免费
  • 零运维:不用管服务器、不用配 Nginx、不用续费
  • 免备案:GitHub 服务器在海外,无需 ICP 备案
  • 版本可追溯:文章就是 Markdown 文件,扔进 Git 仓库,写错了随时回滚

代价是:纯静态、没有后台管理界面、国内访问速度一般。但对技术博客来说,这些都不算问题。

二、准备环境

需要三样东西:

1
2
3
4
5
6
7
8
# 1. Node.js(Hexo 的运行环境),建议 18 以上
node -v

# 2. Git(版本管理 + 部署)
git --version

# 3. 一个 GitHub 账号
# 注册地址:https://github.com/signup

Node.js 装好之后,全局安装 Hexo 命令行工具:

1
2
npm install -g hexo-cli
hexo -v

国内网络如果 npm 很慢,可以临时换成镜像源:npm config set registry https://registry.npmmirror.com

三、本地把站点跑起来

1
2
3
4
5
6
7
8
9
# 初始化一个博客项目,会在当前目录创建 blog 文件夹
hexo init blog
cd blog

# 安装依赖
npm install

# 本地预览(默认 http://localhost:4000)
hexo server

打开 http://localhost:4000,应该能看到一个默认的 Hello World 页面。此时目录结构大致是:

1
2
3
4
5
6
7
8
9
blog/
├── _config.yml # 站点主配置(最重要)
├── package.json # 依赖清单
├── scaffolds/ # 新建文章时的模板
├── source/
│ ├── _posts/ # 你写的文章都放这里
│ └── images/ # 文章引用的图片
├── themes/ # 主题
└── public/ # 编译产物(会自动生成)

理解 source/ 是源文件、public/ 是编译产物,这一点很关键——后面所有配置错误导致的”页面样式全丢”,基本都是这两个目录的关系没搞对。

四、换一个好看的主题

Hexo 自带的主题比较朴素。我用的是 Butterfly,功能全、文档好、中文社区活跃。

1
2
3
4
5
6
7
cd blog

# 把主题克隆到 themes/ 目录(不要用 npm 装主题,容易出问题)
git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/butterfly

# Butterfly 需要额外的渲染器
npm install hexo-renderer-pug hexo-renderer-stylus --save

然后在 _config.yml 里启用:

1
theme: butterfly

Butterfly 的配置不要直接改主题目录里的文件,正确做法是把 themes/butterfly/_config.yml 复制一份到博客根目录,命名为 _config.butterfly.yml,之后只改这一份。这样以后升级主题不会覆盖你的配置。

五、必须改的站点配置

打开 _config.yml,以下几项不改会显得很业余(尤其是申请免费域名时会被拒,后面会讲):

1
2
3
4
5
6
7
8
9
10
11
# Site
title: 你的博客名字
subtitle: '一句话副标题'
description: '站点描述,会出现在搜索引擎结果里'
keywords: 关键词1,关键词2
author: 你的名字
language: zh-CN

# URL —— 这一项最容易出错,见下方说明
url: https://你的地址/
root: /

urlroot 的对应关系,是新手翻车第一名:

你的站点地址 url root
https://用户名.github.io/ https://用户名.github.io/ /
https://用户名.github.io/仓库名/ https://用户名.github.io/仓库名/ /仓库名/
https://自定义域名/ https://自定义域名/ /

只要绑定了自定义域名,root 就必须是 /。写错了不会报错,但整站的 CSS、JS、图片全部 404,页面会变成纯文字——这是最常见的求助帖。

另外顺手检查一下主题配置里的社交链接,模板自带的占位符长这样,记得换成你自己的:

1
2
social:
github: https://github.com/xxxxxx # 换成真实地址

六、部署到 GitHub Pages

GitHub Pages 有两种仓库形态,选错了地址会多出一截

仓库名 访问地址
用户名.github.io https://用户名.github.io/
其他任意名字(如 blogmy-site https://用户名.github.io/仓库名/

也就是说,只有仓库名严格等于 用户名.github.io 时,站点才挂在域名根路径下

下面按”普通仓库”演示(本博客就是这种,仓库名叫 chengrui.github.io,但用户名是 hccc044-bit,所以地址里带了一层后缀)。

1. 在 GitHub 上创建仓库

新建一个公开仓库。如果打算用根路径访问,仓库名就填 你的用户名.github.io

2. 配置 SSH 免密推送

1
2
3
4
5
# 生成密钥(一路回车即可)
ssh-keygen -t ed25519 -C "你的邮箱"

# 查看公钥内容,复制它
cat ~/.ssh/id_ed25519.pub

把复制的内容粘贴到 GitHub → Settings → SSH and GPG keys → New SSH key。然后验证:

1
ssh -T git@github.com

3. 安装部署插件并配置

1
npm install hexo-deployer-git --save

_config.yml 末尾加上:

1
2
3
4
deploy:
type: git
repo: git@github.com:你的用户名/你的仓库名.git
branch: main

4. 构建并部署

1
hexo clean && hexo generate && hexo deploy

三个命令的作用分别是:清空编译产物 → 重新编译 → 推送到 GitHub。顺序不要变clean 放在最前面能避免很多诡异的缓存问题。

部署完等一两分钟,去 https://用户名.github.io/仓库名/ 就能看到了。

如果页面能打开但没有样式,回去看第五节的 url / root 配置。

七、给它绑一个短域名

到这一步,你的地址大概长这样:

1
https://hccc044-bit.github.io/chengrui.github.io/     ← 41 个字符,实在不像个正经网址

有两条路可以缩短它。

路线 A:免费申请 is-a.dev 子域名(本博客采用)

is-a.dev 是一个面向开发者的免费子域名服务,提供给技术相关的个人站点使用。你能拿到形如 你的名字.is-a.dev 的地址,完全免费、自带 HTTPS、由 Cloudflare 提供 DNS

先说一个被排除的选项js.org 看起来更香(名字.js.org 才 10 个字符),但它官方已明确规定不再接受个人博客和作品集,只收 npm 包、JS 库、工具类项目。个人博客提交了也会被拒,不用浪费时间。

申请流程

① 想好名字

子域名结构是 你起的名字.is-a.dev,中间那截 is-a 是固定的、不能拆。

② Fork 官方仓库

打开 github.com/is-a-dev/register,点右上角 Fork

③ 在你的 fork 里创建申请文件

进入 domains/ 文件夹 → 右上角 Add fileCreate new file → 文件名填 你的名字.json(因为在 domains/ 里,完整路径自动就是 domains/你的名字.json)。

内容如下:

1
2
3
4
5
6
7
8
9
{
"owner": {
"username": "你的GitHub用户名",
"email": "你的邮箱"
},
"records": {
"CNAME": "你的GitHub用户名.github.io"
}
}

这两个地方写错必被拒

  • 字段名是 records(复数),写成 record 会 fail validation
  • CNAME 的值不能带仓库名,必须正好是 用户名.github.io。写成 用户名.github.io/仓库名 是错的

④ 提交 Pull Request

在提交页面,选择 Create a new branch for this commit and start a pull request(此时按钮文字会从 Commit changes 变成 Propose changes,两者都对,取决于你选的选项)。

然后跳转到 PR 页面,确认左侧的 base repository 是 is-a-dev/register。如果你停在 你的用户名/register/compare/...,那是 fork 内部对比页,会变成”自己合并到自己”,官方收不到。正确地址格式:

1
https://github.com/is-a-dev/register/compare/main...你的用户名:分支名?expand=1

也可以用更省事的办法:回到 fork 首页,顶部黄色横幅 Compare & pull request 按钮会帮你选好正确方向。

⑤ 填写 PR 模板(这一步决定生死)

模板里有 7 个 Requirements 复选框,必须全部勾上,而且只能改方括号:

1
2
3
- [x]   ← 正确
- [] ← 错误(括号内有空格)
- [X] ← 不推荐(要求小写 x)

编辑方法:找到自己那条 PR 描述 → 右上角 ...Edit → 改完后点 Update comment

模板明确警告:除了复选框,那个区域里其它内容一个字都不能动(包括 <!-- 注释 -->),否则会 fail validation。

另外两处要填的:

  • WEBSITE_PREVIEW 之间填你网站当前已在使用的地址(不是还没生效的新域名),例如 https://用户名.github.io/仓库名/
  • WEBSITE_PURPOSE 之间用一句英文说明网站用途

⑥ 等维护者审核合并

快则几分钟,慢则一两天。这期间可以顺手把网站内容完善一下——模板明确不接受”Hello, world!”或仅做最小改动的模板站,审核者打开你的站点会人工判断。建议提交前清理这些模板痕迹:

  • _config.yml 里还是默认的 title: Hexo、空的 descriptionkeywords: First Blog Test
  • 主题自带的蝴蝶 logo 还当着头像用
  • 还留着 Hexo 默认的 hello-world 文章

⑦ 判断是否已合并(重要,别用 DNS 判断)

先说一个坑:*.is-a.dev 是泛解析。任何不存在的子域都会解析到 Cloudflare 的 IP(104.18.4.103 / 104.18.5.103),所以**”能 ping 通”完全不能说明域名已生效**。

唯一可靠的判据是看申请文件有没有进主分支:

1
2
curl -s -o /dev/null -w "%{http_code}\n" \
https://api.github.com/repos/is-a-dev/register/contents/domains/你的名字.json

返回 404 = 还没合并;返回 200 = 已合并,可以继续下一步了。

合并之后:切换配置(顺序不能错)

① 改 _config.yml

1
2
url: https://你的名字.is-a.dev/
root: / # 必须是 /,不能是原来的 /仓库名/

② 新建 source/CNAME

内容就一行:

1
你的名字.is-a.dev

为什么必须放 source/ 而不是 public/hexo clean 会清空 public/,放那里下次部署绑定就失效了。

⚠️ 一个必须知道的陷阱:只要 source/CNAME 被部署到发布分支,GitHub Pages 会立刻把它当成自定义域名设置。如果此时 DNS 还没生效,你的旧地址也会打不开。所以在 PR 合并之前不要提前把这个文件部署上去——如果需要在审核期间更新站点内容,先把 source/CNAME 移出部署范围(比如移到 _local_backup/),等合并后再移回来。

③ 重新构建部署

1
hexo clean && hexo generate && hexo deploy

④ 开启强制 HTTPS

去仓库的 Settings → Pages,确认 Custom domain 已经是你申请的域名,然后勾选 Enforce HTTPS

证书签发通常几分钟内完成,但那个勾选框要手动点。在勾上之前,你的旧地址会 301 跳转到 http://(注意是 http),访客可能看到”不安全”提示,所以这一步别拖太久。

路线 B:自己买一个域名

如果想要更短或者更正式的名字,就买一个。绑定 GitHub Pages 需要的 DNS 记录如下:

类型 主机记录 记录值
A @ 185.199.108.153
A @ 185.199.109.153
A @ 185.199.110.153
A @ 185.199.111.153
CNAME www 你的用户名.github.io

其余步骤(url / rootsource/CNAME、Enforce HTTPS)与路线 A 完全一致。

几个现实提醒:

  • 字母越少越贵。3 字母以内的 .com 基本被抢光,能注册到的多是冷门后缀,而且很可能被注册局标为溢价域名——结账页显示的价格可能是几百上千元/年
  • 查域名是否被注册可以用 RDAP:https://rdap.org/domain/要查的域名,返回 404 即未被注册
  • 但**”未被注册”不等于”便宜”**,真实价格以注册商结账页为准

八、踩坑清单

把这次遇到的所有问题汇总成一张表,供快速查阅:

现象 原因 解决
全站没有样式,只有纯文字 绑定域名后 root 还是 /仓库名/ root 改成 /
hexo deploySAFE_DELETE_BULK_CONFIRM_REQUIRED 某些环境有批量删除保护,部署要清空 .deploy_git 时被拦 见下方说明
绑了域名但访问不了 DNS 还没生效 等几分钟到 24 小时
xxx.is-a.dev 能解析但打不开 is-a.dev 是泛解析,解析成功不代表已生效 用 API 查 domains/xxx.json 是否返回 200
旧地址打开了”不安全”提示 还没勾 Enforce HTTPS Settings → Pages 勾上
新域名生效后旧地址自动跳转 正常行为,不用管 GitHub 自动 301
换主题后配置被覆盖 直接改了主题目录里的 _config.yml 配置放博客根目录的 _config.butterfly.yml

关于部署被拦截:hexo-deployer-git 在推送前需要清空 .deploy_git 目录下的所有文件,文件较多时会触发环境的批量删除保护。此时 .deploy_git 里通常已经生成了内容正确的提交,可以绕过清理步骤直接推送:

1
2
3
4
5
6
7
8
9
10
11
12
cd .deploy_git

# 恢复被中断清理的工作区
git restore .

# 与编译产物比对,确认内容完整(中文文件名可能有引号编码差异,属正常)
git ls-tree -r HEAD --name-only | sort > /tmp/head.txt
(cd ../public && find . -type f | sed 's|^\./||' | sort) > /tmp/pub.txt
diff /tmp/head.txt /tmp/pub.txt

# 直接推送(本地分支常是 master,远端是 main,用冒号映射)
git push git@github.com:你的用户名/你的仓库名.git master:main

确认是快进推送,不要加 -f

九、日常写作与发布

建站只是开始,真正要做的是持续写。日常流程只有三步:

1
2
3
4
5
6
7
8
# 1. 新建文章(会在 source/_posts/ 下生成带模板的 md 文件)
hexo new post "文章标题"

# 2. 用 Markdown 写内容,本地预览
hexo server

# 3. 发布
hexo clean && hexo generate && hexo deploy

文章开头的 front matter 是元信息:

1
2
3
4
5
6
7
8
9
---
title: 文章标题
date: 2026-09-10 16:30:00
tags:
- 标签一
- 标签二
categories:
- 分类名
---

文章里插入图片,把图片放进 source/images/,然后按绝对路径引用:

1
![图片说明](/images/图片文件名.png)

注意是 /images/ 而不是 images/——开头的斜杠不能少,否则非首页的文章会找不到图片。

十、本博客的最终架构

回头看整个方案,所有组件都是免费或开源的:

环节 用到的服务 成本
静态站点生成 Hexo + Butterfly 主题 免费开源
代码与内容托管 GitHub 仓库 免费
网站托管 GitHub Pages 免费
域名 is-a.dev 子域名 免费
DNS Cloudflare(is-a.dev 提供) 免费
HTTPS 证书 Let’s Encrypt(GitHub 自动配置) 免费,自动续期

最终地址是 https://crui.is-a.dev——16 个字符,纯 HTTPS,证书自动续期。

从 41 个字符到 16 个字符,中间隔着一个 Fork、一次 PR、七个复选框、两次配置修改和一个勾选框。不算复杂,但每一步都有自己的坑。

希望这篇文档能帮你少走一些弯路。如果对文中某个环节有疑问,欢迎通过首页的邮箱联系我。