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.
f158010 parent: 5924473 modified
Makefile +12 -1 | @@ -13,10 +13,15 @@ endif | ||
| 13 | 13 | # in sync by hand. |
| 14 | 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 | 21 | # deploy-% targets aren't listed here: giving an instantiated pattern target |
| 17 | 22 | # (e.g. deploy-en) to .PHONY creates an explicit no-recipe rule for it, which |
| 18 | 23 | # 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 | |
| 20 | 25 | |
| 21 | 26 | help: |
| 22 | 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 | 35 | serve: ## Run the Hugo dev server with live reload |
| 31 | 36 | 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 | 44 | infra-plan: ## Show what infra-apply would change on Cloudflare |
| 34 | 45 | nix run .#infra -- plan |
| 35 | 46 | |
| @@ -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 target | 21 | # 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, which | 22 | # (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 clean | 24 | +.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 reload | 35 | serve: ## Run the Hugo dev server with live reload |
| 31 | nix develop --command hugo server --source blog | 36 | 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 Cloudflare | 44 | infra-plan: ## Show what infra-apply would change on Cloudflare |
| 34 | nix run .#infra -- plan | 45 | nix run .#infra -- plan |
| 35 | 46 | ||
modified
flake.nix +40 -0 | @@ -113,6 +113,46 @@ | ||
| 113 | 113 | ''; |
| 114 | 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 | } |
| @@ -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 | } |