基于容器部署hugo博客 -- hugo个人博客折腾记之后篇

基于容器部署hugo博客 -- hugo个人博客折腾记之后篇

这篇主要讲一下这个博客是怎么部署的

{{< music "32272267" >}}

hugo 主题选取

主题选取这个其实在前篇已经说过了

这里说下这个主题的特点吧:

  1. 5色可选。5种双色主题 (orange is default, red, blue, green, pink)可以直接在配置文件里修改。

  2. Fira Code作为默认等宽字体

  3. 响应式设计,各种分辨率浏览效果都不错

  4. 采用 PrismJS 实现客户端代码高亮

  5. 最重要的是,它简洁。 没错,真的很简洁,没有过多的功能,甚至没有archive页面,没有tag cloud,只自带了3个shortcode。 对于hugo新手来说,越是简单的主题,你才越好下手修改。要不然刚接触hugo,就用了一个功能特别复杂的主题, 这对于掌握主题的DIY技能是没有帮助的。 现在你看到我的博客,我已经是根据自己的需求做了非常多的调整了。

我调整后的terminal主题:

  1. 同样5色可选。
  2. 使用Jetbrains Mono https://github.com/JetBrains/JetBrainsMono 等宽字体进行代码高亮
  3. 响应式设计保持不变
  4. 采用hugo自带的代码高亮方案(输出class) + 自定义的Gruvbox高亮配色
  5. 调整hugo默认的RSS 2.0 feed输出为atom输出,调整RSS摘要输出为全文输出
  6. 增加archive,tagcloud模板,增加基本的KaTex自动渲染LaTex支持。
  7. 增加TOC显示,返回TOP按钮,平滑滚动。
  8. 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_CIDAUTH_GITHUB_CSEC。 目前我实现了mailgun和sendgrid这两个发送邮件的provider, 如果你用mailgun,请修改上面的 EMAIL_PROVIDER=sendgridEMAIL_PROVIDER=mailgun

更多配置信息请去 https://github.com/ttys3/remark42 查看。

envoy容器部署

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

  • 站内搜索功能
  • 友情链接页面