Integración de Webhook

Recibe artículos publicados desde Rankfender directamente en tu sitio o aplicación a través de 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:

  1. You create a database table to store articles
  2. You create a webhook endpoint on your server that receives articles from Rankfender
  3. 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.

Diagram showing the webhook flow: Rankfender publishes article, sends POST request to your endpoint, endpoint saves to database and returns the published URL

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

FieldTypeDescription
titletextThe article's headline
slugtextURL-friendly identifier (unique)
contenttextFull article body in HTML
excerpttextShort summary for listings
meta_titletextSEO title for the page
meta_descriptiontextSEO meta description
keywordstext[]Array of SEO keywords
canonical_urltextCanonical URL if different from page URL
featured_imagetextURL of the featured image
authortextAuthor name
languagetextLanguage 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.

Screenshot of the blog_articles table structure in Supabase

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"
}
Screenshot showing where to configure the webhook URL in Rankfender's integration settings

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] : [],
    },
  };
}
Screenshot of the blog page displaying articles received via webhook

Configuring the Webhook in Rankfender

  1. Go to Project Settings → Integrations
  2. Under "Webhook", click "Configure"
  3. Enter your webhook URL (e.g., https://yourdomain.com/api/rankfender/publish)
  4. Enter your webhook secret key (used in the x-rankfender-key header)
  5. Click "Test" to send a test payload
  6. Verify the test article appears in your database
  7. Click "Save"
Webhook configuration panel in Rankfender project settings showing URL and secret key fields

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:

EventDescription
content.publishedAn article was published -- full article payload included
content.updatedA published article was edited and republished
content.unpublishedAn article was taken offline
content.deletedAn 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

  1. Use the "Test" button in Rankfender's webhook settings to send a sample payload
  2. Check your database for the test article
  3. Verify your blog page displays it correctly
  4. Test error handling by temporarily breaking your endpoint

Troubleshooting

  • 401 Unauthorized: Check that the x-rankfender-key header value matches your RANKFENDER_WEBHOOK_SECRET environment 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)