The source for vanityURLs.link — bilingual (EN/FR) documentation site built with Hugo and deployed on Cloudflare Pages.
- Hugo 0.158.0+ (extended edition). Production is pinned to 0.160.1 in
build.sh;hugo.ymldeclaresmin_version: 0.158.0. - Node.js 20+ (production uses 24.14.1). Needed for Tailwind/PostCSS and pagefind.
- Go 1.23+ (production uses 1.26.1). Needed for Hugo modules.
brew install hugo node # macOS
npm install # install devDependenciesnpm run dev # hugo server with drafts
npm run dev:search # same + build pagefind index first (needed for ⌘K)
npm run build # production: hugo --gc --minify && pagefind
npm run clean # remove public/ and resources/_gen/The pagefind index (public/pagefind/) is generated by the build. Don't commit static/pagefind/ — it's in .gitignore.
npm run lint # markdown + yaml + spell
npm run lint:md:fix # auto-fix markdown
npm run lint:links # lychee link checker
npm run lint:secrets # gitleaks secret scanner- Multi-level sidebar driven by
data/{en,fr}/docs_nav.yaml— paths are language-neutral - Table of contents, breadcrumbs, Edit-on-GitHub, prev/next, mobile
<select>dropdown
- Featured post via
featured: truefront matter (one per language) - Reading progress bar, social share (X, LinkedIn, copy-link), related posts, tags, RSS
- Client-side tag filter with count badges
- Bilingual content:
page.en.md/page.fr.mdside-by-side - UI strings in
i18n/en.yamlandi18n/fr.yaml(45+ keys with pluralization) - Localized dates via
date_format_longi18n key - Language-neutral data file paths (layouts prepend
/en/or/fr/viarelLangURL) - Language switcher preserves current page when translation exists
- Dark mode with no-flash-on-load
- Copy-to-clipboard on every
<pre> - ⌘K search via Pagefind
- Skip-to-content link, arrow-key sidebar nav, anchor hover
- hreflang, Open Graph, JSON-LD (SoftwareApplication, TechArticle, BreadcrumbList)
- Favicon + apple-touch-icon from
/logo.svg - Language-scoped PWA manifest
- Fingerprinted + minified CSS with SRI
{{< callout type="warning" title="Breaking change" >}}
This option was removed in v2.
{{< /callout >}}
{{< code file="config/deploy.yml" lang="yaml" >}}
service: my-app
{{< /code >}}
{{< details title="Why not Kubernetes?" >}}
Kubernetes is overkill for most teams.
{{< /details >}}
{{< cards cols="3" >}}
{{< card title="Installation" icon="download" href="/docs/installation/" >}}
Get up and running in minutes.
{{< /card >}}
{{< /cards >}}
{{< filetree/container >}}
{{< filetree/folder name="config" >}}
{{< filetree/file name="deploy.yml" annotation="// edit this" >}}
{{< /filetree/folder >}}
{{< /filetree/container >}}Available: callout, code, card + cards, details, filetree/*.
hugo new content blog/my-post.en.md
hugo new content blog/my-post.fr.mdFront matter:
title: "My Post Title"
date: 2026-01-15
author: "Benoît H. Dicaire"
description: "One-sentence description for cards and meta."
tags: ["guide"]
featured: falseSet exactly one post as featured: true per language.
title: "My Company"
description: "What they do and why they use vanityURLs."
site_url: "https://example.com"
color: "#0369a1"
logo_char: "M"
tags: ["personal"]
featured: false- Create
content/docs/my-section/my-page.{en,fr}.mdwithtitle,description,nav_order. - Register in both
data/en/docs_nav.yamlanddata/fr/docs_nav.yaml. Paths are language-neutral:
- title: My New Page
url: /docs/my-section/my-page/
children:
- title: Sub-topic
url: /docs/my-section/my-page/sub-topic/Cloudflare Workers Static Assets via wrangler.toml. Build command is ./build.sh, which installs pinned Dart Sass / Go / Hugo / Node.js and then runs hugo build --gc --minify followed by npx pagefind --site public. Assets directory is ./public; custom domain is vanityurls.link.
A small edge Worker at src/worker.mjs wraps HTML page requests and emits a server-side Umami pageview event via ctx.waitUntil() — no client-side JS, no cookies, invisible to ad blockers. Asset requests (CSS, JS, fonts, Pagefind chunks, images, XML feeds) bypass the Worker via negative patterns in assets.run_worker_first, so they remain free static-asset reads.
The Worker needs two secrets set once per environment:
wrangler secret put UMAMI_WEBSITE_ID
# → paste the UUID from the Umami dashboard (Settings → Websites)
wrangler secret put UMAMI_ENDPOINT
# → https://cloud.umami.is/api/send (for Umami Cloud)If either secret is missing, the Worker still serves pages correctly — it just skips the tracking call. This means local wrangler dev runs don't pollute production analytics.
See the Hugo on Cloudflare guide for context.
npm testRuns src/worker.test.mjs, which exercises the routing, payload shape, and edge cases (non-HTML response, non-GET methods, missing secrets) without a Cloudflare dependency.
.
├── hugo.yml # Site config
├── build.sh # Cloudflare build script
├── tailwind.config.js
├── postcss.config.js
├── wrangler.toml # Worker + static assets config
│
├── src/
│ ├── worker.mjs # Edge Worker: server-side Umami tracking
│ └── worker.test.mjs # Node-runnable smoke tests (`npm test`)
│
├── assets/css/main.css
│
├── i18n/
│ ├── en.yml # English UI strings
│ └── fr.yml # French UI strings
│
├── data/
│ ├── docs_nav.{en,fr}.yml # Sidebar (language-neutral paths, translated titles)
│ ├── home.{en,fr}.yml
│ ├── trust.{en,fr}.yml
│ └── docs_index.{en,fr}.yml
│
├── layouts/
│ ├── index.html
│ ├── 404.html
│ ├── _default/ # baseof, single, taxonomy, trust
│ ├── blog/ # list + single
│ ├── docs/ # list + single
│ ├── showcase/ # list + single
│ ├── tags/ # list + taxonomy
│ ├── shortcodes/ # callout, code, card, cards, details, filetree/*
│ └── partials/ # head, header, footer, search*, jsonld, lang-switcher
│
├── content/ # .en.md and .fr.md pairs throughout
│ ├── blog/
│ ├── docs/
│ ├── showcase/
│ └── trust/accessibility/impressum/license/privacy/security/terms/vulnerability/contributing
│
├── static/
│ ├── _headers # CSP + cache rules
│ ├── _redirects # Short-path 301s
│ ├── logo.svg
│ ├── social.png
│ ├── site.webmanifest
│ └── .well-known/security.txt
│
└── .github/workflows/
└── release-please.yml # Conventional commits → changelog + version
For major changes, please open an issue first. See contribution guidelines and code of conduct.
Copyright © 2026 Benoît H. Dicaire. Licensed under the MIT license. See LICENSE.md.
