td-ai-tools 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/README.md +1 -0
- package/skills/cloudflare/SKILL.md +78 -0
- package/skills/cloudflare/agents/openai.yaml +4 -0
- package/skills/cloudflare/assets/deploy.yml +71 -0
- package/skills/cloudflare/assets/wrangler.jsonc +57 -0
- package/skills/cloudflare/references/auth-and-accounts.md +62 -0
- package/skills/cloudflare/references/ci-cd.md +48 -0
- package/skills/cloudflare/references/config-secrets-envs.md +55 -0
- package/skills/cloudflare/references/data-d1-kv-r2.md +96 -0
- package/skills/cloudflare/references/deploy-and-release.md +107 -0
- package/skills/cloudflare/references/domains-and-dns.md +99 -0
- package/skills/cloudflare/references/forms-email-turnstile.md +66 -0
- package/skills/cloudflare/references/launch-new-site.md +193 -0
- package/skills/cloudflare/references/observability-and-incidents.md +78 -0
- package/skills/cloudflare/references/site-lifecycle.md +68 -0
- package/skills/cloudflare/references/troubleshooting.md +71 -0
- package/skills/cloudflare/references/zone-settings.md +123 -0
- package/skills/cloudflare/scripts/cf-api.sh +58 -0
- package/skills/cloudflare/scripts/preflight.sh +83 -0
- package/skills/cloudflare/scripts/verify-site.sh +61 -0
- package/skills/shopify-lint/scripts/__pycache__/shopify_lint.cpython-312.pyc +0 -0
- package/skills/shopify-lint/tests/__pycache__/test_shopify_lint.cpython-312.pyc +0 -0
- package/skills/visual-regression/scripts/__pycache__/visual_regression.cpython-312.pyc +0 -0
- package/skills/a11y-audit/SKILL.md +0 -60
- package/skills/a11y-audit/agents/openai.yaml +0 -4
- package/skills/a11y-audit/package.json +0 -17
- package/skills/a11y-audit/scripts/audit.js +0 -624
- package/skills/a11y-audit/scripts/setup.sh +0 -6
- package/skills/barrage/SKILL.md +0 -236
- package/skills/barrage/agents/openai.yaml +0 -4
- package/skills/barrage/scripts/__pycache__/build_queue.cpython-312.pyc +0 -0
- package/skills/barrage/scripts/__pycache__/manage_queue.cpython-312.pyc +0 -0
- package/skills/barrage/scripts/__pycache__/queue_paths.cpython-312.pyc +0 -0
- package/skills/barrage/scripts/build_queue.py +0 -285
- package/skills/barrage/scripts/manage_queue.py +0 -230
- package/skills/barrage/scripts/queue_paths.py +0 -50
- package/skills/barrage/tests/test_build_queue.py +0 -135
- package/skills/barrage/tests/test_manage_queue.py +0 -176
- package/skills/barrage/tests/test_queue_paths.py +0 -97
- package/skills/cache-reset/SKILL.md +0 -22
- package/skills/cache-reset/agents/openai.yaml +0 -4
- package/skills/client-overview/SKILL.md +0 -93
- package/skills/client-overview/agents/openai.yaml +0 -4
- package/skills/client-overview/scripts/branch_client_overview_context.py +0 -317
- package/skills/shopify-lint/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/skills/shopify-pre-pr/scripts/__pycache__/test_vendor_change_audit.cpython-312.pyc +0 -0
- package/skills/shopify-pre-pr/scripts/__pycache__/vendor_change_audit.cpython-312.pyc +0 -0
- package/skills/stylesheet-migration/SKILL.md +0 -37
- package/skills/stylesheet-migration/agents/openai.yaml +0 -6
- package/skills/stylesheet-migration/scripts/liquid_stylesheet_migrator.py +0 -204
- package/skills/stylesheet-migration/scripts/migrate_stylesheet_tags.py +0 -172
- package/skills/stylesheet-migration/scripts/test_liquid_stylesheet_migrator.py +0 -254
- package/skills/td-js-vanilla-rules/SKILL.md +0 -71
- package/skills/td-js-vanilla-rules/agents/openai.yaml +0 -3
- package/skills/td-review/SKILL.md +0 -123
- package/skills/td-review/agents/openai.yaml +0 -4
- package/skills/td-review/agents/td-theme-reviewer.md +0 -221
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Domains and DNS
|
|
2
|
+
|
|
3
|
+
Wrangler attaches hostnames to a Worker. Everything else about DNS goes through the REST API (`scripts/cf-api.sh`) or the dashboard. Every non-GET here is a production write (confirm first).
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
- Custom domain vs route: which to use
|
|
7
|
+
- Onboard a zone (domain not yet on Cloudflare)
|
|
8
|
+
- DNS records: list, add, change, delete, export
|
|
9
|
+
- Subdomains and staging hosts
|
|
10
|
+
- Domain registered *at* Cloudflare
|
|
11
|
+
- Checking propagation
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Custom domain vs route
|
|
16
|
+
|
|
17
|
+
| | Custom domain (`"custom_domain": true`) | Route (`"pattern": "acme.com/*", "zone_name": "acme.com"`) |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| DNS record | Created for you | You create a proxied record yourself (e.g. `AAAA @ 100::`) |
|
|
20
|
+
| Certificate | Issued for you | Covered by the zone's universal cert (apex + one level of subdomain) |
|
|
21
|
+
| Worker is the origin | Yes, nothing behind it | Worker runs *in front of* whatever the DNS record points to |
|
|
22
|
+
| Use for | **Default for Leyline sites** | Running a Worker on part of a site whose origin lives elsewhere (e.g. `acme.com/careers/*` in front of WordPress) |
|
|
23
|
+
|
|
24
|
+
Leyline sites use custom domains for the apex and www. Routes are for hybrid setups only.
|
|
25
|
+
|
|
26
|
+
Custom domains fail with "a DNS record already exists for this hostname" when an A/AAAA/CNAME exists for that name. Delete the old record (confirm; this is the cutover moment), then deploy again.
|
|
27
|
+
|
|
28
|
+
## Onboard a zone
|
|
29
|
+
|
|
30
|
+
Use this when the domain isn't on Cloudflare yet, or is in a different Cloudflare account (check with `cf-api.sh GET '/zones?name=acme.com'`; an empty result can just mean the token can't see it).
|
|
31
|
+
|
|
32
|
+
**1. Snapshot existing DNS** before touching anything. The zone scan on creation misses records, especially DKIM selectors and anything on unusual subdomains.
|
|
33
|
+
```bash
|
|
34
|
+
D=acme.com
|
|
35
|
+
for t in A AAAA CNAME MX TXT NS CAA SRV; do echo "## $t"; dig +short $t $D; done
|
|
36
|
+
for h in www mail autodiscover _dmarc selector1._domainkey selector2._domainkey google._domainkey k1._domainkey pm._domainkey; do echo "## $h"; dig +short ANY $h.$D; done
|
|
37
|
+
```
|
|
38
|
+
Ask the user whether there are other subdomains in use (shop, portal, app, …), and which email provider the client uses (Google Workspace, Microsoft 365, …). Each provider has known records to look for. Save the snapshot to the scratchpad, or to the site repo if the user wants it kept.
|
|
39
|
+
|
|
40
|
+
**2. Create the zone** (confirm):
|
|
41
|
+
```bash
|
|
42
|
+
bash scripts/cf-api.sh POST /zones '{"name":"acme.com","account":{"id":"'"$CLOUDFLARE_ACCOUNT_ID"'"},"type":"full"}'
|
|
43
|
+
```
|
|
44
|
+
The response contains `name_servers`, the two to give to the registrar.
|
|
45
|
+
|
|
46
|
+
**3. Reconcile records.** List what Cloudflare imported and diff it against the snapshot:
|
|
47
|
+
```bash
|
|
48
|
+
bash scripts/cf-api.sh GET '/zones/{zone:acme.com}/dns_records?per_page=500' | jq -r '.result[] | [.type,.name,.content,.proxied] | @tsv'
|
|
49
|
+
```
|
|
50
|
+
Add anything missing. MX, SPF, DKIM, DMARC and verification TXT records must be **DNS-only** (`"proxied": false`); MX and TXT can't be proxied anyway. Records for other web hosts (subdomains not served by this Worker) can be proxied if their origin supports HTTPS; if unsure, keep them DNS-only to start.
|
|
51
|
+
|
|
52
|
+
Bulk alternative: build a BIND zone file and `POST /zones/{id}/dns_records/import` (multipart form, `file=@zone.txt`). That endpoint needs `curl -F` directly; `cf-api.sh` sends JSON only.
|
|
53
|
+
|
|
54
|
+
**4. Nameserver change.** Give the user the two nameservers and the registrar instructions. Also remind them to **turn off DNSSEC at the current registrar/DNS host first**, if it's on, or the domain will stop resolving. Re-enable it in Cloudflare afterwards (`PATCH /zones/{id}/dnssec {"status":"active"}`, then add the DS record at the registrar).
|
|
55
|
+
|
|
56
|
+
**5. Wait for activation:**
|
|
57
|
+
```bash
|
|
58
|
+
bash scripts/cf-api.sh GET /zones/{zone:acme.com} | jq '.result.status' # pending → active
|
|
59
|
+
```
|
|
60
|
+
To trigger an early recheck: `PUT /zones/{id}/activation_check`. Don't attach custom domains to a pending zone.
|
|
61
|
+
|
|
62
|
+
## DNS records
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# list
|
|
66
|
+
bash scripts/cf-api.sh GET '/zones/{zone:acme.com}/dns_records?per_page=500'
|
|
67
|
+
bash scripts/cf-api.sh GET '/zones/{zone:acme.com}/dns_records?name=www.acme.com'
|
|
68
|
+
# add (confirm)
|
|
69
|
+
bash scripts/cf-api.sh POST '/zones/{zone:acme.com}/dns_records' '{"type":"TXT","name":"acme.com","content":"google-site-verification=…","ttl":1}'
|
|
70
|
+
bash scripts/cf-api.sh POST '/zones/{zone:acme.com}/dns_records' '{"type":"MX","name":"acme.com","content":"aspmx.l.google.com","priority":1,"ttl":1}'
|
|
71
|
+
# change (confirm): PATCH merges, PUT replaces
|
|
72
|
+
bash scripts/cf-api.sh PATCH '/zones/{zone:acme.com}/dns_records/<record_id>' '{"content":"new-target.example.net"}'
|
|
73
|
+
# delete (confirm), get the ID from the list first and show the record to the user
|
|
74
|
+
bash scripts/cf-api.sh DELETE '/zones/{zone:acme.com}/dns_records/<record_id>'
|
|
75
|
+
# backup the whole zone as BIND text, worth doing before bulk edits
|
|
76
|
+
curl -sS -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" "https://api.cloudflare.com/client/v4/zones/$(bash scripts/cf-api.sh zone-id acme.com)/dns_records/export" > acme.com.zone
|
|
77
|
+
```
|
|
78
|
+
`"ttl": 1` means "automatic". TXT content with spaces or quotes: pass it exactly as the provider gives it, and the API handles quoting.
|
|
79
|
+
|
|
80
|
+
Before deleting or changing a record, show the user its current value. DNS edits propagate in seconds on Cloudflare, but resolvers cache the old value for its TTL.
|
|
81
|
+
|
|
82
|
+
## Subdomains and staging hosts
|
|
83
|
+
|
|
84
|
+
- Another Worker on a subdomain: give it its own custom domain in *its* `wrangler.jsonc` (`staging.acme.com`, `cms.acme.com`).
|
|
85
|
+
- A subdomain pointing at a third party (e.g. `shop.` → Shopify): a CNAME as the provider documents, usually **DNS-only**, because many SaaS hosts can't issue certificates behind the Cloudflare proxy.
|
|
86
|
+
- Deep subdomains (`a.b.acme.com`) aren't covered by the free universal cert when proxied. Custom domains handle this automatically; with routes you'd need Advanced Certificate Manager.
|
|
87
|
+
|
|
88
|
+
## Domain registered at Cloudflare
|
|
89
|
+
|
|
90
|
+
If the domain is registered through Cloudflare Registrar, the nameservers are already Cloudflare's and the zone exists. Renewal and contact details live under Domain Registration in the dashboard; the API is `/accounts/{account}/registrar/domains`. Transfers in or out are dashboard-driven, and need the client's involvement (auth codes, approval emails).
|
|
91
|
+
|
|
92
|
+
## Checking propagation
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
dig +short NS acme.com @1.1.1.1
|
|
96
|
+
dig +short acme.com @1.1.1.1 ; dig +short acme.com @8.8.8.8
|
|
97
|
+
curl -sI https://acme.com | grep -i -E '^(server|cf-ray)'
|
|
98
|
+
```
|
|
99
|
+
`server: cloudflare` plus a `cf-ray` header means traffic is flowing through Cloudflare.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Forms, email and Turnstile
|
|
2
|
+
|
|
3
|
+
Leyline forms post to the site's own Worker (`/_leyline/forms/<id>`). It validates the submission, optionally stores it in D1, and sends it through **Postmark**. Cloudflare's part is Turnstile, rate limiting, D1, and (often) the DNS that makes email work.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
- Turnstile widgets
|
|
7
|
+
- Postmark DNS (DKIM / Return-Path)
|
|
8
|
+
- Email Routing (receive mail at the domain)
|
|
9
|
+
- SPF / DMARC hygiene
|
|
10
|
+
- "Form emails aren't arriving"
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Turnstile widgets
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx wrangler turnstile widget list
|
|
18
|
+
npx wrangler turnstile widget create "acme site" --domain acme.com --domain www.acme.com --mode managed --json # (confirm)
|
|
19
|
+
npx wrangler turnstile widget get <sitekey>
|
|
20
|
+
npx wrangler turnstile widget update <sitekey> --domain acme.com --domain www.acme.com --domain staging.acme.com # (confirm)
|
|
21
|
+
```
|
|
22
|
+
(This command group is alpha. Run `--help` if a flag is rejected.)
|
|
23
|
+
|
|
24
|
+
- `sitekey` → `vars.PUBLIC_TURNSTILE_SITE_KEY` in `wrangler.jsonc`; `secret` → `npx wrangler secret put TURNSTILE_SECRET_KEY`.
|
|
25
|
+
- The `--mode` on the widget must be compatible with the `turnstile: { mode }` in the Leyline form config: `managed`, `non-interactive` or `invisible`.
|
|
26
|
+
- Every hostname that renders the form must be in the widget's domain list, including staging and preview hostnames if forms are tested there. A missing hostname shows up as Turnstile error 110200 in the browser console.
|
|
27
|
+
- Leyline checks that the Turnstile *action* matches the form ID.
|
|
28
|
+
- Local dev: Cloudflare's test keys (sitekey `1x00000000000000000000AA`, secret `1x0000000000000000000000000000000AA`) always pass. Leyline allows them through the action check.
|
|
29
|
+
|
|
30
|
+
## Postmark DNS
|
|
31
|
+
|
|
32
|
+
Postmark (Sender Signatures / Domains) gives a **DKIM TXT** record (`<selector>._domainkey`) and a **Return-Path CNAME** (`pm-bounces` → `pm.mtasv.net`). Add both in the zone, **DNS-only** (confirm):
|
|
33
|
+
```bash
|
|
34
|
+
bash scripts/cf-api.sh POST '/zones/{zone:acme.com}/dns_records' '{"type":"TXT","name":"20261007._domainkey.acme.com","content":"k=rsa;p=MIGf…","ttl":1}'
|
|
35
|
+
bash scripts/cf-api.sh POST '/zones/{zone:acme.com}/dns_records' '{"type":"CNAME","name":"pm-bounces.acme.com","content":"pm.mtasv.net","proxied":false,"ttl":1}'
|
|
36
|
+
```
|
|
37
|
+
Then click Verify in Postmark. The `from` address in the Leyline form config must belong to that verified domain or signature.
|
|
38
|
+
|
|
39
|
+
## Email Routing (receive mail at the domain)
|
|
40
|
+
|
|
41
|
+
Use this for clients without a mailbox provider who want e.g. `hello@acme.com` forwarded to a Gmail address. Don't enable it on a domain that already has MX records for Google Workspace or Microsoft 365: it replaces them.
|
|
42
|
+
```bash
|
|
43
|
+
npx wrangler email routing settings acme.com
|
|
44
|
+
npx wrangler email routing enable acme.com # (confirm) adds Cloudflare MX + SPF records
|
|
45
|
+
npx wrangler email routing addresses --help # destination addresses must be verified by the recipient clicking an email
|
|
46
|
+
npx wrangler email routing rules --help # map hello@ → destination
|
|
47
|
+
```
|
|
48
|
+
Leyline forms can `to:` a routed address. Delivery then goes Postmark → Cloudflare Email Routing → the client's inbox.
|
|
49
|
+
|
|
50
|
+
Cloudflare **Email Sending** (`wrangler email sending`) is a newer alternative to Postmark for outbound mail. Leyline doesn't support it as a transport yet, so don't switch a site to it without a code change in Leyline.
|
|
51
|
+
|
|
52
|
+
## SPF / DMARC hygiene
|
|
53
|
+
|
|
54
|
+
- Exactly **one** SPF TXT record at the apex. Merge includes into it rather than adding a second one: `v=spf1 include:_spf.google.com include:spf.mtasv.net ~all`. Postmark technically doesn't need to be in SPF, because it uses its own Return-Path domain. Adding it is harmless.
|
|
55
|
+
- DMARC: `_dmarc` TXT. If there's none, recommend starting with `v=DMARC1; p=none; rua=mailto:…`, and tighten it later with the client's IT contact. Don't tighten DMARC on a client domain unilaterally; it can bounce mail from tools you don't know about.
|
|
56
|
+
|
|
57
|
+
## "Form emails aren't arriving"
|
|
58
|
+
|
|
59
|
+
Work through these in order and stop at the first failure:
|
|
60
|
+
1. **Did the Worker get the submission?** `npx wrangler tail --format pretty --search "[leyline]"`, then submit, or check D1: `SELECT status, error FROM leyline_submissions ORDER BY created_at DESC LIMIT 5`.
|
|
61
|
+
- No row/log → the request isn't reaching the Worker. Check the browser network tab for 403 (origin check), 429 (rate limit) or a Turnstile failure.
|
|
62
|
+
- `status = 'failed'` with an error → Postmark rejected it. The error text says why: an unverified sender, a wrong stream, or an inactive recipient.
|
|
63
|
+
2. **Secrets**: `npx wrangler secret list` shows `POSTMARK_SERVER_TOKEN` and `FORM_SIGNING_SECRET`.
|
|
64
|
+
3. **Postmark activity** (in the Postmark UI): delivered, bounced, or spam-complained. A recipient that once hard-bounced is suppressed in Postmark until reactivated.
|
|
65
|
+
4. **Recipient side**: spam folder, or a Microsoft 365 quarantine. A DKIM/Return-Path that isn't verified makes this much worse.
|
|
66
|
+
5. **Submission marked `[unverified]` or flagged**: Leyline's spam heuristics. See `flags` in D1. That's working as designed unless legitimate messages are being rejected.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Launch a new site
|
|
2
|
+
|
|
3
|
+
The end-to-end runbook, from an empty directory (or a finished repo) to a site live on its domain. Work through the phases in order. Each phase ends in a state you can verify, so a launch interrupted halfway can be resumed from the last phase that verified. Steps marked **(confirm)** are production writes; see the guardrails in SKILL.md.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
0. Gather inputs
|
|
7
|
+
1. Account and auth
|
|
8
|
+
2. Scaffold or adopt the repo
|
|
9
|
+
3. `wrangler.jsonc`
|
|
10
|
+
4. Data: D1 and rate limiting
|
|
11
|
+
5. Secrets and Turnstile
|
|
12
|
+
6. Build and preview locally
|
|
13
|
+
7. First deploy (workers.dev)
|
|
14
|
+
8. Get the zone onto Cloudflare
|
|
15
|
+
9. Attach the domain
|
|
16
|
+
10. Zone settings
|
|
17
|
+
11. Verify and hand over
|
|
18
|
+
Appendix: migrating from an existing host
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 0. Gather inputs
|
|
23
|
+
|
|
24
|
+
Ask for whatever the user hasn't supplied. Don't guess the account or the domain.
|
|
25
|
+
|
|
26
|
+
| Input | Why it matters |
|
|
27
|
+
|---|---|
|
|
28
|
+
| Client name and **production domain** (apex or www as canonical?) | Drives the Worker name, `site.url`, routes and redirects |
|
|
29
|
+
| **Which Cloudflare account**: Theory Digital's, or the client's own? | Everything is created there; moving it later is real work (see `site-lifecycle.md`) |
|
|
30
|
+
| Is the domain **already a zone on Cloudflare**? Who controls the registrar? | Decides whether phase 8 is needed, and who has to change nameservers |
|
|
31
|
+
| Does the domain receive **email** today (MX records)? | A nameserver switch that drops MX/TXT records breaks the client's email. This is the most common launch disaster |
|
|
32
|
+
| Forms? Persist submissions (D1)? Rate limit? Turnstile? | Decides phases 4–5 |
|
|
33
|
+
| Content source: static Markdown or EmDash? | EmDash on Workers needs its own D1 and, usually, R2 (see `data-d1-kv-r2.md`) |
|
|
34
|
+
| Locales | `site.locales`, and the sitemap check |
|
|
35
|
+
|
|
36
|
+
Naming conventions (use these unless the repo already does something else):
|
|
37
|
+
- Worker: `<client>-site`, e.g. `acme-site`. Staging: `acme-site-staging` (that's what `--env staging` produces).
|
|
38
|
+
- D1: `<client>-site`. Turnstile widget: `<client> site`. Auth profile: `td` or `client-<client>`.
|
|
39
|
+
|
|
40
|
+
## 1. Account and auth
|
|
41
|
+
|
|
42
|
+
See `auth-and-accounts.md`. In short:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx wrangler auth list # profiles that already exist
|
|
46
|
+
npx wrangler auth create client-acme # only if the client's account needs its own login
|
|
47
|
+
npx wrangler auth activate client-acme . # bind this directory to that profile
|
|
48
|
+
npx wrangler whoami # confirm the account name and ID
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Write the `account_id` into `wrangler.jsonc` in phase 3. That pins every wrangler command in this repo to the right account, whoever runs it.
|
|
52
|
+
|
|
53
|
+
For phases 8–10 you also need `CLOUDFLARE_API_TOKEN` with zone permissions on that account. Check it now (`bash scripts/cf-api.sh GET /user/tokens/verify`) so you don't stall halfway through the launch.
|
|
54
|
+
|
|
55
|
+
✅ `whoami` shows the intended account.
|
|
56
|
+
|
|
57
|
+
## 2. Scaffold or adopt the repo
|
|
58
|
+
|
|
59
|
+
New site: follow Leyline's README quick start (`npm create astro@latest … --template minimal`, then `npm i @theory-digital/leyline astro @astrojs/cloudflare svelte tailwindcss wrangler`, then `astro.config.ts` with `adapter: cloudflare()` and `leyline({...})`).
|
|
60
|
+
|
|
61
|
+
Existing repo: confirm that `adapter: cloudflare()` is in `astro.config.*` and that `wrangler` is a devDependency (so `npx wrangler` is pinned).
|
|
62
|
+
|
|
63
|
+
Set `site.url` to the **final production origin** now, even before the domain works. Canonicals, hreflang, the sitemap and JSON-LD are all built from it. A workers.dev URL that leaks into production sitemaps is a recurring SEO bug.
|
|
64
|
+
|
|
65
|
+
Make sure `.gitignore` covers `.wrangler/`, `.dev.vars*`, and `dist/`.
|
|
66
|
+
|
|
67
|
+
## 3. `wrangler.jsonc`
|
|
68
|
+
|
|
69
|
+
Copy `assets/wrangler.jsonc` from this skill into the site root and fill in the placeholders. It includes observability, `workers_dev` and `preview_urls`, commented-out routes for phase 9, and the bindings Leyline forms expect. Don't set `main` or `assets`: the Astro adapter generates them in `dist/server/wrangler.json`.
|
|
70
|
+
|
|
71
|
+
✅ `bash scripts/preflight.sh` parses the config and shows the right name and account.
|
|
72
|
+
|
|
73
|
+
## 4. Data: D1 and rate limiting
|
|
74
|
+
|
|
75
|
+
Skip this phase if forms don't use `persist` or `rateLimit`.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npx wrangler d1 create acme-site --location enam # (confirm) choose the location hint nearest the audience; Canada/US East → enam
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Paste the returned `database_id` into `d1_databases`. Alternatively, run `--binding DB --update-config` to let wrangler write it, then check the `migrations_dir`. If the site or client is under EU data rules, use `--jurisdiction eu`. That choice can't be changed later.
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
mkdir -p migrations
|
|
85
|
+
cp node_modules/@theory-digital/leyline/src/forms/migrations/*.sql migrations/
|
|
86
|
+
npx wrangler d1 migrations apply DB --local # for wrangler dev
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Hold the remote apply until phase 7.
|
|
90
|
+
|
|
91
|
+
Rate limiting: the `ratelimits[].namespace_id` is a number you choose. It only has to be unique among the Workers in that account that shouldn't share counters. Use a distinct one per site (e.g. 1001, 1002, …) and record it in the README.
|
|
92
|
+
|
|
93
|
+
✅ `npx wrangler d1 list` shows the database, and the ID in the config isn't the `0000…` placeholder.
|
|
94
|
+
|
|
95
|
+
## 5. Secrets and Turnstile
|
|
96
|
+
|
|
97
|
+
See `config-secrets-envs.md` and `forms-email-turnstile.md` for detail.
|
|
98
|
+
|
|
99
|
+
Local first. Create `.dev.vars` (gitignored):
|
|
100
|
+
```
|
|
101
|
+
FORM_SIGNING_SECRET=<openssl rand -base64 48>
|
|
102
|
+
POSTMARK_SERVER_TOKEN=<from Postmark → server → API tokens>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Turnstile, if enabled. The widget needs the production hostname, plus `localhost` if you want it to work in dev:
|
|
106
|
+
```bash
|
|
107
|
+
npx wrangler turnstile widget create "acme site" --domain acme.com --domain www.acme.com --mode managed --json # (confirm)
|
|
108
|
+
```
|
|
109
|
+
Put the returned `sitekey` in `vars.PUBLIC_TURNSTILE_SITE_KEY` in `wrangler.jsonc`, and the `secret` in `.dev.vars` as `TURNSTILE_SECRET_KEY`. Treat the secret like a password. Don't repeat it back in chat beyond writing it to the file. For local development, Cloudflare's always-pass test keys work. The Leyline action check lets them through.
|
|
110
|
+
|
|
111
|
+
Postmark: the `from` address's domain or sender signature must be verified in Postmark *before* launch. If the client's DNS is moving to Cloudflare in phase 8, add the Postmark DKIM and Return-Path records there.
|
|
112
|
+
|
|
113
|
+
## 6. Build and preview locally
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npx astro build
|
|
117
|
+
npx wrangler dev -c dist/server/wrangler.json --port 4321
|
|
118
|
+
```
|
|
119
|
+
Use this, not `astro preview`. In Astro 7.3 it returns 500s for prerendered pages with `build.format: 'file'`. Click through the main pages and submit a test form (with the Postmark mock or a real token).
|
|
120
|
+
|
|
121
|
+
✅ Pages render, and the form's token endpoint `/_leyline/token/contact` returns 200.
|
|
122
|
+
|
|
123
|
+
## 7. First deploy (workers.dev) (confirm)
|
|
124
|
+
|
|
125
|
+
Deploying first to `<name>.<subdomain>.workers.dev` proves the Worker, bindings and secrets work before any DNS changes.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npx wrangler d1 migrations apply DB --remote # (confirm)
|
|
129
|
+
npx astro build
|
|
130
|
+
npx wrangler deploy --tag "$(git rev-parse --short HEAD)" --message "Initial deploy" # (confirm)
|
|
131
|
+
printf %s "$(grep ^FORM_SIGNING_SECRET= .dev.vars | cut -d= -f2-)" | npx wrangler secret put FORM_SIGNING_SECRET # (confirm)
|
|
132
|
+
printf %s "$(grep ^POSTMARK_SERVER_TOKEN= .dev.vars | cut -d= -f2-)" | npx wrangler secret put POSTMARK_SERVER_TOKEN
|
|
133
|
+
# + TURNSTILE_SECRET_KEY if used
|
|
134
|
+
```
|
|
135
|
+
Use a *different* `FORM_SIGNING_SECRET` in production than in dev if you like. It only has to be random, 32+ characters, and stable. Rotating it invalidates tokens that are already open in visitors' browsers.
|
|
136
|
+
|
|
137
|
+
Note: on the first deploy, wrangler may auto-provision resources that are declared without IDs, such as the adapter's `SESSION` KV namespace. That's expected. Mention it in the report.
|
|
138
|
+
|
|
139
|
+
✅ `bash scripts/verify-site.sh https://acme-site.<sub>.workers.dev --no-www-check`. Expect robots, sitemap and canonical warnings pointing at the production URL; that's correct. The form token endpoint should return 200.
|
|
140
|
+
|
|
141
|
+
## 8. Get the zone onto Cloudflare
|
|
142
|
+
|
|
143
|
+
Skip if `cf-api.sh GET '/zones?name=acme.com'` already returns the zone **in the right account** with `"status": "active"`.
|
|
144
|
+
|
|
145
|
+
Otherwise, see `domains-and-dns.md` → "Onboard a zone". The critical sequence is:
|
|
146
|
+
1. **Snapshot the current DNS** (`dig` every record type for the apex, www and known subdomains, especially MX, TXT/SPF, DKIM selectors, `_dmarc`, and verification records).
|
|
147
|
+
2. Create the zone **(confirm)**. Cloudflare scans and imports the records it can find. Compare that list against the snapshot and add whatever's missing.
|
|
148
|
+
3. Tell the user exactly which **two nameservers** to set at the registrar. They (or the client) have to do this; you can't.
|
|
149
|
+
4. Poll `cf-api.sh GET /zones/{zone:acme.com}` until `status` is `active`. This usually takes minutes, sometimes hours. Don't proceed to phase 9 on a pending zone.
|
|
150
|
+
|
|
151
|
+
## 9. Attach the domain (confirm)
|
|
152
|
+
|
|
153
|
+
Uncomment the routes in `wrangler.jsonc`:
|
|
154
|
+
```jsonc
|
|
155
|
+
"routes": [
|
|
156
|
+
{ "pattern": "acme.com", "custom_domain": true },
|
|
157
|
+
{ "pattern": "www.acme.com", "custom_domain": true }
|
|
158
|
+
]
|
|
159
|
+
```
|
|
160
|
+
Custom domains create the DNS record and certificate automatically. They **fail if a DNS record already exists** for that hostname, which is common after a zone import pointed it at the old host. Delete the conflicting A/AAAA/CNAME first **(confirm)**. That moment is the cutover: the old host stops receiving traffic.
|
|
161
|
+
|
|
162
|
+
Then build and deploy again. Add a www→apex redirect (or the reverse) as described in `zone-settings.md` → "Canonical host redirect". Using both hosts as custom domains plus a redirect rule is the simplest setup that gets an edge certificate on both.
|
|
163
|
+
|
|
164
|
+
Set `"workers_dev": false` if the client shouldn't have a second public copy of the site. Keep `preview_urls` if you use version previews.
|
|
165
|
+
|
|
166
|
+
## 10. Zone settings (confirm)
|
|
167
|
+
|
|
168
|
+
Apply the launch baseline from `zone-settings.md`: SSL Full (strict), Always Use HTTPS, minimum TLS 1.2, **AI-bot blocking off, managed robots.txt off** (Leyline's own robots.txt has to win), and a check that no AI crawler is blocked in AI Crawl Control. Add HSTS only once HTTPS has worked for a few days.
|
|
169
|
+
|
|
170
|
+
## 11. Verify and hand over
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
bash scripts/verify-site.sh https://acme.com --md /about.md
|
|
174
|
+
```
|
|
175
|
+
Then do the things a script can't:
|
|
176
|
+
- Submit the real form. Confirm the email arrives with Reply-To set, and that the D1 row exists (`npx wrangler d1 execute DB --remote --command "SELECT form, status, created_at FROM leyline_submissions ORDER BY created_at DESC LIMIT 3"`). `status` should be `sent`.
|
|
177
|
+
- Submit the sitemap in Search Console (`/sitemap-index.xml` for static, `/sitemap.xml` for EmDash).
|
|
178
|
+
- Run through `docs/DEPLOY-CHECKLIST.md` in the Leyline repo if it's available.
|
|
179
|
+
|
|
180
|
+
Record the launch in the site's README (or wherever the team keeps it): account, Worker name, zone, D1 name and ID, rate-limit namespace ID, Turnstile sitekey, Postmark server, who controls the registrar. Whoever touches the site next will need exactly this list.
|
|
181
|
+
|
|
182
|
+
Report back: the live URL, the version ID, the verify output, and anything deferred (e.g. HSTS, Search Console).
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Appendix: migrating from an existing host (WordPress, Squarespace, Webflow, …)
|
|
187
|
+
|
|
188
|
+
On top of the phases above:
|
|
189
|
+
- **Crawl the old site** for URLs before cutover: its sitemap, plus Search Console's top pages. Map every old URL that changed to its new one, using Astro `redirects` in `astro.config` (or `public/_redirects` for large lists). Losing 301s throws away the client's search ranking.
|
|
190
|
+
- **Lower TTLs** on the old DNS a day ahead if the zone isn't on Cloudflare yet.
|
|
191
|
+
- **Keep the old host running** until verify passes and a day of traffic looks normal. Then decommission it.
|
|
192
|
+
- Old-site forms and integrations (Mailchimp embeds, analytics, chat widgets, tracking pixels): list them and confirm with the user which ones carry over.
|
|
193
|
+
- Check for subdomains still served by the old host (`shop.`, `blog.`, `portal.`). They need their DNS records preserved, not deleted.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Observability and incidents
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
- "The site is down / broken": triage
|
|
5
|
+
- Live logs (tail)
|
|
6
|
+
- Workers Logs (persisted, queryable)
|
|
7
|
+
- Analytics and traffic questions
|
|
8
|
+
- Uptime monitoring
|
|
9
|
+
- Post-incident note
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Triage
|
|
14
|
+
|
|
15
|
+
Go fast and read-only until the cause is known. Report findings as you go.
|
|
16
|
+
|
|
17
|
+
1. **Is it down for everyone, and what kind of failure?**
|
|
18
|
+
```bash
|
|
19
|
+
curl -sS -o /dev/null -w '%{http_code} %{time_total}s\n' https://acme.com/
|
|
20
|
+
curl -sSI https://acme.com/ | grep -iE '^(HTTP|server|cf-ray|location)'
|
|
21
|
+
bash <skill>/scripts/verify-site.sh https://acme.com
|
|
22
|
+
```
|
|
23
|
+
| Symptom | Likely layer |
|
|
24
|
+
|---|---|
|
|
25
|
+
| DNS failure / `curl: (6)` | DNS or nameservers (`domains-and-dns.md`), or an expired domain |
|
|
26
|
+
| Cloudflare error page 1101 / 1102 | The Worker threw or ran out of CPU or memory → tail it |
|
|
27
|
+
| 522 / 523 / 525 / 526 | Origin or SSL problem on a *proxied record that isn't the Worker*: the hostname isn't actually attached to the Worker |
|
|
28
|
+
| 1014 / 1016 / 1001 | DNS record misconfigured or points cross-account |
|
|
29
|
+
| 403 for some visitors | WAF / bot rule / Under Attack mode, or Leyline's origin check (forms only) |
|
|
30
|
+
| 500 on every page right after a deploy | Bad deploy → roll back first, diagnose second |
|
|
31
|
+
| 404 on pages that existed | Build/output problem, `trailingSlash`/`build.format` change, or a routing collision |
|
|
32
|
+
| Redirect loop | SSL mode "Flexible" combined with an HTTPS redirect, or conflicting www/apex rules |
|
|
33
|
+
2. **Did something change?** `npx wrangler deployments list` (deploy time vs incident start), plus a check of zone settings and DNS changes (Audit Log: `bash scripts/cf-api.sh GET '/accounts/{account}/audit_logs?per_page=50'`).
|
|
34
|
+
3. **If a deploy coincides with the start: offer a rollback immediately** (`deploy-and-release.md` → Roll back). Restoring service comes before root-causing.
|
|
35
|
+
4. Tail the errors (below), find the cause, fix it, deploy, and verify.
|
|
36
|
+
|
|
37
|
+
Cloudflare-wide outages do happen. Check https://www.cloudflarestatus.com before digging deep into a site that "just stopped" with no deploy.
|
|
38
|
+
|
|
39
|
+
## Live logs (tail)
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx wrangler tail --format pretty # everything
|
|
43
|
+
npx wrangler tail --format pretty --status error # failures only
|
|
44
|
+
npx wrangler tail --format json --search "[leyline]" # Leyline's own messages
|
|
45
|
+
npx wrangler tail --format pretty --method POST # form posts
|
|
46
|
+
npx wrangler tail --format pretty --ip self # only your own requests while reproducing
|
|
47
|
+
npx wrangler tail --version-id <id> # one version during a gradual rollout
|
|
48
|
+
```
|
|
49
|
+
`tail` is a live stream. Run it in the background (or via Monitor), reproduce the issue, then read the output. On a busy site it samples, so add filters rather than reading a firehose.
|
|
50
|
+
|
|
51
|
+
Leyline logs `[leyline] Form "<id>" rejected|misconfigured|failed|delivery failed …`, `[leyline] FORM_SIGNING_SECRET is not set`, and `[leyline] EmDash … failed`.
|
|
52
|
+
|
|
53
|
+
## Workers Logs (persisted)
|
|
54
|
+
|
|
55
|
+
With `"observability": { "enabled": true }` in `wrangler.jsonc` (the template has it), logs are stored and searchable in the dashboard (Workers → the Worker → Logs) for a retention window that depends on plan. This is how you answer "what happened at 3am" after the fact, which tail can't do. `head_sampling_rate` (0–1) controls volume on high-traffic sites.
|
|
56
|
+
|
|
57
|
+
There's also a query API (`/accounts/{account}/workers/observability/telemetry/query`). Its request shape is involved: fetch `https://developers.cloudflare.com/workers/observability/llms.txt` before using it, rather than guessing.
|
|
58
|
+
|
|
59
|
+
## Analytics and traffic questions
|
|
60
|
+
|
|
61
|
+
- Rough traffic and errors per Worker: the dashboard's Worker → Metrics tab (requests, errors, CPU time).
|
|
62
|
+
- Programmatic access goes through the GraphQL Analytics API at `https://api.cloudflare.com/client/v4/graphql`. Example: requests and errors per day for one Worker:
|
|
63
|
+
```bash
|
|
64
|
+
curl -sS https://api.cloudflare.com/client/v4/graphql -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" -H 'Content-Type: application/json' --data '{
|
|
65
|
+
"query":"query($a:String!,$s:String!,$from:Date!){viewer{accounts(filter:{accountTag:$a}){workersInvocationsAdaptive(limit:31,filter:{scriptName:$s,date_geq:$from},orderBy:[date_ASC]){sum{requests errors} dimensions{date}}}}}",
|
|
66
|
+
"variables":{"a":"'"$CLOUDFLARE_ACCOUNT_ID"'","s":"acme-site","from":"2026-09-01"}}' | jq
|
|
67
|
+
```
|
|
68
|
+
The token needs Account Analytics Read.
|
|
69
|
+
- Zone-level traffic (all hostnames, cache ratio, countries): `httpRequests1dGroups` on `zones(filter:{zoneTag:…})`.
|
|
70
|
+
- Visitor analytics for the client: Cloudflare Web Analytics (privacy-friendly beacon) is set up in the dashboard and needs a script tag in the site. Confirm with the user before adding any tracking to a site.
|
|
71
|
+
|
|
72
|
+
## Uptime monitoring
|
|
73
|
+
|
|
74
|
+
Cloudflare doesn't ping your site for you on free plans. Health Checks and Notifications on paid zones can alert on origin health. For Worker-only sites, an external uptime monitor hitting `/` and `/_leyline/token/contact` (which exercises the Worker and its secret) is the practical choice. Notifications for Worker error spikes can be set up under Notifications in the dashboard.
|
|
75
|
+
|
|
76
|
+
## Post-incident note
|
|
77
|
+
|
|
78
|
+
After resolving an incident, offer a short write-up: the timeline (from deployments, logs and the audit log), the impact (time window, which pages or forms), the cause, the fix, and the follow-up (e.g. "add a check to verify-site", "pin compat date"). Lost form submissions are the item clients care about most. Check D1 for `failed` rows during the window; those can be re-sent by hand.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Site lifecycle
|
|
2
|
+
|
|
3
|
+
Everything after launch: knowing what exists, keeping it current, moving it, and shutting it down.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
- Inventory / audit across accounts
|
|
7
|
+
- Routine upgrades (wrangler, compat date, Leyline)
|
|
8
|
+
- Hand a site to the client's own account
|
|
9
|
+
- Rename a Worker or change the domain
|
|
10
|
+
- Decommission a site
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Inventory / audit
|
|
15
|
+
|
|
16
|
+
"What sites do we have and where?" For each account the login or token can see:
|
|
17
|
+
```bash
|
|
18
|
+
npx wrangler whoami # accounts
|
|
19
|
+
bash scripts/cf-api.sh GET '/accounts/{account}/workers/scripts' | jq -r '.result[] | [.id, .modified_on] | @tsv'
|
|
20
|
+
bash scripts/cf-api.sh GET '/accounts/{account}/workers/domains' | jq -r '.result[] | [.hostname, .service, .environment] | @tsv'
|
|
21
|
+
bash scripts/cf-api.sh GET "/zones?account.id=$CLOUDFLARE_ACCOUNT_ID&per_page=50" | jq -r '.result[] | [.name, .status, .plan.name] | @tsv'
|
|
22
|
+
npx wrangler d1 list
|
|
23
|
+
```
|
|
24
|
+
Present the result as a table (Worker, hostnames, last modified, zone status, D1). Flag these:
|
|
25
|
+
- Workers with no hostname, or not modified in a long time (candidates for decommissioning)
|
|
26
|
+
- Zones that are `pending` or `moved`
|
|
27
|
+
- Hostnames serving a Worker whose repo nobody can identify
|
|
28
|
+
|
|
29
|
+
A per-site health sweep: run `verify-site.sh` against each production hostname, plus the zone audit from `zone-settings.md`.
|
|
30
|
+
|
|
31
|
+
## Routine upgrades
|
|
32
|
+
|
|
33
|
+
**Wrangler**: `npm i -D wrangler@latest` in the site, then `npx wrangler deploy --dry-run`. Read the release notes for major versions.
|
|
34
|
+
|
|
35
|
+
**Compatibility date**: pin it in `wrangler.jsonc`. To bump: set it to today, `astro build`, `wrangler dev -c dist/server/wrangler.json` and click through, then use a gradual rollout (`deploy-and-release.md`) on busy sites. Compat changes rarely break a static-heavy Astro site, but they can change runtime behaviour (streams, `nodejs_compat` details).
|
|
36
|
+
|
|
37
|
+
**Leyline**: `npm i @theory-digital/leyline@<version>`. Check the changelog for new D1 migrations (copy them into `migrations/` and apply them remotely *before* deploying), new secrets or vars, and config changes. Then do the normal deploy.
|
|
38
|
+
|
|
39
|
+
## Hand a site to the client's own account
|
|
40
|
+
|
|
41
|
+
Resources can't be "moved" between accounts. You recreate them in the new account and switch traffic over. Plan the move with the user and confirm each write.
|
|
42
|
+
|
|
43
|
+
1. **Prerequisites**: the client's account exists, TD is a member, and the client has a plan if needed. Create an auth profile for it (`auth-and-accounts.md`).
|
|
44
|
+
2. **Zone**: if the zone is in TD's account, it has to move too. A zone can be *added* in the client account while it's active in TD's; the new one sits `pending`, needing a nameserver change. Alternatively, keep the zone in TD's account and move only the Worker. That doesn't work with custom domains, because they need the zone in the same account as the Worker. Usually both move.
|
|
45
|
+
3. **Recreate** in the client account:
|
|
46
|
+
- D1: `d1 export --remote` from the old account → `d1 create` in the new → `d1 execute --file` the export. Update `database_id`.
|
|
47
|
+
- Secrets: `secret put` each in the new account (values come from the password manager, `.dev.vars`, or new ones generated; Postmark tokens can stay the same).
|
|
48
|
+
- Turnstile: create a new widget in the new account → new sitekey/secret.
|
|
49
|
+
- R2 / KV: copy the objects; `rclone` with the S3 API works for R2.
|
|
50
|
+
4. Update `account_id` in `wrangler.jsonc`. Deploy to the new account's workers.dev and verify there.
|
|
51
|
+
5. **Cut over**: zone nameservers move to the new account's pair (if the zone moved), then attach custom domains in the new account. If both accounts claim the hostname, remove it from the old Worker first, accepting a brief gap, or schedule the change for a quiet time.
|
|
52
|
+
6. Verify, then decommission the old copies (below), and update CI secrets (new token and account ID).
|
|
53
|
+
|
|
54
|
+
## Rename a Worker or change the domain
|
|
55
|
+
|
|
56
|
+
- **Rename**: changing `name` deploys a *new* Worker. Deploy the new name with the routes, confirm it's serving, then `wrangler delete <old-name>` (confirm). Secrets don't carry over; re-put them.
|
|
57
|
+
- **New domain** (rebrand): add the new hostname as a custom domain alongside the old one, then update `site.url` and deploy. Make the old hostname 301 to the new one. Use a Single Redirect if it's in the same zone, Bulk Redirects or a tiny redirect Worker if it's a separate zone. Keep the old domain registered and redirecting for at least a year for SEO.
|
|
58
|
+
|
|
59
|
+
## Decommission a site
|
|
60
|
+
|
|
61
|
+
Confirm with the user, step by step. Deletes here are permanent.
|
|
62
|
+
|
|
63
|
+
1. Ask what the end state should be: redirect to another site, a holding page, or nothing at all.
|
|
64
|
+
2. **Back up**: `d1 export --remote`, a list of secrets (names), the R2 objects if any, `wrangler.jsonc`, and the zone's DNS export.
|
|
65
|
+
3. Detach the hostnames (remove `routes`, deploy), or point them at the replacement.
|
|
66
|
+
4. `npx wrangler delete <worker>` → `d1 delete` → `kv namespace delete` / R2 bucket delete (buckets must be emptied first) → Turnstile widget delete.
|
|
67
|
+
5. Zone: remove it only if the client is moving DNS elsewhere *and* their new DNS host is live. Deleting an active zone takes the domain's DNS down, **including email**.
|
|
68
|
+
6. Archive the repo and record what was deleted and where the backups are.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
Look up the error text. Each entry gives the cause and the fix. For "site is down" triage, start with `observability-and-incidents.md` instead.
|
|
4
|
+
|
|
5
|
+
## Auth and accounts
|
|
6
|
+
|
|
7
|
+
| Error / symptom | Cause → fix |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `Authentication error [code: 10000]` | Token lacks the permission for that endpoint, or belongs to another account. A stale `CLOUDFLARE_API_TOKEN` in the env overrides the OAuth login: `env \| grep CLOUDFLARE_`, unset it, or fix its scopes |
|
|
10
|
+
| `More than one account available but unable to select one in non-interactive mode` | Set `account_id` in `wrangler.jsonc` or `CLOUDFLARE_ACCOUNT_ID` |
|
|
11
|
+
| `You are not authenticated` | `npx wrangler login`, or `auth create <profile>`. In CI, the token secret isn't reaching the step |
|
|
12
|
+
| A deploy landed in the wrong account | Delete the stray Worker there (confirm), pin `account_id`, deploy again. Check whether a custom domain was attached in the wrong account too |
|
|
13
|
+
| OAuth callback never completes (WSL/remote) | Open the printed URL manually; if the localhost callback still fails, use an API token |
|
|
14
|
+
|
|
15
|
+
## Build and deploy
|
|
16
|
+
|
|
17
|
+
| Error / symptom | Cause → fix |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `Missing entry-point` / deploys an empty Worker | No fresh `astro build`, or running from the wrong directory. Build, then `wrangler deploy` from the site root |
|
|
20
|
+
| Deploy uses the source `wrangler.jsonc` instead of the built one | `-c wrangler.jsonc` was passed to deploy. Drop it; the `.wrangler/deploy/config.json` redirect points at `dist/server/wrangler.json` |
|
|
21
|
+
| `Could not resolve "astro"` / Vite can't find svelte | The framework packages must be direct dependencies of the *site* (Leyline README) |
|
|
22
|
+
| `binding DB of type d1 must have a valid id` / `database_id … 0000…` | Placeholder ID left in from the template. `d1 create` and paste the real ID |
|
|
23
|
+
| `A request to the Cloudflare API (/accounts/…/workers/scripts/…) failed` + `code: 10021` | Script validation error, usually a Node built-in import without `nodejs_compat`. Read the full message; add `"compatibility_flags": ["nodejs_compat"]` if a dependency needs it |
|
|
24
|
+
| Vars set in the dashboard disappeared | `wrangler deploy` replaces vars. Move them into `wrangler.jsonc`, or use `--keep-vars` |
|
|
25
|
+
| `Your Worker exceeded the size limit` | Usually a large dependency in the server bundle. Check that images and fonts are in `public/` (assets) and not imported into SSR code |
|
|
26
|
+
| `astro preview` gives 500 on prerendered pages | Known Astro 7.3 issue with `build.format: 'file'`. Use `wrangler dev -c dist/server/wrangler.json` |
|
|
27
|
+
|
|
28
|
+
## Domains and DNS
|
|
29
|
+
|
|
30
|
+
| Error / symptom | Cause → fix |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `Hostname '…' already has externally managed DNS records` (custom domain) | Delete the existing A/AAAA/CNAME for that host (confirm; this is the cutover), then deploy again |
|
|
33
|
+
| `Could not find zone for …` | The zone isn't in this account, or isn't active yet. Check `cf-api.sh GET '/zones?name=…'` |
|
|
34
|
+
| Error 1014 `CNAME Cross-User Banned` | The record points at a hostname proxied in another Cloudflare account. Use a custom domain in the account that owns the zone |
|
|
35
|
+
| Error 1001 / 1016 | The DNS record targets something that doesn't resolve. Usually a leftover record after a Worker was deleted |
|
|
36
|
+
| 522/523/525/526 on a hostname | The hostname is proxied to an origin, not attached to the Worker. Attach it as a custom domain, or fix the origin / SSL mode |
|
|
37
|
+
| `ERR_TOO_MANY_REDIRECTS` | SSL mode "Flexible" plus an HTTPS redirect, or www→apex and apex→www rules both present |
|
|
38
|
+
| Certificate error right after attaching | Edge cert issuance takes a minute or two (rarely longer). Check `cf-api.sh GET '$Z/ssl/certificate_packs'`. CAA records must allow Cloudflare's CAs (letsencrypt.org, pki.goog, ssl.com) |
|
|
39
|
+
| Site works on `www` but not apex (or vice versa) | Only one hostname is attached. Add both custom domains and a redirect |
|
|
40
|
+
|
|
41
|
+
## Leyline runtime
|
|
42
|
+
|
|
43
|
+
| Error / symptom | Cause → fix |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `[leyline] FORM_SIGNING_SECRET is not set` / token endpoint 500 | `secret put FORM_SIGNING_SECRET` (and `--env` if it's staging) |
|
|
46
|
+
| Form shows "please try again" for everyone | Token invalid: the secret was rotated recently (transient), or clock or timing problems. Tail and look for `rejected:` reasons |
|
|
47
|
+
| Form 403 | Origin check: the form was posted from a different host than the Worker sees (preview vs prod mix-up, or a proxy rewriting Host). Astro's `checkOrigin` also blocks cross-site posts |
|
|
48
|
+
| Form 429 | `FORM_RATE_LIMITER` triggered. Expected under spam; if real users hit it, raise `simple.limit` |
|
|
49
|
+
| `Form "x" delivery failed: …` | Postmark rejected it. See `forms-email-turnstile.md` → "Form emails aren't arriving" |
|
|
50
|
+
| Turnstile error 110200 in the browser console | The page's hostname isn't in the widget's domain list. `turnstile widget update` |
|
|
51
|
+
| robots.txt is missing the "AI crawlers are explicitly welcome" group in production | Cloudflare managed robots.txt is on. See `zone-settings.md` |
|
|
52
|
+
| AI crawlers get 403 | "Block AI bots" or an AI Crawl Control block. See `zone-settings.md` |
|
|
53
|
+
| Canonicals/sitemap point at workers.dev or the wrong domain | `site.url` in `astro.config.ts`. Rebuild and deploy |
|
|
54
|
+
| `/about` 404 but `/about.html` works | `trailingSlash`/`build.format` were overridden away from Leyline's defaults (`'never'`/`'file'`) |
|
|
55
|
+
| EmDash content missing in production | `d1()` binding name or database mismatch, or a migration/seed not applied remotely. Tail for `[leyline] EmDash … failed` |
|
|
56
|
+
|
|
57
|
+
## D1
|
|
58
|
+
|
|
59
|
+
| Error / symptom | Cause → fix |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `no such table: leyline_submissions` | Migrations not applied to this database (`--remote`), or applied to a different DB/env |
|
|
62
|
+
| `Migration … failed` | That file was rolled back; earlier ones stay applied. Fix the SQL, re-run `migrations apply` |
|
|
63
|
+
| `D1_ERROR: … too many SQL variables` / statement too long | Batch the inserts in smaller chunks (relevant when importing an export) |
|
|
64
|
+
| `wrangler d1 execute` hangs or waits for input | It's asking for confirmation. Add `-y` only after the user has approved that exact statement |
|
|
65
|
+
|
|
66
|
+
## When the answer isn't here
|
|
67
|
+
|
|
68
|
+
1. `npx wrangler <cmd> --help`, for current flags.
|
|
69
|
+
2. Fetch the product's `llms.txt` (e.g. `https://developers.cloudflare.com/d1/llms.txt`) and read the relevant page.
|
|
70
|
+
3. Search `https://github.com/cloudflare/workers-sdk/issues` for the exact error text. Wrangler regressions get reported there quickly.
|
|
71
|
+
4. Once it's solved, consider adding a row to this file, so the next session doesn't have to rediscover it.
|