Skip to main content
  1. Posts/

用 Hugo 与 Cloudflare Pages 搭建个人网站

xueqi
Author
xueqi
Table of Contents

拥有个人网站的想法由来已久。一个属于自己的赛博房间,不受平台规则约束,无所谓流量好坏,可以把写的东西、随手涂鸦和拍的照片一股脑塞进来,随意摆置。

这个想法一直被遗忘在待办事项列表底端,直到我尝试做出第一册 zine 之后,找不到合适的地方分享,再次感叹「如果有自己的网站就好了」。那么这次说干就干,打开电脑,动动双手,就有了现在你看到的这个页面。

搭建过程中受到了不少博客的帮助与启发。于是,我也决定留下一篇,仅作记录与参考。

1. 框架与工具的选择
#

1.1 Hugo 本地建站
#

平台和框架确实说到底只是介质,但为了呈现效果,还是应当根据自己侧重的需求与考量点进行筛选,找到合适的载体。

我对比了几种主流的建站方式:

Cargo / Squarespace / Wix 等平台比较适合设计师和摄影师这样的视觉创作者。最初我的目光一直停留在 Cargo 上,但很快清醒过来:我既不够艺术,也不够有钱。放弃这个选项后,我仍保留着来自 Cargo 的 newsletter,点开邮件便可以欣赏到许多美丽页面。

WordPress 应该是不太会出错的选项,老牌建站平台,上手简单,有很多功能集成。然而 WordPress 是动态站点,意味着我要为此租一台虚拟服务器,配置并维护它。我不确定自己可以坚持更新多久,是否会让这些资源空置。

Ghost 内部集成了邮件订阅服务,这个功能非常吸引我。因为我平时有订阅一些 newsletter,也计划自己写一些。但 Ghost 同样属于动态站点,而且用户基数和教程数量、主题样式相对来说都不算太多。

重新审视自己想要呈现的内容后,我决定依靠静态页面实现大部分图文需求。邮件订阅一类的功能,也可以集成第三方服务,或是直接跳转外部平台。

性质 价格 页面自由度 数据掌控度
Cargo 动态 ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐
WordPress 动态 ⭐⭐ ⭐⭐⭐ ⭐⭐⭐
Ghost 动态 ⭐⭐ ⭐⭐ ⭐⭐⭐
Hugo 静态 ⭐⭐ ⭐⭐⭐⭐⭐

静态框架基本在 Hexo 与 Hugo 之中抉择。Hexo 要配置 Node.js 比较麻烦,基于 Go 语言的 Hugo 更轻量、编译速度更快,因此我很快选定了 Hugo 作为框架。

下一步是选择一个主题样式。网站页面长什么样可能更取决于主题,而不是框架。Hugo 官网中的 模板库 有很多主题可供选择,也可以自己去 GitHub 以「Hugo theme」作为关键词查找挑选。

卡片式的 Blowfish 主题与我设想中的站点样式最接近,那么就是它了。跟着教程与文档,差不多就能搭建出网站的雏形。网络上可以搜索到许多 Hugo 建站教程,选一两篇对比着参考就行,这里不展开叙述。

对于 Blowfish 主题,我比较推荐主题作者本人写的 Build your homepage using Blowfish and Hugo,以及 example site 上的文档,内容很全面。

对了,在正式开始之前,最好先了解一下「Git」和「终端」相关的知识。

1.2 Cloudflare Pages 自动化部署
#

网站在本地搭建好后,还需要通过服务器部署上线。动态网站往往需要订阅配套的服务,或是自行搭建服务器。而静态网站由简单的 HTML 页面组合而成,比较轻量,利用第三方托管平台就可以完成部署,当然,也可以选择自建服务器。

在不自建服务器的情况下,常见的第三方托管与部署方案有这样几种:GitHub Pages、Vercel、Netlify,以及 Cloudflare Pages 等。这些方案都是免费的。

起初我图省事,打算直接使用 GitHub Pages。后来发现有不少网友选择用 Cloudflare 给 GitHub Pages 或是自建服务器加速,那么我想不如直接部署到 Cloudflare Pages 上去好了。Cloudflare 还有一些优势:

  • 自带 CDN 加速与边缘部署;
  • 可以拉取 private repo 中的文件,而 GitHub Pages 要求仓库必须设定为 public(不是愿不愿意开源的问题,是有开源与否的选择权);
  • 自动部署,无需像 GitHub 一样额外配置 GitHub Actions。

部署的过程相当便捷且流畅:注册 Cloudflare 账号,创建 Pages,绑定站点源码对应的 GitHub 仓库,每一次代码提交推送后,Pages 就会同步自动完成部署。

第一次成功部署后,Pages 会为站点生成一个名为 "project_name".pages.dev 的二级域名。"project_name" 即创建 Pages 时填写的项目名称。这个域名一旦生成,就无法修改。因此如果不打算购买自定义域名,又想获得一个称心的二级域名,应当谨慎设置 Pages 项目名称,并检查名称唯一性。

域名托管
#

Pages 绑定自定义域名,需要先将域名交给 Cloudflare 托管:在 Cloudflare 中添加站点,会获得两个 DNS 服务器地址;然后打开原域名服务商的域名管理界面,替换掉原有的 nameservers 即可。

收到 Cloudflare 发来的域名激活邮件后,再去 Pages 后台设置自定义域名。Cloudflare 会新增一条 DNS 记录,将域名 CNAME 解析至 pages.dev 二级域名。现在就可以用自定义域名来访问部署在 Pages 上的站点了。

域名解析成功后,可以顺手在 Cloudflare 里设置邮箱路由,自定义一个以域名为后缀的邮箱名称,实际收发地址将跳转至其他常用邮箱。主要起到隐私保护作用。

另外,在域名管理后台可以监控网站的数据流量、性能和安全状况等基本指标。如果对数据监测略有需求,但又没有进一步的需求,站点托管到 Cloudflare 还是挺方便的。

1.3 R2 结合 PicGo 搭建图床
#

静态页面最令人头大的一点是图片资源的处理。如果都放在本地 page bundle 里,无疑会使站点文件逐渐臃肿,继而影响页面构建与加载速度。因此,通过图床采用外链引入图片是更优解。这样另一个好处是,即使移动文件路径,也不会影响到图片的呈现。

我对图床的需求是访问稳定、数据可控、价格合适即可。探索了许多解决方案后,发现使用 OSS 搭建图床比较靠谱,于是基本锁定 Backblaze B2 和 Cloudflare R2 这两家存储服务。

两者都提供了 10 GB 免费存储空间,超出部分的计费略有不同:B2 只要 $0.006 / GB-month,R2 为 $0.015 / GB-month。实际上对于轻量级的个人小站,免费额度应该足够使用,即便超出,也都不算贵。

R2 的主要优势在于完全免掉的流量费用,以及相当高的操作数免费额。

–– \ R2 Pricing Free Paid - Rates
Storage 10 GB / month $0.015 / GB-month
Class A Operations 1 million requests / month $4.50 / million requests
Class B Operations 10 million requests / month $0.36 / million requests

其中,A 类操作指试图改变桶现状的请求(例如上传),B 类操作指试图读取桶内容的请求(例如下载及访问)。

综合考量后选择 R2 作为对象存储服务,这样也稍稍避免了为其他存储服务配置 Cloudflare CDN 和重写 URL 等一系列繁琐操作。

创建 R2 存储桶
#

无论是 B2 还是 R2,创建存储桶时都需要填写信用卡信息(B2 是创建 public bucket 的时候需要,且要预付 1 美元)。R2 支持银联和 PayPal 支付。

提交完支付信息后,就可以开始创建存储桶。先设置 bucket name,注意这个源桶名不要暴露,不能太简单或是太容易被猜到,否则可能会被恶意刷流量与请求。

设置好名称、选定地区,桶就创建完成。接下来,可以设置域名并公开桶。存储桶公开后,才可以使用外链访问存储的图片等资源。

将存储桶设置为公开,可以通过两种途径:绑定自定义域名,或是采用 R2 提供的子域名。

如果购买了域名

customize domin

在存储桶的设置里找到 public access,选择自定义域名,给自己的网站起一个子域名,然后填入,Cloudflare 就会自动新增一条 CNAME 规则,将这个子域名指向存储桶。前提是主域名已经托管在 Cloudflare。

如果没有自己的域名

r2 dev domin

没有域名的话,只能在设置中手动开启 R2.dev 子域名。

但 Cloudflare 不是太建议采用这个选项,因为有一些缓存和访问限制等措施在自定义域名才可使用。

到这一步,R2 已经可以正常使用了,接下来可以通过一些设置来控制对存储资源的利用,同时提高访问速度。

设置缓存规则
#

setting page rules

在 Cloudflare 中进入域名管理后台,找到 Rules > Page Rules,设置缓存级别、浏览器缓存 TTL,以及边缘缓存 TTL。

TTL(Time to Live)就是缓存的有效期,自己酌情设置时间即可。

开启防盗链
#

防盗链主要用于限制其他站点对本站静态资源的引用。开启防盗链后,图片链接在浏览器中还是可以直接打开的,也能够在本站及白名单站点内正常浏览、下载,当其他网站直接引用本站图床链接时,图片则无法显示。这样做一定程度上能规避恶意请求,减少带宽消耗,防止资源被滥用。

从原理上来讲,浏览器通常会自动在 HTTP 请求头中添加 Referer 字段,记录用户从哪个网站跳转过来。防盗链正是通过 Referer 来识别访客身份并过滤,根据规则判断返回资源或是拒绝访问。

利用 Cloudflare 的防火墙机制可以完成防盗链的设置。找到 Security > WAF,添加自定义规则。我采取的基本策略是当请求不来源于本站时,先全部阻止引用。

WAF block

这条规则生效后,查看后台 log,已经立马阻挡了一些不明来源的请求。与此同时,在本地 Markdown 编辑器和 localhost:1313 测试环境中都看不到图片了。编辑器看不到问题不大,因为我通常使用测试环境实时预览页面效果。localhost 无法引用图片就比较困扰。

WAF Whitelist

于是我又为本地环境增加了一条白名单规则,设置当 Referer 包含 localhost 时,绕过防火墙设置。这一规则只在本地测试预览时短暂开启。

这样一来,对盗链情况可以防个大概。有的情况,像是通过隐藏 Referer 等手段破解防盗链,暂时没有简便的方法处理。至少已经在很大程度上减少了本站内容被随意引到奇怪的地方去的几率。

浏览 Leo′s Blog 时发现,Cloudflare 提供了一键开启防盗链的方式:通过 Scrape Shield 页面打开 Hotlink Protection 开关;白名单则是在 Rules > Configuration Rules 中添加。但这一方式只对部分格式的图片起作用,如果希望其他静态资源如 .zip 或是 .pdf 又或是音视频等内容受到防盗保护,建议还是通过 WAF 进行设置。

PicGo 图床管理
#

图片不多的时候,R2 直接用着其实也还行,使用体验有点像无缩略图版的 Google Drive。长远考虑,利用图床管理工具会更加便捷。

目前比较多人在用的是 PicGo,它提供了快捷上传、自动复制图片链接、自定义文件路径以及文件名哈希化等多种功能。

PicGo 默认支持的图床中没有 R2,需要通过 PicGo Amazon S3 插件实现支持(R2 兼容 S3 API)。在 PicGo 的「插件设置」中搜索 S3 并安装,然后参考 插件说明 进行配置。

配置插件的过程中需要接入 R2 API,因此可以先去 R2 后台页面创建 API Token,设置允许对象读和写,生成密钥。然后返回 PicGo,在 Amazon S3 插件设置界面中输入对应信息。

至此,图床已配置完成,关于站点的基本设施也都调试完毕。

2. 页面初始化
#

接下来就是一些缝缝补补的工作。在主题样式的基础上做个性化调整,这也是自建站的乐趣之一。

2.1 样式调整
#

更改全局字体
#

Blowfish 主题提供了便捷的 字体修改方式:在 static 里新建 fonts 文件夹,放入想要替换的 .ttf 字体文件,并在 assests/css/ 中新建 custom.css 文件,自定义字体即可。记得注意字体的版权

@font-face {
    font-family: font;
    src: url('/fonts/font.ttf');
} 

html {
    font-family: font;
}

更改代码字体
#

代码块中的字体没有跟着全局字体变动,需要另外设置。

HTML 中通常用 <pre> 标签来展示计算机源码等预格式化的文本,而代码框通常用 .highlight 类来定义。因此,可以通过 .highlight pre 选择器来定位代码框中的代码,实现对代码样式的修改。

/* 设置代码字体 */
.highlight pre {
    font-family: "noto sans mono", "noto sans sc", monospace; 
    font-size: 12px;
}

/* 引入自定义字体 */
@font-face {
    font-family: noto sans sc;
    src: url('/fonts/noto sans sc.ttf');
}

@font-face {
    font-family: noto sans mono;
    src: url('/fonts/noto sans mono.ttf');
}

html {
    font-family: noto sans sc, noto sans mono;
}

其他
#

还有不少零零碎碎的改动,例如:

  • 卡片样式以及各种元素调整:基本思路是利用开发者工具查看相应的 HTML 结构,确认 CSS 类名,再去 csslayouts 文件夹寻找,然后根据情况在 custom.css 中修改,或在根目录下覆盖一个相同名称的 .html 文件。
  • Favicon:准备一张 PNG 图,扔进 favicon.io,它会帮忙生成多个尺寸的图标和 ICO 文件,用于适配不同设备与浏览器;下载好后把解压过的文件直接放入 static/ 文件夹即可。需要清理缓存或是等待一会儿才能看见效果。
  • 正文两端对齐、字号调整:.article-content {text-align: justify; font-size: 15px;}
  • 限制代码框最大高度,溢出部分滚动显示:.highlight pre {max-height: 35rem; overflow: auto;}
  • 变更主题色(待更新):参考 css/schemes 中的写法 override 一个,然后在 params.toml 中修改参数。Tailwind CSS 提供了一系列现成的 调色板,可以直接从中选择,但我试了几种觉得效果都不太好,打算自己调个色。在调整颜色的时候记得注意对比度,保证页面的 可访问性(Accessibility)。

2.2 Shortcodes
#

Shortcodes 大概是 Hugo 用户搞网站装修时都喜欢玩一玩的项目。这个主题预提供的 shortcodes 已经比较够用,我又根据自身需求添加了两则之后大概率会用到的短代码。

NeoDB Card
#

网络冲浪时发现了 NeoDB 的条目卡片,觉得好看且实用,于是我直接采用了 这篇文章 中的原始样式,并对 CSS 做了各种微小调整;接着在 这篇 提供的思路上修改了滚动条的呈现。

调整后的卡片样式:

{{< neodb "https://neodb.social/game/5rrQab2d2xhxGWrjJOZUs0" >}}
9.8
《塞尔达传说 荒野之息》(日语:ゼルダの伝説 ブレス オブ ザ ワイルド,英语:The Legend of Zelda: Breath of the Wild,港台译作“萨尔达传说 荒野之息”)是一款动作冒险游戏。本作由任天堂企划制作本部与任天堂旗下子公司Monolith Soft协力开发。游戏最早计划为Wii U平台独占发行,之后宣布将在任天堂新的混合型游戏主机任天堂Switch上发售,并将成为该主机的首发游戏之一。同时任天堂确认本作会是任天堂在Wii U上开发的最后一款游戏。 游戏最初于2013年公布,并计划于2015年发行。之后任天堂宣布游戏延期并最终定于2017年3月3日发行。 剧情 很久很久以前,海拉尔王国的各个族群和平共存。其中名为锡克族(Sheikah)的族人们制造了许多先进的科技来帮助海拉尔的人们。但是有一天,邪恶的梦魇加侬(Calamity Ganon)击败了勇者并试图占领海拉尔王国。情势紧迫下锡克族人启动了他们暗中制造、用来保护家园的机械巨兽和守卫者。这些机械士兵成功的将梦魇加侬封印在海拉尔城堡中,但是机械士兵们却遭到邪恶力量的控制而反过来攻击海拉尔人。因为惧怕著锡克族的力量,海拉尔王将他们统统放逐出海拉尔王国。 一百年后的现代,勇者林克从沉睡中苏醒,却失去了所有的记忆。林克在一个未知的女性声音的带领下遇见了一位长者,长者告诉他梦魇加侬依旧被封印在城堡内并日渐壮大自己的力量,林克必须在封印被破坏前拯救海拉尔王国。 《塞尔达传说 荒野之息》获得业界极高的好评。国外多家游戏媒体给予满分评价。
game

豆瓣条目也可以调用(但实际上调用的内容还是来自 NeoDB,区别是点开链接会跳转到豆瓣):

{{< neodb "https://movie.douban.com/subject/1294194/" >}}
9.1
普通人的命运在轰轰烈烈的时代面前总是渺小到可以忽略不计。个人如果勇敢地站出来想阻止时代洪流,多少像奋力扑向风车的唐吉坷德,往往只能当殉道者,而更多时候,个人甚至连选择当旁观者的权利也没有。可是普通人的悲剧,无疑能照出历史的荒谬与残忍。 1945年,日本无条件投降后台湾光复,基隆一户林姓人家眼见也要过上好日子,但人算不如天算,林家大小波折从此不断。“二・二八”事件发生后,家中的四兄弟更是只剩下老老实实开着一家照相馆的聋哑人老四林文清(梁朝伟 饰)。然而悲剧并没到此终止,因为和进步人士有联系,林文清也没能逃脱被逮捕的命运。到此,林家男子只剩他和吴宽美(辛树芬 饰)尚呆在襁褓中咿呀地学语的幼儿……
movie

默认滚动条样式过于粗糙,网友给出的方案直接把它隐藏了起来。考虑到滚动条作为「下面还有内容」的提示,我决定将它重新放出来,并动手美化一下。

.db-card-abstract {
    -ms-overflow-style: none;  /* IE and Edge */
    scrollbar-width: none;  /* Firefox */
}

.db-card-abstract::-webkit-scrollbar-track {
    background-color: #ffffff00; /* 轨道背景色 */
}
.db-card-abstract::-webkit-scrollbar-thumb {
    background-color: #b6b6b65d; /* 滑块颜色 */
    border-radius: 5px; /* 滑块圆角 */
}
.db-card-abstract::-webkit-scrollbar {
    width: 6px; /* 滚动条宽度 */
}
.db-card-abstract::-webkit-scrollbar-thumb:hover {
    background-color: #9f9f9f97; /* 悬停时的滑块颜色 */
}

好了,现在在 Chrome 和 Safari 这类支持 Webkit 内核的浏览器上就可以看见调整后的滚动条。其他浏览器样式规则不同,继续做了隐藏处理。另外还调整了滚动条与文字的间距,让卡片看起来更整洁。

页面中的其他滚动条,例如代码框滚动条 pre::-webkit-scrollbar 都可以通过这一形式来修改。隐藏滚动条时,设置 display: none 即可。

Spotify Widget
#

Spotify 提供了写好的 widget 样式,调用它的 iFrame API 就可以将小组件嵌入页面。搜索了一圈后,发现大家几乎都在使用同一个 解决方案

然而这个写法没有实现小组件的背景色调整。我是在摸索的过程中发现 Spotify 还为嵌入式组件提供了两种颜色模式:跟随主题色,或是深灰色背景。测试时遇到了一些颜色过于鲜艳的播客单集,这时候就希望背景不要跟随主题色。

最后终于在网页播放器中找到了这个路径略微有些深的参数:

find the embed widget

spotify widget with theme

spotify widget without theme

可以看到,当组件颜色没有跟随主题时,代码中的 URL 字段多了 theme=0 参数。而这一串后缀在 iFrame API 文档中没有被提到,我把它加进了 shortcodes。

layouts/shortcodes 中新建 spotify.html

<!--
Parameters:
    type - (Required) album / artist / track / playlist / episode / podcast
    id - (Required) Target ID
    width - (Optional) width
    height - (Optional) height
    theme - (Optional) theme
-->

{{ if .IsNamedParams }}
<iframe src="https://open.spotify.com/embed/{{ .Get "type" }}/{{ .Get "id" }}?utm_source=generator&theme={{ .Get "theme" }}"
    width="{{ default "100%" (.Get "width") }}"
    height="{{ default "152" (.Get "height") }}"
    frameborder="0"
    allowtransparency="true"
    allow="encrypted-media"></iframe>
{{ else }}
<iframe src="https://open.spotify.com/embed/{{ .Get 0 }}/{{ .Get 1 }}?utm_source=generator&theme={{ .Get "2" }}"
    width="{{ default "100%" (.Get 3) }}"
    height="{{ default "152" (.Get 4) }}"
    frameborder="0"
    allowtransparency="true"
    allow="encrypted-media"></iframe>
{{ end }}

💽 示例一:

{{< spotify type="album" id="1024AIZX8jeI3kr6pQo4FR" >}}

📻 示例二:

{{< spotify type="episode" id="1cIoynaHxBRAHmX0geB7tj" theme=0 height="80" >}}

上述五个参数里,typeid 是必填的,其余为可选项。

  • Spotify 把 widget 的样式规定得比较死,基本只能修改宽和高。width=100% 时,在不同尺寸的屏幕上可以自适应;缩减百分比后,小屏幕上的组件有可能无法显示完全。因此修改宽度时建议根据屏幕尺寸调试。
  • height 值虽说也可以手动修改,实际上只有在 height="352" "152" "80" 这三个值时,显示效果较好,否则组件下方会出现奇怪的留白。
  • theme 默认跟随主题色,设置 theme=0 后,小组件背景就会变为深灰色。
测试后发现 Spotify widget 在墙内网络下无法显示 :( 理想情况应该是:在不挂梯子时可以显示组件,只是无法播放。

2.3 引入 Twikoo 评论系统
#

评论留言这类动态功能无法直接在静态页面中实现,需要从第三方引入与配置。

Hugo 内置了 Disqus 模板,不过 Disqus 目前在国内无法访问,而且广告比较多,强制注册登录带来的使用体验也不是太好。

Giscus 看着不错,但要用 GitHub 登录才能评论,对于非技术类博客来说,不是最适用的选择。

稍微对比了几种常见的评论系统后,发现 Twikoo 基本符合需求:

  • 界面干净简洁;
  • 可以自己选择服务器部署后端;
  • 可以关掉评论区用户 IP 地址、设备型号等隐私项;
  • 不强制登录第三方平台账号,用户名与邮箱都可选填。

根据 文档 配置好 MongoDB,将 Twikoo 云函数顺利部署在了 Netlify 上。由于大部分 Hugo 主题没有自带对 Twikoo 的支持,前端部分需要修改源码,手动引入 JS 文件。不同主题引入文件的地方可能不太一样。

就 Blowfish 主题而言,我是先在 VS Code 里搜索关键词「comments」,发现 single.html 文件中预留了评论区的位置:

{{ partial "article-pagination.html" . }}
{{ if .Params.showComments | default (.Site.Params.article.showComments | default false) }}
{{ if templates.Exists "partials/comments.html" }}
<div class="pt-3">
  <hr class="border-dotted border-neutral-300 dark:border-neutral-600" />
  <div class="pt-3">
    {{ partial "comments.html" . }}
  </div>
</div>

那么根据源码提示,新建 layouts/partials/comments.html 文件,放入 Twikoo 提供的 引入代码 即可。

到这里前后端部署都已经完成,并能够在本地测试环境中实时预览。由于主题没有预先适配,呈现效果或许不佳,这时可以按需调整 CSS,让页面看起来更舒适。

另外记一个奇怪的坑:开启评论管理面板中的 HIDE_ADMIN_CRYPT,隐藏管理入口后,在前端键入暗号却不生效,无法再度打开管理面板,只好去 MongoDB 修改掉该字段。

再补一个问题:Twikoo 系统默认拉取头像的 CDN 资源貌似不太稳定,刚用上一天就持续崩溃,后续可能会看情况决定是否寻找更好的替代方案。

3. FINALLY!
#

借助框架和模板搭建一个这样的站点并不难,只是略微繁琐,林林总总竟也记下来这么长一篇。

原本打算再实现一些需求后一并放进来,例如为图片灯箱(lightbox)加上导航:Blowfish 主题采用的图片放大工具是 medium-zoom,它有很好的响应式效果和美观简约的样式,缺憾是太简约了,无法在图片放大后切换上一张、下一张,对于一系列连贯的图片场景,使用起来非常不方便。

然而由于没有现成的实例可参考,要么自己写,要么换一个 JS 库引用,都需要一些时间,那么新手日记就先写到这里。