Chapter 20Operate
Troubleshooting and known traps
By symptom: what you observe, what it is, what to do. Plus the ten traps people fall into even while knowing them.
Where to look, in order
-
The browser console
The overlay writes there the technical detail of what the client sees in plain language. First place to look when “it does not work” comes from a client.
-
The function logs, on the host
[save],[upload],[content],[auth]: the cause is named there, never the secret. -
The build log
If content was published but the site did not change, this is where it happens.
-
The repository
Did the commit land? On which branch? With which author?
At setup
| Symptom | Cause | Fix |
|---|---|---|
/admin returns 404 |
Site served by npm run dev without a build, or the integration is missing from astro.config.mjs. |
npm run build then npm run serve:functions. |
/api/* returns 404 |
The Astro server alone does not serve /functions. |
npm run serve:functions. |
| “Charte introuvable” (theme not found) | src/styles/theme.css is missing, or the theme option points elsewhere. |
Create the file, or fix the option. |
| The integration refuses to start | output is not 'static'. |
Rule 1: static output is not negotiable. |
| “Invalid language code” | A language declared outside the two-lowercase-letter format. | Fix locales. |
At authentication
| Symptom | Cause | Fix |
|---|---|---|
| “Incorrect key” with the right key | EDITOR_KEY_HASH missing or from another generation. Variables are only re-read on (re)start. |
Check .dev.vars or the secrets, then restart. |
| Wait message after five attempts | Rate limiting is working. | Wait the quarter of an hour. Not a failure. |
| The session is lost on every page | Site served over HTTP: the cookie carries Secure and is not sent. |
Move to HTTPS. Locally, 127.0.0.1 counts as a secure origin. |
| All sessions drop at once | SESSION_SECRET changed. |
Normal: that is the intended effect during a rotation. |
| Five wrong keys pass with no wait message | The RATE_LIMIT binding is missing, or the counter is in memory. |
Do not ship. Declare the binding on the host. |
At publish time
| Log | Cause | Fix |
|---|---|---|
[save] write failed (not_found) | GIT_REPO mistyped, branch missing, or the token has no access. | Check all three, then test-git-provider --online. |
[save] write failed (unauthorized) | Token expired, revoked, or missing the Contents permission. | Regenerate the token, fine-grained. |
[save] write failed (conflict) | The file changed since the page was opened. | The client reloads and publishes again. If it happens every time, another process is writing to the repository. |
[save] content refused by the schema | A value outside an enum, an empty alt, an over-long title. | The offending path is in the log. Rare from the interface, common on direct calls. |
[save] content refused: invalid media reference | An image outside the whitelist, or an inconsistent video id. | Check the file name and the provider/id pair. |
[save] content refused: manifestly hostile markup | The route was called directly with forbidden markup. | Nothing to fix: that is the expected behaviour. |
| The commit is rejected by the forge | Branch protection: mandatory review or check. | Allow the machine account, or lift the constraint on that branch. |
After publishing
| Symptom | Cause |
|---|---|
| The commit is there, the site does not change | The host is not watching that branch, or the build fails. Read the build log. |
| The build fails after a publish | Nearly impossible: the function validates with the same schema. Look instead for an image referenced but missing from src/media. |
| The image is published but does not show | The file is in the repository, the HTML is not rebuilt yet. Wait for the build. |
| The site is up to date, the browser shows the old one | Browser cache. Force reload. |
At the checks
| Failure | Most frequent cause |
|---|---|
check-html: a value missing from the HTML | A client:* directive on a content component. |
check-html: editable zone inside a hydrated island | client:load around a data-cms. Move the zone out of the island. |
check-html: data-cms with no key | A key renamed in the JSON, not in the page. |
check-locales: key missing from a language | A field added in a single language. |
check-logs: suspicious log | A console.error(error) added for debugging. |
check-secrets: secret in the build | A variable set as a build variable instead of a runtime secret. |
| The check passes although a page is broken | The page is not in the check-html.mjs list. |
The ten known traps
-
client:loadadded by reflexThe project's number one trap. The content is in the HTML, but the framework re-renders it on load and wipes the client's edits in progress. Invisible in the source.
check-htmlis the net; vigilance in review comes first. -
The temptation of server output
To “simplify” a dynamic route. Always go through
/functions: static output is what guarantees the content is in the served HTML. -
src/pages/api/*endpointsThey seem to work in development and do nothing in static production: they run at build time, not per request.
-
A field added in a single language
check-localescatches it before the commit — provided you run it. -
A reassigned item id
Breaks the DOM/JSON link and loses that item's edits. An id is never reused, even after deletion.
-
A collection without its item template
Adding becomes impossible without rewriting rendering in the browser. The
Collectioncomponent avoids it; a hand-written list rendering does not. -
The overlay duplicated site by site
It must stay a shared, versioned dependency. Beyond three sites, maintenance becomes unmanageable.
-
Copy-paste from a word processor
The client will do it in the first week. Without full overwriting of inline styles, the page ends up with Calibri 11 pt in the middle of the theme.
-
Checking the key client-side
A tempting shortcut while building the overlay, and a total hole. The key goes to
/api/auth, nowhere else. -
Forgetting rate limiting
The code works perfectly without it, which makes it easy to postpone “for later”. It belongs to the first batch, not to final hardening.
Jargon leaking into a displayed message. That is how technical vocabulary creeps back into the interface. Check every displayed string, including error messages added in a hurry.
Frequent questions
Can the client break the site?
No. Invalid content is refused before entering the repository, by the same schema as the build. And since every publish is a commit, any previous state is restorable.
What happens if two people edit at the same time?
The second publish is refused with a conflict message. Nothing is overwritten, and the local draft is not lost: reload, publish again.
Can you edit from a phone?
No, deliberately: text editing on a small screen gives poor results. A message explains it rather than shipping a degraded interface.
How long before a change is live?
Thirty to sixty seconds: the time to rebuild the site. It is announced in the interface.
What does a visitor without the key see?
The site, and 175 bytes of JavaScript reading a cookie. The overlay is not downloaded.
Can a lost key be recovered?
No: only its hash exists on the server. It is regenerated — that is the rotation procedure.
Can it be hosted somewhere other than Cloudflare Pages?
Yes, without writing a line: a created site already ships three adapters — one for a host
that discovers routes from a file tree, one for Netlify, and a Node server for everything
else. Unused folders can be deleted. Anywhere else, there is a single file to write, which
calls api.handle and decides nothing. See
Deployment.
The site displays, but the key is refused — 404 or 502 on /api/auth
It is not the key. A site deployed without its functions displays perfectly and has no
routes: curl -i https://the-site.com/api/auth must answer 405. HTML
or a 404 means the functions are not running. On Netlify, a
502 “handler is not a function” has one precise cause:
node_bundler declared in netlify.toml, which emits CommonJS and
makes the function be taken for a v1. Remove the line.