Building a Hugo + Tailwind technical blog from scratch: Taking INFINI Labs Blog as an example

Building a Hugo + Tailwind technical blog from scratch: Taking INFINI Labs Blog as an example

Source: Dev.to

Project Overview ## 1. Directory Structure: Where Do Content, Theme, Config, and Assets Live? ## 1) Content Layer: content/ ## 2) Configuration Layer: hugo.toml + config/_default/* ## 3) Theme Layer: themes/hugoplate/ ## 4) Asset Layer: assets/ vs static/ ## 2. Build Pipeline: What Happens From Markdown to Final HTML/CSS/JS? ## 1) Build entry points: package.json scripts ## 2) Hugo Modules: Why does CI install Go? ## 3) How does Tailwind generate only the CSS you actually use? ## 4) How are CSS/JS bundled, minified, and fingerprinted? ## 3. Search: How Do index.json and /static/assets/* Work Together? ## 1) Where does the index come from? ## 2) Where does the search UI come from? ## 4. Building a Similar Project From Scratch (Minimum Viable Steps) ## 1) Prepare the toolchain ## 2) Install dependencies and start development ## 3) Add a new post ## 4) Build for production ## 5. Deployment & CI/CD: How Netlify / GitHub Pages / Vercel Build It ## 1) Netlify ## 2) GitHub Pages (GitHub Actions) ## 3) Vercel ## Wrap-up: The Boundary Between Build-Time and Run-Time This blog is a typical static-site blog project: content is written in Markdown, then Hugo (a Go-based static site generator) renders it into HTML during the build phase, and TailwindCSS + PostCSS bundle the final CSS during the build phase as well. After deployment, there is no backend required—any static hosting (GitHub Pages / Netlify / Vercel / S3+CDN) can serve it. Two key aspects of this project are especially worth noting: The rest of this article explains both “how to set it up” and “how it works” in detail. You can think of this repository as four layers: Each post starts with front matter (YAML/TOML/JSON are all supported; this repo uses YAML). Typical fields include: Templates read these fields. For example, themes/hugoplate/layouts/posts/single.html displays the cover image, author, categories, publish date, the main content, and the table of contents (TOC). Hugo supports splitting configuration under the config/ directory. This project uses: Root config: hugo.toml Language config: config/_default/languages.toml Site parameters: config/_default/params.toml Hugo Modules: config/_default/module.toml The theme defines the page structure and Hugo Pipes: Note: you’ll see {{ partial "image" ... }} in templates, but there is no partials/image.html inside the theme directory. That partial comes from Hugo Modules (the hugo-modules/images module imported in config/_default/module.toml). This is a common pattern: “theme + modular capabilities.” These two directories behave differently in Hugo: assets/: resources processed by Hugo Pipes (compile, fingerprint, minify, etc.) static/: copied to public/ as-is So Hugo drives the whole build. Node is mainly here for PostCSS/Tailwind. Also, CI/hosting often runs this before build: This script comes from the Hugoplate template project and is used to “move” the exampleSite/ layout into the real project layout. Since this repository already has a themes/ directory, the script typically prints Project already setup (i.e., it does nothing and is safe to keep in CI). Because config/_default/module.toml uses Hugo Modules, such as: These modules are fetched during build and participate in rendering. The repo’s go.mod locks module versions (you can treat it as the dependency manifest for Hugo Modules). So the division of responsibilities is: That’s why CI installs both Hugo and Go (see netlify.toml and .github/workflows/hugo.yml). Hugo emits hugo_stats.json while rendering templates, recording the classes/tokens used by the generated HTML. Tailwind reads this file and can very accurately generate only the required utility classes, avoiding scanning the entire template/content tree (which can lead to false positives and a much larger CSS bundle). In addition, hugo.toml includes a module mount: This is a well-established integration approach in Hugoplate-based setups. In themes/hugoplate/layouts/partials/essentials/style.html, you can see the Hugo Pipes flow: JS is handled similarly in themes/hugoplate/layouts/partials/essentials/script.html. This also explains two important runtime facts: hugo.toml sets outputs.home = ["HTML", "RSS", "WebAppManifest", "JSON"] and configures JSON output with baseName = "index". So the build generates: The template that creates it is: It iterates over site pages and packs fields such as title, URL, tags, category, description, and the plain-text content into a JSON array. This is a great data source for client-side search, especially on static sites. themes/hugoplate/layouts/_default/baseof.html hardcodes: These files live in static/assets/ and are copied to public/assets/ by Hugo, so browsers can request them directly after deployment. At runtime (when users open the site): This is a common approach to “enhance” static sites: generate data at build time, consume it with front-end code at runtime. Here is a practical checklist if you want to replicate this repository’s approach. This repo declares pnpm, but the scripts are compatible with npm/yarn as well. This starts the Hugo dev server. Create a Markdown file under content/english/posts/YYYY/, for example: It’s recommended to put images under assets/images/posts/... and reference them as /images/posts/.... After building, they will be emitted under public/images/posts/.... The output directory is public/ (see netlify.toml: publish = "public"). This repository supports multiple hosting platforms: .github/workflows/hugo.yml does: vercel-build.sh runs on the build machine: All platforms do the same thing in the end: provision the build toolchain (Hugo + Go + Node) and produce public/. The core idea of a Hugo-based blog is simple: Once you understand this boundary, it becomes natural to extend the site: when adding new features, first ask “can we generate the data at build time?” and then “can the browser consume it at run time?” Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: pnpm install pnpm dev # or: npm install && npm run dev Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: pnpm install pnpm dev # or: npm install && npm run dev COMMAND_BLOCK: pnpm install pnpm dev # or: npm install && npm run dev COMMAND_BLOCK: --- title: "My First Post" description: "A short summary" date: "2025-12-20T09:00:00+08:00" categories: ["Engineering"] tags: ["Hugo"] image: "/images/posts/2025/some-folder/cover.jpg" author: "Rain9" lang: "en" category: "Technology" subcategory: "Engineering" draft: true --- # Hello Write something here. Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: --- title: "My First Post" description: "A short summary" date: "2025-12-20T09:00:00+08:00" categories: ["Engineering"] tags: ["Hugo"] image: "/images/posts/2025/some-folder/cover.jpg" author: "Rain9" lang: "en" category: "Technology" subcategory: "Engineering" draft: true --- # Hello Write something here. COMMAND_BLOCK: --- title: "My First Post" description: "A short summary" date: "2025-12-20T09:00:00+08:00" categories: ["Engineering"] tags: ["Hugo"] image: "/images/posts/2025/some-folder/cover.jpg" author: "Rain9" lang: "en" category: "Technology" subcategory: "Engineering" draft: true --- # Hello Write something here. CODE_BLOCK: pnpm build Enter fullscreen mode Exit fullscreen mode - It uses Hugo Modules to reuse theme capabilities (such as image processing, PWA, SEO, and UI components), so CI must install Go (to fetch/manage module dependencies). - The homepage additionally generates a search index index.json, and the pages inject a prebuilt search UI (/static/assets/*), enabling offline/local search in the browser. - content/english/posts/: blog posts (Markdown + front matter) - content/english/authors/: author pages (Markdown + front matter) - title / description: used for page title and SEO - date: publish time (affects ordering and display) - categories / tags: used for category and tag pages - image: cover image - author: author name (linked to the corresponding author page) - Root config: hugo.toml theme = "hugoplate" outputs.home includes JSON to generate public/index.json build.buildStats.enable = true for accurate Tailwind scanning (explained later) - theme = "hugoplate" - outputs.home includes JSON to generate public/index.json - build.buildStats.enable = true for accurate Tailwind scanning (explained later) - Language config: config/_default/languages.toml English language with contentDir = "content/english" - English language with contentDir = "content/english" - Site parameters: config/_default/params.toml logo / favicon, theme colors, announcement bar, cookie banner, sidebar widgets, etc. - logo / favicon, theme colors, announcement bar, cookie banner, sidebar widgets, etc. - Hugo Modules: config/_default/module.toml Imports modules like github.com/gethugothemes/hugo-modules/images, pwa, seo-tools, etc. (this is where the Go dependency comes from) - Imports modules like github.com/gethugothemes/hugo-modules/images, pwa, seo-tools, etc. (this is where the Go dependency comes from) - theme = "hugoplate" - outputs.home includes JSON to generate public/index.json - build.buildStats.enable = true for accurate Tailwind scanning (explained later) - English language with contentDir = "content/english" - logo / favicon, theme colors, announcement bar, cookie banner, sidebar widgets, etc. - Imports modules like github.com/gethugothemes/hugo-modules/images, pwa, seo-tools, etc. (this is where the Go dependency comes from) - themes/hugoplate/layouts/_default/baseof.html: base layout skeleton - themes/hugoplate/layouts/index.html: homepage list - themes/hugoplate/layouts/posts/single.html: post detail page - themes/hugoplate/layouts/index.json: template that generates the JSON search index (important) - assets/: resources processed by Hugo Pipes (compile, fingerprint, minify, etc.) Images live under assets/images/... and are emitted to public/images/... during build Styles/scripts source files also live under themes/hugoplate/assets/* and are bundled via Hugo Pipes - Images live under assets/images/... and are emitted to public/images/... during build - Styles/scripts source files also live under themes/hugoplate/assets/* and are bundled via Hugo Pipes - static/: copied to public/ as-is This project includes static/assets/index-*.css/js and pizza_wasm_bg-*.wasm baseof.html hardcodes imports for /assets/index-*.css and /assets/index-*.js - This project includes static/assets/index-*.css/js and pizza_wasm_bg-*.wasm - baseof.html hardcodes imports for /assets/index-*.css and /assets/index-*.js - Images live under assets/images/... and are emitted to public/images/... during build - Styles/scripts source files also live under themes/hugoplate/assets/* and are bundled via Hugo Pipes - This project includes static/assets/index-*.css/js and pizza_wasm_bg-*.wasm - baseof.html hardcodes imports for /assets/index-*.css and /assets/index-*.js - dev: hugo server - build: hugo --gc --minify --templateMetrics --templateMetricsHints --forceSyncStatic - project-setup: node ./scripts/projectSetup.js - github.com/gethugothemes/hugo-modules/images - github.com/gethugothemes/hugo-modules/pwa - github.com/gethugothemes/hugo-modules/seo-tools/basic-seo - github.com/hugomods/mermaid - Hugo renders the site - Go pulls and manages Hugo Module dependencies - hugo.toml enables build stats: [build.buildStats] enable = true - tailwind.config.js uses: content: ["./hugo_stats.json"] - Mounts hugo_stats.json into assets/watching/hugo_stats.json - Combined with cache-busting config so CSS rebuilds trigger correctly - Collect plugin CSS (from hugo.toml params.plugins.css) - Compile scss/main.scss (requires Hugo extended) - resources.Concat to merge - css.PostCSS to run PostCSS (Tailwind + autoprefixer) - In production: minify | fingerprint | resources.PostProcess - Output <link href="...style.<hash>.css" integrity="..."> - After deployment, the site is pure static files (HTML/CSS/JS/images), with no runtime compilation. - Fingerprinting stabilizes caching: when content changes, the asset URL changes, so browsers/CDNs won’t serve stale assets. - public/index.json - themes/hugoplate/layouts/index.json - <link rel="stylesheet" href="/assets/index-*.css"> - <script type="module" src="/assets/index-*.js"></script> - The browser loads the static page - It loads the search UI JS/CSS - The search UI fetches index.json (and potentially WASM assets) and builds an index in the browser - Searches run locally in the browser—no backend API required - Hugo extended (this project requires >= 0.139.2, see config/_default/module.toml) - Go (CI uses 1.23.3 for Hugo Modules) - Node (for PostCSS/Tailwind; CI uses Node 20, but a common LTS should work locally) - build command: yarn project-setup; yarn build - publish: public - env: HUGO_VERSION=0.139.2, GO_VERSION=1.23.3 - install Node - download Hugo extended - npm run project-setup - npm install - npm run build - upload public as Pages artifact and deploy - install Hugo extended - npm run project-setup - npm install - npm run build - Build-time: Hugo compiles content, templates, modules, and assets into static files (and also performs minification, fingerprinting, and index generation). - Run-time: the CDN/static server only serves files; the browser runs a small amount of front-end JS (like the search UI). No backend API is required.