mykiwi/mykiwi.blogpublic Fork
⑂ f158010
Commits
⬇ Clone ▾
git clone https://git.rickub.com/mykiwi/mykiwi.blog.git
git clone ssh://git@rickub.com/mykiwi/mykiwi.blog.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

Add make prod-server to serve the built site as Cloudflare doesUnverified

hugo server is convenient but it is not what readers get. Pages
answers /posts with a 308 to /posts/, redirects /index.html to /,
and - with no 404.html in the site - serves the root index.html with
a 200 for an unknown path instead of a 404. None of that is Hugo's
behaviour, so a page could work locally and not in production.

Serve ./result/<lang> through wrangler pages dev instead, which is
the same Pages runtime Cloudflare runs, on the same artifact deploy
uploads. Status codes, redirects, content types and bytes were
compared against https://mykiwi.blog/ and match on every path
checked.

Two deliberate gaps: the language switcher uses absolute
https://fr.mykiwi.blog/ URLs, so switching language leaves for
production exactly as it would from a single deployed host, and
Wrangler binds .env into the Worker it shims around the assets
(--env-file does not opt out). Nothing can read it without Pages
Functions, which the site does not ship.

The variable is SITE_LANG, not LANG: make imports the environment,
where LANG is the locale and would point this at result/en_US.UTF-8.
Romain Gautier committed 2026-09-20T12:57:19+02:00 Browse files
f158010 parent: 5924473
modified Makefile +12 -1
@@ -13,10 +13,15 @@ endif
1313 # in sync by hand.
1414 LANGS := $(shell cat languages.txt)
1515
16+# Deliberately not named LANG: make imports the environment, and LANG is the
17+# locale (en_US.UTF-8), which would silently point prod-server at result/en_US.
18+SITE_LANG ?= $(firstword $(LANGS))
19+PROD_PORT ?= 8788
20+
1621 # deploy-% targets aren't listed here: giving an instantiated pattern target
1722 # (e.g. deploy-en) to .PHONY creates an explicit no-recipe rule for it, which
1823 # then blocks the deploy-% pattern rule below from ever supplying a recipe.
19-.PHONY: help dev build serve infra-plan infra-apply deploy clean
24+.PHONY: help dev build serve prod-server infra-plan infra-apply deploy clean
2025
2126 help:
2227 @grep -E '^[a-zA-Z_-]+:.*?## .*$$' Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}'
@@ -30,6 +35,12 @@ build: ## Build the site (all languages) into ./result
3035 serve: ## Run the Hugo dev server with live reload
3136 nix develop --command hugo server --source blog
3237
38+# Wrangler's Pages runtime over ./result, so routing, redirects, 404 handling
39+# and content types are Cloudflare's rather than Hugo's approximation of them.
40+# Depends on build: it serves the deploy artifact, so it is only as fresh.
41+prod-server: build ## Serve the built site exactly as deployed, e.g. `make prod-server SITE_LANG=fr PROD_PORT=8080`
42+ nix run .#prod-server -- $(SITE_LANG) $(PROD_PORT)
43+
3344 infra-plan: ## Show what infra-apply would change on Cloudflare
3445 nix run .#infra -- plan
3546
@@ -13,10 +13,15 @@ endif
13 # in sync by hand.13 # in sync by hand.
14 LANGS := $(shell cat languages.txt)14 LANGS := $(shell cat languages.txt)
15 15
16+# Deliberately not named LANG: make imports the environment, and LANG is the
17+# locale (en_US.UTF-8), which would silently point prod-server at result/en_US.
18+SITE_LANG ?= $(firstword $(LANGS))
19+PROD_PORT ?= 8788
20+
16 # deploy-% targets aren't listed here: giving an instantiated pattern target21 # deploy-% targets aren't listed here: giving an instantiated pattern target
17 # (e.g. deploy-en) to .PHONY creates an explicit no-recipe rule for it, which22 # (e.g. deploy-en) to .PHONY creates an explicit no-recipe rule for it, which
18 # then blocks the deploy-% pattern rule below from ever supplying a recipe.23 # then blocks the deploy-% pattern rule below from ever supplying a recipe.
19-.PHONY: help dev build serve infra-plan infra-apply deploy clean24+.PHONY: help dev build serve prod-server infra-plan infra-apply deploy clean
20 25
21 help:26 help:
22 @grep -E '^[a-zA-Z_-]+:.*?## .*$$' Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}'27 @grep -E '^[a-zA-Z_-]+:.*?## .*$$' Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}'
@@ -30,6 +35,12 @@ build: ## Build the site (all languages) into ./result
30 serve: ## Run the Hugo dev server with live reload35 serve: ## Run the Hugo dev server with live reload
31 nix develop --command hugo server --source blog36 nix develop --command hugo server --source blog
32 37
38+# Wrangler's Pages runtime over ./result, so routing, redirects, 404 handling
39+# and content types are Cloudflare's rather than Hugo's approximation of them.
40+# Depends on build: it serves the deploy artifact, so it is only as fresh.
41+prod-server: build ## Serve the built site exactly as deployed, e.g. `make prod-server SITE_LANG=fr PROD_PORT=8080`
42+ nix run .#prod-server -- $(SITE_LANG) $(PROD_PORT)
43+
33 infra-plan: ## Show what infra-apply would change on Cloudflare44 infra-plan: ## Show what infra-apply would change on Cloudflare
34 nix run .#infra -- plan45 nix run .#infra -- plan
35 46
modified flake.nix +40 -0
@@ -113,6 +113,46 @@
113113 '';
114114 }}/bin/mykiwi-blog-deploy";
115115 };
116+
117+ # `nix run .#prod-server -- <lang> [port]` - serves the already-built
118+ # `./result/<lang>` (the very artifact `deploy` uploads) through
119+ # Wrangler's Pages runtime, which is what Cloudflare runs in
120+ # production. `hugo server` only approximates that: Pages answers
121+ # /posts with a 308 to /posts/, redirects /index.html to /, and - with
122+ # no 404.html in the site - serves the root index.html with a 200 for
123+ # an unknown path rather than a 404. Those three, the content types
124+ # and the bytes were checked against https://mykiwi.blog/ and match.
125+ #
126+ # Two things it still is not: the language switcher points at the real
127+ # https://fr.mykiwi.blog/ and friends (absolute, hx-boost=false), so
128+ # switching language leaves for production exactly as it would from a
129+ # single deployed host; and Wrangler binds whatever is in .env, i.e.
130+ # CLOUDFLARE_API_TOKEN, into the Worker it shims around the assets.
131+ # --env-file does not opt out of that. Harmless - the site ships no
132+ # Pages Functions, so nothing can read it - but it is not a clean room.
133+ prod-server = {
134+ type = "app";
135+ program = "${pkgs.writeShellApplication {
136+ name = "mykiwi-blog-prod-server";
137+ runtimeInputs = [ wrangler pkgs.git ];
138+ text = ''
139+ lang="''${1:?usage: nix run .#prod-server -- <lang> [port]}"
140+ port="''${2:-8788}"
141+ root="$(git rev-parse --show-toplevel)"
142+ dir="$root/result/$lang"
143+ if [ ! -d "$dir" ]; then
144+ echo "$dir does not exist - run 'make build' first." >&2
145+ exit 1
146+ fi
147+ # Same reason as deploy above: Pages insists on discovering
148+ # wrangler.toml itself rather than being pointed at one.
149+ cd "$root"
150+ # Pinned so the emulated runtime cannot drift between runs -
151+ # bump it deliberately, the way you would a dependency.
152+ wrangler pages dev "$dir" --port "$port" --compatibility-date=2026-09-03
153+ '';
154+ }}/bin/mykiwi-blog-prod-server";
155+ };
116156 };
117157 };
118158 }
@@ -113,6 +113,46 @@
113 '';113 '';
114 }}/bin/mykiwi-blog-deploy";114 }}/bin/mykiwi-blog-deploy";
115 };115 };
116+
117+ # `nix run .#prod-server -- <lang> [port]` - serves the already-built
118+ # `./result/<lang>` (the very artifact `deploy` uploads) through
119+ # Wrangler's Pages runtime, which is what Cloudflare runs in
120+ # production. `hugo server` only approximates that: Pages answers
121+ # /posts with a 308 to /posts/, redirects /index.html to /, and - with
122+ # no 404.html in the site - serves the root index.html with a 200 for
123+ # an unknown path rather than a 404. Those three, the content types
124+ # and the bytes were checked against https://mykiwi.blog/ and match.
125+ #
126+ # Two things it still is not: the language switcher points at the real
127+ # https://fr.mykiwi.blog/ and friends (absolute, hx-boost=false), so
128+ # switching language leaves for production exactly as it would from a
129+ # single deployed host; and Wrangler binds whatever is in .env, i.e.
130+ # CLOUDFLARE_API_TOKEN, into the Worker it shims around the assets.
131+ # --env-file does not opt out of that. Harmless - the site ships no
132+ # Pages Functions, so nothing can read it - but it is not a clean room.
133+ prod-server = {
134+ type = "app";
135+ program = "${pkgs.writeShellApplication {
136+ name = "mykiwi-blog-prod-server";
137+ runtimeInputs = [ wrangler pkgs.git ];
138+ text = ''
139+ lang="''${1:?usage: nix run .#prod-server -- <lang> [port]}"
140+ port="''${2:-8788}"
141+ root="$(git rev-parse --show-toplevel)"
142+ dir="$root/result/$lang"
143+ if [ ! -d "$dir" ]; then
144+ echo "$dir does not exist - run 'make build' first." >&2
145+ exit 1
146+ fi
147+ # Same reason as deploy above: Pages insists on discovering
148+ # wrangler.toml itself rather than being pointed at one.
149+ cd "$root"
150+ # Pinned so the emulated runtime cannot drift between runs -
151+ # bump it deliberately, the way you would a dependency.
152+ wrangler pages dev "$dir" --port "$port" --compatibility-date=2026-09-03
153+ '';
154+ }}/bin/mykiwi-blog-prod-server";
155+ };
116 };156 };
117 };157 };
118 }158 }