Building a Static Website Hugo Tailwind and AlpineJS - Part 4

13 minute read | | Author: Murtaza Nooruddin

This section we will cover more advanced topics, such as blogging with Hugo, 404 pages and google analytics and extras.

Blogging with Hugo

Most static websites don’t change much. Which might tempt you to build a simple static website without a static website generator such as Hugo. Save all the hassle of configurations, variables.

The biggest power of CMS such as wordpress is blogs - dynamic content. However, with static website generators, you can match that power, as it lets you do that, generating categories or tags taxonomy pages for you, updating sitemaps and building blog lists, pagination etc. Wordpress or other blogging tools with an interface, have all of that out of the box, and they are designed for it, but with static, you do get unmatched loading speed and practically unhackable site that you don’t need to worry about.

Lets continue on our project. for blogs, I will prefer markdown content, as it’s easier to type and write. You can choose to have html and just have to rename extensions.

Create archetype file for blog, /archetypes/ and paste the following, I have taken the liberty of putting some dummy content, so when you create sample blogs, it gives you a better look and feel. This means every time you create a new blog with .md extension, it will apply this archetype. However, if you want to also create .html files for blogs, you can and add another archetype blog.html.

Also, if you prefer to call blog as article or posts, feel free to modify and replace. But just be cautious.

title: "{{ replace .Name "-" " " | title }}"
draft: false
categories: ["General", "Featured" ]
tags: ["cloud", "static", "fast" ]
keywords: ["hugo", "bootstrap","serverless", "hosting"]
author: "{{ .Site.Params.Author }}"

"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim *ad minima veniam*, quis nostrum exercitationem ullam **corporis suscipit laboriosam**, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"

# Blog Contents

- Option 1
- Option 2
- Option 3

## Sub Heading

"At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat."

Now modify our site’s default list template. Open /layouts/_default/list.html and lets create a template that lists all blogs neatly. Later on you can modify it according to your need.

{{ define "main" }}
<div class="container mx-auto my-10 ">

    <div class="flex flex-wrap">
        <div class="flex-none">
            Categories: {{range ($.Site.GetPage "taxonomyTerm" "categories").Pages }}
            <a class="inline-block bg-blue-500 text-white rounded-full px-3 py-1 text-sm font-semibold mr-2"
        <div class="flex-none">
            Tags: {{range ($.Site.GetPage "taxonomyTerm" "tags").Pages }}
            <a class="inline-block bg-green-500 text-white rounded-full px-3 py-1 text-sm font-semibold mr-2"

    {{ if ne .Kind "taxonomy" }}
    <div class="grid grid-cols-3 mt-2 gap-4">

        {{ range .Pages }}

        <div class="flex flex-col">
                <img loading="lazy" class="w-full h-auto rounded" src="{{ .Params.featuredImage }}" alt="featured image {{.Title }}">
            <div class="w-full bg-gray-100 p-3">

                <a class="text-black no-underline" href="{{ .RelPermalink }}">
                    <h2 class="text-lg font-bold">{{ .Title }}</h2>

                <time datetime="{{ .Lastmod.Format `006-01-02T15:04:05Z07:00` | safeHTML }}">
                    {{ .Lastmod.Format "Sep 2,2023" }}</time>
                Categories: {{ range .Params.categories }}
                <a class="text-black no-underline px-1" href="/categories/{{ . | urlize}}">{{ . }}</a>

                Tags:{{ range .Params.tags }}
                <a class="text-black no-underline px-1" href="/tags/{{ . | urlize}}">{{ . }}</a>


                {{ .Content | truncate 400 }}
                <a class="text-black no-underline hover:text-gray-600" href="{{.Permalink}}">...Read Article</a>


        {{ end }}

    {{ end }}


{{ end }}

Next up, open hugo.toml. Add the menu item under menus section.

    name = "Blogs"
    url = "/blog/"
    weight = 50

# this should already exist from previous copy/paste you did
  date = ["date", "publishDate", "lastmod"]
  lastmod = ["lastmod", ":fileModTime", ":default"]

# add this if you haven't added earlier
    author = "Blog Author"
    description = "This is an awesome Hugo tailwind site"

Note the [frontmatter] section two auto-generated date variables in front matter based on last modification of file. This helps us automatically adjust date on the blog whenever you make edits. Menu item for blogs should be self-explanatory. Also note we added [params] to add some custom variables you can use. We will use author to create default author for the blog, which can override in actual page’s front matter.

Later on you will see how [params] can be used for more automation and inject page specific variables such as meta data, structured data (microcodes or json/ld) etc.

Now generate some content:

hugo new blog/

hugo new blog/

hugo new blog/

hugo new blog/

This will create 4 new blogs as expected. Go ahead and modify the front matter of these by grouping 2 or more with same category and different tags.

Add a few sample images to /static/images folder and link each in one of the blogs as a featuredImage front-matter.

featuredImage: "/images/catering.jpg"

This will give a nice look to the blog list and view it generates.

Take a look at your site:

Blog List View

Clearly there is a lot of room for styling and I leave it to you to theme it up. Also note how all the category links, tags automatically work by creating links and list pages. I haven’t covered pagination, but feel free to explore Hugo docs to create pagination of the blog list as they will inevitably grow. I will at some point update this article on how to paginate blog list.

If you are picky about URL management and want blogs/posts to appear by date or certain format, study Hugo’s URL Management to find out how to setup permalinks.

Build your site for production

Now that most of your site is ready, you might want to see how a static output will look like and if anything works?

This is the easist of all things Hugo can do. Just type:


That’s it!. It generates a public folder, with static site built and ready to be deployed. All the hugo tags, variables and template are replaced with actual HTML code, with css and javascript assembled in one minified file. So clean and so fast. You can upload this on a test/production hosting server. Check my serverless hosting blog, on how to host this on AWS S3 and Cloudfront.

You can experiment with other hugo flags such as --minify if you want more compact HTML to be generated and --cleanDestinationDir to remove any unwanted static files in public folder in case you removed images that no longer needs to be deployed. I generally prefer deleting public directory altogher and building fresh copy before deployment.

Optional Extras

By default links open within the same window, but you might prefer links to open in new tabs, especially if they are external. Also as per good SEO recommendations, you need noreferrer and/or noopener to be added to your <a> tags generated by the blog. Especially when you are writing blogs in markdown format, you can’t control this behaviour. However, I only want it in blogs and not in the rest of my site.

Fortunately, there is a solution.

Create a folder /layouts/blog/_markup Create a file /layouts/blog/_markup/render-link.html

Inside the file you just created:

<a href="{{ .Destination | safeURL }}"{{ with .Title}} title="{{ . }}"{{ end }}{{ if strings.HasPrefix .Destination "http" }} target="_blank" rel="noopener"{{ end }}>{{ .Text | safeHTML }}</a>

This lets you create safe links whenever you create a link in your blogs.

FYI, creating a link is quite simple in markdown, example: [Noorix Digital]( will create Noorix Digital as a link text and generate following code when site is rendered: <a href="" target="_blank" rel="noopener">Noorix Digital</a>

Add 404 Error page

Another template file that we commonly use is 404.html for page not found. Usually, webmasters prefer they have a 404 page that still includes site header/footer and structure and has custom actions for 404.

Create a file /layouts/404.html

{{ define "main"}}
    <main id="main">
      <div class="text-center mt-5">
       <h1 >
        404 Not Found   
        <h2>Looks like this page is not on our website</h2>
        <a class="mt-5 btn themed-button" href="{{ "/" | relURL }}">Go back to homepage</a>
{{ end }}
Note, if you just put some random url next to your site in your Hugo development server, it WILL NOT redirect to 404.html. However, you can test it by using the actual link http://localhost:1313/404.html

In production, you will have to add a trigger to your 404 file path where you host it.

Add Google Analytics 4 to your site

This one is as simple as it gets

Add your google analytics code to hugo.toml

googleAnalytics = "GA-1511XXXXX"

Next add Hugo’s built-in internal template google analytics template in /layouts/partials/footer.html right before the closing </footer> tag

  {{ template "_internal/google_analytics_async.html" . }}

Configure Structured Data Markup

Search engines will appreicate a well written clean HTML page, but even better if you can inject microdata on your web page. This helps search engines classify pages more accurately, such as recipes, organisation, article etc. These results can then appear in rich snippets in google search results. Check the library of google recognised structured data to see which ones you can apply.

They are clearly optional, but here is an example of you can inject in Hugo. You can add as many of these with as many conditions as you like.

create a new partial: layouts/partials/articleschema.html

<script type="application/ld+json">

      "@context": "",
      "@type": "Article",
      "headline": {{ .Title }},
      "image": {{ .Params.featuredImage | absURL }},
      "datePublished": {{ .PublishDate }},
      "dateModified": {{ .Lastmod }},
      "author": {{ .Param "author" }},
      "mainEntityOfPage": { "@type": "WebPage" },
       "publisher": {
        "@type": "Organization",
        "name": {{ .Site.Title }},
        "logo": {
          "@type": "ImageObject",
          "url": {{ .Site.Params.logo }}
      "wordcount" : {{ .Content | countwords }},
      "description": {{ .Summary | plainify | safeHTML }},
      "keywords": [{{ range $i, $e := .Params.keywords }}{{ if $i }}, {{ end }}{{ $e }}{{ end }}]

Note how the json structure is dynamically created with current page front matter data and site level data. The logo url is missing, so edit hugo.toml and under [params] add another entry, and replace the link with your logo link


  logo = ""


Note how .Summary and word count features automatically create a summary of the content into the structure.

Now time to inject this in the page. Open /layouts/partials/head.html

    <title>{{ .Title }}</title>
    <meta charset="utf-8">
    <meta name="description"  content='{{ .Param "Description" }}' />
    <meta name="dc.relation" content="{{ .Site.BaseURL }}" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#1A94D2" />

    {{ $options := (dict "targetPath" "css/style.css" "outputStyle" "compressed") }}

    {{ $style := resources.Get "sass/main.scss" | toCSS  $options | minify }}
    <link rel="stylesheet" href="{{ $style.RelPermalink }}" media="screen">

        include a favicon for your site if you have it, else omit the line below 
        Location of favicon can be in /static/images/favicon.ico
    <link rel='shortcut icon' type='image/x-icon' href='/favicon.ico' />

      <!-- will load json/ld depending on page type-->
      {{ if and (.IsPage) (eq .Section "blog") }}
         {{- partial "articleschema.html" .  -}}
      {{ end }}


While we were here, I took the liberty of updating site meta description with {{ .Param "Description" }} as well.

Save and test your site, note that when you are on a blog (not blog list view), you will in page source the json code injected. Here is an example of how it transforms:

<script type="application/ld+json">

      "@context": "",
      "@type": "Article",
      "headline": "My First Blog",
      "image": "http://localhost:1313/images/image1.jpg",
      "datePublished": "0001-01-01T00:00:00Z",
      "dateModified": "2021-01-06T13:03:49.346186377+11:00",
      "author": "Blog Author",
      "mainEntityOfPage": { "@type": "WebPage" },
       "publisher": {
        "@type": "Organization",
        "name": "My Hugo Site",
        "logo": {
          "@type": "ImageObject",
          "url": ""
      "wordcount" :  259 ,
      "description": "\u0026ldquo;Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.",
      "keywords": ["hugo", "bootstrap", "serverless", "hosting"]

Facebook Opengraph, Twitter cards

Sharing your website link on social media? You need to inject Opengraph , developed by Facebook (I think), and twitter card as well, which tells the target social media platfrom to pick the right image for your website and description. I believe Linked and Instagram will also use opengraph.

Inject built-in Hugo templates in /layouts/partials/head.html. Anywhere inside the <head> tag will do.

    {{ template "_internal/opengraph.html" . }}
    {{ template "_internal/twitter_cards.html" . }}

Add image entry in hugo.toml under [params]. Hugo internal template will use that to build the link when it generates opengraph meta tags.

    author = "Blog Author"
    description = "This is an awesome Hugo Tailwind site"
    logo = ""
    images = ["/images/site-feature-image.jpg"]


Check out source page of your site and you should see more meta data in the head of your HTML.

Create Sitemap XML

A good site must have a sitemap. Infact almost compulsory when you submit it on Google Search Console.

Hugo takes care of this by generating a basic sitemap.xml automatically when you build your site with hugo command. Check your public folder. You can tweak it and check Hugo docs for more informationon using sitemap templates.

Enable robots.txt

Easy to add that. Open hugo.toml


enableRobotsTXT = true


Create layouts/robots.txt

Put whatever you want to configure… mine is something like this:

User-agent: *
Disallow: 404.html
Sitemap: {{ "" | absLangURL }}

Other Enhancements

Blog Commenting

The list of features you can add can be exhaustive, but there are a few other things you may want to research on your own. For example, blog commenting. You can integerate third party commenting systems, such as disqus, or build your own which is the scenic route to wasting your time. Luckily Hugo has built-in template for disqus .

Serverless contact form using AWS

I am in the process of creating a blog on how to create a serverless form and integrate in Hugo. But if you want a head start, there is an official AWS tutorial

While it’s a good article, it does not show the complete process end to end on how to setup email service on SES, google recaptcha etc.

If you are running into any compilation issues or want a shortcut. You can grab a copy of the project from my GitHub repo.

Continue to Hosting Static Website with AWS S3 & CloudFront

decor decor

Have a great product or start up idea?
Let's connect, no obligations

Book a web meeting decor decor