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; }
}

Submitting to Hugo Themes requires:

  • theme.toml — theme metadata (name, description, tags, min_version, etc.)
  • go.mod — Hugo module definition
  • exampleSite/ — a working demo site
  • Screenshots — images/screenshot.png (1500x1000px, 3:2 ratio) and images/tn.png (thumbnail)
  • README.md — installation and customization instructions
  • LICENSE — 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.yaml and i18n/en.yaml, referenced via {{ i18n "readMore" }}
  • Font configuration: Made Google Fonts configurable through params.fonts in hugo.yaml
  • Accessibility: Ensured touch targets of at least 44px and text contrast ratio of 4.5:1 or higher
  • JSON-LD: Used jsonify instead of safeHTML for structured data output (XSS prevention)
  • Pagefind: Added conditional loading via params.pagefind.enabled for users who don’t use Pagefind

Parallel Review Process

Before publishing the theme, nine review areas were examined in parallel:

  1. Accessibility (WCAG 2.1 AA compliance)
  2. SEO (meta tags, structured data)
  3. i18n (multilingual coverage)
  4. CSS (unused styles, responsive design)
  5. Performance (Lighthouse scores)
  6. Security (CSP, external resources)
  7. Hugo compatibility (minimum version, deprecated functions)
  8. Theme conventions (theme.toml, exampleSite)
  9. 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.