Deploy Hexo Project Using GitHub Actions

Hexo & GitHub Actions - “No artifacts named ‘github-pages’” Error

After migrating to Hexo, I tried setting up GitHub Pages deployment via GitHub Actions, following the official guide GitHub Pages. Unfortunately, my workflow consistently failed with the error:

No artifacts named "github-pages" were found for this workflow run.

Solution:

My guess is there could be a potential incompatibility or version mismatch between the actions/upload-pages-artifact v3 and actions/deploy-pages v4.

To address this, just to explicitly ensure the Pages configuration was up-to-date before the deployment step.

1
2
- name: Configure GitHub Pages
uses: actions/configure-pages@v4

My Complete GitHub Actions Workflow:

Here’s the GitHub Actions workflow file (.github/workflows/your-workflow-name.yml) that now works:

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
name: GH Pages Deploy

on:
push:
branches:
- main

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
pages: write
id-token: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Cache NPM dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install Dependencies
run: npm install
- name: Build
run: npm run build
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./public
deploy:
needs: build
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Configure Pages
uses: actions/configure-pages@v4
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

Hopefully, this helps anyone else struggling with the same GitHub Actions deployment error for their static sites!

Getting Started With Hexo -- Creating Posts

In a previous post, I shared my migration from Jekyll to Hexo for my GitHub Pages blog. In this post, I’ll go over some of the common commands I use to create posts and test my blog locally.

✍️ Create a New Post or Draft

The commands in Hexo are quite human-readable. To create a new blog post or draft, use:

1
2
$ hexo new post "My Post"
$ hexo new draft "My Draft"

This will create a Markdown file in the source/_posts folder for posts, or in the source/_drafts folder for drafts. The keywords "post" and "draft" refer to the layout used for the file. You can customize these layouts in the scaffolds folder.

You can also create your own custom layout file inside the scaffolds folder and use it when creating a new post. For example:

1
$ hexo new mylayout "My Layout"

To move a draft to the posts folder, use the publish command instead of new. This will move the file from source/_drafts to source/_posts :

1
$ hexo publish post "My Draft"

🖥️ Run the Blog Locally

To run your blog locally, you first need to generate the static files:

1
$ hexo generate

Or use the shorthand:

1
$ hexo g

Then, start the local development server:

1
$ hexo server

By default, this will start a server at http://localhost:4000. Open that link in your browser and you’ll see your website.

Although Hexo is supposed to watch for local changes and update automatically, there are cases where a simple browser refresh doesn’t reflect your changes — especially when you modify configuration files like _config.yml. In those cases, it’s best to regenerate the static files and restart the server.

Also, we can use the clean command to remove previously generated files, to keep local repository clean.

1
$ hexo clean

✅ Summary

Here’s a quick cheat sheet:

Action Command
Create a new post hexo new post "Post Title"
Create a new file using other layouts hexo new <layout name> "Post Title"
Publish a draft to a post hexo publish post "Draft Name"
Generate static files hexo generate or hexo g
Run local server hexo server
Clean old files hexo clean

Create Your Markdown Files From Template

Markdown is incredibly useful, especially for text management. I use it extensively for almost anything related to text. Most of the time, I like to keep file structures the same across a project. Using a template file really helps with that. And since Markdown doesn’t have something like @date to automatically add the current date, we can just take care of that ourselves too.

To automate the generation of a markdown file from a template, you need template.md file and a bash file create.sh.

Markdown template

Basically, a template file is the same as a normal Markdown file, but without specific content. I like to add headings or empty tables that are unlikely to change, as well as meta-information such as the date and title. Here’s an example of a template file.

1
2
3
4
5
6
| Created  |   Title   | Tags |
| -------- | --------- | ---- |
| {{date}} | {{title}} | |


# Summary

Bash command

To automate the creation of a file, we can use the powerful hash command. Here’s an example:

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
# create.sh
# Check for input
if [ -z "$1" ]; then
echo "Usage: $0 \"your-title-here\""
exit 1
fi

# Title name gets from input
raw_title="$*"

# Format input "title with space" => "Title-With-Space"
formated_title=$(echo "$raw_title" | awk '{
for (i=1; i<=NF; i++) {
$i = toupper(substr($i,1,1)) tolower(substr($i,2))
}
print
}' | xargs | tr ' ' '-')

# Get the date
today=$(date +%F)

# Combine the file name
filename="${today}-${formated_title}.md"

# Uncomment if you don't need date or file name in your template
# cp template.md "$filename"

# Uncomment one if you are using date or file name or both in your template
# sed "s/{{date}}/$today/g" template.md > "$filename"
# sed "s/{{title}}/${raw_title//\//-}/g" template.md > "$filename"
# sed -e "s/{{title}}/${raw_title//\//-}/g" -e "s/{{date}}/$today/g" template.md > "$filename"

echo "$filename Created."

Depending on whether you want to handle variables in your template, you can uncomment the appropriate option.

Usage

In the terminal, simply use:

1
./create.sh "file name"

A file named YEAR-MONTH-DAY-File-Name.md will be created in the current folder, with the date and file name pre-filled.

If you got permission denied. Run this command

1
chmod +x create.sh

Migrating From Jekyll to Hexo

Been using Jekyll for GitHub Pages since the beginning, but after switching out my old laptop, I never managed to get the local environment set up properly. Then the theme I was using became unmaintained—and not being able to tweak the layout locally when everything started breaking was seriously frustrating.

Recently came across Hexo—found it pretty similar to use and super easy to host locally. What really impressed me was that it only took about 2 hours to migrate my old project to Hexo, and honestly, half of that time was just me browsing for a theme I liked.

The migration

Step 1: Create a new Hexo project

As stated on the official Hexo website, you only need Git, Node.js, and npm installed locally. Once that’s set up, just follow the instructions:

1
2
3
$ hexo init <folder>
$ cd <folder>
$ npm install

After running these commands, you should see a few files and folders created in the project directory.

Step 2: Configuration

Just like in Jekyll, configurations in Hexo are defined in the _config.yml file. You can modify the settings there as needed.

There are two key configurations to pay attention to:

  1. new_post_name:
    By default, Hexo generates new post filenames based on the input title, like title.md. To match Jekyll’s convention more closely, you can update it to include the date:

    1
    new_post_name: ':year-:month-:day-:title.md'
  2. Asset folder management:
    Unlike Jekyll, Hexo offers built-in asset folder handling. There are two ways to manage assets:

    • Global asset folder (similar to Jekyll):
      You can manually organize files however you like. In this case, no changes to _config.yml are needed.
    • Post-specific asset folders:
      A more structured approach, where each post has its own associated asset folder. To enable this, update your config as follows:

      1
      2
      3
      4
      post_asset_folder: true
      marked:
      prependRoot: true
      postAsset: true

      If you’ve updated new_post_name as shown above, make sure to also update the permalink setting to ensure asset paths are correctly resolved:

      1
      permalink: ':year/:month/:day/:title/'

Step 3: Move data

With the configurations set, we’re good to go. The nice thing is that Hexo’s file structure is quite similar to Jekyll’s—you just need to move everything inside your old _posts folder into the source/_posts folder in your Hexo project.

For asset folder, as mentioned earlier, if you prefer to organize files manually, simply copy the asset folders into the source directory. Everything under the source folder will be copied to the public folder when generating static pages. This means you can reference images directly using syntax like ![](/images/image.jpg). It won’t show the image immediately in the markdown editor, but it will point to the correct location once the project is built.

If you’re using post_asset_folder = true to manage asset folders, there’s a bit more setup. Run the following command to create asset folders for your existing posts and then move the corresponding files into them:

1
mkdir $(basename -a -s .md ./*.md)

Also, make sure to simplify your image paths—you only need something like ![](image.jpg) inside the post.

Step 4: Choose a theme

One last thing, choose a theme for the website. There are way more themes than I expected, so I ended up picking one at random: Icarus. Also, switching between themes is super easy—just follow the instructions, and it shouldn’t take more than five minutes.

Step 5: Deploy

The website offers two deployment options. The more common approach is to use One-Command Deployment. In this case, you don’t need to update your repository manually—the deployer will upload all the static files (HTML, CSS, etc.) to GitHub once the build is complete.
Note: If you choose this method, the repository will only contain the generated static files after deployment.

The other way to deploy is to commit all your local files (make sure public/ is added to your .gitignore), then use a GitHub Action to build and deploy the website.

Voila! The migration is officially done.

Quotes From Criminal Minds S10 E13

Reid: I know I’m not being very rational, and I know I haven’t seen him in a really long time, but I think about him all the time. And I knew he was always out there, and now it just feels empty.

Rossi: Yeah. The time will past and slowly you’ll forget how much it hurts. Maybe you’ll find something else to fill that empty space.

Reid: I won’t find something else.

Quotes From a Tale of Two Cities

It was the best of times
It was the worst of times
It was the age of wisdom
It was the age of foolishness
It was the epoch of belief
It was the epoch of incredulity
It was the season of Light
It was the season of Darkness
It was the spring of hope
It was the winter of despair
We had everything before us
We had nothing before us
We were all going direct to Heaven
We were all going direct the other way


In short, the period was so far like the present period, that some of its noisiest authorities insisted on its being received, for good or for evil, in the superlative degree of comparison only.

L'Amour Change Tout

Le travail sans amour rend esclave
La justice sans amour rend impitoyable
La vétité sans amour rend critique
Le savoir sans amour rend présomptueux
La responsabilité sans amour rend autoritaire
L’intelligence sans amour rend calculateur
La mémoire sans amour - c’est-à-dire sans pardon - rend rancunier
La gentillesse sans amour rend hypocrite
L’honneur sans amour rend orgueilleux
La richesse sans amour rend avare
L’offrande sans amour rend amer
La foi sans amour rend fanatique


Seigneur parce que tu es Amour
Tu illumines notre route
Tu donnes du sens à la vie
Tu donnes du sens à nos vies

Quotes From Castle S07E06

Kate: The moment that I met you,my life became extraordinary.You taught me to be my best self, to look forward to tomorrow’s adventures.When I was vunlnerable, you were strong.I love you , Richard Castle.And I want to live my life in the warmth of your smile and the strength of your embrace. I promise you I will love you ,I will be your friend and you partner in crime and in life,always.

Castle: The moment that we met,my life became extraordinary.You taught me more about myself that I knew there was to learn.You are the joy of my heart. You’re the last person I want to see every night when i close my eyes.I love you, Katherine Beckett.And the mystery of you is the one i want to spend the rest of my life exploring. I promise to love you,to be your friend,and your partner in crime and life till death do us part,and for the time of our lives.

Quotes From Marley & Me

A dog has no use for fancy cars or big homes or designer clothes
A waterlogged stick will do just fine.
A dog doesn’t care if you are rich or poor…
clever or dull, smart or dumb.
Give’em your heart and he’ll give you his.

How many people can you say that about?
how many people can make you feel rare and pure and special?
how many people can make you feel… extraordinary?

Quotes From Time Traveler's Wife

“ Long ago, men went to sea, and women waited for them, standing on the edge of the water, scanning the horizon for the tiny ship. Now I wait for Henry. He vanishes unwillingly, without warning. I wait for him. Each moment that I wait feels like a year, an eternity. Each moment is as slow and transparent as glass. Through each moment I can see infinite moments lined up, waiting. Why has he gone where I cannot follow? ”