React / Next.js Integration
Publish AI-optimized content to your React or Next.js site via webhooks
When you publish content in Reaudit, the full article is pushed to your site via webhook. Your site stores it locally and serves it from your own domain — giving you full control and maximum AI search visibility.
Overview
How It Works
Publish in Reaudit
Click “Publish to React” in the dashboard. Reaudit sends the full article to your webhook.
Your Site Stores It
Your webhook endpoint verifies the signature, saves the article to your local database.
Serve on Your Domain
Your site serves content from its own database — AI search engines index YOUR domain, not Reaudit's.
Prerequisites
- ✓Reaudit Account — Sign up at reaudit.io
- ✓React Connection — Create a connection in Dashboard → Tools → React/Next.js
- ✓Webhook URL — A publicly accessible endpoint on your site (e.g., /api/reaudit-webhook)
- ✓Database — MongoDB, Postgres, or any database to store articles locally
- ✓React/Next.js Project — Next.js 13+ recommended (App Router)
Step 1: Environment Setup
Create a .env.local file in your project root:
# .env.local
REAUDIT_WEBHOOK_SECRET=your_api_secret_here.env.local to your .gitignore.Where to find your credentials
- Go to Reaudit Dashboard
- Navigate to Tools → React/Next.js
- Click “Connect React Site”
- Enter your webhook URL (e.g.,
https://yoursite.com/api/reaudit-webhook) - Copy your API Secret — this is your webhook signature secret
Step 2: Create Article Model
Create a model to store articles in your local database:
// models/Article.ts
import mongoose, { Schema, Document } from 'mongoose';
export interface IArticle extends Document {
externalId: string; // Reaudit content ID
title: string;
slug: string;
content: string; // Full HTML
excerpt?: string;
metaTitle?: string;
metaDescription?: string;
author?: string;
authorCard?: Record<string, any>;
keywords: string[];
categories: string[];
language: string;
section: string; // e.g., 'blog', 'news'
featuredImage?: string;
schemaMarkup?: string;
markdownContent?: string; // For AI crawlers
wordCount?: number;
publishedAt: Date;
}
const ArticleSchema = new Schema<IArticle>({
externalId: { type: String, required: true, unique: true },
title: { type: String, required: true },
slug: { type: String, required: true, unique: true },
content: { type: String, required: true },
excerpt: String,
metaTitle: String,
metaDescription: String,
author: String,
authorCard: Schema.Types.Mixed,
keywords: [String],
categories: [String],
language: { type: String, default: 'en' },
section: { type: String, default: 'blog' },
featuredImage: String,
schemaMarkup: String,
markdownContent: String,
wordCount: Number,
publishedAt: { type: Date, default: Date.now },
}, { timestamps: true });
export const Article =
mongoose.models.Article ||
mongoose.model<IArticle>('Article', ArticleSchema);Step 3: Webhook Endpoint
Create the endpoint that receives published articles from Reaudit:
// app/api/reaudit-webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
import { connectToDatabase } from '@/lib/db';
import { Article } from '@/models/Article';
function verifySignature(
payload: string,
signature: string,
secret: string
): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
export async function POST(req: NextRequest) {
try {
// 1. Verify webhook signature
const payload = await req.text();
const signature = req.headers.get('x-webhook-signature');
const secret = process.env.REAUDIT_WEBHOOK_SECRET!;
if (!signature || !verifySignature(payload, signature, secret)) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
// 2. Parse — full article is included in the payload
const webhook = JSON.parse(payload);
const article = webhook.data;
// 3. Save to your local database (upsert)
await connectToDatabase();
await Article.findOneAndUpdate(
{ externalId: webhook.contentId },
{
externalId: webhook.contentId,
title: article.title,
slug: article.slug,
content: article.content,
excerpt: article.excerpt,
metaTitle: article.metaTitle,
metaDescription: article.metaDescription,
author: article.author,
authorCard: article.authorCard,
keywords: article.tags || [],
categories: article.categories || [],
language: article.language || 'en',
section: article.section || 'blog',
featuredImage: article.featuredImage,
schemaMarkup: article.schemaMarkup,
markdownContent: article.markdownContent,
wordCount: article.wordCount,
publishedAt: new Date(webhook.timestamp),
},
{ upsert: true, new: true }
);
return NextResponse.json({ success: true });
} catch (error) {
console.error('Webhook error:', error);
return NextResponse.json(
{ error: 'Processing failed' },
{ status: 500 }
);
}
}Webhook Payload
Reaudit sends the complete article in the webhook — no second download call needed:
// Webhook payload — sent automatically by Reaudit
{
"event": "content.published",
"contentId": "691c3d31e5034e2c607dfe72",
"timestamp": "2026-02-12T10:30:00.000Z",
"data": {
"title": "Guide: AI Search Optimization",
"slug": "ai-search-optimization",
"content": "<h1>...</h1><p>Full HTML...</p>",
"excerpt": "Brief excerpt...",
"metaTitle": "AI Search Optimization | Guide",
"metaDescription": "Learn how to...",
"author": "Rose Samaras",
"authorCard": { "name": "...", "bio": "..." },
"tags": ["seo", "ai-search"],
"categories": ["guides"],
"language": "en",
"section": "blog",
"featuredImage": "https://...",
"schemaMarkup": "{\"@context\":\"https://schema.org\"...}",
"markdownContent": "---\ntitle: ...\n---\n...",
"wordCount": 2500,
"seoScore": 85,
"readabilityScore": 75
}
}X-Webhook-Signature header — an HMAC SHA-256 of the payload using your API Secret. Always verify this.Step 4: Blog Article Page
Create app/blog/[slug]/page.tsx — reads from your local database:
// app/blog/[slug]/page.tsx
import { connectToDatabase } from '@/lib/db';
import { Article } from '@/models/Article';
import { notFound } from 'next/navigation';
export async function generateMetadata({ params }) {
await connectToDatabase();
const article = await Article.findOne({ slug: params.slug }).lean();
if (!article) return { title: 'Not Found' };
return {
title: article.metaTitle || article.title,
description: article.metaDescription || article.excerpt,
};
}
export async function generateStaticParams() {
await connectToDatabase();
const articles = await Article.find({ section: 'blog' })
.select('slug')
.lean();
return articles.map((a) => ({ slug: a.slug }));
}
export default async function BlogArticle({ params }) {
await connectToDatabase();
const article = await Article.findOne({ slug: params.slug }).lean();
if (!article) notFound();
return (
<main className="max-w-4xl mx-auto px-4 py-12">
{article.featuredImage && (
<img
src={article.featuredImage}
alt={article.title}
className="w-full h-auto rounded-lg mb-8"
/>
)}
<h1 className="text-4xl font-bold mb-4">{article.title}</h1>
{article.excerpt && (
<p className="text-xl text-gray-600 mb-4">{article.excerpt}</p>
)}
<div
className="prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: article.content }}
/>
{article.schemaMarkup && (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: article.schemaMarkup }}
/>
)}
</main>
);
}Step 5: Article Listing
Create app/blog/page.tsx:
// app/blog/page.tsx
import { connectToDatabase } from '@/lib/db';
import { Article } from '@/models/Article';
import Link from 'next/link';
export default async function BlogListing() {
await connectToDatabase();
const articles = await Article.find({ section: 'blog' })
.sort({ publishedAt: -1 })
.limit(12)
.lean();
return (
<main className="max-w-7xl mx-auto px-4 py-12">
<h1 className="text-4xl font-bold mb-8">Blog</h1>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{articles.map((article) => (
<Link
key={article._id.toString()}
href={`/blog/${article.slug}`}
className="group"
>
<article className="border rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
{article.featuredImage && (
<img
src={article.featuredImage}
alt={article.title}
className="w-full h-48 object-cover"
/>
)}
<div className="p-6">
<h2 className="text-xl font-bold mb-2 group-hover:text-blue-600">
{article.title}
</h2>
<p className="text-gray-600 line-clamp-3">
{article.excerpt}
</p>
</div>
</article>
</Link>
))}
</div>
</main>
);
}Step 6: LLM-Optimized Markdown
markdownContent in every webhook — serve it for 3-5x higher AI citation rates.6a. Create Markdown Endpoint
Serve markdown from your local database:
// app/blog/[slug]/markdown/route.ts
import { connectToDatabase } from '@/lib/db';
import { Article } from '@/models/Article';
export async function GET(req, { params }) {
await connectToDatabase();
const article = await Article.findOne({ slug: params.slug }).lean();
if (!article?.markdownContent) {
return new Response('Not Found', { status: 404 });
}
return new Response(article.markdownContent, {
headers: {
'Content-Type': 'text/markdown; charset=utf-8',
'Cache-Control': 'public, max-age=3600',
},
});
}6b. Add Alternate Link to Blog Pages
// In your blog page generateMetadata
export async function generateMetadata({ params }) {
const article = await getArticle(params.slug);
return {
title: article.title,
description: article.metaDescription,
alternates: {
types: {
// Markdown version for LLM crawlers
'text/markdown': `/blog/${article.slug}/markdown`,
},
},
};
}6c. Create Your Sitemap
Generate a sitemap pointing to YOUR domain:
// app/sitemap.ts
import { connectToDatabase } from '@/lib/db';
import { Article } from '@/models/Article';
export default async function sitemap() {
await connectToDatabase();
const articles = await Article.find({}).lean();
return articles.map((article) => ({
url: `https://yourdomain.com/blog/${article.slug}`,
lastModified: article.publishedAt,
changeFrequency: 'weekly',
priority: 0.8,
}));
}6d. Update robots.txt
# public/robots.txt
User-agent: ChatGPT-User
User-agent: PerplexityBot
User-agent: ClaudeBot
User-agent: Google-Extended
Allow: /
Sitemap: https://your-domain.com/sitemap.xmlBenefits
- 3-5x higher AI citation rates
- Better ChatGPT visibility
- Improved Perplexity ranking
- Claude-friendly format
What's Included
- Clean markdown content
- Enhanced JSON-LD schema
- SEO metadata & YAML frontmatter
- Structured headings
Step 7: Deploy to Production
Add your webhook secret to your hosting platform:
Vercel
Project Settings → Environment Variables → Add REAUDIT_WEBHOOK_SECRET
Self-hosted / Docker
Add REAUDIT_WEBHOOK_SECRET to your .env or docker-compose.yml
https://yoursite.com/api/reaudit-webhook). Content will be pushed automatically whenever you publish.Advanced Examples
Filter by Category
// Filter by category from local DB
const seoArticles = await Article.find({
section: 'blog',
categories: 'AI Search',
}).lean();Filter by Language
// Filter by language from local DB
const greekNews = await Article.find({
section: 'news',
language: 'el',
}).lean();Pagination
const page = 2;
const limit = 12;
const articles = await Article.find({ section: 'blog' })
.sort({ publishedAt: -1 })
.skip((page - 1) * limit)
.limit(limit)
.lean();
const total = await Article.countDocuments({ section: 'blog' });
console.log('Total pages:', Math.ceil(total / limit));Troubleshooting
Webhook not received
- Webhook URL must be publicly accessible (not localhost)
- Endpoint must accept POST requests
- Check Reaudit dashboard for delivery errors
Signature verification failed
- Use API Secret (not API Key) for verification
- Hash the raw request body as text, not parsed JSON
- Check for extra spaces in environment variable
Content not appearing on site
- Check your database for the saved article
- Verify the webhook handler saves correctly
- Clear Next.js cache if using ISR:
rm -rf .next
You're all set!
Your blog now receives content directly from Reaudit via webhooks. Content lives on your domain for maximum AI search visibility.
- →Create more content in the Reaudit dashboard
- →Configure content sections (Blog, News, etc.) for organization
- →Add search functionality to your blog
- →Set up multilingual content with language filters
On this page