Hugo + Alpine.js + TailwindCSS Quickstart

Using my development environment setup I’m going to generate a new hugo site.

The first time we access the hugo container we need to ensure the hugo/src folder exists:

mkdir hugo/src

We will start by opening a bash terminal to our hugo container:

./run.sh hugo bash

Then we can verify our Hugo version:

hugo version
# => hugo v0.107.0-2221b5b30a285d01220a26a82305906ad3291880 linux/amd64 BuildDate=2022-11-24T13:59:45Z VendorInfo=gohugoio

From here, we can refer to the official Hugo quick start and initialize Hugo:

hugo new site . --format yaml

Next we will setup tailwind, alpinejs, @tailwindcss/aspect-ratio, @tailwindcss/typography and a few other javascript modules used for development:

npm init -y
npm install -D \
  @tailwindcss/aspect-ratio \
  @tailwindcss/typography \
  alpinejs \
  autoprefixer \
  concurrently \
  postcss \
  postcss-cli \
  postcss-import \
  tailwindcss

We will leverage the Tailwind CLI to manage the Tailwind build outside of Hugo. We will also modify the hugo/src/package.json scripts to add some helper scripts for managing the hugo and tailwind build pipelines:

// hugo/src/package.json
{
  "scripts": {
    "start": "concurrently \"npm:watch:*\"",
    "release": "concurrently -m 1 \"npm:build\" \"npm:deploy\"",

    "watch:tailwind": "npm run build:tailwind -- --watch",
    "watch:hugo": "npm run hugo:memory -- -D server --baseURL ${HUGO_BASEURL:-localhost:1313} --bind=0.0.0.0 --templateMetrics",

    "build": "concurrently -m 1 \"npm:build:tailwind\" \"npm:build:hugo\"",
    "build:tailwind": "tailwindcss -c ./tailwind/config.js -i ./tailwind/app.css -o ./assets/app.css",
    "build:hugo": "hugo -v --minify",

    "deploy": "concurrently \"npm:deploy:*\"",
    "deploy:hugo": "hugo deploy -v",

    "hugo:memory": "hugo",
    "hugo:disk": "hugo --renderToDisk --cleanDestinationDir --disableFastRender"
  },
  "devDependencies": {
    "@tailwindcss/aspect-ratio": "^0.4.2",
    "@tailwindcss/typography": "^0.5.8",
    "alpinejs": "^3.10.5",
    "autoprefixer": "^10.4.13",
    "concurrently": "^7.6.0",
    "postcss": "^8.4.19",
    "postcss-cli": "^10.1.0",
    "postcss-import": "^15.0.0",
    "tailwindcss": "^3.2.4"
  }
}

The next step is setting up TailwindCSS which requires two files:

// hugo/src/tailwind/config.js

/* global module require */
module.exports = {
  mode: 'jit',
  content: [
    './layouts/**/*.html',
    './content/**/*.{md,html}',
  ],
  plugins: [
    require('@tailwindcss/aspect-ratio'),
    require('@tailwindcss/typography'),
  ]
};
/* hugo/src/tailwind/app.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

With those files added we can uncomment the lines in our hugo/Dockerfile and run ./up.sh in a new terminal session. Once started we can visit our site at http://localhost:1313 and see an “Empty Response” page because we dont have any content files or templates in our Hugo site.

Let’s add the required files to actually see something.

<!-- hugo/src/layouts/_default/baseof.html -->
<!doctype html>
<html lang="{{ .Lang }}">
  <head>
    {{ block "head" . }}
      <title>{{ .Title }}</title>
      {{ partial "style" . }}
    {{ end }}
  </head>
  <body>
    {{ block "main" . }}
    {{ end }}
    
    {{ partial "script" . }}
  </body>
</html>
<!-- hugo/src/layouts/_default/home.html -->
{{ define "main" }}
  <div x-data="sampleComponent({{ jsonify .Title }})">
    {{ .Content }}
  </div>
{{ end }}
<!-- hugo/src/layouts/partials/script.html -->
{{ $build := dict
  "targetPath" "app.js"
  "params" (dict
    "basePath" ("/" | relURL)
  )
}}

{{ with resources.Get "script/index.js" }}
  {{ $script := js.Build $build . | resources.Fingerprint "sha512" }}
  <script src="{{ $script.RelPermalink }}" integrity="{{ $script.Data.Integrity }}"></script>
{{ end }}
<!-- hugo/src/layouts/partials/style.html -->
{{ with resources.Get "app.css" }}
  {{ $style := minify . | resources.Fingerprint "sha512" }}
  
  <link rel="stylesheet" href="{{ $style.RelPermalink }}" />
{{ end }}
<!-- hugo/src/content/_index.md -->
---
title: Hello World Title
---

# Hello World
// hugo/src/assets/script/sample-component/index.js
import { basePath } from '@params';

export default (...initArgs) => {
  return {
    init() {
      console.info(JSON.stringify({ basePath, initArgs }, null, 2));
    }
  };
};
// hugo/src/assets/script/index.js
import Alpine from 'alpinejs';
import sampleComponent from './sample-component';

Alpine.data('sampleComponent', sampleComponent);

Alpine.start();

With all of those files added we can run ./up.sh and load up our page at http://localhost:1313/ and see a TailwindCSS styles “Hello World”. If we open up the javascript console in the developer tools we should see an object output that looks like this:

{
  "basePath": "/",
  "initArgs": [
    "Hello World Title"
  ]
}

Note that the basePath is embedded in the code from Hugo on build while the initArgs are passed in at runtime when we initialize the Alpine.js component with x-data.

The last little bit before we commit the project to git is adding the .gitignore which should look something like this:

# .gitignore
.env
hugo/src/node_modules
hugo/src/.hugo_build.lock
hugo/src/assets/app.css