基于容器部署hugo博客 -- hugo个人博客折腾记之后篇
这篇主要讲一下这个博客是怎么部署的
{{< music "32272267" >}}
hugo 主题选取
主题选取这个其实在前篇已经说过了
这里说下这个主题的特点吧:
5色可选。5种双色主题 (orange is default, red, blue, green, pink)可以直接在配置文件里修改。
Fira Code作为默认等宽字体
响应式设计,各种分辨率浏览效果都不错
采用 PrismJS 实现客户端代码高亮
最重要的是,它简洁。 没错,真的很简洁,没有过多的功能,甚至没有archive页面,没有tag cloud,只自带了3个shortcode。 对于hugo新手来说,越是简单的主题,你才越好下手修改。要不然刚接触hugo,就用了一个功能特别复杂的主题, 这对于掌握主题的DIY技能是没有帮助的。 现在你看到我的博客,我已经是根据自己的需求做了非常多的调整了。
我调整后的terminal主题:
- 同样5色可选。
- 使用
Jetbrains Mono
https://github.com/JetBrains/JetBrainsMono 等宽字体进行代码高亮 - 响应式设计保持不变
- 采用hugo自带的代码高亮方案(输出class) + 自定义的Gruvbox高亮配色
- 调整hugo默认的RSS 2.0 feed输出为atom输出,调整RSS摘要输出为全文输出
- 增加archive,tagcloud模板,增加基本的KaTex自动渲染LaTex支持。
- 增加TOC显示,返回TOP按钮,平滑滚动。
- cover图片显示增加page bundle支持
如果你碰巧也喜欢这个主题,老灯的代码是开放的,自取: https://github.com/ttys3/hugo-theme-terminal/tree/ttys3
代码高亮
这里要说一下,在hugo里面,代码高亮有两种选择,一种是客户端实现方式,另一种是后端实现方式。 客户端实现就是用markdown的fenced code使代码在输出时保持原样(当然,跟html本身冲突的<
或>
会进行处理), 然后由客户端加载js进行高亮渲染。后端的方式,即是采用hugo默认的markdown解析引擎goldmark来调用 chroma进行高亮渲染.
chroma跟python里的Pygments 采用一致的方式进行高亮,甚至连风格样式也保持一致。 所有Chroma的配色都是使用_tools/style.py
脚本自Pygments的配色转换而来。 所以一般来说,这两个的渲染结果应该是一致的,Pygments有什么配色,chroma就有什么配色,自然, hugo就会有什么配色。配色效果可以去Chroma配色相册查看
虽然Chroma是porting自Pygments, 但是也不是完全一样,比如作者解释了为什么Chroma比Pygments支持的高亮语言更少一些:
I mostly only converted languages I had heard of, to reduce the porting cost.
不过这完全不是问题,因为常用的语言它都支持了,而那些名字都没听过的语言,我也不需要高亮。
那么,既然hugo自带高亮, 主题也自带高亮,我是要用主题的前端方案呢?还是用后端的hugo方案呢?
前后端高亮方案对比
对比项 | hugo后端高亮 | PrismJS前端高亮 |
---|---|---|
RSS feed输出 | 可保持高亮配色(当noClasses=true) | 无高亮 |
浏览器兼容性 | 不相关 | 相关 |
inline CSS支持 | 支持 | 不支持 |
官方配色数量 | 较多 | 很少(8) |
第三方配色数量 | 较多 | 较多 |
看上去两者相比不分高下。所以,主要看需求了。 如果要求RSS feed输出也保持一致的代码高亮,则要使用hugo自带的高亮且启用inline CSS输出。
如果要求支持所有浏览器,则也应该选择hugo自带的方案。 see PrimsJS Limitations
No IE 6-10 support. If someone can read code, they are probably in the 85% of the population with a modern browser.
如果是使用hugo自带的高亮且使用的是class的方式的话,我觉得应该用这两个都OK。 就看你喜欢的那个配色在对应的方案里有没有提供。
package main
import "fmt"
// This is a comment
func main() {
fmt.Println("hello world")
}
输出对比: 我把二者的输出用在线工具格式化一下方便对比 https://codebeautify.org/htmlviewer/
PrismJS
<pre style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4" class=" language-go">
<code class=" language-go" data-lang="go">
<span class="token keyword">package</span> main
<span class="token keyword">import</span>
<span class="token string">"fmt"</span>
<span class="token keyword">func</span>
<span class="token function">main</span>
<span class="token punctuation">(</span>
<span class="token punctuation">)</span>
<span class="token punctuation">{</span>
fmt
<span class="token punctuation">.</span>
<span class="token function">Println</span>
<span class="token punctuation">(</span>
<span class="token string">"hello world"</span>
<span class="token punctuation">)</span>
<span class="token punctuation">}</span>
</code>
</pre>
hugo
<pre class="chroma">
<code class="language-go" data-lang="go">
<span class="kn">package</span>
<span class="nx">main</span>
<span class="kn">import</span>
<span class="s">"fmt"</span>
<span class="kd">func</span>
<span class="nf">main</span>
<span class="p">()</span>
<span class="p">{</span>
<span class="nx">fmt</span>
<span class="p">.</span>
<span class="nf">Println</span>
<span class="p">(</span>
<span class="s">"hello world"</span>
<span class="p">)</span>
<span class="p">}</span>
</code>
</pre>
二者的解析引擎都是基于正则表达式的, 有错漏肯定是难免的。 看上去解析结果差不太多,不过貌似PrismJS 没有把 fmt.Println
调用里的 fmt
解析正确。 chroma的输出结果使用了更简短的class名称,看上去更精简一些。
最终我还是选定了hugo内置的高亮。放弃了前端方案。
内置的配色我感觉没有喜欢的,于是看看base64有没有chroma的配色,还真找到一个。
对于这个porting不太满意,因为它太多紫色的东西。 jetbrains 有一个 gruvbox theme自带的gruvbox color各方面都不错,我把它port过来了 https://github.com/ttys3/base16-chroma/blob/master/chroma-base16-css/cssvar/base16-gruvbox-dark-jb.css
最终形成我自己的fork: https://github.com/ttys3/base16-chroma
由于 base16 配色并不是专门为Chroma或Pygments设计的,因此,得把base16的 scheme 人肉转换到 Chroma 过来。
最终效果就是现在你看到的高亮效果。
这期间花了我不少时间,关于如何制作新的 base16 配色并应用于 hugo, 后续有空我再单独发文分享。
另外这个主题默认的Fira Code
我给它换成Jetbrains Mono
https://github.com/JetBrains/JetBrainsMono woff2字体,woff2体积几乎比woff小一半。 如果你查看兼容数据库 https://caniuse.com/#woff2 , 你会发现只有IE11 和 Opera Mini不支持。 所以,可以果断地用woff2了。
内容组织
由于 hugo 不像 WP 这类有数据库的动态博客系统, hugo是生成静态页面的。
用目录来组织文章, markdown文件一般不会遇到问题,但是图片和其它附件呢?
如何有效的管理?方便写文章和修改,同时方便后期的查找和管理?
幸好,hugo 有一种叫 page bundle的模式。
就是一个页面下可以包含其它资源。
如果是一个叶节点页面bundle, 那么它下面可以放一个index.md 及 图片和其它附件。
比如我现在这个文章deploy-hugo-blog-witn-container
就是一个page bundle,文件结构如下:
content/post/hugo/deploy-hugo-blog-witn-container
├── cover-2020-04-16-03-50-14.png
└── index.md
这样的好处非常明显, 文章自己的图片放在文章自己的目录。
如果是公用的图片怎么办?放到 static/img
下面啊。
还有一个问题,在写文章的时候,怎么方便的在截图完成后,把图片存到当前目录并自动引入markdown中?
有一个神奇的 VS Code 插件叫Markdown Paste(telesoho.vscode-markdown-paste-image
), 这个非常方便的功能就由它实现了。
用起来非常丝滑顺手。
剪切板中的图片,Ctrl+Alt+V 就能直接到markdown文件所在目录,并且自由粘贴markdown的图片代码。
这里主要是要感谢王不对, 如果没有看他的文章,我还一直不会去了解hugo的page bundle这个功能。
当然,这是图片存在本服务器的情况,如果是用又拍云、七牛或者腾讯云COS做图床的话,VS Code里另外有其它的插件可以现实。
因为我是全站走CDN了,因此就懒得再弄CDN的图床了。
配置
配置的话,采用最小化配置。 也就是,凡是hugo默认的配置,我们不写出来,我们只增加自己要修改的部分。
除了个单个config.toml配置,hugo还引入配置目录的功能。
每个顶层配置对象都拥有一个文件,如下图示:
├── config
│ ├── _default
│ │ ├── config.toml
│ │ ├── languages.toml
│ │ ├── menus.en.toml
│ │ ├── menus.zh.toml
│ │ └── params.toml
│ ├── production
│ │ ├── config.toml
│ │ └── params.toml
│ └── development
│ ├── config.toml
│ └── params.toml
考虑到上述的结构,运行hugo --environment development
时,hugo将读取config/_default
的默认配置 然后和development合并。 也就是_default
是默认配置,production 或 development 可以覆盖,也可以增加配置
直接运行
hugo serve
时的默认环境是development
直接运行hugo
时的默认环境是production
这样非常方便本地开发和部署到线上时切换不同的配置。
但是看了一下,官方的文档对于这个功能的描述非常的不清晰。 看了这个功能相关的issue 大概了解了一些用法。 我想还是暂是不使用这个功能。
评论系统
由于hugo是静态博客生成器,因此,我们要引入评论功能,只能通过API来嵌入评论。 hugo本身已经内置了Disqus,只需要配置一个id就能启用。
虽然国外的 Disqus评论系统其实还算好用,但是国内很无奈,主域名基本无法直接访问,然而它的js里也嵌入了api请求域名, 算起来差不多有三四个域名,所以,用反向代理我也嫌弃麻烦。放弃之。
国内的有changyan,也提供免费使用,但是肯定是不考虑的。原因不用多说。
基于gihtub的评论系统也有一些,比如gitment, gitalk等,但是我还是不想用这种,因为最近连github也是不能直接访问了。
还有个Valine是基于LeanCloud的服务的,这个我也不想用,用的什么对象存储,然后严重依赖LeanCloud这个国内的服务。 说是“无后端评论系统”,其实是把后端放在了LeanCloud。另外,这个新版本也要移除邮件提醒功能了, 需要邮件提醒还得再整一个第三方邮件提醒。
isso是基于py的,貌似很久没人维护了,这个也不满足条件。
go-isso实现还没完成,暂时也不是可用状态,继续找。
找到一个叫remark42的评论系统,这个评论系统啥都好,就是: 一,没有中文。二,不记录邮箱和网址。三,发邮件提醒只能用SMTP。 关于第一点,我已经提交了一个PR给官方,官方已经把中文语言merge进去了。关于第二点,基于隐私,不记录邮箱, 这个我可以理解,但是不记录网址,博客与博客之间就达不到那种互动互踩的效果了。其它博主在你的博客留言了,你却不知道他的博客网址。 不记录评论者网址这一点,我有时间再想办法改进一下。 第三点,我提交了一个使用sendgrid和mailgun的WEB API发邮件的PR给官方,然后官方拒绝了。 理由是“这会使代码膨胀”, 这是什么理由? 你集成了oauth登录功能,加了常见的github, google, twiiter,突然有一天你觉得yandex 也要加上了,然后你就把yandex也加上了,你的代码就不“膨胀”了?
为什么SMTP已经是“能用”状态了,带要用web api发送邮件?很简单,我都是用自己的VPS架设的博客,基于安全考虑,不希望SMTP协议暴露源站 IP。用WEB API可以很轻松地解决这个问题。
然后remark42官方提议,我另外弄一个side container跑一个SMTP到WEB API发送邮件的bridge服务。 然后,发邮件的流程就变成了:remark42 => SMTP-WEB_API-bridge => SendGrid/Mailgun WEB API
问题是,明明可以通过代码解决了,再加个side container,至于么?
这是把开发者的责任,转架到了使用者身上。
并且,他拒绝我这个PR,然而他自己的oauth实现也是这样实现的。如果按照他这个逻辑,这个oauth登录功能也会使“代码膨胀”,
因为提供oauth登录服务的厂家永久在变动的。你们俄罗斯的用yandex, 到我们这了可能就要加别的厂家的登录接口功能了。
另外,我看了下他那个Dockerfile, 写得层数实在太多。我数了一下他最终生成的镜像层数,不多不少,正好17层。
一般我做镜像,基本上都会控制在10层以下,一般在5层左右。
因为docker用的是overlayfs, 一层一层覆盖的,层数越多,io效率越低。
那你可能会多,我层数多,加一个配置文件都是一个新层,方便更新啊。如果你要这样说的话,那我反问你, 做base image的人,是不是也要一个文件一个软件地加,给你弄个100层?方便你更新?
但是个人维护这么一个项目是极其要时间了,这也是为什么,我还是比较倾向于让官方接受我这个PR。
但是remark42的项目主负责人对于源站IP泄漏这种安全问题压根没有什么意识,在我的PR里他还反问我,大家的服务器都是public IP了, 这个IP本身就是公开的,还存在什么源站IP泄漏?我只能呵呵了。
我回了一句:你的服务器没有被DDos 攻击,你当然不会意识到这是一个问题。
最后,欢迎大家关注我的remark42 fork: https://github.com/ttys3/remark42/
同时也欢迎有兴趣的童鞋一起完善改进。
我现在使用的remark42是编译自我的fork的,然后通过github actions自动提交到了dockerhub:
https://hub.docker.com/r/80x86/remark42
remark42部署效果图:
LaTeX
hugo目前的goldmark
妈蛋解析引擎还不支持latex。 虽然结合katex js,实现一些简单的LaTeX解析是没问题的,但是如果LaTeX里有_
和[]
这种妈蛋里也有的标记, 就会导致LaTeX的标记走丢了(被妈蛋抢走了)。
容器部署
hugo产出的HTML直接用nginx容器跑http服务。 remark42容器提供评论API。 前端采用envoy做代理,hugo和remark42端口由envoy开放。
nginx容器部署
一个基本的适用于hugo站点的nginx配置(/etc/nginx/conf.d/default.conf
)如下:
# nginx config for hugo site
# author: ttys3
server {
listen 8080;
server_name localhost;
#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;
index index.html;
# absolute_redirect must off for hugo site if no `$uri/index.html` on try_files
# absolute_redirect off;
# port_in_redirect off;
# server_name_in_redirect off;
root /app/public;
location / {
try_files $uri $uri/index.html =404; #fixup for hugo
}
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
然后直接运行docker官方的nginx容器nginx:1.17.9
,映射/etc/nginx/conf.d/default.conf
配置文件,并映射/app/public目录:
sudo docker run -d --name hugo \
-v hugo-public目录位置:/app/public:ro,z \
-v default.conf文件位置:/etc/nginx/conf.d/default.conf:z \
--mount type=tmpfs,destination=/tmp \
nginx:1.17.9
remark42容器部署
怎么申请github, google, twitter的oauth登录接口,这里有详细的步骤: https://github.com/ttys3/remark42#register-oauth2-providers
sudo docker run -d --name remark42 \
-e REMARK_PORT=8081 \
-e REMARK_URL='https://remark42的访问地址' \
-e PUID=1000 \
-e TIME_ZONE="Asia/Shanghai" \
-e SITE='网站域名(不需要带http和端口)' \
-e ADMIN_SHARED_ID='管理员的用户id' \
-e ADMIN_SHARED_EMAIL='管理员邮件地址@example.com' \
-e ADMIN_PASSWD='管理员密码' \
-e SECRET='用于加密的密钥,自己随便md5一下弄个串出来放这就好了' \
-e AUTH_EMAIL_ENABLE=false \
-e AUTH_EMAIL_FROM='邮件认证或通知提醒时的发件人邮箱@sg.ttys3.net' \
-e AUTH_GITHUB_CID='github-api-key填写这' \
-e AUTH_GITHUB_CSEC='github-api-key-secret填写这' \
-e AUTH_GOOGLE_CID='谷歌api-key填写这' \
-e AUTH_GOOGLE_CSEC='谷歌api-key-secret填写这' \
-e AUTH_TWITTER_CID='twitter-api-key填写这' \
-e AUTH_TWITTER_CSEC='twitter-api-key-secret填写这' \
-e NOTIFY_TYPE=email \
-e AUTH_EMAIL_SUBJ='荒野無燈weblog登录确认' \
-e NOTIFY_EMAIL_VERIFICATION_SUBJ='确认订阅来自荒野無燈weblog的评论' \
-e NOTIFY_EMAIL_ADMIN=true \
-e EMAIL_PROVIDER=sendgrid \
-e EMAIL_SG_API_KEY='你的sendgrid密钥填这' \
-e EMOJI=true \
-v /home/data/remark42:/srv/var:rw,z \
80x86/remark42:latest
AUTH_GITHUB_CID / AUTH_GITHUB_CID 这几个不是必须的。 比如你只想启用github登录评论,则只需要配置AUTH_GITHUB_CID
和 AUTH_GITHUB_CSEC
。 目前我实现了mailgun和sendgrid这两个发送邮件的provider, 如果你用mailgun,请修改上面的 EMAIL_PROVIDER=sendgrid
为 EMAIL_PROVIDER=mailgun
更多配置信息请去 https://github.com/ttys3/remark42 查看。
envoy容器部署
Envoy 是专为大型现代 SOA(面向服务架构)架构设计的 L7 代理和通信总线。
Envoy Proxy 在代理方面性能咋样呢?老灯这里放两张2018年的图(来源):
Envoy Proxy 主要是用来在微服务架构中做service mesh的, 但是这里我们用它来做edge proxy对它来说当然更是小菜一碟。
关于做edge proxy的例子,官方的最佳实践文档中有: Configuring Envoy as an edge proxy
这里老灯分享上我的配置envoy.yaml
:
# https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/edge
overload_manager:
refresh_interval: 0.25s
resource_monitors:
- name: "envoy.resource_monitors.fixed_heap"
typed_config:
"@type": type.googleapis.com/envoy.config.resource_monitor.fixed_heap.v2alpha.FixedHeapConfig
# TODO: Tune for your system.
max_heap_size_bytes: 2147483648 # 2 GiB
actions:
- name: "envoy.overload_actions.shrink_heap"
triggers:
- name: "envoy.resource_monitors.fixed_heap"
threshold:
value: 0.95
- name: "envoy.overload_actions.stop_accepting_requests"
triggers:
- name: "envoy.resource_monitors.fixed_heap"
threshold:
value: 0.98
admin:
access_log_path: "/dev/null"
address:
socket_address:
address: 0.0.0.0
port_value: 9901
static_resources:
listeners:
- address:
socket_address:
address: 0.0.0.0
port_value: 80
per_connection_buffer_limit_bytes: 32768 # 32 KiB
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
stat_prefix: ingress_http
use_remote_address: true
common_http_protocol_options:
idle_timeout: 3600s # 1 hour
http2_protocol_options:
max_concurrent_streams: 100
initial_stream_window_size: 65536 # 64 KiB
initial_connection_window_size: 1048576 # 1 MiB
stream_idle_timeout: 60s # 1 mins, must be disabled for long-lived and streaming requests
request_timeout: 60s # 1 mins, must be disabled for long-lived and streaming requests
access_log:
- name: envoy.file_access_log
config:
path: "/dev/stderr"
route_config:
virtual_hosts:
- name: comment
domains:
- "cmt.ttys3.net"
routes:
- match: { prefix: "/" }
route:
cluster: service_comment
idle_timeout: 15s # must be disabled for long-lived and streaming requests
- name: hugo
domains:
- "ttys3.net"
routes:
- match: { prefix: "/" }
route:
cluster: service_hugo
idle_timeout: 15s # must be disabled for long-lived and streaming requests
- name: cgit
domains:
- "git.ttys3.net"
routes:
- match: { prefix: "/" }
route:
cluster: service_cgit
idle_timeout: 15s # must be disabled for long-lived and streaming requests
# hugo nginx only listen on http
http_filters:
- name: envoy.router
config: {}
clusters:
- name: service_hugo
connect_timeout: 3s
per_connection_buffer_limit_bytes: 32768 # 32 KiB
lb_policy: round_robin
type: "STRICT_DNS"
hosts:
- socket_address:
address: hugo
port_value: 8080
- name: service_comment
connect_timeout: 3s
per_connection_buffer_limit_bytes: 32768 # 32 KiB
lb_policy: round_robin
type: "STRICT_DNS"
hosts:
- socket_address:
address: remark42
port_value: 8081
- name: service_cgit
connect_timeout: 3s
per_connection_buffer_limit_bytes: 32768 # 32 KiB
lb_policy: round_robin
type: "STRICT_DNS"
hosts:
- socket_address:
address: cgit
port_value: 8082
注意:
Envoy Proxy的配置文件是YAML格式,注意要用空格,不要用tab. docker的DNS服务会自动把容器名称映射到容器的IP地址,因此clusters配置这里的
address: hugo
要与前面创建的hugo容器名称hugo
对应。
这里老灯开了3个服务,一个hugo, 一个comment, 一个cgit. 我在创建容器的时候,分别让hugo监听8080, remark42 comment监听 8081, cgit 监听8082端口。
- hugo使用域名:
ttys3.net
- 评论使用域名:
cmt.ttys3.net
- cgit使用域名:
git.ttys3.net
(暂未架设,预留)
然后为了简单起见,我们只让Envoy Proxy对外暴露出80端口,因此我这里也没有配置SSL。 为什么呢?因为通过走cloudflare CDN之后,CF可以自动提供SSL证书。 如果要弄SSL,咱也可以直接弄个自签名的SSL就行了,因为 CF 也认。 或者,讲究一点,弄个ACME自动证书。
这里开放的9901 端口是用于查询一些Envoy Proxy的状态的,注意这个端口是不需要认证就能访问的,因此用容器跑的时候, 一般不要绑定到0.0.0.0
让外网能访问到。 因为通过这个WEB接口是能关掉Envoy Proxy,甚至能修改配置的。
运行Envoy Proxy容器:
sudo docker run -d --name envoy \
-v envoy.yaml文件的绝对路径:/etc/envoy/envoy.yaml:z,ro \
-p 127.0.0.1:9901:9901 \
-p 80:80 \
envoyproxy/envoy:v1.14.1
这里我们只开放了80端口,如果启用了SSL, 需要加上-p 443:443 \
podman注意事项
由于老灯目前是在用podman,没有用docker, 也就是以上命令里的docker替换成podman就能跑了。
使用docker的注意以上运行容器的命令都要加上--restart=unless-stopped
, 不然机器重启后容器不会自动启动。
使用podman的则要使用podman generate systemd 容器id或名称
来生成systemd unit文件并启用服务来实现开机自启。
其它
Envoy Proxy容器可不可以用nginx做基于域名的虚拟主机来做反向代理替换掉? 完全可以。
hugo public目录发布的问题
我暂时用了最简单的方法,直接一个up.sh
脚本完事
#!/bin/sh
env TZ='Asia/Shanghai' hugo --gc --minify --enableGitInfo && \
rsync -ravz --progress --checksum --delete public/ root@服务器地址:/home/http/html/ttys3.net
这里的/home/http/html/ttys3.net
就是hugo网站的根目录了。
也可以采用在服务端架设gitolite,然后用git hook实现自动发布hugo站点,这个后续有时间再分享。
其它部署方式?
hugo支持很多部署方式,包括直接托管在github pages 或者用Github Actions实现自动化部署Hugo博客, 以及托管在netlify. 更多部署方式可以在hugo官网找到。
邮件发送API的选择
Mailgun和SendGrid都提供免费的plan,默认情况下,Mailgun每月的发送量送得更多(限制1万封)。 SendGrid在试用期过后,每天的邮件数量会限制在100封,感觉可用性不是很高。
Mailgun(https://www.mailgun.com/) Free Plan provides 10,000 Emails per month
SendGrid(https://sendgrid.com) Trial Plan provides 40,000 emails for 30 days. After your trial ends, you can send 100 emails/day for free
优秀的hugo主题推荐
浅色系:
https://github.com/olOwOlo/hugo-theme-even
https://github.com/xianmin/hugo-theme-jane
https://github.com/zhengzangw/hugo-theme-ztyblog
暗黑系:
https://github.com/panr/hugo-theme-hello-friend
hello-friend的第三方fork版: https://github.com/rhazdon/hugo-theme-hello-friend-ng
更多主题请去 https://themes.gohugo.io/ 查看。
hugo基础教程?
Hugo 从入门到会用 https://blog.olowolo.com/post/hugo-quick-start/
博客迁移——Hugo使用整体概览 https://www.rectcircle.cn/posts/blog-migration/
TODO
- 站内搜索功能
- 友情链接页面