Skip to main content

【Lab】Static Website with Hugo + AWS

·6 mins

Overview #

This lab covers building a personal website from scratch using Hugo as a static site generator, the Congo theme for design, and AWS S3 + CloudFront for hosting and content delivery.

Tech Stack:

  • Hugo v0.162.0 (snap) on Ubuntu WSL2
  • Theme: Congo (Tailwind CSS based)
  • Hosting: AWS S3 + CloudFront
  • Domain: ofurotime.ca

Phase 1: Environment Setup #

Installing Hugo #

Ubuntu’s apt package manager ships an outdated version of Hugo. To get the latest version, use snap instead:

# Remove outdated apt version
sudo apt remove hugo

# Install latest via snap
sudo snap install hugo

# Verify
hugo version
# hugo v0.162.0 ... snap:0.162.0

Key lesson: Always use snap install hugo on Ubuntu — apt install hugo gives you an outdated version that is incompatible with modern themes.

Project Structure #

Hugo lives inside a subdirectory of the workspace:

hugo-workspace/
├── CLAUDE.md              ← AI copilot instructions
├── .gitignore
├── my-website/            ← Hugo root (always run commands from here)
│   ├── hugo.toml          ← Base Hugo config
│   ├── config/_default/   ← Congo theme config files
│   │   ├── hugo.toml
│   │   ├── languages.en.toml
│   │   ├── menus.en.toml
│   │   ├── params.toml
│   │   └── module.toml
│   ├── content/           ← Markdown content
│   ├── layouts/           ← Custom HTML overrides
│   ├── static/            ← Images, CSS, JS
│   ├── themes/            ← Hugo themes
│   └── public/            ← Generated output (git ignored)
└── docs/                  ← Planning notes

Key rule: Always run Hugo commands from my-website/, never from a subdirectory.


Phase 2: Theme Installation #

Why Congo? #

Congo was chosen for its combination of bio/profile homepage layout and knowledge base support, built on Tailwind CSS, with dark/light mode and strong SEO out of the box.

Installing Congo #

cd ~/hugo-workspace/my-website
git clone https://github.com/jpanther/congo themes/congo

Key lesson: Use git clone instead of git submodule for simplicity. Git submodule can result in incomplete file downloads causing template errors.

Version Compatibility Issue #

The first attempt used Hugo v0.123.7 from apt, which was incompatible with the latest Congo theme, causing this error:

partial "functions/warnings.html" not found

Fix: Upgrade Hugo to v0.162.0 via snap.


Phase 3: Hugo Configuration #

How Hugo Config Works #

Hugo merges multiple config files with this priority order:

languages.en.toml    ← Highest priority
params.toml          ← Medium priority  
hugo.toml            ← Base defaults (lowest priority)

This means languages.en.toml overrides hugo.toml for settings like title and description.

Config File Responsibilities #

FileControls
hugo.tomlbaseURL, theme name
languages.en.tomltitle, description, locale
params.tomlauthor, homepage layout, theme params
menus.en.tomlnavigation menu items

Base Config (config/_default/hugo.toml) #

baseURL = "https://ofurotime.ca/"
locale = "en"
title = "CENG Lab -- Ryo Ueda"
theme = "congo"

Language Config (config/_default/languages.en.toml) #

locale = "en"
label = "English"
title = "CENG Lab -- Ryo Ueda"
[params]
  description = "Cloud Network Engineer based in Toronto, Canada"

Theme Params (config/_default/params.toml) #

Key settings added:

colorScheme = "congo"
enableSearch = true

[author]
  name = "Ryo Ueda"
  headline = "Cloud Network Engineer in Canada"
  bio = "Cloud Network Engineer from Japan. Currently working in Toronto, Canada."
  links = [
    { linkedin = "https://www.linkedin.com/in/ryo-ueda-network-engineer/" },
    { github = "https://github.com/ryo3009" },
  ]

[homepage]
  layout = "profile"
  showRecent = true
  recentLimit = 3

Key lesson: In params.toml, use [author] not [params.author] — the params. prefix is implicit.


Phase 4: Content Structure #

Directory Layout #

content/
├── _index.md          ← Homepage content
├── about/
│   └── index.md       ← Bio page
├── posts/             ← Blog articles
└── knowledge-base/    ← Study notes
    ├── _index.md
    ├── cloud/
    ├── networking/
    └── canada/

Homepage (content/_index.md) #

---
title: "Ryo Ueda"
description: "Cloud Network Engineer based in Toronto, Canada"
---

## Welcome

I am a Cloud Network Engineer based in Toronto, Canada.
This site is my personal bio and knowledge base for Cloud & Networking study.

## Knowledge Base

- [☁️ Cloud](/knowledge-base/cloud/) — AWS, Azure, GCP
- [🌐 Networking](/knowledge-base/networking/) — BGP, OSPF, SD-WAN
- [🍁 Canada](/knowledge-base/canada/) — Co-op, Working Holiday, Life in Canada

Phase 5: Local Development #

How Hugo Server Works #

hugo server is a local development preview only — it is not a real production server.

hugo server -D
      ↓
Builds site IN MEMORY (temporary)
      ↓
Serves at http://localhost:1313
      ↓
Only accessible on your machine

WSL2 Access from Windows Browser #

By default, WSL binds to 127.0.0.1 which is only accessible inside WSL. To access from Windows browser:

hugo server -D --bind="0.0.0.0" --baseURL="http://localhost:1313"

--bind="0.0.0.0" exposes the server to all network interfaces including Windows.

Hugo Lifecycle #

Development                    Production
──────────────────             ──────────────────
hugo server -D    →  preview   hugo --minify
localhost:1313                       ↓
(memory only)                  /public folder
                                     ↓
                               Deploy to S3

Phase 6: AWS Hosting #

Architecture #

Hugo Build → S3 Bucket → CloudFront CDN → Users

Step 1: Build the Site #

cd ~/hugo-workspace/my-website
hugo --minify

This generates the public/ folder with all static HTML, CSS, JS files.

Step 2: Upload to S3 #

aws s3 sync ~/hugo-workspace/my-website/public/ \
  s3://ofurotime-web-bucket/ \
  --delete

What each part does:

PartPurpose
aws s3 syncSmart copy — only uploads changed files
public/Source: Hugo’s generated output
s3://bucket/Destination: S3 bucket root
--deleteRemove S3 files that no longer exist locally

Step 3: S3 Bucket Configuration #

  • Enable Static Website Hosting
  • Set index document to index.html
  • Set error document to 404.html
  • Attach bucket policy allowing CloudFront OAC access

Step 4: CloudFront Configuration #

SettingValue
Origin domainofurotime-web-bucket.s3.ca-central-1.amazonaws.com
Origin path(empty — files are at bucket root)
Origin accessOAC (Origin Access Control)
Default root objectindex.html
Viewer protocolRedirect HTTP to HTTPS

Custom error page:

  • HTTP error: 403 / 404
  • Response page: /404.html
  • Response code: 404

Step 5: Invalidate Cache After Deploy #

aws cloudfront create-invalidation \
  --distribution-id YOUR-DISTRIBUTION-ID \
  --paths "/*"

Full Deploy Script #

#!/bin/bash
cd ~/hugo-workspace/my-website

echo "Building site..."
hugo --minify

echo "Uploading to S3..."
aws s3 sync public/ s3://ofurotime-web-bucket/ --delete

echo "Invalidating CloudFront cache..."
aws cloudfront create-invalidation \
  --distribution-id YOUR-DISTRIBUTION-ID \
  --paths "/*"

echo "✅ Deploy complete!"

Key Lessons Learned #

  1. Hugo commands must run from the Hugo root — not from subdirectories like content/
  2. Never edit themes/congo/ directly — override in layouts/ instead
  3. apt install hugo is outdated on Ubuntu — always use snap install hugo
  4. languages.en.toml overrides hugo.toml for title and description
  5. In params.toml, drop the params. prefix — it is implicit
  6. hugo server is memory-only — not a real production server
  7. CloudFront origin path should be empty when S3 files are at bucket root
  8. Always invalidate CloudFront cache after deploying new content

Architecture Diagram #

┌─────────────────────────────────────────────────┐
│                  Development                     │
│                                                  │
│  Markdown files → Hugo → localhost:1313          │
│  (WSL2 Ubuntu)    (memory)  (Windows browser)    │
└─────────────────────────────────────────────────┘
                        ↓ hugo --minify
┌─────────────────────────────────────────────────┐
│                  Production                      │
│                                                  │
│  /public → S3 Bucket → CloudFront → ofurotime.ca│
│  (static)   (origin)    (CDN+HTTPS)  (domain)   │
└─────────────────────────────────────────────────┘

References #