【Lab】Static Website with Hugo + AWS
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 #
| File | Controls |
|---|---|
hugo.toml | baseURL, theme name |
languages.en.toml | title, description, locale |
params.toml | author, homepage layout, theme params |
menus.en.toml | navigation 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:
| Part | Purpose |
|---|---|
aws s3 sync | Smart copy — only uploads changed files |
public/ | Source: Hugo’s generated output |
s3://bucket/ | Destination: S3 bucket root |
--delete | Remove 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 #
| Setting | Value |
|---|---|
| Origin domain | ofurotime-web-bucket.s3.ca-central-1.amazonaws.com |
| Origin path | (empty — files are at bucket root) |
| Origin access | OAC (Origin Access Control) |
| Default root object | index.html |
| Viewer protocol | Redirect 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 #
- Hugo commands must run from the Hugo root — not from subdirectories like
content/ - Never edit
themes/congo/directly — override inlayouts/instead apt install hugois outdated on Ubuntu — always usesnap install hugolanguages.en.tomloverrideshugo.tomlfor title and description- In
params.toml, drop theparams.prefix — it is implicit hugo serveris memory-only — not a real production server- CloudFront origin path should be empty when S3 files are at bucket root
- 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) │
└─────────────────────────────────────────────────┘