Setup a free personal blog using Hexo

Notes to set up a Hexo blog site, as a reminder for the future me. 😁

Setup Node JS

Install the node package manager (npm) if you want to preview/build the website on the local machine.

  • Windows: Download and install from the official website.
  • MacOS / Linux: nvm is recommended to install and manage your local version(s) of npm.

The easy way: copy my template site

This template includes these features:

  • Hexo static site generator.

  • Fast and elegant Next theme.

  • markdown-it renderer and KaTeX math.

  • GitHub actions and GitLab CI included.

  • Hosting on GitHub: click use as template to copy this template under your GitHub account.

  • Hosting on GitLab: import this repo.

In _config.yml, change baseurl and your personal settings.

_config.yml
1
2
url: https://username.github.io
root: /repo-name/ # or "/" for personal/group website (username.github.io)

Also see Hexo configuration for more details.

In _config.next.yml, change the settings of the Next theme. Also see Next theme settings for further customizations.

The self-serving way

Also checkout getting started in Hexo Next.

Hexo website and and Next theme

  1. install the hexo command:
1
npm install hexo-cli -g
  1. setup Next theme:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Created the website in `blog` folder.
hexo init blog
cd blog

# Install Hexo npm dependencies
npm install

# Install Next theme
npm install hexo-theme-next

# Copy the theme config file for future customizations
cp ./node_modules/hexo-theme-next/_config.yml _config.next.yml

# Remove the original landscape theme
rm -rf ./themes/landscape
  1. use Next theme in the config
_config.yml
1
theme: next
  1. preview your setup.
1
2
3
4
# Test your setup.
# This command will open a browser and visit http://localhost:4000.
# Ctrl+C to exit
hexo clean && hexo server -o --debug

You can start blogging using the hexo new command e.g. hexo new post hello or start to customize your settings. Press Ctrl + C to exit preview.

Browser sync

By default, you need to press the refresh button when you make a change to the markdown file, which is tedious.

The hexo-browsersync package comes to the rescue. It issues browser refresh automatically upon filesystem changes in your website in server mode.

1
npm i hexo-browsersync -D

Useful hexo plugins

1
2
3
4
5
6
7
8
# Word and read time counter
npm i hexo-word-counter

# Local search
npm i hexo-generator-searchdb

# Emoji tag plugin
npm i hexo-filter-emoji

Switch to a better renderer

The default renderer hexo-renderer-marked does not fully support LaTeX math syntax. Thus, we could switch to a better one.

See the math rendering post to switch the renderer and setup the math typesetting libraries.

Additional markdown-it settings

Install some other useful plugins:

1
npm i markdown-it-imsize markdown-it-named-headings markdown-it-task-checkbox markdown-it-kbd

Add the settings of hexo-renderer-markdown-it in _config.yml like this:

_config.yml
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
markdown:
render:
html: true
xhtmlOut: false
breaks: true
linkify: true
typographer: false
quotes: ''
plugins:
- markdown-it-abbr
- markdown-it-deflist
- markdown-it-footnote
- markdown-it-latex2img
- markdown-it-mark
- markdown-it-task-checkbox
- markdown-it-kbd
- name: markdown-it-emoji
options:
shortcuts: {}
anchors:
# Minimum level for ID creation. (Ex. h2 to h6)
level: 2
# A suffix that is prepended to the number given if the ID is repeated.
collisionSuffix: 'v'
# If `true`, creates an anchor tag with a permalink besides the heading.
permalink: true
# Class used for the permalink anchor tag.
permalinkClass: header-anchor
# Set to 'right' to add permalink after heading
permalinkSide: 'left'
# The symbol used to make the permalink
permalinkSymbol: ""
# Transform anchor to (1) lower case; (2) upper case
case: 1
# Replace space with a character
separator: '-'

Deployment

Create a file .github/workflows/gh-pages.yml for GitHub actions.

Also see GitHub action for GitHub pages

With pandoc

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
name: github pages
env:
NODE_ENV: production
PDC_VER: "2.17.1.1"

on:
push:
branches:
- main

jobs:
build:
name: Build website
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Restore last modified time
run: "git ls-files -z | while read -d '' path; do touch -d \"$(git log -1 --format=\"@%ct\" \"$path\")\" \"$path\"; done"
- uses: actions/setup-node@v2.1.4
with:
node-version: '14'
- name: Cache Dependencies
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Setup Pandoc
run: wget -qO- https://github.com/jgm/pandoc/releases/download/${PDC_VER}/pandoc-${PDC_VER}-linux-amd64.tar.gz | sudo tar xvz --strip-components 1 -C /usr/local
- name: Build website
run: npm ci && npm run build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
personal_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public
full_commit_message: ${{ github.event.head_commit.message }}

With markdown-it

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
name: github pages
env:
NODE_ENV: production

on:
push:
branches:
- main

jobs:
build:
name: Build website
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Restore last modified time
run: "git ls-files -z | while read -d '' path; do touch -d \"$(git log -1 --format=\"@%ct\" \"$path\")\" \"$path\"; done"
- uses: actions/setup-node@v2.1.4
with:
node-version: '14'
- name: Cache Dependencies
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Build website
run: npm ci && npm run build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
personal_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public
full_commit_message: ${{ github.event.head_commit.message }}

Create a file .gitlab-ci.yml for GitLab CI/CD. Also see the documentation of GitLab pages.

With pandoc

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
# https://hub.docker.com/r/tarampampam/node/
image: tarampampam/node:lts-alpine

# Cache modules in between jobs: https://docs.gitlab.com/ee/ci/caching/#caching-nodejs-dependencies
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .npm/

variables:
NODE_ENV: "production"
PDC_VER: "2.11.3.1"

before_script:
- "git ls-files -z | while read -d '' path; do touch -d \"$(git log -1 --format=\"@%ct\" \"$path\")\" \"$path\"; done"
- "wget -qO- https://github.com/jgm/pandoc/releases/download/${PDC_VER}/pandoc-${PDC_VER}-linux-amd64.tar.gz | tar xvz --strip-components 1 -C /usr/local"
- npm ci --cache .npm --prefer-offline
- npm run build

test:
stage: test
script:
- echo "Done"
except:
- main

pages:
stage: build
script:
- apk add --update brotli
- find public -type f -regex '.*\.\(htm\|html\|txt\|text\|js\|css\|svg\|xml\)$' -exec gzip -f -k {} \; || echo 'Gzip failed. Skipping...'
- find public -type f -regex '.*\.\(htm\|html\|txt\|text\|js\|css\|svg\|xml\)$' -exec brotli -f -k {} \; || echo 'Brotli failed. Skipping...'
artifacts:
paths:
- public
only:
- main

With markdown-it

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
# https://hub.docker.com/r/tarampampam/node/
image: tarampampam/node:lts-alpine

# Cache modules in between jobs: https://docs.gitlab.com/ee/ci/caching/#caching-nodejs-dependencies
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .npm/

variables:
NODE_ENV: "production"

before_script:
- "git ls-files -z | while read -d '' path; do touch -d \"$(git log -1 --format=\"@%ct\" \"$path\")\" \"$path\"; done"
- npm ci --cache .npm --prefer-offline
- npm run build

test:
stage: test
script:
- echo "Done"
except:
- main

pages:
stage: build
script:
- apk add --update brotli
- find public -type f -regex '.*\.\(htm\|html\|txt\|text\|js\|css\|svg\|xml\)$' -exec gzip -f -k {} \; || echo 'Gzip failed. Skipping...'
- find public -type f -regex '.*\.\(htm\|html\|txt\|text\|js\|css\|svg\|xml\)$' -exec brotli -f -k {} \; || echo 'Brotli failed. Skipping...'
artifacts:
paths:
- public
only:
- main