[{"content":"In this post, I’ll walk through how I deployed my personal static blog using Hugo, the PaperMod theme, GitHub, and Cloudflare Workers.\nA 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 \u0026amp; Pages section, which is why the deploy commands below use wrangler deploy (a Workers command) rather than the older Pages workflow.\nThis is the setup I use for my own blog:\nGitHub 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:\n1 git submodule update --init --recursive \u0026amp;\u0026amp; hugo --gc --minify 1 npx wrangler deploy --assets=public --name personal-blog --compatibility-date 2026-06-21 No wrangler.jsonc file is required in the repository for this setup.\nWhy Hugo? Hugo is a static site generator written in Go. It is fast, simple, and works very well for technical blogs.\nA static blog is a good fit for my use case because I mostly write posts that contain:\nMarkdown 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.\nWhy PaperMod? I chose PaperMod because it is clean, fast, and works well for technical writing.\nSome features I wanted:\nClean 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.\nCreate 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:\n1 2 3 hugo new site personal-blog --format yaml cd personal-blog git init Then add PaperMod as a Git submodule:\n1 git submodule add https://github.com/adityatelange/hugo-PaperMod themes/PaperMod Because PaperMod is added as a submodule, the repository will include a .gitmodules file.\nYou can check it with:\n1 cat .gitmodules Example:\n1 2 3 [submodule \u0026#34;themes/PaperMod\u0026#34;] path = themes/PaperMod url = https://github.com/adityatelange/hugo-PaperMod Configure Hugo Here is a simplified hugo.yaml configuration:\n1 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 55 56 57 58 59 60 61 62 baseURL: \u0026#34;https://blog.canhdinh.com/\u0026#34; locale: \u0026#34;en-us\u0026#34; title: \u0026#34;Canh Dinh\u0026#34; theme: \u0026#34;PaperMod\u0026#34; pagination: pagerSize: 10 outputs: home: - HTML - RSS - JSON params: env: \u0026#34;production\u0026#34; description: \u0026#34;Notes on DevOps practices, self-hosting and software delivery.\u0026#34; author: \u0026#34;Canh Dinh\u0026#34; mainSections: - posts homeInfoParams: Title: \u0026#34;Hi there 👋\u0026#34; Content: \u0026gt; Welcome to my blog. defaultTheme: \u0026#34;auto\u0026#34; disableThemeToggle: false ShowReadingTime: true ShowShareButtons: false ShowPostNavLinks: true ShowBreadCrumbs: true ShowCodeCopyButtons: true ShowWordCount: true ShowRssButtonInSectionTermList: true ShowToc: true TocOpen: false UseHugoToc: true socialIcons: - name: \u0026#34;github\u0026#34; url: \u0026#34;https://github.com/vancanhuit\u0026#34; menu: main: - identifier: \u0026#34;tags\u0026#34; name: \u0026#34;Tags\u0026#34; url: \u0026#34;/tags/\u0026#34; weight: 20 - identifier: \u0026#34;archives\u0026#34; name: \u0026#34;Archives\u0026#34; url: \u0026#34;/archives/\u0026#34; weight: 30 - identifier: \u0026#34;search\u0026#34; name: \u0026#34;Search\u0026#34; url: \u0026#34;/search/\u0026#34; weight: 40 markup: highlight: codeFences: true guessSyntax: true lineNos: true lineNumbersInTable: true noClasses: false The most important values are:\n1 2 baseURL: \u0026#34;https://blog.canhdinh.com/\u0026#34; theme: \u0026#34;PaperMod\u0026#34; The baseURL should match the production domain.\nA few PaperMod-specific options are worth highlighting:\nmainSections: [posts] makes the home page (/) list posts from content/posts/, so the landing page acts as the posts archive. homeInfoParams renders an intro block (title plus content) at the top of the home page, followed by the post list. outputs.home adds a JSON output, 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.\nCreate the archives page:\n1 hugo new content archives.md 1 2 3 4 5 6 --- title: \u0026#34;Archives\u0026#34; layout: \u0026#34;archives\u0026#34; url: \u0026#34;/archives/\u0026#34; summary: \u0026#34;archives\u0026#34; --- Create the search page:\n1 hugo new content search.md 1 2 3 4 5 6 7 --- title: \u0026#34;Search\u0026#34; layout: \u0026#34;search\u0026#34; url: \u0026#34;/search/\u0026#34; summary: \u0026#34;search\u0026#34; placeholder: \u0026#34;Search posts...\u0026#34; --- 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.\nAdd a blog post Create a new post:\n1 hugo new content posts/hello-world.md Example post:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 --- title: \u0026#34;Hello World\u0026#34; date: 2026-06-21T10:00:00+07:00 draft: false tags: [\u0026#34;hugo\u0026#34;, \u0026#34;cloudflare\u0026#34;, \u0026#34;devops\u0026#34;] categories: [\u0026#34;devops\u0026#34;] showToc: true --- This is my first post using Hugo, PaperMod, GitHub, and Cloudflare. ```go package main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;Hello from my blog\u0026#34;) } ``` Make sure the post has:\n1 draft: false If draft is set to true, Hugo will not publish it in production builds.\nRun locally For local development, run:\n1 hugo server -D --disableFastRender Then open:\n1 http://localhost:1313 The -D flag includes draft posts.\nIf the site looks stale after changing CSS, fonts, or theme files, clean the generated output:\n1 2 3 rm -rf public resources/_gen hugo --gc --minify hugo server -D --disableFastRender Self-host fonts I self-host two fonts so the site does not depend on a third-party font CDN:\nInter for body and UI text. Maple Mono NL for code blocks. Font files are stored under:\n1 2 static/fonts/inter/ static/fonts/maple-mono/ For example:\n1 2 3 static/fonts/maple-mono/MapleMonoNL-Regular.ttf.woff2 static/fonts/maple-mono/MapleMonoNL-Bold.ttf.woff2 static/fonts/maple-mono/MapleMonoNL-Italic.ttf.woff2 Then I define the font in:\n1 assets/css/extended/custom-fonts.css Example:\n1 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 @font-face { font-family: \u0026#34;Maple Mono NL\u0026#34;; src: url(\u0026#34;/fonts/maple-mono/MapleMonoNL-Regular.ttf.woff2\u0026#34;) format(\u0026#34;woff2\u0026#34;); font-weight: 400; font-style: normal; font-display: swap; } @font-face { font-family: \u0026#34;Maple Mono NL\u0026#34;; src: url(\u0026#34;/fonts/maple-mono/MapleMonoNL-Bold.ttf.woff2\u0026#34;) format(\u0026#34;woff2\u0026#34;); font-weight: 700; font-style: normal; font-display: swap; } code, pre, kbd, samp, .highlight pre, .highlight code, .chroma, .chroma code { font-family: \u0026#34;Maple Mono NL\u0026#34;, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \u0026#34;Liberation Mono\u0026#34;, monospace; font-feature-settings: \u0026#34;liga\u0026#34; 0, \u0026#34;calt\u0026#34; 0; font-variant-ligatures: none; } I disable ligatures because I prefer code to display exactly as typed.\nFor body text, I load Inter as a variable font (two woff2 files cover every weight) and set it as the base font:\n1 2 3 4 5 6 7 8 9 10 11 @font-face { font-family: \u0026#34;Inter\u0026#34;; src: url(\u0026#34;/fonts/inter/inter-latin-wght-normal.woff2\u0026#34;) format(\u0026#34;woff2-variations\u0026#34;); font-weight: 100 900; font-style: normal; font-display: swap; } body { font-family: \u0026#34;Inter\u0026#34;, -apple-system, BlinkMacSystemFont, \u0026#34;Segoe UI\u0026#34;, Roboto, Helvetica, Arial, sans-serif; } Because the extended stylesheet loads after the theme\u0026rsquo;s CSS, this body rule overrides PaperMod\u0026rsquo;s default font stack.\nCustom 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:\n1 assets/css/extended/custom.css The main tweaks are:\nAn 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\u0026rsquo;s own --theme, --content, and --border) and an --accent variable defined separately for light and .dark modes, so the styling adapts to both themes:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 :root { --accent: #2563eb; } .dark { --accent: #6ea8fe; } .post-content a:not(.anchor) { color: var(--accent); text-decoration: none; } .post-content blockquote { border-inline-start: 0.25rem solid var(--accent); background: var(--quote-bg); } 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.\nPush to GitHub Commit and push the project:\n1 2 3 4 5 git add . git commit -m \u0026#34;Initial Hugo blog with PaperMod\u0026#34; git branch -M main git remote add origin https://github.com/vancanhuit/personal-blog.git git push -u origin main Configure Cloudflare deployment In the Cloudflare dashboard, go to Workers \u0026amp; Pages, create a new Worker, connect the GitHub repository, and configure the project with these commands.\nBuild command 1 git submodule update --init --recursive \u0026amp;\u0026amp; hugo --gc --minify This does two things:\nFetches 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.\nDeploy command 1 npx wrangler deploy --assets=public --name personal-blog --compatibility-date 2026-06-21 This deploys the generated public/ directory as static assets.\nIn this setup, I do not need a wrangler.jsonc file in the repository because the deploy command provides the required Workers deployment options directly:\n1 2 3 --assets=public --name personal-blog --compatibility-date 2026-06-21 Environment variables I also recommend setting HUGO_VERSION in Cloudflare:\n1 HUGO_VERSION = 0.163.3 Using a fixed Hugo version helps avoid unexpected build differences between local and Cloudflare environments.\nNormal publishing workflow After the initial setup, publishing a new post is simple.\nCreate a post:\n1 hugo new content posts/my-new-post.md Preview locally:\n1 hugo server -D --disableFastRender When ready, commit and push:\n1 2 3 git add . git commit -m \u0026#34;Add new blog post\u0026#34; git push Cloudflare will automatically rebuild and redeploy the site.\nTroubleshooting Theme is missing If Cloudflare cannot find PaperMod, make sure the theme submodule is committed:\n1 2 git submodule status cat .gitmodules Also make sure the build command includes:\n1 git submodule update --init --recursive Code blocks look broken Clean Hugo’s generated files locally:\n1 2 3 rm -rf public resources/_gen hugo --gc --minify hugo server -D --disableFastRender Also check the Markdown code fence format:\n1 2 3 ```go fmt.Println(\u0026#34;hello\u0026#34;) ``` New font does not appear Hard refresh the browser:\n1 Ctrl + Shift + R On macOS:\n1 Cmd + Shift + R If fonts are cached aggressively, rename the font folder or font files and update the CSS paths.\nDeployment fails after successful build If the log shows that Hugo built successfully but deployment failed, check the deploy command.\nFor this setup, use:\n1 npx wrangler deploy --assets=public --name personal-blog --compatibility-date 2026-06-21 not just:\n1 npx wrangler deploy 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:\n1 2 3 4 5 6 7 8 9 10 Hugo static site PaperMod theme GitHub repository Cloudflare build command: git submodule update --init --recursive \u0026amp;\u0026amp; hugo --gc --minify Cloudflare deploy command: npx wrangler deploy --assets=public --name personal-blog --compatibility-date 2026-06-21 No wrangler.jsonc required ","permalink":"https://blog.canhdinh.com/posts/deploy-static-site-with-hugo-cloudflare/","summary":"\u003cp\u003eIn this post, I’ll walk through how I deployed my personal static blog using \u003ca href=\"https://gohugo.io/\"\u003eHugo\u003c/a\u003e, the \u003ca href=\"https://github.com/adityatelange/hugo-PaperMod\"\u003ePaperMod\u003c/a\u003e theme, \u003ca href=\"https://github.com/\"\u003eGitHub\u003c/a\u003e, and \u003ca href=\"https://developers.cloudflare.com/workers/\"\u003eCloudflare Workers\u003c/a\u003e.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eA note on Workers vs. Pages:\u003c/strong\u003e Cloudflare is consolidating static site hosting from Cloudflare Pages into Cloudflare Workers. As of 2025, \u003ca href=\"https://developers.cloudflare.com/workers/static-assets/\"\u003eWorkers Static Assets\u003c/a\u003e is the recommended way to host new static sites, and Pages is in maintenance mode. In the dashboard both still live under the unified \u003cstrong\u003eWorkers \u0026amp; Pages\u003c/strong\u003e section, which is why the deploy commands below use \u003ccode\u003ewrangler deploy\u003c/code\u003e (a Workers command) rather than the older Pages workflow.\u003c/p\u003e","title":"Deploy Static Site With Hugo and Cloudflare Workers"}]