Build an SEO Audit API with Next.js in 50 Lines
How to build a simple SEO audit API route using the Indxel SDK. Fetch a URL, validate metadata, return a score. Deploy to Vercel in minutes.
---
title: "SEO Audit API in Next.js — Build It in 50 Lines"
description: "How to build a simple SEO audit API route using the Indxel SDK. Fetch a URL, validate metadata, return a score. Deploy to Vercel in minutes."
tags: ["api", "nextjs", "sdk"]
---You pushed a component refactor on Friday. Monday morning, organic traffic dropped 40%. The culprit: 23 pages lost their <meta name="description"> tags because a layout prop changed. You shipped broken HTML to Googlebot, and it penalized you. Relying on marketing teams to manually click through staging links is a failed strategy. SEO validation belongs in the deployment pipeline, right next to your unit tests and ESLint checks.
Here is how you build an automated SEO audit API route in Next.js using the Indxel SDK.
Why does Next.js App Router require runtime SEO validation?
The Next.js App Router generates metadata dynamically at runtime based on database queries, meaning static analysis tools cannot detect missing tags before the page renders.
When you use generateMetadata, your SEO tags depend on external data. If your CMS API returns a null value for a post excerpt, Next.js renders the page without a meta description. ESLint cannot catch this. TypeScript cannot catch this. The failure happens at runtime.
To catch these regressions, you must audit the fully rendered HTML. You need a system that requests the URL, parses the DOM, checks the 15 critical metadata rules, and fails the build if the output is malformed.
Why build an SEO audit API instead of using a SaaS scanner?
Building your own API gives you programmatic control to block bad deployments in CI/CD, avoiding the latency and cost of generic third-party web crawlers.
External crawlers find out you broke SEO after you shipped to production. That is too late. By exposing an internal API route in your Next.js application, you can validate metadata locally, against preview deployments, and during GitHub pull request checks. You own the rules. You own the infrastructure. You control the timeout thresholds.
When you build the audit logic directly into your Next.js app, you avoid dealing with firewalls and authentication bypasses. The API route runs in the same environment as your application.
How do you set up the Indxel SDK in a Next.js project?
Install the @indxel/sdk package via npm and configure your API key in your environment variables to unlock the validation engine.
First, install the package:
npm install @indxel/sdkNext, add your API key to your .env.local file. You can generate a free key in the Indxel dashboard.
INDXEL_API_KEY=idx_live_xxxxxxxxxxxxxxxxxThe SDK ships with built-in TypeScript definitions. It exposes a single Indxel class that handles HTTP fetching, JavaScript rendering, DOM parsing, and rule evaluation. You do not need to install Puppeteer or Cheerio. The SDK handles the extraction layer.
How to write the Next.js API route in under 50 lines?
Create an App Router GET handler in app/api/audit/route.ts that accepts a URL query parameter, executes indxel.audit(), and returns the validation payload.
This route takes a url parameter, validates it, passes it to the Indxel engine, and formats the response.
// app/api/audit/route.ts
import { NextResponse } from 'next/server';
import { Indxel } from '@indxel/sdk';
const indxel = new Indxel({ apiKey: process.env.INDXEL_API_KEY });
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const targetUrl = searchParams.get('url');
if (!targetUrl) {
return NextResponse.json({ error: 'Missing url parameter' }, { status: 400 });
}
try {
// Basic URL validation
new URL(targetUrl);
} catch {
return NextResponse.json({ error: 'Invalid URL format' }, { status: 400 });
}
try {
const result = await indxel.audit(targetUrl, {
rules: ['strict-metadata', 'json-ld', 'canonical-match'],
timeoutMs: 8000,
renderJavaScript: true,
});
if (result.score < 50) {
console.warn(`[Indxel] Critical SEO failure on ${targetUrl}: Score ${result.score}`);
}
return NextResponse.json({
url: targetUrl,
score: result.score,
grade: result.grade,
issues: result.issues.map(issue => ({
rule: issue.ruleId,
message: issue.message,
severity: issue.severity,
})),
});
} catch (err) {
const message = err instanceof Error ? err.message : 'Audit failed';
return NextResponse.json({ error: message }, { status: 500 });
}
}This 46-line file is a complete SEO validation microservice. It handles input validation, orchestrates the headless browser execution via the SDK, maps the output to a clean JSON structure, and catches runtime errors.
What does the JSON response look like?
The API returns a strict JSON payload containing an aggregate score out of 100, a letter grade, and an array of specific rule violations with severity levels.
When you hit the endpoint GET /api/audit?url=https://your-domain.com/blog/hello-world, the SDK evaluates the 15 core rules. If a rule fails, it appends an issue to the array.
{
"url": "https://your-domain.com/blog/hello-world",
"score": 82,
"grade": "B",
"issues": [
{
"rule": "title-length",
"message": "Title is 72 characters. Expected 50-60 characters.",
"severity": "error"
},
{
"rule": "og-image-valid",
"message": "og:image URL returned HTTP 404.",
"severity": "error"
},
{
"rule": "meta-desc-length",
"message": "Meta description is 110 characters. Target 150-160 for optimal display.",
"severity": "warning"
}
]
}You get a deterministic score. 100 means perfect compliance. Anything below 90 requires attention. The severity field differentiates between critical indexing blockers (error) and optimization opportunities (warning).
What specific metadata rules does the SDK validate?
The Indxel SDK enforces 15 strict rules covering title length, description presence, og:image HTTP status, canonical URL resolution, and JSON-LD schema validity.
We do not test for keyword density or other abstract marketing metrics. We test for structural HTML integrity.
| Rule ID | Severity | Validation Logic |
|---|---|---|
title-present | Error | Fails if <title> is missing or empty. |
title-length | Warning | Fails if <title> is outside the 50-60 character range. |
meta-desc-present | Error | Fails if <meta name="description"> is missing. |
canonical-match | Error | Fails if <link rel="canonical"> does not match the requested URL. |
og-image-valid | Error | Extracts og:image URL and executes a HEAD request. Fails if status != 200. |
json-ld-valid | Error | Parses <script type="application/ld+json">. Fails if JSON is malformed. |
noindex-guard | Warning | Flags if robots meta tag contains noindex. Useful for catching staging leaks. |
The og-image-valid rule physically fetches the image URL. If your Next.js opengraph-image.tsx route is throwing a 500 error, this rule catches it before you share a broken link on Twitter.
How do you handle edge cases and timeouts in the audit route?
Wrap the execution in a try/catch block to return HTTP 400 for invalid URLs, HTTP 415 for non-HTML responses, and HTTP 504 for timeout failures.
If you deploy this API route to Vercel, you are bound by the serverless function timeout limits (10 seconds on the Hobby tier, 15 seconds on Pro). The Indxel SDK timeoutMs parameter must be strictly lower than your Vercel timeout limit to ensure the function returns a clean JSON error instead of an ugly Vercel 504 page.
You also need to guard against users passing non-HTML endpoints (like an image URL or a PDF) to the audit route. The SDK handles this internally by reading the content-type header before downloading the full payload, but you can configure the error handling:
// Catching specific Indxel errors
} catch (err) {
if (err.name === 'IndxelTimeoutError') {
return NextResponse.json({ error: 'Target URL took too long to respond' }, { status: 504 });
}
if (err.name === 'IndxelContentTypeError') {
return NextResponse.json({ error: 'Target URL is not text/html' }, { status: 415 });
}
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}How do you deploy this API and block PRs in CI/CD?
Push your code to Vercel, then add a bash script in your GitHub Actions workflow that queries the deployed API and exits with code 1 if the SEO score drops below 90.
Once your API route is deployed, it becomes a programmable gatekeeper. You can configure GitHub Actions to wait for the Vercel Preview URL to finish building, then run an audit against critical pages.
Create .github/workflows/seo-audit.yml:
name: SEO Audit
on: [pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- name: Wait for Vercel Preview
uses: patrickedqvist/wait-for-vercel-preview@v1.3.1
id: vercel
with:
token: ${{ secrets.GITHUB_TOKEN }}
max_timeout: 300
- name: Audit Homepage
run: |
URL="${{ steps.vercel.outputs.url }}"
API_URL="$URL/api/audit?url=$URL"
echo "Auditing $URL via $API_URL..."
RESPONSE=$(curl -s "$API_URL")
SCORE=$(echo "$RESPONSE" | jq '.score')
if [ -z "$SCORE" ] || [ "$SCORE" == "null" ]; then
echo "Failed to retrieve score. Response: $RESPONSE"
exit 1
fi
echo "SEO Score: $SCORE/100"
if [ "$SCORE" -lt 90 ]; then
echo "❌ SEO score below 90. Failing build."
echo "$RESPONSE" | jq '.issues[] | "[\(.severity)] \(.rule): \(.message)"'
exit 1
fi
echo "✅ SEO validation passed."When a developer removes a required prop from the <SEO /> component, the GitHub Action hits the /api/audit route. The route returns a score of 65. The bash script parses the score using jq, prints the specific rule violations to the GitHub Actions console, and exits with code 1. The pull request turns red. The bad code never reaches production.
How does Indxel compare to generic SEO crawlers?
Indxel runs natively in your application code with sub-second execution, whereas generic crawlers like Semrush rely on external HTTP scraping that takes minutes or hours.
If you are a developer, Semrush and Ahrefs are the wrong tools for pipeline validation. They are built for marketers analyzing competitor backlinks. You cannot easily trigger a Semrush site audit from a GitHub Action, and you certainly cannot get the results back in 3 seconds to block a pull request.
Indxel is objectively better for developers. The CLI and SDK output warnings in the same format as ESLint — one line per issue, with the rule ID and severity.
| Feature | Indxel SDK | Generic SEO Crawlers |
|---|---|---|
| Execution Environment | Native Next.js API / CI Pipeline | External SaaS Platform |
| Validation Speed | ~1.2 seconds per page | 10+ minutes per crawl |
| CI/CD Integration | Direct bash/YAML integration | Webhooks (if available) |
| Output Format | Strict JSON / ESLint-style CLI | PDF Reports / Dashboard UI |
| Target User | Developers writing code | Marketers analyzing trends |
What is the real-world impact of automated SEO validation?
Automating SEO audits in CI saves an average of 4 hours per release and guarantees that 100% of your pages pass core web metadata requirements before hitting production.
A typical Next.js app with 50 pages takes 3 seconds to validate using a parallelized map over the sitemap.xml. That is 3 seconds in CI that prevents missing canonical tags from reaching Googlebot.
Compare that to the alternative. You ship a bug. Googlebot crawls the broken page 12 hours later. The page drops out of the index. Traffic falls. Three days later, someone notices. You spend 4 hours debugging the Vercel deployment history to find the bad commit. You push a fix. You wait 48 hours for Google to recrawl the site.
You lose a week of organic traffic because you didn't have a 50-line API route guarding your deployment.
Frequently Asked Questions
Can I add custom validation rules?
Yes, you can pass a custom rules array to the indxel.audit() method to enforce company-specific metadata structures.
If your company requires every <title> to end with | Acme Corp, you can define a regex rule in the configuration object. The SDK will evaluate your custom rule alongside the built-in 15 rules and adjust the score accordingly.
const result = await indxel.audit(targetUrl, {
customRules: [
{
id: 'brand-title',
severity: 'error',
evaluate: (dom) => dom.title.endsWith('| Acme Corp')
}
]
});How does the API handle JavaScript-rendered content?
The Indxel SDK uses a headless browser engine under the hood to execute JavaScript, ensuring it sees the exact DOM that Googlebot sees.
If you are using Next.js Pages Router with heavy client-side rendering, or if you inject JSON-LD via a useEffect hook, a simple fetch() request will not see the metadata. By passing renderJavaScript: true to the audit function, Indxel waits for the DOM to hydrate before extracting the tags.
Does this API route add latency to my Next.js app?
No, because the API route is only invoked asynchronously by your CI/CD pipeline or admin dashboard, never blocking client-side page loads.
The /api/audit route sits completely outside of your user's critical rendering path. It consumes zero resources until your GitHub Action makes a GET request.
What happens if the target URL requires authentication?
You can pass custom headers, including Authorization tokens, directly through the Indxel SDK configuration object.
If you are auditing a staging environment protected by Vercel Authentication or Basic Auth, inject the required headers into the audit call:
const result = await indxel.audit(targetUrl, {
headers: {
'Authorization': `Bearer ${process.env.STAGING_TOKEN}`
}
});Validate your first URL
You don't need to wait for your next major release to start catching metadata errors. You can test the Indxel validation engine directly from your terminal right now.
Run the CLI to audit your local development server or a live URL:
npx indxel check https://your-domain.com