Official website for Machinemens (black/death metal band), replacing the band's Linktree.
| Tool | Purpose | Cost |
|---|---|---|
| Hugo (SSG) | Builds the static HTML from layouts/ + content/ + static/ |
Free |
| Tailwind CDN | Styling (Play CDN, no build step for CSS) | Free |
| Alpine.js CDN | EN/NL/PT trilingual toggle + data-driven pages | Free |
| GitHub Pages | Production hosting + HTTPS | Free |
| Cloudflare Pages | Staging hosting (staging.<domain>) |
Free |
| Domain registrar: Namecheap | Custom domain (machinemens.com, registered) |
~€10-15/year |
Monthly cost: €0 (just the domain renewal once a year)
Hugo builds the site as multilingual, one page per language: every UI string lives in
i18n/{en,nl,pt}.toml and the same content is rendered under a language-prefixed URL —
/en/… (English), /nl/… (Dutch), /pt/… (Portuguese). The bare root / is a tiny JS
redirect that sends each visitor to their saved/detected language. Output is minified in CI.
This repo intentionally mirrors the setup of
csguth/gatoweb.nl (branching model,
staging/production split, i18n pattern) so both projects are easy to maintain with the same
mental model.
Install Hugo extended, then from the repo root:
hugo # build the static site into site/ (git-ignored)
hugo server # live-preview at http://localhost:1313
The CI workflows install a pinned Hugo version and run hugo --gc --minify before substituting
the __SITE_URL__ / __ENV_LABEL__ placeholders with sed. hugo --minify only minifies the
rendered HTML/CSS; the static JS in site/js/ (copied verbatim from static/js/) is minified
separately in CI via npx terser (no dependency added to the repo — the tool is only ever
invoked inside the workflow, never locally, so the site stays zero-build).
hugo.toml Hugo config (multilingual en/nl/pt, publishDir = site, disables generated sitemap/robots/RSS)
i18n/ Compile-time UI translations
en.toml, nl.toml, pt.toml All user-facing strings, referenced via {{ i18n "key" }}
layouts/
_default/baseof.html Shared page skeleton (head + body + header/footer partials + blocks)
alias.html Root "/" redirect template (loads js/root-redirect.js)
partials/head.html <head> (meta/OG/Twitter/JSON-LD-on-home + hreflang) — per-page title/description
partials/header.html Staging banner + nav + EN/NL/PT language selector (links to each language URL)
partials/footer.html Footer
index.html Home page "main" block (hub: social links, teasers of latest release/next show)
music/list.html /music/ "main" block (full discography + live sessions)
shows/list.html /shows/ "main" block (upcoming + past shows)
shop/list.html /shop/ "main" block (product grid + cart + PayPal Smart Buttons checkout)
contact/list.html /contact/ "main" block (bookings + general contact link-cards)
content/ Per-language front matter (title/description), translated by filename:
_index.{en,nl,pt}.md Home
music/_index.{en,nl,pt}.md /music/
shows/_index.{en,nl,pt}.md /shows/
shop/_index.{en,nl,pt}.md /shop/
contact/_index.{en,nl,pt}.md /contact/
static/ Copied verbatim into the build output (served as-is):
favicon.png Root favicon used by all pages + the bare "/" redirect page
apple-touch-icon.png Root Apple touch icon, kept in sync with the favicon asset
css/site.css Styles: brand colors, staging banner, shared nav/listen-get button styles
js/lang-persist.js Saves the current page's language into localStorage on load
js/root-redirect.js Root "/" redirect to /en|nl|pt/ based on saved/detected language
js/tailwind-config.js Tailwind Play CDN theme extension (brand colors)
js/music-page.js Alpine component for /music/ (fetches data/releases.json)
js/shows-page.js Alpine component for /shows/ (fetches data/shows.json)
js/shows-teaser.js Alpine component for the home "next show" teaser
js/cart-store.js Shared localStorage cart (add/setQty/remove/clear), used by cart-badge.js
and shop-page.js so the header badge and /shop/ page stay in sync
js/cart-badge.js Alpine component for the header cart-count badge (loaded on every page)
js/shop-page.js Alpine component for /shop/ (fetches data/products.json, renders the
cart, and mounts PayPal JS SDK Smart Buttons client-side -- no backend)
data/releases.json Discography data consumed by /music/ (Alpine fetch + x-for) — add a new
release here, including its per-store links, when it drops
data/shows.json Shows/agenda data consumed by /shows/ and the home teaser
data/products.json Product catalog consumed by /shop/ (id/price/image) -- product display
names are translated i18n keys (shop_product_<id>), resolved server-side
in layouts/shop/list.html and passed into the Alpine component
images/logo.png Band logo (wordmark)
images/favicon.png Source favicon image copied to the root favicon paths above
robots.txt, sitemap.xml, CNAME
site/ Hugo build output (git-ignored; what actually gets deployed)
.github/workflows/
deploy-pages.yml Production deploy -> GitHub Pages (push to main)
deploy-staging-cloudflare.yml Staging deploy -> Cloudflare Pages (push to staging)
deploy-preview-cloudflare.yml PR preview -> Cloudflare Pages (pull_request into staging),
posts a sticky comment with the preview URL on the PR
guard-main-merges.yml Enforces the staging -> main promotion order (see below)
.github/skills/github-project-management/SKILL.md
Copilot skill with the exact gh CLI commands/IDs to manage the
project board (issues, Status, Priority) without rediscovering IDs.
| Production | Staging | |
|---|---|---|
| Branch | main |
staging |
| URL | SITE_URL var (https://machinemens.com) |
SITE_URL var (https://staging.machinemens.com) |
| Hosting | GitHub Pages | Cloudflare Pages (project machinemens-com-staging) |
| Workflow | deploy-pages.yml |
deploy-staging-cloudflare.yml |
| GitHub Environment | github-pages |
staging |
| Visual indicator | none (data-env="production", banner hidden) |
amber "STAGING" banner (data-env="staging") |
Both environments are built from the exact same source files — the only difference is which GitHub Environment's variables get substituted at deploy time.
New work follows a feature → staging → manual test → production flow, enforced by branch protection (not just convention):
git checkout staging && git pull
git checkout -b feature/my-change
# ... work, commit ...
# Open a PR: feature/my-change -> staging
# -> deploy-preview-cloudflare.yml deploys an isolated PR preview and posts
# the URL as a sticky comment on the PR automatically
# Merge -> auto-deploys to staging (Cloudflare Pages)
# Test manually on staging
# Open a PR: staging -> main (use a regular merge, NEVER squash/rebase,
# so the exact tested commit reaches main)
# Merge -> auto-deploys to production (GitHub Pages)
Hotfixes can branch directly from main and be merged back into main via a PR labeled
hotfix (bypasses the "must come from staging" guard) — then the same fix should be
cherry-picked/merged into staging too so both branches stay in sync.
Rules enforced on GitHub:
- Both
mainandstagingrequire a Pull Request to merge (no direct pushes) and block force-pushes/deletions. .github/workflows/guard-main-merges.ymlfails any PR targetingmainwhose source branch isn'tstaging, unless the PR is labeledhotfix(emergency bypass for urgent production fixes).
Before first deploy, add these repository or environment variables
(Settings → Secrets and variables → Actions → Variables). Use Secrets only for actual credentials.
SITE_URL— e.g.https://machinemens.com(production) /https://staging.machinemens.com(staging)ENV_LABEL—productionorstaging, drives the visible staging banner (data-envattribute)- PAYPAL_CLIENT_ID — PayPal REST app Client ID (the public/publishable one, safe to embed client-side; never the Secret) used by the PayPal JS SDK Smart Buttons on /shop/. Use a Sandbox app's Client ID for staging/previews and a Live app's for production.
CLOUDFLARE_ACCOUNT_ID(repo variable) andCLOUDFLARE_API_TOKEN(repo secret) — used by the staging deploy and PR preview workflows, same Cloudflare account as gatoweb.nl, dedicated Pages projectmachinemens-com-staging.
Notes:
- Language & URL: the site is served per-language under
/en/,/nl/and/pt/. The bare root/runsjs/root-redirect.js, which picks the language from the visitor's saved choice (localStorage.machinemens_lang), else browser language (pt/nl), else English. The header language selector is plain links to each language's URL, so the URL always changes with the language;js/lang-persist.jsre-saves the current page's language on every load. - Placeholders
__SITE_URL__/__ENV_LABEL__/__PAYPAL_CLIENT_ID__(in the Hugo layouts,static/robots.txt,static/sitemap.xml) are kept verbatim in the generated HTML and substituted withsedat deploy time — see each workflow's "Build site with Hugo and inject variables" step, which runshugo --gc --minifyfirst and then thesedsubstitution.
Changes go live automatically:
- push/merge to
staging→ deploys to the staging Cloudflare Pages URL in ~1-2 minutes - push/merge to
main→ deploys to production GitHub Pages in ~1-2 minutes
Settings → Pages: Source = GitHub Actions; set Custom domain tomachinemens.com; enable "Enforce HTTPS" after DNS propagates.CNAMEfile at the repo root holds the production domain (machinemens.com).- Namecheap DNS: 4x
Arecords to GitHub Pages IPs (185.199.108-111.153) for@, plus awwwCNAME tocsguth.github.io.— same pattern as gatoweb.nl.
- Create a Cloudflare Pages project named
machinemens-com-staging(production branch:staging) in the same Cloudflare account used for gatoweb.nl. - Attach
staging.machinemens.comas a custom domain (CNAMEstaging→machinemens-com-staging.pages.devat Namecheap). - Create a
CLOUDFLARE_API_TOKEN(Pages edit permission) and set it as a repo secret; setCLOUDFLARE_ACCOUNT_IDas a repo variable.
Roadmap is tracked on the Machinemens — Website Roadmap
project board. See .github/skills/github-project-management/SKILL.md for the exact gh CLI
commands/IDs to triage issues onto it (Status: Todo/In Progress/Done, Priority: High/Medium/Low).
Workflow: every feature/issue is added to the board first with a Priority, then planned (favoring
simplicity), then implemented via a PR targeting staging.
v1 = Linktree replacement (this repo's initial content: logo + official links, trilingual EN/NL/PT).
Planned next (tracked as issues on the board, not yet built):
- Video player / embedded media section
- Shows / tour dates (agenda)
- Contact form
- Brand: Machinemens (black/death metal band)
- Languages: English / Nederlands (auto-detected + manual toggle, persisted)
- Links: Spotify (artist + individual releases), YouTube, Instagram — sourced from the band's previous Linktree (https://linktr.ee/machinemens)