Skip to content

Commit caec920

Browse files
committed
ci: update config
1 parent cc8913f commit caec920

5 files changed

Lines changed: 169 additions & 93 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,6 @@ Thumbs.db
4343
##### Environment variables and secret keys #####
4444
.env
4545
.env.test
46+
.dev.vars
47+
.dev.vars.*
4648
*.pem

README-PATCH.md

Lines changed: 92 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,120 @@
1-
# vanityURLs — round 8: fix 404 + normalize YAML extensions
2-
3-
Two related cleanups:
4-
5-
## 1. Fix the broken 404 page
6-
7-
**Root cause:** `content/404.en.md` and `content/404.fr.md` contained Hugo template code (`{{ define "main" }}`, `{{ i18n "not_found_title" }}`) as plain Markdown text. Hugo's Markdown renderer doesn't interpret `{{ ... }}` — it passes them through as literal characters. That's why the screenshot showed `{{ i18n "not_found_title" }}` rendered verbatim.
8-
9-
On top of that, the URL in the screenshot was `/en/404/` (directory-style) — Hugo was treating the broken `content/404.en.md` as a regular content page and generating `/en/404/index.html`. Cloudflare's `not_found_handling = "404-page"` actually looks for `/en/404.html` (file-style), so even if the content had rendered correctly, Cloudflare wouldn't have found it where it needs to.
1+
# vanityURLs — round 13: privacy-first server-side analytics
2+
3+
Adds an edge Worker that emits a Umami pageview event for every HTML page request, without any client-side JavaScript. Asset requests bypass the Worker entirely and remain billed as free static-asset reads.
4+
5+
## Design
6+
7+
**HTML page request (`/en/docs/`):**
8+
1. Cloudflare's edge receives the request.
9+
2. wrangler.toml's `assets.run_worker_first = ["/*", "!/js/*", ...]` matches — Worker runs.
10+
3. `src/worker.js`:
11+
- Delegates to `env.ASSETS.fetch(request)` — Cloudflare serves the `.html` bytes with the `_headers` rules applied (CSP, cache-control, etc.).
12+
- Checks the response: Content-Type `text/html` and status 200 or 404?
13+
- If yes → `ctx.waitUntil(trackPageview(...))` fires a background POST to Umami. Response is already flushed; analytics never blocks.
14+
- Returns the original response unchanged.
15+
16+
**Asset request (`/css/main.abc123.css`):**
17+
1. Negative pattern `!/css/*` excludes the path — Worker does not run.
18+
2. Asset handler serves the CSS with `_headers` applied. Billed as a free static-asset read.
19+
20+
## What lands at Umami
21+
22+
```json
23+
{
24+
"type": "event",
25+
"payload": {
26+
"hostname": "vanityurls.link",
27+
"language": "fr-CA",
28+
"referrer": "https://google.com/",
29+
"url": "/en/docs/getting-started/",
30+
"website": "<UMAMI_WEBSITE_ID>",
31+
"userAgent": "Mozilla/5.0 (...)",
32+
"ip": "203.0.113.42"
33+
}
34+
}
35+
```
1036

11-
**Fix applied:**
37+
For 404s, payload also includes `"name": "404"` so you can filter these out of pageview charts in Umami.
1238

13-
- **Delete `content/404.en.md` and `content/404.fr.md`** (they were shadowing and breaking the real layout).
14-
- The existing `layouts/404.html` already handles both languages correctly via Hugo's built-in 404 convention. With the shadow files gone, Hugo now emits:
15-
- `public/en/404.html` — handles misses under `/en/*`
16-
- `public/fr/404.html` — handles misses under `/fr/*`
17-
- **Add a step in `build.sh`** to copy `public/en/404.html` to `public/404.html` so top-level URLs (e.g. `/typo` with no language prefix) also have a 404 to walk up to.
39+
We pass `userAgent` and `ip` because Umami uses them for browser/OS/device detection and country derivation. Without overrides, every visitor would be tagged as "some Cloudflare server in Chicago." Umami hashes IPs rather than persisting them.
1840

19-
Your `wrangler.toml` is correct as-is — `not_found_handling = "404-page"` is the right value for this site (static site generation, not SPA). See [Cloudflare's SSG docs](https://developers.cloudflare.com/workers/static-assets/routing/static-site-generation/) for reference.
41+
## Required secrets
2042

21-
## 2. Normalize all YAML extensions to `.yml`
43+
```bash
44+
wrangler secret put UMAMI_WEBSITE_ID
45+
# Paste the UUID from the Umami dashboard → Settings → Websites
2246

23-
Before this patch the repo mixed `.yaml` (hugo config, i18n files, nested docs_nav) and `.yml` (home, trust, docs_index). Normalized to `.yml` throughout.
47+
wrangler secret put UMAMI_ENDPOINT
48+
# Paste: https://cloud.umami.is/api/send
49+
```
2450

25-
Also flattened `data/en/docs_nav.yaml` and `data/fr/docs_nav.yaml` to `data/docs_nav.en.yml` and `data/docs_nav.fr.yml`, matching the pattern used by the other per-language data files (`home.en.yml`, `docs_index.en.yml`, `trust.en.yml`).
51+
For local `wrangler dev`, create `.dev.vars` (already gitignored):
2652

27-
### File operations
53+
```
54+
UMAMI_WEBSITE_ID=deadbeef-...
55+
UMAMI_ENDPOINT=https://cloud.umami.is/api/send
56+
```
2857

29-
**Rename (no content change):**
30-
- `hugo.yaml``hugo.yml`
31-
- `i18n/en.yaml``i18n/en.yml`
32-
- `i18n/fr.yaml``i18n/fr.yml`
58+
If either secret is missing, the Worker still serves pages correctly — it just skips the analytics call. Local dev won't pollute production stats.
3359

34-
**Move + rename:**
35-
- `data/en/docs_nav.yaml``data/docs_nav.en.yml`
36-
- `data/fr/docs_nav.yaml``data/docs_nav.fr.yml`
60+
## Files in this patch
3761

38-
**Delete:**
39-
- `data/en/` (empty after move)
40-
- `data/fr/` (empty after move)
41-
- `content/404.en.md` (broken 404 source)
42-
- `content/404.fr.md` (broken 404 source)
62+
| File | Change |
63+
|---|---|
64+
| `src/worker.js` | **New** — edge Worker (138 lines) |
65+
| `src/worker.test.js` | **New** — 10 Node-runnable smoke tests |
66+
| `wrangler.toml` | Added `main`, `assets.binding`, selective `run_worker_first` patterns |
67+
| `package.json` | Added `type: module` + `test` script |
68+
| `.gitignore` | Added `.dev.vars` / `.dev.vars.*` |
69+
| `README.md` | Documented the Worker layer, secrets setup, project tree |
70+
| `content/privacy.en.md` | Rewrote — server-side analytics disclosed |
71+
| `content/privacy.fr.md` | Matching FR rewrite |
72+
| `content/security.en.md` | "No analytics" → "No client-side analytics" with Umami disclosure |
73+
| `content/security.fr.md` | Matching FR update |
4374

44-
### Code changes
75+
## Apply
4576

46-
**`layouts/docs/list.html` and `layouts/docs/single.html`:**
47-
```go
48-
// before
49-
{{ $nav := index .Site.Data $lang "docs_nav" }}
50-
// after
51-
{{ $nav := index .Site.Data (printf "docs_nav.%s" $lang) }}
77+
```bash
78+
cd /Volumes/Tarmac/code/vanityURLs/website
79+
unzip -o ~/Downloads/vanityurls-round13.zip
80+
git add -A
81+
git commit -m "feat: privacy-first server-side analytics via Umami at the edge"
5282
```
53-
The lookup pattern now matches the other data files (`home.{en,fr}.yml`, `docs_index.{en,fr}.yml`).
5483

55-
**`layouts/partials/footer.html`:** comment updated from `hugo.yaml` to `hugo.yml`.
84+
## Before deploy
5685

57-
**`build.sh`:** new step copies `public/en/404.html``public/404.html` after Hugo build.
86+
1. Create the Umami site at cloud.umami.is → Settings → Websites. Copy the UUID.
87+
2. Set both secrets via `wrangler secret put`.
88+
3. Push.
5889

59-
**`package.json`:** `lint:yaml` and `lint:spell` scripts updated to reference `hugo.yml`, `*.yml`, `data/*.yml`.
90+
If secrets aren't set, the Worker serves pages correctly but skips tracking. Safe default.
6091

61-
**`README.md`:** file paths and project-layout tree updated to reflect the new names.
62-
63-
## Applying
64-
65-
Because this patch combines file renames, directory deletions, and content changes, the zip alone won't do the job — it can't tell `git` to delete files. Use this sequence:
92+
## Validate after deploy
6693

6794
```bash
68-
cd /Volumes/Tarmac/code/vanityURLs/website
95+
curl -sI https://vanityurls.link/en/ | head
96+
curl -sI https://vanityurls.link/logo.svg | head
97+
curl -sI https://vanityurls.link/pagefind/pagefind.js | head
98+
```
6999

70-
# 1. Remove broken 404 content and the now-empty nested dirs
71-
git rm content/404.en.md content/404.fr.md
72-
git rm data/en/docs_nav.yaml data/fr/docs_nav.yaml
73-
rmdir data/en data/fr
100+
Load a few pages in browser, check Umami dashboard real-time view. Country should match your real location (from IP override), browser detected from UA override.
74101

75-
# 2. Rename config and i18n files
76-
git mv hugo.yaml hugo.yml
77-
git mv i18n/en.yaml i18n/en.yml
78-
git mv i18n/fr.yaml i18n/fr.yml
102+
If nothing shows up, check `wrangler tail` and confirm `wrangler secret list` has both secrets.
79103

80-
# 3. Overlay the patch (adds the renamed docs_nav files and the code changes)
81-
unzip -o ~/Downloads/vanityurls-round8.zip
82-
83-
# 4. Stage and commit
84-
git add -A
85-
git commit -m "fix(404): remove broken content templates; rename all yaml to yml"
86-
```
87-
88-
## Validation after deploy
104+
## Local development
89105

90106
```bash
91-
# Each language's 404 serves correctly
92-
curl -sI https://vanityurls.link/en/nonexistent | head -1
93-
# → HTTP/2 404
94-
95-
# Top-level 404 works too
96-
curl -sI https://vanityurls.link/does-not-exist | head -1
97-
# → HTTP/2 404
98-
99-
# Content — should see "Page not found" / "Page introuvable" rendered properly,
100-
# not "{{ i18n ... }}" literal text
101-
curl -s https://vanityurls.link/en/nonexistent | grep -o "Page not found"
102-
curl -s https://vanityurls.link/fr/nonexistent | grep -o "Page introuvable"
107+
npm run build
108+
wrangler dev
103109
```
104110

105-
In the browser: visit any URL that doesn't exist under `/en/` or `/fr/` and verify the 404 page renders with proper styling (brand-colored button, docs fallback link, correct language chrome).
111+
Open `http://127.0.0.1:8787/en/`. With `.dev.vars` set, events land in Umami; without, silently skipped.
106112

107-
## What I verified in my sandbox
113+
## Rollback
108114

109-
- Hugo builds successfully with the renamed files (Hugo accepts both `.yml` and `.yaml`; renaming to `.yml` is officially supported).
110-
- Docs sidebar still renders (17 sidebar links in `/en/docs/getting-started/`) — the `docs_nav` data is being loaded from the new flat filename.
111-
- `/en/404.html` and `/fr/404.html` are generated with correct i18n strings ("Page not found" / "Page introuvable"), full header/footer chrome, and no raw template tags.
112-
- No stale references to `hugo.yaml`, `docs_nav.yaml`, `i18n/en.yaml`, `i18n/fr.yaml`, `data/en/`, or `data/fr/` anywhere in the repo.
115+
```bash
116+
# Quickest: unset secrets — Worker still serves pages, just no tracking
117+
wrangler secret delete UMAMI_WEBSITE_ID
118+
119+
# Fuller: revert wrangler.toml main + assets.binding/run_worker_first
120+
```

README.md

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ npm run lint:secrets # gitleaks secret scanner
4141
## Key features
4242

4343
### Documentation
44-
- Multi-level sidebar driven by `data/docs_nav.{en,fr}.yml` — paths are language-neutral
44+
- Multi-level sidebar driven by `data/{en,fr}/docs_nav.yaml` — paths are language-neutral
4545
- Table of contents, breadcrumbs, Edit-on-GitHub, prev/next, mobile `<select>` dropdown
4646

4747
### Blog
@@ -53,7 +53,7 @@ npm run lint:secrets # gitleaks secret scanner
5353

5454
### i18n
5555
- Bilingual content: `page.en.md` / `page.fr.md` side-by-side
56-
- UI strings in `i18n/en.yml` and `i18n/fr.yml` (45+ keys with pluralization)
56+
- UI strings in `i18n/en.yaml` and `i18n/fr.yaml` (45+ keys with pluralization)
5757
- Localized dates via `date_format_long` i18n key
5858
- Language-neutral data file paths (layouts prepend `/en/` or `/fr/` via `relLangURL`)
5959
- Language switcher preserves current page when translation exists
@@ -137,7 +137,7 @@ featured: false
137137
### New docs page
138138

139139
1. Create `content/docs/my-section/my-page.{en,fr}.md` with `title`, `description`, `nav_order`.
140-
2. Register in both `data/docs_nav.en.yml` and `data/docs_nav.fr.yml`. Paths are language-neutral:
140+
2. Register in both `data/en/docs_nav.yaml` and `data/fr/docs_nav.yaml`. Paths are language-neutral:
141141

142142
```yaml
143143
- title: My New Page
@@ -149,19 +149,45 @@ featured: false
149149

150150
## Deployment
151151

152-
Cloudflare Pages 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`.
152+
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`.
153+
154+
A small edge Worker at `src/worker.js` 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.
155+
156+
The Worker needs two secrets set once per environment:
157+
158+
```bash
159+
wrangler secret put UMAMI_WEBSITE_ID
160+
# → paste the UUID from the Umami dashboard (Settings → Websites)
161+
162+
wrangler secret put UMAMI_ENDPOINT
163+
# → https://cloud.umami.is/api/send (for Umami Cloud)
164+
```
165+
166+
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.
153167

154168
See the [Hugo on Cloudflare guide](https://gohugo.io/host-and-deploy/host-on-cloudflare/) for context.
155169

170+
### Worker smoke tests
171+
172+
```bash
173+
npm test
174+
```
175+
176+
Runs `src/worker.test.js`, which exercises the routing, payload shape, and edge cases (non-HTML response, non-GET methods, missing secrets) without a Cloudflare dependency.
177+
156178
## Project layout
157179

158180
```text
159181
.
160182
├── hugo.yml # Site config
161-
├── build.sh # Cloudflare Pages build script
183+
├── build.sh # Cloudflare build script
162184
├── tailwind.config.js
163185
├── postcss.config.js
164-
├── wrangler.toml
186+
├── wrangler.toml # Worker + static assets config
187+
188+
├── src/
189+
│ ├── worker.js # Edge Worker: server-side Umami tracking
190+
│ └── worker.test.js # Node-runnable smoke tests (`npm test`)
165191
166192
├── assets/css/main.css
167193

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
{
22
"name": "vanityurls-website",
3-
"version": "2.4.2",
3+
"version": "2.3.1",
44
"description": "VanityURls.link Website and documentation",
5+
"type": "module",
56
"scripts": {
67
"dev": "hugo server --buildDrafts",
78
"dev:search": "hugo --minify && pagefind --site public && hugo server --buildDrafts",
89
"build": "hugo --gc --minify && npm run index",
910
"index": "pagefind --site public",
1011
"clean": "rm -rf public resources/_gen",
12+
"test": "node src/worker.test.js",
1113
"lint": "npm run lint:md && npm run lint:yaml && npm run lint:spell",
1214
"lint:md": "markdownlint-cli2 'content/**/*.md'",
1315
"lint:md:fix": "markdownlint-cli2 --fix 'content/**/*.md'",
14-
"lint:spell": "cspell --no-progress 'content/**/*.md' '*.yml' '*.toml'",
16+
"lint:spell": "cspell --no-progress 'content/**/*.md' '*.yaml' '*.toml'",
1517
"lint:spell:fix": "cspell --no-progress --words-only 'content/**/*.md' | sort -u >> cspell-words.txt",
16-
"lint:yaml": "yamllint hugo.yml i18n/*.yml data/*.yml 2>/dev/null || npx js-yaml hugo.yml > /dev/null",
18+
"lint:yaml": "yamllint hugo.yml i18n/*.yaml data/**/*.yaml 2>/dev/null || npx js-yaml hugo.yml > /dev/null",
1719
"lint:links": "lychee content/**/*.md",
1820
"lint:secrets": "gitleaks detect --source . --no-banner"
1921
},

wrangler.toml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,51 @@
11
name = 'website'
2+
main = 'src/worker.js'
23
compatibility_date = '2025-07-31'
34

45
[build]
56
command = 'chmod a+x build.sh && ./build.sh'
67

8+
# Static assets with selective Worker invocation.
9+
#
10+
# Worker invocations on run_worker_first=true patterns are BILLED against
11+
# your Workers quota; asset-only requests are free. So we want the Worker
12+
# to fire only for HTML page views (analytics tracking) and let asset
13+
# fetches (CSS, fonts, JS bundles, Pagefind chunks, images, XML feeds, etc.)
14+
# bypass the Worker entirely.
15+
#
16+
# Strategy: Worker-first globally, then NEGATIVE patterns exclude asset
17+
# prefixes. Cloudflare's pattern matcher gives negatives precedence.
718
[assets]
819
directory = './public'
20+
binding = 'ASSETS'
921
not_found_handling = '404-page'
22+
run_worker_first = [
23+
"/*",
24+
# Hugo build outputs
25+
"!/js/*",
26+
"!/css/*",
27+
"!/fonts/*",
28+
"!/pagefind/*",
29+
"!/.well-known/*",
30+
# Single-file static resources at the root
31+
"!/logo.svg",
32+
"!/favicon.ico",
33+
"!/humans.txt",
34+
"!/robots.txt",
35+
"!/sitemap.xml",
36+
"!/site.webmanifest",
37+
"!/social.png",
38+
"!/_redirects",
39+
# Per-language RSS/sitemap feeds
40+
"!/*/sitemap.xml",
41+
"!/*/index.xml",
42+
"!/**/index.xml",
43+
]
1044

1145
[[routes]]
1246
pattern = "vanityurls.link"
1347
custom_domain = true
48+
49+
# Secrets (set via `wrangler secret put <name>`, never committed):
50+
# UMAMI_WEBSITE_ID — UUID from Umami dashboard → Settings → Websites
51+
# UMAMI_ENDPOINT — Full POST URL, e.g. https://cloud.umami.is/api/send

0 commit comments

Comments
 (0)