This is how I built rdgy.at – and how you can replicate it:
- Hugo for the site
- Blowfish for the theme
- Azure Static Web Apps for hosting
- OpenTofu for infrastructure
- GitHub Actions for automated deployment
The final URL structure looks like this:
www.example.com/ -> homepage
www.example.com/blog/ -> blog
www.example.com/projects/
www.example.com/services/
example.com -> forwards to https://www.example.comIf you need this setup for your own business – whether as a simple blog, company website, or a standalone product like Belegmappe.at – get in touch at office@rdgy.at. We’ll talk through what you need, and I’ll be straight with you about whether and how I can help.
German is the default language. Content is written first under content/de, then translated into content/en.
Phase 1 – Build the Site Locally#
1. Install the tools#
You need:
- Hugo Extended
- Git
- OpenTofu
- Azure CLI
On macOS one command is enough:
brew install hugo opentofu azure-cliThen verify everything installed correctly:
hugo version
tofu version
az version2. Create the site and add Blowfish#
hugo new site my-site
cd my-site
git init
git submodule add -b main \
https://github.com/nunocoracao/blowfish.git \
themes/blowfish
mkdir -p config/_default
cp themes/blowfish/config/_default/*.toml config/_default/3. Configure Hugo#
The two most important config files:
config/_default/hugo.toml– general settings, languages, navigationconfig/_default/params.toml– layout and appearance
config/_default/hugo.toml#
theme = "blowfish"
baseURL = "https://www.example.com/"
defaultContentLanguage = "de"
defaultContentLanguageInSubdir = false
enableRobotsTXT = true
summaryLength = 0
[outputs]
home = ["HTML", "RSS", "JSON"]
[taxonomies]
tag = "tags"
category = "categories"
[languages]
[languages.en]
weight = 2
contentDir = "content/en"
[languages.de]
weight = 1
contentDir = "content/de"
[languages.en.menu]
[[languages.en.menu.main]]
name = "Blog"
pageRef = "blog"
weight = 10
[[languages.en.menu.main]]
name = "Projects"
pageRef = "projects"
weight = 20
[[languages.en.menu.main]]
name = "Services"
pageRef = "services"
weight = 30
[languages.de.menu]
[[languages.de.menu.main]]
name = "Blog"
pageRef = "blog"
weight = 10
[[languages.de.menu.main]]
name = "Projekte"
pageRef = "projects"
weight = 20
[[languages.de.menu.main]]
name = "Leistungen"
pageRef = "services"
weight = 30A few things to note:
baseURLalways points towww- German lives at
/, English at/en/ - Navigation is defined per language here
config/_default/params.toml#
defaultBackgroundImage = "img/background.jpg"
[homepage]
layout = "background"
homepageImage = "img/background.jpg"
showRecent = true
showRecentItems = 3
[article]
showDate = true
showAuthor = true
showReadingTime = true
showTableOfContents = true
showTaxonomies = true
showPagination = true
showHero = true
heroStyle = "thumbAndBackground"
[list]
showHero = true
heroStyle = "background"
showSummary = true
showCards = true
cardView = trueThis gives you:
- Homepage with background image
- Hero sections on blog and project pages
- Latest posts on the homepage
- Card view for blog and projects
4. Set up the content structure#
Sections (blog, projects) get a _index.md. Individual pages (posts, projects) are regular .md files.
mkdir -p content/de/blog
mkdir -p content/de/projects
mkdir -p content/en/blog
mkdir -p content/en/projects
mkdir -p static/imgThe structure looks like this:
content/
├── de/
│ ├── _index.md
│ ├── blog/
│ │ ├── _index.md
│ │ └── first-post/index.md
│ ├── projects/
│ │ ├── _index.md
│ │ └── product-a.md
│ └── services.md
└── en/
├── _index.md
├── blog/
├── projects/
└── services.mdHomepage example#
content/de/_index.md
---
title: "Dominic"
description: "Cloud engineer from Innsbruck."
---
I build cloud infrastructure for teams that prefer to hand off
production deployments to someone experienced.Blog index example#
content/de/blog/_index.md
---
title: "Blog"
description: "Notes on Cloud, Infrastructure as Code and Software Delivery."
---Project page example#
content/de/projects/product-a.md
---
title: "Product A"
description: "Short product description."
summary: "Short text for the project card in the overview."
showDate: false
showReadingTime: false
---
## Idea
What the product is about.
## Challenges
- Challenge one
- Challenge two
## Tech Stack
- Hugo
- PostgreSQL
- Azure
## Live Project
[product-a.com](https://product-a.com)Write the German content first, then create the matching translation under content/en/.
5. Test the site locally#
hugo serverOpen in your browser:
http://localhost:1313/– German versionhttp://localhost:1313/en/– English version
Once the site looks the way you want locally, move on to deployment.
Phase 2 – Infrastructure with OpenTofu#
OpenTofu describes the Azure infrastructure as code. Write it once, reuse and version it forever.
6. Provision Azure with OpenTofu#
Create an infra/ directory in the project root and add the four files below. Then get your credentials and roll everything out with tofu apply.
mkdir infrainfra/variables.tf#
This file declares all the inputs OpenTofu needs. The actual values don’t live here – they come from terraform.tfvars. This keeps the structure and the secrets cleanly separated.
sensitive = true tells OpenTofu never to print these values in the terminal – not even during apply.
location has a default value (westeurope) that is used unless you specify otherwise.
variable "subscription_id" {
type = string
sensitive = true
}
variable "client_id" {
type = string
sensitive = true
}
variable "client_secret" {
type = string
sensitive = true
}
variable "tenant_id" {
type = string
sensitive = true
}
variable "location" {
type = string
default = "westeurope"
}infra/main.tf#
This is the actual infrastructure. OpenTofu reads this file and creates the described resources in Azure – or updates them if they already exist.
terraform block – specifies the provider version. azurerm is the official Azure plugin. Version ~> 3.0 allows all patch updates within version 3, but no major version jump.
provider "azurerm" – connects OpenTofu to your Azure account using the variables above.
azurerm_resource_group – a resource group is a logical container in Azure. All related resources land in it. Delete the group and everything inside it goes with it.
azurerm_static_web_app – the actual hosting. Azure Static Web Apps serves the site, distributes it globally via CDN, and handles HTTPS automatically. The Free tier is fully sufficient for a regular website.
azurerm_static_web_app_custom_domain – links your own domain (www.example.com) to the Static Web App. cname-delegation means Azure checks that the CNAME record in DNS points to the Azure hostname before activating the domain.
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
required_version = ">= 1.6.0"
}
provider "azurerm" {
features {}
subscription_id = var.subscription_id
client_id = var.client_id
client_secret = var.client_secret
tenant_id = var.tenant_id
}
resource "azurerm_resource_group" "site" {
name = "rg-my-site"
location = var.location
}
resource "azurerm_static_web_app" "site" {
name = "stapp-my-site"
resource_group_name = azurerm_resource_group.site.name
location = azurerm_resource_group.site.location
sku_tier = "Free"
sku_size = "Free"
}
resource "azurerm_static_web_app_custom_domain" "www" {
static_web_app_id = azurerm_static_web_app.site.id
domain_name = "www.example.com"
validation_type = "cname-delegation"
}infra/outputs.tf#
After apply, OpenTofu prints these values in the terminal. You’ll need them for the next steps – domain setup and the GitHub secret.
site_url– the auto-generated Azure URL for your site (e.g.https://proud-rock-abc123.azurestaticapps.net). Use this to verify the deployment worked before pointing your domain.deployment_token– the secret key GitHub Actions needs to upload files to Azure. Comes next as a GitHub secret.www_cname_target– the hostname yourwwwCNAME record at your DNS provider needs to point to.
output "site_url" {
value = "https://${azurerm_static_web_app.site.default_host_name}"
}
output "deployment_token" {
value = azurerm_static_web_app.site.api_key
sensitive = true
}
output "www_cname_target" {
value = azurerm_static_web_app.site.default_host_name
}Get your Azure credentials#
Before filling in terraform.tfvars, you need the four values from Azure. One-time setup:
1. Log in to Azure
az loginA browser window opens – sign in with your Microsoft account.
2. Get the Subscription ID
az account show --query id -o tsvUse this value as subscription_id.
3. Create a Service Principal
A service principal is a technical account that OpenTofu uses to create resources in Azure – think of it like an API key, but for Azure.
az ad sp create-for-rbac \
--name "sp-my-site" \
--role Contributor \
--scopes /subscriptions/<YOUR_SUBSCRIPTION_ID>The output looks like this:
{
"appId": "...", <- this is client_id
"password": "...", <- this is client_secret
"tenant": "..." <- this is tenant_id
}The secret is only shown once – save it immediately.
infra/terraform.tfvars#
Now fill in the four values. This file contains real credentials – it must never go into the Git repository. Add it to .gitignore right away:
echo "infra/terraform.tfvars" >> .gitignoresubscription_id = "..."
client_id = "..."
client_secret = "..."
tenant_id = "..."Apply the infrastructure#
cd infra
tofu init
tofu apply
cd ..tofu init downloads the Azure provider. tofu apply shows you a preview of all resources to be created first – confirm with yes.
Phase 3 – CI/CD with GitHub Actions#
From here on, every deployment runs automatically: push to main → Hugo builds the site → Azure publishes it.
7. Set up the GitHub repository#
If you don’t have one yet, create an empty repository on github.com. Then connect it locally:
git remote add origin https://github.com/<your-username>/my-site.git8. Configure GitHub Actions#
Create .github/workflows/deploy.yml:
name: Deploy rdgy.at to Azure
on:
push:
branches: [main]
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
fetch-depth: 0
- name: Setup Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-version: '0.157.0'
extended: true
- name: Build
run: hugo --minify
- name: Deploy to Azure Static Web Apps
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
repo_token: ${{ secrets.GITHUB_TOKEN }}
action: "upload"
app_location: "public"
output_location: ""
skip_app_build: trueHugo builds the site into public/ – the deploy step uploads that folder directly to Azure.
9. Add the GitHub secret#
Read the deployment token from OpenTofu:
cd infra
tofu output -raw deployment_token
cd ..Add it as a secret in GitHub:
- Repository → Settings → Secrets and variables → Actions → New repository secret
Name:
AZURE_STATIC_WEB_APPS_API_TOKENPhase 4 – Domain & Go Live#
10. Configure the domain#
Step 1: Get the CNAME target
cd infra
tofu output www_cname_target
cd ..The output looks something like:
proud-rock-abc123.azurestaticapps.netStep 2: Set the DNS records
At your DNS provider (e.g. Cloudflare, Namecheap, Google Domains) create two records:
| Type | Name | Value |
|---|---|---|
| CNAME | www | proud-rock-abc123.azurestaticapps.net |
| Redirect / ALIAS | @ (Apex) | https://www.example.com |
The www record connects your domain directly to Azure. The apex record (example.com without www) is set as an HTTP redirect – most DNS providers call it “Redirect” or “URL Forward”.
Step 3: Wait for domain validation
Azure automatically checks whether the CNAME record is set correctly (cname-delegation from main.tf). This can take a few minutes to an hour depending on your DNS provider. HTTPS is then activated automatically – no manual certificate request needed.
Check the status:
az staticwebapp hostname list --name stapp-my-site --resource-group rg-my-siteOnce provisioningState shows Succeeded, the domain is live.
11. Push and go live#
git add .
git commit -m "feat: initial site"
git push origin mainGitHub Actions kicks off automatically, builds the site, and deploys it to Azure Static Web Apps.
Summary#
| Phase | What happens |
|---|---|
| 1 – Local | Set up Hugo + Blowfish, write content, test locally |
| 2 – IaC | Create Azure infrastructure with OpenTofu |
| 3 – CI/CD | Set up GitHub Actions for automated deployment |
| 4 – Live | Configure the domain, make the first push |
The result is a fast, static website with a blog, projects, and bilingual routing – no backend required.
