This is the recommended approach for a Git-backed static blog (Next.js App Router, Astro, etc.) on a host that auto-deploys on push. If your framework supports writing files at runtime and on-demand revalidation, the simpler Static Sites flow may suit you instead.
How it works
- SEOPilot sends the
article.generatedwebhook to your endpoint. - Your handler verifies the signature, then maps the payload to an MDX file.
- The handler commits the file to
content/blog/<slug>.mdxvia the GitHub API. - Your host (Vercel/Netlify) sees the new commit and redeploys.
- Your existing static pipeline renders the post — live in a minute or two.
Prerequisites
- A Next.js blog that reads MDX from a
content/blog/directory (this guide uses the App Router). - The repo hosted on GitHub and connected to Vercel (or Netlify) with auto-deploy on push to your production branch.
- Your SEOPilot webhook secret from Settings → Integrations.
The payload
SEOPilot POSTs this JSON envelope to your endpoint. The handler in the next step mapsdata.article (and data.keyword) to an MDX file; the remaining fields — delivery_id, created_at, data.site, meta_title, internal_links — are there if you want them. See the Webhook guide for the full field reference.
payload-example.json
Step 1: Add the webhook route
Create a route handler that verifies the request, converts the payload to an MDX file, and commits it. The two helpers below keep the logic readable. First, the helpers — signature verification and payload-to-MDX mapping:lib/seopilot.ts
app/api/seopilot/route.ts
This route uses
node:crypto and Buffer, so it must run on the Node.js runtime — the default for App Router route handlers. Don’t add export const runtime = "edge". gray-matter (npm i gray-matter) safely serializes the frontmatter as YAML.toMdx to match your blog’s schema.
Step 2: Create a GitHub access token
The route commits files, so it needs a token with write access to your repo’s contents. Use a fine-grained personal access token scoped to a single repository.1
Open the token page
Go to GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens and click Generate new token.
2
Name it and set expiration
Give it a clear name (e.g.,
seopilot-publish) and choose an expiration. Fine-grained tokens expire, so set a reminder to rotate it.3
Limit repository access
Under Repository access, choose Only select repositories and select the repo your blog lives in.
4
Grant Contents write
Under Permissions → Repository permissions, set Contents to Read and write. Leave everything else as No access (GitHub adds Metadata: Read-only automatically — that’s expected).
5
Generate and copy
Click Generate token and copy it immediately — GitHub won’t show it again.
Step 3: Add environment variables
Add these to your hosting provider (in Vercel: Project → Settings → Environment Variables, Production), then redeploy so they take effect.Step 4: Connect and test
1
Set the webhook URL
In SEOPilot, go to Settings → Integrations and enter your endpoint, e.g.
https://yoursite.com/api/seopilot. Copy the signing secret into SEOPILOT_WEBHOOK_SECRET.2
Send a test delivery
Click Test webhook. You should see a
200 response in the delivery log.3
Confirm the commit and deploy
Check that a new commit appears under
content/blog/, your host redeploys, and the post is live at /blog/<slug> within a minute or two.Updates and retries
The handler is idempotent. When SEOPilot retries a delivery (up to 5 attempts) or regenerates an article, thecommitFile helper looks up the existing file first:
- No file yet → it creates one.
- Same content → it returns
skipped, so retries never produce duplicate commits. - Changed content → it updates the file with the existing SHA.
lastUpdated field to the frontmatter when the file already exists.
Security checklist
- Verify the raw body. Always call
verifySignatureon the unparsed body before doing anything else, and reject with401on failure. - Sanitize the slug. It becomes a file path — restrict it to
[a-z0-9-]and cap its length to block path traversal. - Sanitize the body.
sanitizeMdxBodyescapes{,}, and<and neutralizesimport/exportin prose (while preserving code blocks), so a stray brace can’t break your build and injected MDX can’t execute at build time. - Scope the token. Use a fine-grained, single-repo, Contents-only token. Store it only in your host’s environment variables — never commit it.