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.
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.
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
https://yoursite.com/api/reaudit-webhook)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);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 }
);
}
}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.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>
);
}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>
);
}markdownContent in every webhook — serve it for 3-5x higher AI citation rates.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',
},
});
}// 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`,
},
},
};
}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,
}));
}# 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
What's Included
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.// Filter by category from local DB
const seoArticles = await Article.find({
section: 'blog',
categories: 'AI Search',
}).lean();// Filter by language from local DB
const greekNews = await Article.find({
section: 'news',
language: 'el',
}).lean();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));Webhook not received
Signature verification failed
Content not appearing on site
rm -rf .nextYou're all set!
Your blog now receives content directly from Reaudit via webhooks. Content lives on your domain for maximum AI search visibility.
On this page