Webhook-Integration
Empfangen Sie veröffentlichte Artikel von Rankfender direkt in Ihrer Website oder Anwendung über Webhooks.
What are Webhooks?
Webhooks allow Rankfender to push data to your application in real time. Instead of polling the API, your server receives an HTTP request whenever an event occurs -- such as an article being published.
This is ideal for custom websites not built on WordPress. Whether you use Next.js, Nuxt, Astro, SvelteKit, or any other framework, webhooks let Rankfender publish content directly to your site.
How It Works
The integration follows three simple steps:
- You create a database table to store articles
- You create a webhook endpoint on your server that receives articles from Rankfender
- Your blog pages read from the database instead of hardcoded content
When Rankfender publishes an article, it sends the full article data to your endpoint. Your endpoint saves it to the database and returns the published URL. That's it.

Step 1: Database Table
Create a single table to store the articles Rankfender sends. Below is an example using Supabase (PostgreSQL), but this works with any database.
Table Schema: blog_articles
CREATE TABLE blog_articles (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
title text NOT NULL,
slug text NOT NULL UNIQUE,
content text NOT NULL,
excerpt text DEFAULT '',
meta_title text DEFAULT '',
meta_description text DEFAULT '',
keywords text[] DEFAULT '{}',
canonical_url text DEFAULT '',
featured_image text DEFAULT '',
author text DEFAULT 'Rankfender',
language text DEFAULT 'en',
status text DEFAULT 'published',
published_at timestamptz DEFAULT now(),
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);Field Descriptions
| Field | Type | Description |
|---|---|---|
| title | text | The article's headline |
| slug | text | URL-friendly identifier (unique) |
| content | text | Full article body in HTML |
| excerpt | text | Short summary for listings |
| meta_title | text | SEO title for the page |
| meta_description | text | SEO meta description |
| keywords | text[] | Array of SEO keywords |
| canonical_url | text | Canonical URL if different from page URL |
| featured_image | text | URL of the featured image |
| author | text | Author name |
| language | text | Language code (en, fr, es, etc.) |
Security (Row Level Security)
If using Supabase, enable Row Level Security to ensure the public can only read articles and never write:
-- Enable RLS ALTER TABLE blog_articles ENABLE ROW LEVEL SECURITY; -- Public can read published articles CREATE POLICY "Public can read published articles" ON blog_articles FOR SELECT USING (status = 'published'); -- Only the service role (server-side) can insert/update -- No INSERT/UPDATE/DELETE policies for anon or authenticated -- The service role key bypasses RLS entirely
The webhook endpoint uses the service role key (server-side only) to write articles. This key bypasses RLS and is never exposed to the browser.

Step 2: Webhook Endpoint
Create an API route on your server that receives the article payload from Rankfender. Below is a complete example using Next.js API Routes with Supabase.
Endpoint
POST /api/rankfender/publish
Payload Sent by Rankfender
When an article is published, Rankfender sends a POST request with this JSON body:
{
"title": "10 Best SEO Tools for Small Businesses in 2025",
"slug": "best-seo-tools-small-businesses-2025",
"content": "<h2>Introduction</h2><p>Finding the right SEO tools...</p>...",
"excerpt": "Discover the top SEO tools that help small businesses...",
"meta_title": "10 Best SEO Tools for Small Businesses (2025 Guide)",
"meta_description": "Compare the best SEO tools for small businesses...",
"keywords": ["seo tools", "small business seo", "seo software"],
"canonical_url": "",
"featured_image": "https://cdn.rankfender.com/images/seo-tools-hero.jpg",
"author": "Rankfender",
"language": "en"
}Example: Next.js API Route
Create the file pages/api/rankfender/publish.ts (or app/api/rankfender/publish/route.ts for the App Router):
// app/api/rankfender/publish/route.ts
import { createClient } from '@supabase/supabase-js';
import { NextResponse } from 'next/server';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY! // Server-side only!
);
export async function POST(request: Request) {
// 1. Verify the request comes from Rankfender (optional but recommended)
const apiKey = request.headers.get('x-rankfender-key');
if (apiKey !== process.env.RANKFENDER_WEBHOOK_SECRET) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// 2. Parse the payload
const body = await request.json();
const { title, slug, content, excerpt, meta_title,
meta_description, keywords, canonical_url,
featured_image, author, language } = body;
// 3. Validate required fields
if (!title || !slug || !content) {
return NextResponse.json(
{ error: 'Missing required fields: title, slug, content' },
{ status: 400 }
);
}
// 4. Upsert into the database (insert or update if slug exists)
const { data, error } = await supabase
.from('blog_articles')
.upsert(
{
title,
slug,
content,
excerpt: excerpt || '',
meta_title: meta_title || title,
meta_description: meta_description || excerpt || '',
keywords: keywords || [],
canonical_url: canonical_url || '',
featured_image: featured_image || '',
author: author || 'Rankfender',
language: language || 'en',
status: 'published',
published_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
{ onConflict: 'slug' }
)
.select()
.maybeSingle();
if (error) {
return NextResponse.json(
{ error: 'Database error', details: error.message },
{ status: 500 }
);
}
// 5. Return the published URL so Rankfender knows where the article lives
const lang = language || 'en';
const publishedUrl = `/${lang}/resources/blog/${slug}`;
return NextResponse.json({
success: true,
url: publishedUrl,
id: data?.id,
});
}Expected Response
Your endpoint must return a JSON response with the published URL. Rankfender uses this to confirm the article was received:
// Success (200)
{
"success": true,
"url": "/en/resources/blog/best-seo-tools-small-businesses-2025"
}
// Error (400/401/500)
{
"error": "Missing required fields: title, slug, content"
}
Step 3: Blog Reads from Database
Update your blog listing page and article page to fetch content from the database. Here are examples for Next.js with Supabase.
Blog Listing Page
// app/[lang]/resources/blog/page.tsx
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
export default async function BlogPage({ params }) {
const { lang } = params;
const { data: articles } = await supabase
.from('blog_articles')
.select('title, slug, excerpt, featured_image, published_at, author')
.eq('status', 'published')
.eq('language', lang)
.order('published_at', { ascending: false });
return (
<div>
<h1>Blog</h1>
{articles?.map((article) => (
<a key={article.slug} href={`/${lang}/resources/blog/${article.slug}`}>
<h2>{article.title}</h2>
<p>{article.excerpt}</p>
</a>
))}
</div>
);
}Individual Article Page
// app/[lang]/resources/blog/[slug]/page.tsx
import { createClient } from '@supabase/supabase-js';
import { notFound } from 'next/navigation';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
export default async function ArticlePage({ params }) {
const { slug } = params;
const { data: article } = await supabase
.from('blog_articles')
.select('*')
.eq('slug', slug)
.eq('status', 'published')
.maybeSingle();
if (!article) return notFound();
return (
<article>
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.content }} />
</article>
);
}
// Generate SEO metadata
export async function generateMetadata({ params }) {
const { slug } = params;
const { data: article } = await supabase
.from('blog_articles')
.select('meta_title, meta_description, keywords, featured_image')
.eq('slug', slug)
.maybeSingle();
if (!article) return {};
return {
title: article.meta_title,
description: article.meta_description,
keywords: article.keywords?.join(', '),
openGraph: {
images: article.featured_image ? [article.featured_image] : [],
},
};
}
Configuring the Webhook in Rankfender
- Go to Project Settings → Integrations
- Under "Webhook", click "Configure"
- Enter your webhook URL (e.g.,
https://yourdomain.com/api/rankfender/publish) - Enter your webhook secret key (used in the
x-rankfender-keyheader) - Click "Test" to send a test payload
- Verify the test article appears in your database
- Click "Save"

Security
API Key Verification
Rankfender sends a secret key in the x-rankfender-key header with every webhook request. Your endpoint should verify this header to ensure the request comes from Rankfender:
const apiKey = request.headers.get('x-rankfender-key');
if (apiKey !== process.env.RANKFENDER_WEBHOOK_SECRET) {
return new Response('Unauthorized', { status: 401 });
}Service Role Key
The webhook endpoint uses the Supabase service role key to write to the database. This key:
- Bypasses Row Level Security (required for server-side writes)
- Must only be used server-side (API routes, edge functions)
- Must never be exposed in client-side code
Public Read Access
Blog pages use the anon key to read articles. With the RLS policy above, the public can only read published articles -- they can never insert, update, or delete.
Webhook Events
Rankfender can send webhooks for multiple event types:
| Event | Description |
|---|---|
| content.published | An article was published -- full article payload included |
| content.updated | A published article was edited and republished |
| content.unpublished | An article was taken offline |
| content.deleted | An article was permanently deleted |
Retry Policy
If your endpoint returns an error (non-2xx status), Rankfender retries the webhook:
- 1st retry: after 1 minute
- 2nd retry: after 5 minutes
- 3rd retry: after 30 minutes
- After 3 failed retries, the webhook is marked as failed in the Activity Feed
Testing Your Webhook
- Use the "Test" button in Rankfender's webhook settings to send a sample payload
- Check your database for the test article
- Verify your blog page displays it correctly
- Test error handling by temporarily breaking your endpoint
Troubleshooting
- 401 Unauthorized: Check that the
x-rankfender-keyheader value matches yourRANKFENDER_WEBHOOK_SECRETenvironment variable - 400 Bad Request: The payload is missing required fields (title, slug, or content)
- 500 Server Error: Check your database connection and ensure the table exists
- Webhook not firing: Verify the webhook URL is saved in Rankfender's integration settings and that the article was published (not just saved as draft)