How to Publish a Blog Post from Scratch: A Step-by-Step Guide
This blog runs on Hexo 8 with the Icarus theme, as two sites: an English one and a Chinese one. This guide walks through every step from a blank page to a live post, with the exact commands for this setup.
How the blog is organized
| English blog | Chinese blog | |
|---|---|---|
| Folder | ~/projects/testblog |
~/projects/testblog/zh-CN |
| Posts | source/_posts/ |
zh-CN/source/_posts/ |
| Config | _config.yml, _config.icarus.yml |
zh-CN/_config.yml, zh-CN/_config.icarus.yml |
| Live at | https://songchen.science/blog/ |
https://songchen.science/blog/zh-CN/ |
Hexo turns the Markdown files in source/_posts/ into a static website. The script build.sh builds both blogs, copies them into the GitHub Pages repository ~/projects/song.io and pushes it, which puts them online.
Pick the blog by the language of the post. Every command below is run from that blog’s folder unless it says otherwise.
Step 0: One-time setup
You only need this on a new computer, or if something is missing.
Node.js 24 (Hexo 8 needs Node 20.19 or newer). It is already the nvm default. To check:
1
2node --version # should print v24.x
nvm install 24 && nvm alias default 24 # only if it doesn'tInstall the blog’s packages, once per blog:
1
2cd ~/projects/testblog && npm install
cd ~/projects/testblog/zh-CN && npm installSave the photo-gallery upload password in the macOS Keychain.
build.shuploads the gallery before it builds the blogs, and stops if it can’t find this password. The command asks you to type the password (your typing stays hidden), so run it in a real terminal window:1
security add-generic-password -U -a "$USER" -s gallery-sftp -w
Optional: pandoc, only needed to import articles from links (see “Shortcut” below):
brew install pandoc.
Step 1: Create the post
Go to the blog folder and create the post:
1 | cd ~/projects/testblog # Chinese blog: cd ~/projects/testblog/zh-CN |
TZ=Europe/Paris matters. The blog’s config uses Paris time, so if your computer’s clock is set to another time zone, the post’s date ends up hours off and it can show “published in 6 hours”.
Hexo creates two things, named after the title:
source/_posts/My-Post-Title.md, the post itselfsource/_posts/My-Post-Title/, an empty folder for the post’s images
Step 2: Fill in the front matter
The block between the two --- lines at the top of the file is the front matter. A filled-in example:
1 |
|
What each field does:
title: the post title. Wrap it in quotes if it contains:(colon and space).date: publish time, in Paris time. It sets the order on the home page and in the archive.tags: keywords. Reuse existing tags where possible: lowercase with hyphens for concepts (machine-learning), normal capitals for names (PyTorch).categories: a nested list. The example above files the post under Python > Tools.cover: the banner image at the top of the post and on its home-page card. Leave it empty for no banner.thumbnail: the small image in the sidebar’s “Recent posts” list. Optional.abbrlink: added by Hexo automatically. It becomes the post URL (/posts/<abbrlink>/). Never change it after publishing, or old links and comments break.abstractandpassword: only for encrypted posts (Step 4).
Step 3: Write the content
Write Markdown below the front matter.
Headings: use ## for sections and ### for subsections. They build the table of contents in the sidebar.
Excerpt: put <!-- more --> on its own line after the opening paragraph. The home page shows only the text above it, plus a link to the full post.
Images: copy the image into the post’s folder (source/_posts/My-Post-Title/), then use just the file name:
1 |  |
Images from the web can use their full URL instead.
Code: fence it with three backticks and a language name to get syntax highlighting:
1 | ```python |
Links and tables use normal Markdown. Raw HTML also works.
Watch out for {{, {% or {# in normal text, for example Vue or Jinja snippets. Hexo reads them as template tags, and the build fails or the text comes out garbled. Add disableNunjucks: true to the front matter, as this guide does.
Step 4 (optional): Encrypt the post
Add a password and a teaser to the front matter:
1 | password: choose-a-password |
Readers see only the abstract until they type the password. Posts tagged encryptAsDiary or encryptAsTips are encrypted automatically with the passwords set under encrypt: in _config.yml.
Step 5 (optional): Keep it as a draft
1 | npx hexo new draft "My Draft" # goes to source/_drafts/ |
The Chinese blog’s config has render_drafts: true, so drafts there may be built and published too. Keep unfinished Chinese posts outside source/, or change that setting to false.
Step 6: Preview locally
From the blog folder:
1 | npx hexo server |
Then open:
- English blog:
http://localhost:4000/blog/ - Chinese blog:
http://localhost:4000/blog/zh-CN/
The page updates when you save the file, after a refresh. Check the title, the “published … ago” time, headings, images, code blocks, tables and the excerpt on the home page.
To run both blogs at once, start the second one on another port: npx hexo server -p 4001. Stop the server with Ctrl+C before you publish.
Step 7: Publish
From the main folder, run build.sh with the gallery password read from the Keychain:
1 | cd ~/projects/testblog |
build.sh then:
- builds the photo gallery and uploads changed photos;
- copies the blogroll into the Chinese blog;
- builds the Chinese blog, then the English blog;
- copies both into
~/projects/song.io; - commits and pushes to GitHub.
GitHub Pages updates a minute or two after the push. If the site keeps showing old content, run the same command with ./build.sh --clean at the end instead of ./build.sh. It clears Hexo’s cache and old output first.
Step 8: Check the live post
Find abbrlink: in the post’s front matter (Hexo adds it automatically). The post is at:
- English:
https://songchen.science/blog/posts/<abbrlink>/ - Chinese:
https://songchen.science/blog/zh-CN/posts/<abbrlink>/
Also check that it appears on the home page and on its category and tag pages.
Shortcut: repost an article from a link
To turn an existing web article into a credited repost:
1 | cd ~/projects/testblog |
The script downloads the article, converts it to Markdown, saves its images in the post folder, adds a source badge and credit line, and puts the post in the blog that matches the article’s language. Then continue from Step 2 (tags and categories) and Step 6. Add --dry-run to only preview the result, or --blog zh / --blog en to choose the blog yourself.
Troubleshooting
| Problem | Cause and fix |
|---|---|
Set GALLERY_SFTP_PASSWORD before running the gallery upload. |
The password wasn’t passed in. Use the Step 7 command, after saving the password once (Step 0). Password prompts only work in a real terminal window, not from a chat Run button. |
| The post says “published in 6 hours” | Its date isn’t Paris time. Correct the date in the front matter, and create posts with TZ=Europe/Paris. |
ERR_REQUIRE_ESM or other errors as soon as Hexo starts |
Wrong Node version. Run nvm use 24. |
Template render error, or text containing {{ disappears |
Add disableNunjucks: true to the front matter. |
| An image doesn’t show | It isn’t in the post’s own folder, or its file name doesn’t match exactly, including upper and lower case. |
| The preview won’t start because port 4000 is in use | Another server is running. Stop it, or use npx hexo server -p 4001. |
| The live site shows old content | Wait two minutes and refresh. If it’s still old, publish again with --clean. |
Checklist
1 | cd ~/projects/testblog # Chinese blog: ~/projects/testblog/zh-CN |
Then open the post’s URL to check it (Step 8).
How to Publish a Blog Post from Scratch: A Step-by-Step Guide



