This post documents the process of migrating a Hugo blog from the PaperMod theme to a custom theme built with Tailwind CSS v4, and submitting it to the official Hugo Themes gallery.
Hugo + Tailwind CSS v4 Integration
Hugo 0.157+ supports the css.TailwindCSS pipeline. Tailwind v4 uses @import "tailwindcss" syntax and no longer requires tailwind.config.js.
/* assets/css/main.css */
@import "tailwindcss";
@theme {
--color-primary: #2563eb;
--font-sans: "Inter", sans-serif;
}
@variant dark (&:where(.dark, .dark *));
In templates, call css.TailwindCSS to process the stylesheet:
{{ with resources.Get "css/main.css" | css.TailwindCSS }}
<link rel="stylesheet" href="{{ .RelPermalink }}">
{{ end }}
One important note: @tailwindcss/cli must be in the PATH at build time. For deployment environments like Cloudflare Pages:
# hugo.yaml (build settings for Cloudflare Pages)
build:
command: "PATH=$PWD/node_modules/.bin:$PATH hugo --minify && npx pagefind --site public"
Migration from PaperMod
Theme switching in Hugo only requires changing theme: in hugo.yaml, so it was possible to develop the custom theme while keeping the option to revert to PaperMod at any time.
# hugo.yaml
theme: hugo-theme-flavor # Change to "PaperMod" to revert
The main migration work involved consolidating layout override files from the layouts/ directory into the theme. References to PaperMod-specific structures (partial calls, CSS variable names) needed to be rewritten.
Stacking Context Issue
Placing a position: fixed mobile menu inside a position: sticky header caused the menu to render behind other elements, regardless of z-index values.
This is due to CSS stacking context rules. A position: sticky element creates a new stacking context, so its children’s z-index values only apply within that context.
The fix was to move the mobile menu DOM element outside of <header>:
<header class="sticky top-0 z-40">
<!-- Header content -->
</header>
<!-- Menu placed outside header -->
<div id="mobile-menu" class="fixed inset-0 z-50 hidden">
<!-- Menu content -->
</div>
Tailwind v4 @layer Priority
In Tailwind v4, rules inside @layer components have lower priority than rules outside any layer. This follows the CSS Cascade Layers specification.
Media queries initially placed inside @layer components were being overridden by Tailwind utilities. Moving them outside the layer resolved the issue.
/* NG: Inside @layer, utilities win */
@layer components {
@media (max-width: 768px) {
.sidebar { display: none; }
}
}
/* OK: Outside any layer */
@media (max-width: 768px) {
.sidebar { display: none; }
}
Hugo Themes Gallery Requirements
Submitting to Hugo Themes requires:
theme.toml— theme metadata (name, description, tags, min_version, etc.)go.mod— Hugo module definitionexampleSite/— a working demo site- Screenshots —
images/screenshot.png(1500x1000px, 3:2 ratio) andimages/tn.png(thumbnail) README.md— installation and customization instructionsLICENSE— an open source license
Registration is done by submitting a PR to the hugoThemesSiteBuilder repository, adding the theme’s GitHub URL to hugo-themes.txt.
Generalizing the Theme
Converting a personal blog theme into a publishable theme required several changes:
- i18n: Moved hardcoded strings to
i18n/ja.yamlandi18n/en.yaml, referenced via{{ i18n "readMore" }} - Font configuration: Made Google Fonts configurable through
params.fontsinhugo.yaml - Accessibility: Ensured touch targets of at least 44px and text contrast ratio of 4.5:1 or higher
- JSON-LD: Used
jsonifyinstead ofsafeHTMLfor structured data output (XSS prevention) - Pagefind: Added conditional loading via
params.pagefind.enabledfor users who don’t use Pagefind
Parallel Review Process
Before publishing the theme, nine review areas were examined in parallel:
- Accessibility (WCAG 2.1 AA compliance)
- SEO (meta tags, structured data)
- i18n (multilingual coverage)
- CSS (unused styles, responsive design)
- Performance (Lighthouse scores)
- Security (CSP, external resources)
- Hugo compatibility (minimum version, deprecated functions)
- Theme conventions (theme.toml, exampleSite)
- Documentation (README, CHANGELOG)
Issues from all areas were collected first, then fixed in a batch. This approach appeared more efficient than iterating through individual fix-review cycles.