Every surprise, in one list
If you only read one page, read this one. Each line was a real problem during the rebuild.
Next.js 16
Section titled “Next.js 16”notFound()in a root layout underapp/[segment]shows Next’s default 404; call it from the page and addnot-found.tsx(The app).- Metadata images with a dynamic param need their own
generateStaticParams, or they render on demand (The app). - Reading the current time during a prerender must be cached (
'use cache') or deferred (The app). - Typed routes reject paths that exist only as rewrites; use
as Route(The app). - An empty
RESEND_API_KEY=line failed validation until “empty” was treated as “unset” (Contact form). getByRole("alert")also matches Next’s route announcer (Tests).
The contact form
Section titled “The contact form”- Turnstile tokens are valid on the hostname and every subdomain; an exact
hostname check would have rejected every
www.visitor (Contact form). - An auto-reply that echoes visitor input is a spam relay from your domain (Contact form).
Firewall
Section titled “Firewall”- Docker-published ports bypass
INPUT, ufw and firewalld; filter inDOCKER-USER(Firewall). - A final
-j DROPinDOCKER-USERalso blocks containers’ outbound connections — no certificates, no API calls (Firewall). - One Cloudflare range was missing from the allowlist (Firewall).
- Trailing
# commentson rules makeiptables-restorefail; a rules file had never loaded (Firewall).
Traefik and the infra starter
Section titled “Traefik and the infra starter”- One misplaced field (
passiveHealthCheck) made Traefik drop the whole dynamic config (Traefik). - Rate limits keyed on the connecting IP are shared by everyone behind Cloudflare (Traefik).
- The shared security-headers middleware sent
noindex(Traefik). LOG_LEVEL=warncrash-loops socket-proxy v0.5;loadbalancer.server.strategyisn’t a field;traefik healthcheckneeds its own--ping(Traefik).make upstarts Portainer, whose first visitor becomes admin; it also doesn’t include your own service files (Traefik, First deploy).- A pinned image (
henrygd/beszel-hub) didn’t exist (Traefik). .gitignorelisted secrets one by one, so new ones weren’t ignored (Image and secrets).- The backup stack’s
$$escaping andRESTIC_REPOname meant it could never authenticate (Backups).
Certificates
Section titled “Certificates”- Let’s Encrypt staging certificates fail Cloudflare Full (strict); test staging on the server itself (Certificates).
Images and CI
Section titled “Images and CI”- An image built on Apple Silicon won’t run on an x86 server (Image and secrets).
- The Gitea runner is now
gitea/runner5.x;act_runner0.6 is the end of the old line (Runner). - Gitea’s registry rejects the job’s
GITEA_TOKENfordocker login, even withpackages: write; tokens need package scope, not repository (CI). - CI jobs can’t SSH into a server whose
sshdis loopback-only — so the server pulls (Deploys). - This guide’s own first deploy shipped without search. Pagefind 1.6.0’s Linux
binary needs AVX2; the server is an older Xeon (Ivy Bridge) without it, so the
binary died with “Illegal instruction”, and
astro buildstill exited 0. It built fine on a Mac, because emulation there supplies AVX2. The fixes: pinpagefindto 1.5.2 with npmoverrides, and makenpm run buildfail ifdist/pagefind/pagefind.jsis missing. Check your CPU withgrep -o -w avx2 /proc/cpuinfo | head -1before trusting a prebuilt binary.
Process
Section titled “Process”- The server “with Traefik and Gitea on it” had neither running — only their data (Audit).
- A long
tarlooked hung; it had finished (Audit). - Search results from a docs mirror can favour obsolete versions (Working docs-first).