0%

Hexo搭建GitPage静态博客


作者: 耗子007


环境准备

  • docker
  • nodejs镜像
  • hexo
  • git相关

docker安装

docker的安装请参考官方文档:https://docs.docker.com/engine/installation/

nodejs镜像

国内可以使用Docker官方的加速地址,具体配置参考官方文档: https://www.docker-cn.com/registry-mirror

本文使用修改config的方式:

1
2
3
{
"registry-mirrors": ["https://registry.docker-cn.com"]
}

然后下载nodejs的官方镜像:

1
docker pull node

安装hexo

首先启动一个node的容器

1
2
docker run -it -p 4000:4000 node /bin/bash
# 映射容器的4000端口到host的4000端口,是为了方便测试hexo生成的静态网站是否正常

然后,安装hexo

1
npm install -g hexo-cli

创建git仓库

gitpage对仓库的要求就是仓库名的格式必须为:username.github.io,例如本文仓库名:duguhaotian.github.io

配置git公钥

首先,在容器中生成公钥

1
ssh-keygen

然后拷贝公钥到你Git上,具体步骤百度。

1
cat ~/.ssh/id_rsa.pub

配置git

配置用户名和邮箱

1
2
git config --global user.name   xxxx
git config --global user.email xxxx@xxx.com

构建hexo工程

创建工程目录,然后通过hexo初始化目录

1
2
mkdir test
hexo init test

生成的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
~/test# tree -L 1
.
|-- _config.yml
|-- db.json
|-- node_modules
|-- package-lock.json
|-- package.json
|-- public
|-- scaffolds
|-- source
|-- themes

增加博客的方式有两种:

  • 通过hexo生成新的博客文件,然后写博客
  • 或者把写好的博客文件(markdown格式),放入test/source/_posts/目录

安装依赖库

由于hexo依赖一些库,如支持推送静态页面到git的库等。最好安装下面所有库

1
npm install

生成静态网站

在test目录执行:

1
2
3
4
5
6
7
# 生成静态页面
hexo g
# 可以选择在generate的时候,watch源文件的变化,hexo感知到源文件变化,会自动重新出发generate,从而达到动态更新博客的效果
hexo g -w

# 部署本地静态网站(localhost:4000)
hexo s

注意:这里的watch会为我们后续自动更新博客做到很好的支持

查看本地静态网站是否构建正常,如果无问题,直接推送到github仓库。

推送到GitPage

当本地网站验证无误,就可以推送到你的Git仓库了,然后Github会自动部署你的GitPage。

首先,安装依赖的插件:

1
npm install hexo-deployer-git --save

然后,修改hexo的配置文件:_config.yml ,增加deploy的配置

1
2
3
4
deploy:
type: git
repository: https://github.com/duguhaotian/duguhaotian.github.io.git
branch: master

配置文件默认情况如下:

1
2
deploy:
type:

因此,我们增加type和对应的repository地址,还有git分支。

最后,直接利用hexo的deploy功能把hexo生成的静态页面推送到Github上我们新建的仓库。

1
2
3
hexo d
# 或者
hexo g -d

跟换主题

首先,在hexo官网找到自己需要的皮肤:https://hexo.io/themes/
例如,material的皮肤,然后获取git地址:https://github.com/viosey/hexo-theme-material

主要步骤:

  • 把该目录拷贝到themes/下面
  • 重命名为material
  • 修改test/_config.yml配置文件中theme为:material
  • 把test/theme/material/_config.template.yml拷贝一份为:test/themes/material/_config.yaml,不然hexo生成静态页面会错误

主题配置

主题可以在github上面,搜索hexo-theme,然后找到适合自己的主题。本文已hexo-theme-next为例。

安装方法

hexo工作目录为hexospace。

1
2
$ cd hexospace
$ git clone https://github.com/theme-next/hexo-theme-next themes/next

修改hexospace/_config.yml的theme项为theme: next

添加tags和categories页面

分别生成对应的index.md

1
2
hexo new page categories
hexo new page tags

修改生成的index.md为如下:

1
2
3
4
5
6
cat source/tags/index.md 
---
title: tags
date: 2019-09-01 14:41:38
type: "tags" //手动增加
---
1
2
3
4
5
6
cat source/categories/index.md 
---
title: categories
date: 2019-09-01 14:42:03
type: "categories" //手动增加
---

配置gitalk

首先,生成授权需要的id和secret,网址:https://github.com/settings/applications/new

具体配置参考下图:

gitalk oauth

然后配置next/_config.yml:

1
2
3
4
5
6
7
gitalk:
enable: true
github_id: 你的github账号 # GitHub repo owner
repo: 只需要repo名字就行了,例如test # Repository name to store issues
client_id: 上面生成的id # GitHub Application Client ID
client_secret: 上面生成的秘钥 # GitHub Application Client Secret
admin_user: 你的github账号 # GitHub repo owner and collaborators, only these guys can initialize gitHub issues

更多配置可以参考下面的手册:https://theme-next.org/docs/getting-started/

支持mermaid

1
npm install -s hexo-filter-mermaid-diagrams

next主题支持mermaid,需要开启,开启方法:

1
2
3
4
5
6
# Mermaid tag
mermaid:
enable: true
# Available themes: default | dark | forest | neutral
theme: default
cdn: //cdn.jsdelivr.net/npm/mermaid@8/dist/mermaid.min.js

自动更新博客

当我们的博客源文件存储在github上面的时候,那么在修改、删除和新增博客时,每次都需要把博客拷贝到我们的hexo的工作目录,加上上面的watch功能,可以自动生成新的博客,并且更新hexo服务器中的博客。

那么,如果我们借助github的webhook功能,动态感知博客源文件仓库的变化,然后自动更新博客源文件,然后出发hexo的自动更新功能。答案是可以的。下面是一个简单的尝试 ,后续有时间会继续优化。

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
package main

import (
"bytes"
"encoding/json"
"flag"
"io/ioutil"
"log"
"net/http"
"os/exec"
"path"
"sync"
)

var (
notesPath string
hexoPath string
lock sync.Mutex
)

func init() {
flag.StringVar(&notesPath, "notes", "./notes", "path where store notes")
flag.StringVar(&hexoPath, "hexo", "./test/source/_posts/", "path where store hexo source post")
}

type User struct {
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
Username string `json:"username,omitempty"`
}

// github webhook commit format
type Commit struct {
ID string `json:"id"`
Tree_id string `json:"tree_id"`
Distinct bool `json:"distinct"`
Message string `json:"message,omitempty"`
Timestamp string `json:"timestamp,omitempty"`
Url string `json:"url,omitempty"`
Author *User `json:"author,omitempty"`
Committer *User `json:"committer,omitempty"`
Added []string `json:"added,omitempty"`
Removed []string `json:"removed,omitempty"`
Modified []string `json:"modified,omitempty"`
}

func runCmd(cmdStr string, args []string) error {
cmd := exec.Command(cmdStr, args...)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
log.Printf("run %s failed: %s", cmdStr, err.Error())
return err
}
return nil
}

func updateHandle(files []string) {
for _, f := range files {
_, fname := path.Split(f)
runCmd("cp", []string{"-f", notesPath + "/" + f, hexoPath + "/" + fname})
}
}

func removeHandle(files []string) {
for _, f := range files {
_, fname := path.Split(f)
runCmd("rm", []string{"-f", hexoPath + "/" + fname})
}
}

func syncGit() bool {
err := runCmd("/bin/bash", []string{"-c", "cd " + notesPath + "; git pull"})
if err != nil {
log.Printf("sync notes from git failed: %s\n", err.Error())
return false
}
return true
}

func changes(data []byte) ([]string, []string) {
var copys []string
var rms []string
var commit Commit

err := json.Unmarshal(data, &commit)
if err != nil {
log.Printf("Invalid commit: %s", string(data))
return nil, nil
}

if len(commit.Added) > 0 {
copys = commit.Added
}
if len(commit.Modified) > 0 {
copys = append(copys, commit.Modified...)
}
if len(commit.Removed) > 0 {
rms = commit.Removed
}

log.Printf("copys: %v, removes: %v\n", copys, rms)
return copys, rms
}

func handleCommits(commits []interface{}) {
var updates []string
var removes []string
for _, commit := range commits {
data, err := json.Marshal(commit)
if err != nil {
log.Printf("Invalid commit: %v", commit)
continue
}
us, rs := changes(data)
updates = append(updates, us...)
removes = append(removes, rs...)
}

lock.Lock()
defer lock.Unlock()
if !syncGit() {
return
}
updateHandle(updates)
removeHandle(removes)
}

func main() {
flag.Parse()
log.Printf("notes path: %s\n", notesPath)
log.Printf("hexo path: %s\n", hexoPath)

helloHandler := func(w http.ResponseWriter, req *http.Request) {
var fullData map[string]interface{}

robots, err := ioutil.ReadAll(req.Body)
req.Body.Close()
if err != nil {
log.Fatal(err)
}

if err := json.Unmarshal([]byte(robots), &fullData); err != nil {
log.Fatal(err)
}
commits, ok := fullData["commits"]
if !ok {
log.Printf("Cannot found commits in %s", fullData)
return
}
if t, ok := commits.([]interface{}); ok {
handleCommits(t)
}
log.Println("get message")
}

http.HandleFunc("/notes", helloHandler)
log.Fatal(http.ListenAndServe(":8088", nil))
}

博客系统的完整结构

sequenceDiagram
    participant A as User
    participant B as GithubRepo
    participant C as Listener
    participant D as HexoServer
    activate C
    activate D
    A ->> B: 更新、增加或者删除博客
    B ->> C: webhook push event
    loop new commit
        C ->> C: 1. 更新博客repo;2. 更新hexo博客源文件
    end
    deactivate C
    loop hexo服务
        D ->> D: 1. 监听博客源文件变化;2.自动生成新的静态页面;3. 更新服务器内容
    end
    deactivate D

我们需要两个服务器:

  • 一个是接收github的webhook推送信息,并且根据推送的信息,更新hexo的博客源文件
  • 一个是hexo的服务器,用于提供hexo博客服务

一个流程基本如上图所示:

  1. 用户更新博客,并且推送到github的博客仓库;
  2. github根据配置的webhook,发送commit信息到listener服务器;
  3. listener服务器根据,commit信息,更新当前hexo管理的博客源文件;
  4. hexo在generate的时候,配置了watch,因此在感知到源文件变化时,会重新生成静态页面。

总结

整体结构还是可以的,现在有几个问题:

  1. 写的listener比较简单,需要优化;
  2. hexo的watch功能,能更新文件,但是存在概率出现文章生成的不完整,不知道什么鬼???

参考文章