How To Generate a Static Website with Hugo framework and Tailwind CSS, and Deploy to AWS Using Parima


We built Parima to allow front end developers an easy way to launch their sites into the cloud that gives them full control over a scaleable and cost-effective deployment and distribution process, using Amazon Web Services (AWS). While Parima works for static HTML sites and JavaScript frameworks like Angular, ReactJS, and Vue, it can also be used with static site generators like Hugo or Gatsby.

To illustrate this, we have created this tutorial that will show you the quickest way to build a multi-page Hugo-generated website and blog with a sane utility class CSS (Tailwind), and then to deploy it to AWS using Parima. We are handling this step by step from installation to deployment, so even developers who are brand new to Hugo or static site generators should hopefully be able to follow along. You can find this Tutorial project on GitHub.

Using Hugo requires installing the Hugo Command Line Interface (CLI), which takes your template and content files (written in HTML, in our case, but other formats are available) and creates a static website, where pages share common features like navbars and footers without you needing to copy and paste large amounts of HTML and CSS, like you would using HTML and CSS on its own. It also doesn't require a backend database or server code, unlike WordPress, and new content (like blog posts or static content pages) can be added quickly without having to update links on multiple pages.

Hugo also allows you to include any JavaScript modules you would like, and has its own variables and functions to customize your generated site.

What You Need for this Tutorial

  • Around one hour
  • A favorite text editor or IDE
  • Hugo CLI (to be installed as part of Step 1)
  • Node v12.16.3 and NPM v6.14.4 (to be installed as part of Step 1)
  • Access to an AWS account, preferably with Root Access

Note: The full Source Code for this Tutorial is available on GitHub.

Step 1: Install Hugo CLI and Node/NPM

In addition to installing Hugo CLI (version v0.75.1) for this tutorial, we will use Node and NPM for installing additional modules to integrated Hugo with Tailwind CSS. We are using Node v12.16.3 and NPM v6.14.4.

Instructions for installing Hugo CLI can be found on the Hugo website.

Note Regarding Node/NPM: For Mac/Linux, we recommend using Node Version Manager (nvm) when working on multiple projects using Node or NPM. (Windows has, but we have not tried it out.)

Step 2: Create a New Site

Hugo is often used with a theme, but we will go without one, using Tailwind CSS in our templates and content pages for layout and styling.

Note: we are adapting our Tailwind CSS implementation from

Step 3: Add a package.json File to Enable NPM

We can enable NPM and the dependencies we need to use Tailwind CSS with Hugo by creating ./package.json:

Step 4: Install Node Modules

Run the following to install the node modules needed:

Note: If you receive an error mentioning package-lock.json, try running `npm install tailwindcss@^1.8.10` and then running `npm install` again. (It seems that the npx package expected package-lock.json to be generated before installing.)

Step 5: Add the Tailwind CSS Configuration File

Create ./tailwind.config.js with the following code:

Step 6: Add gulp Configuration

gulp is used for Tailwind CSS, both to include Tailwind CSS utility classes from its Node Module, and to remove any unnecessary Tailwind CSS classes before generating the static website.

Create ./gulpfile.js with the following code:

Step 7: Add ./src/css/page.css

In order for gulp to add Tailwind CSS utility classes for our site to use, we need to import those CSS classes. Create ./src/css/page.css and add the following code:

We can now test that we haven't made any errors in our configuration by starting the local development server for Hugo:

You may see warning about deprecations or future risks, but the end result should look something like this:

At this point, loading the browser screen will provide a blank screen, as we haven't created our templates or any content.

Step 8: Create a Content Page Archetype

Hugo is built around the idea that you should be able to create templates and a basic site structure, add content with some info (or front matter) that describes the content (such as a title, a created date, and whether or not its currently a draft), and then any future additions or changes to your content will not usually require modifications to your layouts.

In some cases, such as blogs that consist of one main page and any posts that are created, you may not need to categorize the types of content that you will use.

In this tutorial, we want to create a site with two different types of content that are commonly seen: Content Pages (examples include any non-blog homepage, a contact page, or an about page) and Blog Posts. To customize how your layout reacts to these two different types of content, we can use Hugo's Archetypes. An archetype is basically a kind of template/blueprint for a type of item, and in Hugo, any content page has a type of content (which determines its front matter) and a format. In our case, we will use HTML as our format.

Create ./archetypes/page.html and add the following code (its front matter):

When we create a new content page, we will assign it this archetype, and it will not only have the front matter we have placed in this file, but will also be able to use its "Type" value to customize our layouts.

Step 9: Create a "Post" HTML archetype for Blog Posts

Blog posts will have a different set of front matter, including a created date.

Create ./archetypes/post.html and add the following code (its front matter):

Step 10: Create the base HTML template and some components it will use

The HTML pages in your static site will all be generated starting with the base HTML template. Create ./layouts/_default/baseof.html and add the following code:

You can see that this template has very little HTML content; aside from the HTML, HEAD, and BODY tags, it has one DIV tag with some Tailwind CSS utility classes, and multiple lines of code wrapped in braces (or curly brackets), such as "{{ partial "head.html" . }}" This is how Hugo knows to insert another template component into a template.

This tutorial uses two different types of components: Partials and Blocks. These two types of components are very similar, and are even stored within the ./layouts/partials folder. The difference between them is not easily apparent when staring out with Hugo, but one way to look at it is to think of Partials as being small snippets that would be added to templates and possibly to blocks within those templates, while Blocks are components that might make use of their own Partials.

So for our tutorial, we use a Partial for the HEAD content for our HTML template, while we use Blocks the various sections of layout within the template, such as the navbar.

Create ./layouts/partials/head.html and add the following code:

The head.html Partial includes quite a bit of Hugo Logic, to allow you to add extra information in your Configuration or Content files, and have it included in your generated HEAD section. These check for optional information, such as a site title or extra custom CSS files (such as a CSS file from a CDN), and display if that information is available.

None of this customization needs to be done for this tutorial, but having this in place will help you to extend this sample project.

Next, create ./layouts/partials/navbar.html and add the following code:

NOTE: You can see that this is a block due to the "define" and "end" Hugo directives, which aren't used in partials.

For the final component, create ./layouts/partials/footer.html and add the following code:

Step 11: Create the Home Page Layout

While we have a baseof.html template, we still need a page template for our home page in order to see our site layout in action.

Create ./layouts/index.html and add the following code:

You should now be able to see the layout if you open your browser. Run 'npm run start' and open http://localhost:1313

However, there is one tweak we will want to make. The footer is meant to be at the bottom of the screen, and we had added some responsive utility classes to place it there, "min-h-screen-85" and "md:min-h-screen-90", but they don't seem to be working.

That's because this directive, to use a minimum height of 85 (85 what?) or 90 on medium and larger screens, is not part of the Tailwind CSS library of utility classes. We need to add these custom classes in ./tailwind.config.js:

Because this change isn't part of the HTML or CSS files, neither hugo nor gulp will notice and re-compile the Tailwind imports automatically, so you will need to stop the dev server with [CTRL] + [C] and restart with 'npm run start' in order to see the change:

Step 12: Create Home Page Content

So we've created the Home Page Layout, but now we want to be able to add our own content. It's possible to add the content to that layout file, but organizing your content will likely be easier if you keep your home page content within the content folder.

Hugo CLI allows you to quickly generate a new content page, and the path you specify will help Hugo to understand how that page should be generated. We will also add a flag to tell Hugo what Type (i.e., Archetype) this new content item belongs to:

This will create an HTML file in the root of the content folder, ./content/_index.html. The underscore in front of its name is partly to differentiate it from regular content pages, and also a great way of ensuring the default (home) page is listed at the top of the list of files.

We will need to give that new Content Page a title. Open the file and add a title:

The three dashes at the top and bottom of this block of text are the delimiters (boundaries) of the front matter. This information is not displayed automatically on the content page, but is information that can be used anywhere on the site, including in any layout file. We will use the front matter information more when we work with Blog Posts.

Step 13: Create the Content Page Template

The home page is a special case, a content page that uses its own layout. Other pages will use a common template by default. Create ./layouts/_default/single.html and add the following code:

Step 14: Create Contact Page Content

Now we will start to see how Hugo can speed up content creation. Now that we have a default layout for single content pages, we can add those new pages using Hugo CLI:

By default, Hugo takes the filename of the new page and uses that for its "title" value in the front matter. So without having to edit the content page, you can already see a header for our new Contact page:

At this point, you can add content to the Contact page, such as an address, an embedded map, or a contact form. Of course, if you wanted to add a Block or Partial to the content page, something you may want to use in a few places on the site, you could add it here.

Step 15: Create a Section Template and a Summary Template for a Blog

In Hugo, subfolders in your content are automatically treated as sections. This can be useful for sections of content subpages, but another common use of sections is to create a blog section within your site; your section can then use the Section template to automatically display all pages within that section. So for a blog, you can add a new post, and it can be set up to be automatically added to your main blog page.

Create ./layouts/_default/section.html and add the following code:

This layout calls its own partial called "summary.html", but using "range" to retrieve and loop through all pages it finds within the folder, it calls the summary partial once for each page. This is how you automatically list all posts for your blog.

Create ./layouts/partials/summary.html and add the following code:

This partial has access to all of the properties attached to the page it is displaying; that's what the period after the partial name does, gives the partial a reference point. So ".Title" means to take the "title" of the page's front matter (not the difference in capitalization), while ".Summary" will display a "summary" value. Of course, if a content item does not have either value, it will show up as an empty string.

Note: Rather than using the standard {{ .Permalink }} directive that adds a trailing slash to the end of the link, we need to remove the trailing slash. This is because CloudFront does not handle trailing slashes in static sites, and Hugo does not currently have a feature to easily disable trailing slashes in permalinks. We are using Hugo's TrimRight function to remove any slashes at the end (right side) of the url that is generated by Hugo.

We will want to add a copy of the default Sitemap template with our permalinks adjusted as well, placing it at ./templates/_default/sitemap.xml:

Step 16: Create a Blog by Adding the First Blog Post

So with our section template and summary partial in place, we can create our first blog post by using Hugo CLI:

This new page will do two things:

  1. create a blog section, which will automatically use the section.html template
  2. create a blog post, a page within the blog section that should be included in the summary.html partial used in section.html

However, this blog section and blog post will not be generated in the build by default; this is due to the "draft" property in the front matter of the blog post. As long as "draft" is set to "true", Hugo will ignore this page. And since this is the only page in the section, Hugo will also ignore this section.

Another thing to consider is the type of page we've created; by using "-k post", we are using the "Post" HTML archetype we created, instead of the "Page" archetype we used for Content Pages. This creates more properties in the front matter that we can customize, such as "summary". It also automatically creates a "date" property with the current date and time from when you ran the Hugo CLI command, in your local timezone. This date value can be displayed on the blog post page or the summary partial in the blog section template.

Note: The date property can also be used in a way similar to the "draft" property. If a page has a date value that is set into the future, Hugo will also not include it in the site. (And in the case of our blog section, if all its posts have timestamps set for the future, it also will not show.) Keep in mind that this does not mean Hugo will release the page once the time comes; Hugo would need to regenerate the site to show that page.

You should be able to view the site in the browser and access the blog section now through the navigation link at the top. If not, check that your blog post "draft" property has been set to "false".

You may have noticed that the Blog Section page has a title of "Blogs", and not "Blog". If we had wanted to, we could have used a folder name of "posts", but because we went with "blog" for our folder within content, the default setting in Hugo is to create a plural of that folder. How do we change this? We would need to edit the "title" of the Blog Section's front matter. But by default, there is no content file for the blog section, so we will need to create it:

In reality, we don't need to edit the title now, since creating that _index.html page automatically used "Blog" singular as its title, so our heading will be fixed and you should be able to see that in the browser:

Note: we used the Page HTML Archetype to create the Blog Section content item, since it's a Content Page and not a Blog Post.

Step 16: Add JavaScript to Handle the Responsive Hamburger Menu

If you haven't checked the generated Tutorial site on a mobile view, you may not have noticed the Hamburger Menu:

Since Hugo is a static site generator, it does not include JavaScript by default. But we can add any JavaScript we want, including some code to handle our Hamburger Menu, by adding a partial with our JavaScript code.

Create ./layouts/partials/js.html and add the following JavaScript code block:

Our navbar.html Block already contains what it needs, the element IDs and onclick property on the button, but we will need to add the reference to this js.html partial before we can see the Hamburger Menu in action.

Add a reference to the js.html partial below the closing BODY tag, right above the closing HTML tag. Your file show now look like this:

Now you should be able to click the Hamburger Menu button and see the menu itself appear:

Step 17: Add More Functionality to the Blog

We can create a second post and add a few more features to the blog, to bring it closer to what we would expect for a real-world blog. We can use Hugo CLI to create that second post:

Once that blog post has had "draft" set to "false", you should be able to see the second post on the section page, placed to the left of the original, first post.

This is because Hugo orders pages with "date" properties by most recent; while you can adjust dates to adjust order, you can also use the "weight" value to create a sticky post; a higher weight means it is ranked higher.

If you click on a blog post, you can see the title, and space below that where any content would be displayed. What is not visible, however, is any kind of breadcrumb navigation on the base. Too add breadcrumbs, we will need to create a partial to use on the content template, i.e., single.html.

Create ./layouts/partials/breadcrumbs.html

This partial defines a template and renders that template, taking the current page (that is being displayed by single.html), assigning it to the name "p1", and then checking whether or not that page has a parent (a section page above it). If there is a parent, a link to that section page is rendered.

The If Statement that is used in this partial checks both if the current page has a parent, but also checks the path of the page. This is because of our Contact page; as it also uses single.html, it has a parent, which is the section page above it. Its section page is the root section page, which also happens to be the home page. Since we only want breadcrumbs for our blog posts, we use the If Statement to avoid adding the breadcrumb to the Contact page.

NOTE: If you have a need for an additional level of breadcrumbs, you can go up a level higher on parent, i.e., ".Parent.Parent.File", but only if ".Parent.Parent" exists:

To include the breadcrumb.html partial in the single.html layout, we need to add the following code to ./layouts/_default/single.html, preferably right above the HEADER tag:

This should create a breadcrumb link on each blog post, but not on the Contact page:

The last thing this tutorial will add to the blog is a display of each blog post's created date. The date value in the front matter is a timestamp, e.g., "date: 2020-10-06T16:36:21-05:00". In order to display a friendlier format, we need to use a built-in Hugo function: .Format. We will add this date to both the single.html template for the blog post, as well as the summary.html partial for the posts listed in blog section page.

Add the following code to ./layouts/_default/single.html, below the H1 tag containing the title:

As with the breadcrumb, the blog post page will show the date, while the Contact page (which has no date) will not be affected.

For the summary, we will add a shorter formatted date. Add the following code to ./layouts/partials/summary.html, below the H2 tag that displays the post title:

This is all of the changes we'll add to the blog, but a logical next step would be to incorporate paging. Hugo does have a relatively easy way to incorporate splitting the post summaries onto more than one page. For more information, see the official Hugo documentation on Pagination.

Step 18: Generate Your Static Production Website with Hugo

Now that you have the required templates and content for a workable website, you can generate your static site using the following Hugo CLI command:

By default, this will place your static site into ./public. This is the folder you will use for AWS S3 Sync (Step 20).

Step 19: Create your AWS Cloudfront Distribution using Parima

Follow the instructions from the Parima README on GitHub to use Parima to launch your static site, as there are two different methods to choose from, through the AWS Console and through AWS CLI. There is also a Git deployment option in our roadmap as well, which will be added to the README once available. You can use a Custom Domain or a CloudFront URL will be generated for you.

Once the CloudFormation template has been completed, you can click on the "Outputs" tab under your Completed Stack in CloudFormation to view the information you will need to set up the AWS S3 Sync that will deploy your static site code.

Step 20: Deploy your Static Site using AWS S3 Sync

Follow the instructions from the Parima README on GitHub to run the AWS S3 Sync command that is listed in your "Outputs" tab in CloudFormation.

Step 21: Test Your Static Site on AWS CloudFront

Open the WebsiteUrl provided in the "Outputs" tab in CloudFormation to view your newly deployed static website.


That's the whole process for using Hugo to generate a static website that includes Tailwind CSS, and then deploying it to AWS using Parima.

If you would like to know how to migrate any existing website or application to AWS using Parima, check out this tutorial: How To Transfer Your Existing Website to Amazon Web Services powered by S3 and CloudFront, Using Parima

A Few More Things: Content Subpages, RSS, and Taxonomies

In addition to the blog section, we could have an About section, which has its own content pages, such as "Our Mission" and "Our Team". We could add the extra "About" link to the navbar.html (being sure to add both the desktop and mobile views), and then we could create new about pages using the Hugo CLI:

But if we don't want the About section page to list our subpages like they are blog posts, we'll need to update the section.html page to only include the summaries of pages if they belong to the "Page" Archetype (we can use the $scratch object to help with this):

But that leaves us with no links to our sub pages from either the About section page or the navbar. We could add links to the About section page:

We could also look at using JavaScript to create a drop-down menu in the navbar, or add links to the footer.

Our tutorial also includes some code for RSS; a popular feed might be ./blog/feed.xml. You can find out more information on customizing feeds from the official Hugo documentation on RSS Templates.

Note: You will need to copy the default Hugo RSS template and place a new one under ./layouts/_default/rss.xml in order to drop trailing slashes from your permalinks with TrimRight.

Another feature which would add value to the blog (but can have any number of uses) is Hugo Taxonomies. This allows you to use keywords and add those keywords into your navigation. More information is available from the official Hugo documentation on Taxonomies.

If you would like to know how to migrate any existing website or application to AWS using Parima, check out this tutorial: How To Transfer Your Existing Website to Amazon Web Services powered by S3 and CloudFront, Using Parima