skills/netlify-forms-preflight/SKILL.md
Verify a Netlify-hosted form is registered and accepting submissions BEFORE distributing any offline asset (flyer, QR code, business card, print ad, physical signage) that points at the form. Catches the silent failure mode where Netlify only indexes forms seen during a build with form-detection enabled — submissions made before the form is registered hit the static 404 and are irrecoverable. Use this skill whenever the user is about to print/distribute QR codes, flyers, business cards, or any offline material that drives leads to a Netlify-hosted form; whenever a form "works in prod but no submissions are showing up"; whenever the user says "I enabled forms but can't see anything"; or proactively after merging any PR that ships a new Netlify-forms-backed lead capture page. Also covers verifying email notifications are wired so leads are not silently sitting in a dashboard nobody checks.
npx skillsauth add razbakov/skills netlify-forms-preflightInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Netlify Forms has a subtle failure mode that burns lead-gen launches: forms only catch submissions after Netlify's build-time HTML scanner has seen them in a deploy with form detection enabled. Before that moment, the form's POST target does not exist server-side — visitors' browsers send the POST, Netlify returns the static 404 page, and the submission vanishes. There is no buffer, no queue, no recovery path.
This matters most when offline assets are already in circulation. Once QR codes are printed and flyers are handed out, every scan is a one-shot chance to capture a lead. If the form isn't registered the moment the first flyer hits the street, those leads are gone.
The fix is a five-minute pre-flight check. Do it BEFORE printing. If it's too late — flyers are out — do it now and at least bound the loss to what's already been missed.
Invoke this skill when:
data-netlify="true", form-name, or Netlify form detection in a troubleshooting contextnetlify status. If not logged in, user runs netlify login.my-site) or the public URL is enough — the skill looks up the site_id./contact), and the value of the form's name attribute / form-name hidden input.Netlify's API indexes everything by site_id (a UUID), not by site name. List the user's sites and filter:
netlify api listSites 2>&1 | python3 -c "
import sys, json
d = json.load(sys.stdin)
q = '<name-or-url-substring>' # e.g. 'razbakov' or 'mysite.com'
for s in d:
if q.lower() in (s.get('name','') + s.get('url','')).lower():
print(s.get('name'), '|', s.get('id'), '|', s.get('url'))
"
Capture the UUID into a shell var for reuse: SITE_ID=<uuid>.
Fetch the page and confirm the form markup is present in the prerendered HTML. Netlify's scanner only sees what's in the static HTML at build time — forms that only exist after client-side Vue/React hydration will not be detected.
curl -s "https://<site-domain>/<form-page>" -o /tmp/page.html
# Confirm form markup is server-rendered (not Vue/React-only):
grep -oE '<form[^>]*name="[^"]+"[^>]*>' /tmp/page.html
grep -oE 'data-netlify="[^"]*"|netlify-honeypot|name="form-name"' /tmp/page.html | sort -u
grep -oE '<input[^>]*name="[^"]+"' /tmp/page.html | sort -u
What to check:
<form> element with a name= attribute matching what the skill expectsdata-netlify="true" OR the netlify boolean attribute (both work)<input name="form-name" value="<form-name>"> — Netlify requires this on submission so it knows which form the POST belongs toIf any of the above is missing, the form will never be detected. Fix the markup and redeploy before continuing.
This is the step that catches the most common failure.
netlify api listSiteForms --data="{\"site_id\":\"$SITE_ID\"}" 2>&1 | python3 -c "
import sys, json
forms = json.load(sys.stdin)
if not forms:
print('NO FORMS REGISTERED — submissions will 404')
for f in forms:
print(f\"{f['name']:<30} submissions={f['submission_count']:<4} last={f.get('last_submission_at')} created={f['created_at']}\")
"
Decision tree:
created_at is recent → Registration is fresh. Any submissions that happened before created_at were lost. Note this timestamp — the skill will need to report it to the user so they can estimate the loss.created_at is old → Form is live and has been for a while. Continue to Step 5.If the form is not registered:
https://app.netlify.com/sites/<site-name>/settings/forms → toggle "Form detection" on.cd <project-dir>
git commit --allow-empty -m "chore: trigger rebuild for Netlify form detection"
git push
Alternative without a git push: the user can click "Trigger deploy → Deploy site" in the Netlify dashboard.
The form is registered — but there's one more thing that can go wrong: the POST URL. Netlify forms POST to / by default (or to the form's action URL). If the site is behind redirects, edge functions, or a custom router that swallows root POSTs, submissions still die.
Test with a real POST using an obviously-test payload:
TS=$(date +%s)
curl -sS -o /tmp/resp.html -w "HTTP %{http_code}\n" \
-X POST "https://<site-domain>/" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "form-name=<form-name>&<field1>=preflight-probe-$TS&<field2>=...&bot-field="
What to expect:
HTTP 200 → submission accepted. Go to Step 6 and verify it arrived.HTTP 303 or 302 → also normal; Netlify often redirects to a success page.HTTP 404 → form is NOT handled at this URL. Either the action attribute points somewhere else, or a redirect rule is eating the POST. Fix and retry.HTTP 400 → Netlify's spam filter blocked it. Look less suspicious (don't put "debug" / "test" in the payload), or proceed — real submissions with normal content will pass.Include a bot-field= empty value in the POST if the form has data-netlify-honeypot="bot-field" — the honeypot expects that field to exist but be empty.
netlify api listSiteSubmissions --data="{\"site_id\":\"$SITE_ID\"}" 2>&1 | python3 -c "
import sys, json
subs = json.load(sys.stdin)
print(f'total: {len(subs)}')
for s in subs[:5]:
print(s.get('created_at'), '|', s.get('form_name'), '|', json.dumps(s.get('data', {}), ensure_ascii=False)[:200])
"
The probe submission should appear at the top with a timestamp matching the POST from Step 5. If it does, the form is fully wired: server accepts POSTs, registers them against the right form, stores the data.
If the submission is NOT there despite a 200 from Step 5, something is eating it between Netlify's edge and the form processor. This is rare — most likely a site redirect rule or an overly aggressive spam filter. Check the Netlify deploy logs for a "form submission" line.
Form submissions default to dashboard-only visibility. If the user doesn't check the Netlify dashboard every day, leads rot there. Before declaring the form ready:
https://app.netlify.com/sites/<site-name>/forms.This step is not API-automatable via the public netlify CLI — it's a dashboard-only action. Hand it to the user with the exact URL.
Produce a concise status report:
Netlify Forms pre-flight — <site-name>
Form: <form-name> (id <form-id>)
Registered: <created_at> → submissions before this timestamp were lost
Submission count: <n>
Probe POST (Step 5): HTTP <code>, arrived in dashboard: yes/no
Email notifications: configured / NOT YET — user action required
Dashboard: https://app.netlify.com/sites/<site-name>/forms
Ready to distribute: YES / NO
Blockers: <list>
If distribution has already happened and registration is recent, explicitly state the loss window:
Flyers distributed before <created_at> scanned into a 404. Submissions from that window are not recoverable.
curl -sI <url> | grep -iE 'server|cache-status' — Netlify Edge hits show up in cache-status.For any lead-gen form that drives from offline assets (where failure is especially costly), instrument a second source of truth alongside Netlify Forms:
posthog.capture('form_submit', {...}) (or GA equivalent) on client-side form submit, before the POST. If Netlify silently breaks, analytics still shows the intent.This is not part of the pre-flight check itself — it's a hardening recommendation to make the next failure recoverable (see the form-signup-recovery skill for how to back-fill from a Resend email log).
development
Seed a new or empty Instagram account with a 9-post grid (3×3) so the profile looks established the moment a new visitor lands. Designed for festivals, new businesses, product launches, conferences, communities — any time an empty IG profile would hurt conversion from external traffic (QR scans, flyer drops, cross-promo). Generates assets via /image-from-gemini (per content-publishing rules — never HTML), writes captions with hashtag sets, and outputs a posting order + cadence plan. Trigger generously: phrases like '9 posts for instagram', 'fill my IG', 'starter grid', 'launch grid', 'instagram seed', '9-post grid', 'IG account not to look empty', 'first instagram posts', 'feed bootstrap', '3x3 grid', 'instagram launch content'. Even if the user mentions only one piece (just the images, just the captions, just the order), use this skill — the grid only works as an integrated bundle.
testing
Translate one English blog post into multiple target languages via parallel sub-agents, preserving frontmatter conventions, hero image, and brand voice. Use when the user shares a published English post URL or markdown path and says 'translate it', 'add other languages', 'publish in DE/ES/RU/UK', 'translate to 5 languages', or asks for localized versions of a specific post.
development
Build a complete press kit for an event, product launch, or campaign — in multiple languages — and publish it as a shareable Google Drive folder ready to send to journalists, partners, or a delegate. Produces press releases (typically DE/EN/ES, or configurable), uploads press photos and flyers, creates an Overview document for at-a-glance briefing, and creates a Handover document with pending tasks, contacts, risks, and decisions so press distribution can be delegated. Use when the user says 'I need a press release', 'create a press kit', 'press release in X languages', 'set up a Drive folder for press', 'handover doc for someone else to run press', or has an upcoming announcement that needs to be sent to media. Trigger generously: even partial requests (just a press release, just a flyer folder) typically evolve into the full kit.
development
Track ticket sales for a live event (concert, festival, conference, workshop) with daily snapshots, generate a burndown chart comparing actual sales to ideal-linear targets and tier-cumulative milestones, and report whether the event is on pace. Use when the user asks how sales are going, wants to know if their event will sell out, asks for a daily sales report, wants to set up sales tracking for an upcoming event, or asks about ticket pace / velocity / projection. Trigger generously: phrases like 'how is concert sales going', 'burndown for my event', 'are we going to sell out', 'sales velocity', 'daily ticket chart', 'how many tickets do we need to sell', or any case where the user has a ticketed event with a fixed sales window and wants visibility on pacing.