Featured image of post Hugo Blog Github Pages / Private Deployment Guide

Hugo Blog Github Pages / Private Deployment Guide

Hugo CI/CD automated deployment, internationalization configuration.

Hugo is an open-source static site generator written in Go. It typically takes only a few seconds to generate a website and is known as the “world’s fastest framework for building websites.” Users can write blog content using Markdown syntax. Combined with GitHub Pages, you can deploy your own blog without needing a server. It also supports private deployment methods using HTTP servers like Nginx and Apache. This article, based on the deployment process of this blog, details the above two deployment methods, as well as CI/CD automated deployment and internationalization (i18n) configuration.

English site - Github Pages access URL:

zoyao.github.io

Chinese site - Nginx private deployment access URL:

zoyao.site

Hugo Installation

Local Hugo installation: It is recommended to install the Hugo client locally when using it for the first time to facilitate debugging. During subsequent use, you can operate without the local Hugo client; simply editing Markdown files locally and uploading them to GitHub will trigger automated deployment. For details, see the GitHub Pages Automated Deployment and Private Automated Deployment sections.

  • Hugo Installation

    Installation process - taking Windows winget as an example, installing the Extended version:

    1
    2
    3
    4
    5
    
    // Install
    winget install Hugo.Hugo.Extended
    
    // Uninstall
    winget uninstall --name "Hugo (Extended)"
    
 1
 2
 3
 4
 5
 6
 7
 8
 9
10

    For other operating systems and installation methods, please refer to the [official documentation][hugo-installation].

- Create a New Site

    Execute the following command to create a new site named `mysite` in the current folder:

    ```bash
    hugo new site mysite
    
  • Preview

    If executed successfully, you can preview the website locally on port 1313:

    1
    
    hugo server
    
1
2
3
4
5
6
7
8

- Build

    Once the build is complete, the static website will be generated in the `public` directory:

    ```bash
    hugo
    
  • Environment Configuration

    Hugo manages environment configuration files under the config folder (this directory does not exist by default and needs to be created by the user). The directory structure is as follows:

    1
    2
    3
    4
    5
    6
    7
    
    mysite
    ├── config.toml
    └── config
        ├── development
        │   └── config.toml
        └── production
            └── config.toml
    
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

    As shown above, `development` and `production` environment configurations are created. You can specify the configuration file using `--environment development`. If not specified, then:

     The `hugo server` preview command uses the `development` environment by default.

     The `hugo` build command uses the `production` environment by default.

    Startup commands specifying the configuration file:

    ```bash
    // Preview
    hugo server --environment development

    // Build
    hugo --environment development
    

Theme Configuration

Thanks to Hugo’s rich theme library, we can easily and quickly obtain many personalized Hugo themes. You can browse the official theme library to pick a theme you like.

Below, we will use the hugo-theme-stack theme as an example.

  • Git Initialization

    1
    2
    3
    4
    5
    
    // Enter the directory
    cd mysite
    
    // Git initialization
    git init
    
1
2
3
4
5
6
7

- Add a submodule for easy theme updates later

    ```bash
    // In the mysite/themes directory, pull hugo-theme-stack and create a theme named stack
    git submodule add [https://github.com/CaiJimmy/hugo-theme-stack.git](https://github.com/CaiJimmy/hugo-theme-stack.git) themes/stack
    
  • Configure the Theme

    Open the newly downloaded theme files, copy the configuration file from the themes/stack/exampleSite directory to the root directory, overwrite the original configuration file, edit the configuration file, and set the theme field to the theme name created in the previous step, which is stack.

    Partial configuration description for hugo-theme-stack:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    // Base URL, used during page jumps
    baseurl: [https://example.com/](https://example.com/)
    
    // Theme name, should be stack here
    theme: hugo-theme-stack
    
    // Website title
    title: Example Site
    
    // Website copyright info
    copyright: Example Person
    
 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

    After configuring, restart to see the theme changes.

    For other hugo-theme-stack configurations, please refer to the [official documentation][hugo-theme-stack-config].

## GitHub Pages Automated Deployment CI/CD

GitHub Pages is a web hosting service provided by GitHub, which can be used to host static web pages, including blogs, project documentation, etc. Hugo can quickly build static websites and naturally supports deployment using GitHub Pages.

- GitHub Pages Configuration

    To use GitHub Pages, you need to create a repository that starts with your GitHub username and ends with `.github.io`, and it must be a public repository. The specific process is:

    1. New repository

    2. Repository name: xxxx(username).github.io    

    3. Public

    4. Create repository

    Once created, open the repository's Settings, and modify the Default branch to change the default displayed branch.

    ![Current configuration is main branch](image-4.png)

    Upload a static `index.html` file to the specified branch of the repository:

    ```html
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="utf-8">
    <title>Hello World</title>
    </head>
    <body>
        <h1>Hello World</h1>
    </body>
    </html>
    
Open `https://username.github.io/` to see the static page you just uploaded.
  • Linking the Hugo Repository for Automated Deployment

    Here, we use two different GitHub repositories:

    • blog repository – used to store the original blog files, mainly in Markdown format.

    • GitHub Pages repository – the repository created in the previous step, used to store the built static website.

    The specific process is as follows:

    1. Connect the permissions of the two repositories

      • Open the GitHub global settings (not repository settings): Settings / Developer settings / Personal access tokens / Fine-grained tokens / Generate new token.

        alt text

      • Token name can be filled in as you like.

      • Expiration: It is recommended to choose No expiration to avoid it becoming unusable after expiring.

      • Repository access: For security reasons, it is recommended to choose Only select repositories and select only the GitHub Pages repository you just created.

        alt text

      • Permissions configuration: Change Contents to Read and write.

        alt text

      • Save and copy the automatically generated token.

      • Add the token configuration to the blog repository. The path is: blog repository / Settings / Security / Secrets and variables / Actions / Repository secrets / New repository secret. Name it PERSONAL_TOKEN, and paste the token copied in the previous step into the Secret and save it.

        alt text

    2. Hugo Automatic Build and Publish

      In the blog repository’s .github/workflows directory, create deploy.yml with the following configuration:

       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: deploy
      
      on:
          # Execute action on git push
          push:
          workflow_dispatch:
          schedule:
              # Runs everyday at 8:00 AM
              - cron: "0 0 * * *"
      
      jobs:
          build:
              runs-on: ubuntu-latest
              steps:
                  - name: Checkout
                  uses: actions/checkout@v2
                  with:
                      submodules: true
                      fetch-depth: 0
      
                  - name: Setup Hugo
                  uses: peaceiris/actions-hugo@v2
                  with:
                      hugo-version: "latest"
      
                  # Hugo build
                  - name: Build Web
                  run: hugo
      
                  # Push to specified repository after building
                  - name: Deploy Web
                  uses: peaceiris/actions-gh-pages@v3
                  with:
                      PERSONAL_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
                      EXTERNAL_REPOSITORY: zoyao/zoyao.github.io
                      PUBLISH_BRANCH: master
                      PUBLISH_DIR: ./public
                      commit_message: ${{ github.event.head_commit.message }}
      
 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

        After configuration, when a `git push` is executed, the automated deployment will be triggered. You can see the relevant workflow in the Actions tab of the `blog` repository.

        ![alt text](image-5.png)

## Private Automated Deployment CI/CD

Using GitHub Pages is very convenient for automated deployment, but GitHub domains can be slow to access in certain network environments. If you have this requirement, you can use private deployment to deploy it to your own server.

- Private Deployment

    Here we use Nginx for private deployment.

    1. Copy the built static blog files to the private server, under the `/app/blog/` directory.

    2. Configure Nginx. In Nginx's `./nginx/conf.d` directory, create a new configuration file, named here as `blog-8888.conf`:

        ```nginx
        server {
            # Listen on port 8888
            listen 8888;
            # IP access is used here first, it will be changed to domain name later during HTTPS configuration
            server_name Server_Public_IP;
            # server_name www.zoyao.site;

            # Specify the location of static resources
            location / {
                root   /app/blog;
                index  index.html index.htm;
            }
        }
        
3. After updating the Nginx configuration, access `http://Server_Public_IP:8888/` to see the blog homepage.
  • Automated Deployment

    To use automated deployment, you first need to configure SSH key login. It is recommended to create a new user first for proper permission isolation.

    1. Use the adduser command to create a new user, named git here (customizable):

      1
      
      sudo adduser git
      
1
2
3
4
5
6

    2. Directory authorization. You need to authorize `/app/blog` to the user created in the previous step. Switch to the `git` user to execute:
    
        ```bash
        sudo chmod -R 777 /app/blog
        
3. Switch to the `git` user, create an SSH key, and save it in GitHub secrets.

    - Switch to the git user:

        ```bash
        su git
        ```

    - Create a new directory `.ssh` under the `/home/git` directory and authorize it:

        ```bash
        mkdir -p ~/.ssh && chmod 700 ~/.ssh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

        - Execute `ssh-keygen` to generate a key pair. It is recommended to use `ed25519` as it is more secure than `rsa`:
        
            ```bash
            ssh-keygen -t ed25519 -C "github-actions-deploy-key" -f ~/.ssh/github_actions
            ```

        - Copy the public key to `authorized_keys`:
        
            ```bash
            sh -c 'cat ~/.ssh/github_actions.pub >> ~/.ssh/authorized_keys'
            
    - Authorize `authorized_keys`:
    
        ```bash
        chmod 600 ~/.ssh/authorized_keys
1
2
3
4
5
6

        - View and copy the contents of the private key for later use:
        
            ```bash
            cat ~/.ssh/github_actions
            
    - Use SSH locally to verify if the login is successful.

    - Add the key configuration to the `blog` repository. The path is: `blog` repository / Settings / Security / Secrets and variables / Actions / Repository secrets / New repository secret. Name it `SERVER_SSH_KEY`, and paste the private key copied in the previous step into the Secret and save it.

        ![alt text](image-6.png)
    
4. Configure GitHub Automated Deployment

    In the `blog` repository's `.github/workflows` directory, create `deploy.yml`. The configuration content is as follows. Change the Server IP to your own private server IP.

    ```yaml
    name: deploy

        on:
            push:
            workflow_dispatch:
            schedule:
                # Runs everyday at 8:00 AM
                - cron: "0 0 * * *"

        jobs:
            build:
                runs-on: ubuntu-latest
                steps:
                    - name: Checkout
                    uses: actions/checkout@v2
                    with:
                        submodules: true
                        fetch-depth: 0

                    - name: Setup Hugo
                    uses: peaceiris/actions-hugo@v2
                    with:
                        hugo-version: "latest"

                    - name: Build Web
                    run: hugo

                    - name: Deploy Web Self
                    uses: easingthemes/ssh-deploy@main
                    with:
                        SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
                        REMOTE_HOST: Server_IP
                        REMOTE_USER: git
                        SOURCE: ./public/
                        TARGET: /app/blog/hugo/
                        ARGS: -avz --delete --chown=git:git    
    ```

HTTPS Configuration

Here we use Alibaba Cloud’s free SSL certificate for HTTPS configuration. However, since the free certificate has a short validity period, we need to install acme for automatic renewal and updates.

  1. Apply for Alibaba Cloud API

    • Create an acme access user

      After logging into the Alibaba Cloud backend, go to Resource Access Management (RAM). Select Identities / Users, create a new user, check OpenAPI Access (AccessKey), confirm, and save the corresponding key and secret for later use.

      alt text

    • Configure permissions

      Click Add Permissions and grant the new user the following permissions:

      AliyunRAMFullAccess

  2. Configure acme Automatic Updates

    • Install acme

      1
      
      curl [https://get.acme.sh](https://get.acme.sh) | sh -s email=xxxx@xxxx.com
      
    • Use the key and secret saved in step 1 to configure Alibaba Cloud’s system parameters:

      1
      2
      
      export Ali_Key="key"
      export Ali_Secret="Secret"
      
    • Issue the certificate:

      1
      
      acme.sh --issue --dns dns_ali -d zoyao.site -d '*.zoyao.site' --dnssleep 300 --debug
      
1
2
3
4
5
6
    
    - Save the certificate. Because I deployed Nginx using Docker, I need to restart Nginx after pulling the certificate, so I added `--reloadcmd "docker restart nginx"`. You can delete or modify it to your own command based on actual needs.

        ```bash
        acme.sh --install-cert -d zoyao.site --key-file       /app/nginx/ssl/zoyao.site/key.pem  --fullchain-file /app/nginx/ssl/zoyao.site/cert.pem --reloadcmd     "docker restart nginx"
        
- Once successfully executed, you can view the certificate in the `/app/nginx/ssl/zoyao.site/` directory.
  1. Nginx SSL Configuration

    In Nginx’s ./nginx/conf.d directory, create a new configuration file, named here as common.conf.

    In the previous private deployment, we used port 8888 as the blog’s access port. Here we only need to forward the SSL 443 port to the 8888 port and configure the SSL certificate address. The configuration file is as follows:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;
    
        # Configure blog domain name
        server_name www.zoyao.site zoyao.site;
    
        # The SSL certificate address saved in the previous step. Because it's a Docker deployment, the mapped address is configured here
        ssl_certificate "/app/ssl/zoyao.site/cert.pem";
        ssl_certificate_key "/app/ssl/zoyao.site/key.pem";
        ssl_session_cache shared:SSL:1m;
        ssl_session_timeout  10m;
        ssl_ciphers HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers on;
    
        location / {
            # Forward the request to the port where the blog is actually deployed
            proxy_pass  [http://127.0.0.1:8888](http://127.0.0.1:8888); # Forwarding rule
            proxy_set_header Host $proxy_host; # Modify forwarding request header so the app on port 8080 can receive the real request
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    
    }
    

    At this point, the HTTPS deployment is complete. Open https://www.zoyao.site to access it.

i18n Internationalization

Hugo provides a complete internationalization solution natively. You can define different language setups in hugo.yaml.

Here, I have defined two languages: Chinese and English.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
languages:
    en:
        languageName: English
        title: zoyao’s Travel Diary
        weight: 1
        params:
            sidebar:
                subtitle: Let's bust our asses and meet again at the pinnacle of success.
    zh-cn:
        languageName: 中文
        title: zoyao的旅行日记
        weight: 2
        params:
            sidebar:
                subtitle: 顶峰相见

Correspondingly, in the articles, you also need different Markdown files to distinguish between different languages.

For example, for this article, index.zh-cn.md is used for Chinese.

In the same directory, you can create index.en.md for English.

You can configure the website’s default language through DefaultContentLanguage:

1
2
DefaultContentLanguage: zh-cn
languageCode: zh-cn

This website uses different default languages for different deployment methods. In zoyao.site, it defaults to Chinese, while in zoyao.github.io, it defaults to English. You just need to define the corresponding configuration in the config folder under the root directory.

1
2
3
4
5
6
7
mysite
├── config.toml
└── config
    ├── development
    │   └── config.toml
    └── production
        └── config.toml

Different configuration files use a different DefaultContentLanguage.

Similarly, during automated deployment, you can specify different configuration files to complete the deployment for the corresponding language environment:

1
2
- name: Build Web
    run: hugo --environment production