In this post, I’ll walk through how I deployed my personal static blog using Hugo, the PaperMod theme, GitHub, and Cloudflare Workers.
A note on Workers vs. Pages: Cloudflare is consolidating static site hosting from Cloudflare Pages into Cloudflare Workers. As of 2025, Workers Static Assets is the recommended way to host new static sites, and Pages is in maintenance mode. In the dashboard both still live under the unified Workers & Pages section, which is why the deploy commands below use
wrangler deploy(a Workers command) rather than the older Pages workflow.
This is the setup I use for my own blog:
- GitHub repository: vancanhuit/personal-blog
- Live site: blog.canhdinh.com
- Static site generator: Hugo
- Theme: PaperMod
- Hosting/deployment: Cloudflare Workers (Static Assets)
The final Cloudflare build and deploy commands are:
| |
| |
No wrangler.jsonc file is required in the repository for this setup.
Why Hugo?
Hugo is a static site generator written in Go. It is fast, simple, and works very well for technical blogs.
A static blog is a good fit for my use case because I mostly write posts that contain:
- Markdown content
- Source code examples
- DevOps notes
- Infrastructure experiments
Since Hugo generates plain HTML, CSS, JavaScript, images, and fonts, the final site is easy to host on Cloudflare.
Why PaperMod?
I chose PaperMod because it is clean, fast, and works well for technical writing.
Some features I wanted:
- Clean blog layout
- Dark/light mode
- Table of contents
- Reading time
- Tags and categories
- Good source code block rendering
- Code copy button
- Minimal maintenance
For a technical blog, PaperMod is a practical choice because it stays out of the way and lets the content stand out.
Create the Hugo site
Create a new Hugo project. By default Hugo generates a TOML config file, so pass --format yaml to get hugo.yaml instead:
| |
Then add PaperMod as a Git submodule:
| |
Because PaperMod is added as a submodule, the repository will include a .gitmodules file.
You can check it with:
| |
Example:
| |
Configure Hugo
Here is a simplified hugo.yaml configuration:
| |
The most important values are:
| |
The baseURL should match the production domain.
A few PaperMod-specific options are worth highlighting:
mainSections: [posts]makes the home page (/) list posts fromcontent/posts/, so the landing page acts as the posts archive.homeInfoParamsrenders an intro block (title plus content) at the top of the home page, followed by the post list.outputs.homeadds aJSONoutput, which generates the search index used by the search page.
Home, archives, and search pages
PaperMod renders the post list on the home page automatically, but the Archives and Search pages each need a content file that selects the right layout. Without them, the menu links return 404.
Create the archives page:
| |
| |
Create the search page:
| |
| |
The search page relies on the JSON home output configured above. PaperMod ships with Fuse.js and performs fuzzy, client-side search over that index, so no server-side component is required.
Add a blog post
Create a new post:
| |
Example post:
| |
Make sure the post has:
| |
If draft is set to true, Hugo will not publish it in production builds.
Run locally
For local development, run:
| |
Then open:
| |
The -D flag includes draft posts.
If the site looks stale after changing CSS, fonts, or theme files, clean the generated output:
| |
Self-host fonts
I self-host two fonts so the site does not depend on a third-party font CDN:
- Inter for body and UI text.
- Maple Mono NL for code blocks.
Font files are stored under:
| |
For example:
| |
Then I define the font in:
| |
Example:
| |
I disable ligatures because I prefer code to display exactly as typed.
For body text, I load Inter as a variable font (two woff2 files cover every weight) and set it as the base font:
| |
Because the extended stylesheet loads after the theme’s CSS, this body rule overrides PaperMod’s default font stack.
Custom styling
PaperMod automatically bundles any CSS placed in assets/css/extended/, loaded after the theme styles. I use a custom.css file there to make the content stand out and improve reading comfort:
| |
The main tweaks are:
- An accent color for in-content links, with the default underline removed and an underline that animates in on hover.
- Styled blockquotes with an accent border, a tinted background, and a decorative quote mark.
- Subtle borders on inline code, rounded tables with a shaded header row, and a slim centered horizontal rule.
- A softer light palette and brighter dark-mode text, plus a larger line height for more comfortable reading.
All colors are driven by CSS variables (including PaperMod’s own --theme, --content, and --border) and an --accent variable defined separately for light and .dark modes, so the styling adapts to both themes:
| |
PaperMod underlines content links through .md-content a:not(.anchor). Because that selector has a higher specificity than a plain .post-content a, the override matches it with .post-content a:not(.anchor) so the default underline is actually removed.
Push to GitHub
Commit and push the project:
| |
Configure Cloudflare deployment
In the Cloudflare dashboard, go to Workers & Pages, create a new Worker, connect the GitHub repository, and configure the project with these commands.
Build command
| |
This does two things:
- Fetches the PaperMod theme submodule.
- Builds and minifies the Hugo site into the
public/directory.
The git submodule update --init --recursive part is important because PaperMod is stored as a Git submodule.
Deploy command
| |
This deploys the generated public/ directory as static assets.
In this setup, I do not need a wrangler.jsonc file in the repository because the deploy command provides the required Workers deployment options directly:
| |
Environment variables
I also recommend setting HUGO_VERSION in Cloudflare:
| |
Using a fixed Hugo version helps avoid unexpected build differences between local and Cloudflare environments.
Normal publishing workflow
After the initial setup, publishing a new post is simple.
Create a post:
| |
Preview locally:
| |
When ready, commit and push:
| |
Cloudflare will automatically rebuild and redeploy the site.
Troubleshooting
Theme is missing
If Cloudflare cannot find PaperMod, make sure the theme submodule is committed:
| |
Also make sure the build command includes:
| |
Code blocks look broken
Clean Hugo’s generated files locally:
| |
Also check the Markdown code fence format:
| |
New font does not appear
Hard refresh the browser:
| |
On macOS:
| |
If fonts are cached aggressively, rename the font folder or font files and update the CSS paths.
Deployment fails after successful build
If the log shows that Hugo built successfully but deployment failed, check the deploy command.
For this setup, use:
| |
not just:
| |
Useful links
- Hugo official website
- Hugo documentation
- PaperMod GitHub repository
- PaperMod installation guide
- Cloudflare Workers
- Cloudflare Workers static assets
- Migrate from Pages to Workers
- Wrangler deploy command
Final setup
The final setup is:
| |