Custom Webhook Integration
Connect BlogSEO to any platform or custom application using webhooks. Receive article data via HTTP POST requests whenever articles are published.
Custom webhooks allow you to integrate BlogSEO with any system that can receive HTTP requests, including custom CMS platforms, static site generators, headless CMS systems, or your own backend services.
Setting Up Webhook Integration
Navigate to Settings → Integrations in your BlogSEO dashboard, click Custom Webhook, then click Create Webhook. You'll need to configure:
1. Webhook URL
Enter the endpoint URL where BlogSEO will send article data. This must be a publicly accessible HTTPS URL that accepts POST requests.
https://your-app.com/api/webhooks/blogseo
2. Content Format
Choose how you want to receive article content:
- Markdown: Raw markdown content with formatting (recommended for further processing)
- HTML: Pre-converted HTML ready for display
3. Authentication Method
Select how BlogSEO should authenticate requests to your endpoint:
Generated Shared Secret (Recommended)
BlogSEO generates a secure 64-character hex string that is sent with every request in the X-Webhook-Secret header. You'll need to store this secret and verify it on your server.
Custom Header
Use your own authentication header. Specify a custom header name and value (e.g., Authorization: Bearer your-api-key).
Webhook Payload Structure
When an article is published, BlogSEO sends a POST request with the following JSON payload:
{
"article": {
"id": "01234567-89ab-cdef-0123-456789abcdef",
"slug": "your-article-title-as-url-slug",
"title": "Your Article Title",
"content": "# Article content in markdown or HTML...",
"format": "markdown",
"published_at": "2024-01-15T10:30:00.000Z",
"main_image_url": "https://storage.blogseo.io/images/article-image.webp",
"meta_description": "An SEO-optimized description for search engine results, under 160 characters.",
"keyword": "target seo keyword"
},
"main_image": {
"url": "https://storage.blogseo.io/images/article-image.webp",
"alt": "Your Article Title"
},
"website": {
"id": "fedcba98-7654-3210-fedc-ba9876543210",
"baseUrl": "https://your-website.com"
},
"timestamp": "2024-01-15T10:30:00.000Z"
}
Payload Fields
| Field | Type | Description |
|---|---|---|
article.id | string (UUID) | Unique identifier for the article |
article.slug | string | URL-friendly slug generated from the title |
article.title | string | The article headline |
article.content | string | Article content in markdown or HTML format |
article.format | string | Either "markdown" or "html" |
article.published_at | string | ISO 8601 timestamp of publication |
article.main_image_url | string | URL of the featured image |
article.meta_description | string | SEO meta description for search engines (under 160 characters) |
article.keyword | string | null | The targeted SEO keyword, or null if none |
main_image.url | string | Same as article.main_image_url |
main_image.alt | string | Alt text for the image (same as title) |
website.id | string (UUID) | Unique identifier for the website |
website.baseUrl | string | Base URL of your website |
timestamp | string | ISO 8601 timestamp of when the webhook was sent |
Slug
The slug field is an SEO-optimized, URL-friendly string automatically generated from the article title. Use it as the pathname for the page displaying the article content (e.g., /blog/your-article-slug). If you're storing articles in a database, consider indexing this field for better query performance.
Receiving Article Updates
When you republish an article to your custom webhook (for example, after editing the content in BlogSEO), your endpoint receives the same webhook request format as for new articles. The key to distinguishing between new articles and updates is the article.id field.
The article.id is a unique UUID that remains identical across all publications of the same article. This allows you to:
- Deduplicate requests: Check if an article with this ID already exists in your database
- Update existing content: Replace the content of the existing article instead of creating a duplicate
- Track article history: Maintain version history by comparing the new content with the previous version
Implementation Example
Handling article updates
// Check if article already exists
const existingArticle = await db.article.findUnique({
where: { blogseoId: article.id },
});
if (existingArticle) {
// Update existing article
await db.article.update({
where: { blogseoId: article.id },
data: {
title: article.title,
content: article.content,
slug: article.slug,
updatedAt: new Date(),
},
});
} else {
// Create new article
await db.article.create({
data: {
blogseoId: article.id,
title: article.title,
content: article.content,
slug: article.slug,
},
});
}
Store the article.id (BlogSEO's UUID) in your database to enable proper
deduplication. This is separate from your own internal article IDs.
Verifying Webhook Requests
Always verify that incoming webhook requests are authentic by checking the shared secret or custom header.
Verifying a BlogSEO webhook request
const signingSecret = request.headers['x-webhook-secret'];
if (signingSecret === process.env.BLOGSEO_WEBHOOK_SECRET) {
// Process request
} else {
throw new Error('Invalid signature');
}
Do not expose your webhook signing secret to the public. In particular, make sure your signing secret is not included in your git history and do not hardcode it in your application code. Use environment variables to store your signing secret securely.
Handling the Webhook Payload
After verifying the request, parse the JSON payload and process the article data.
Processing a BlogSEO webhook payload
// app/api/webhooks/blogseo/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const signingSecret = request.headers.get('x-webhook-secret');
if (signingSecret !== process.env.BLOGSEO_WEBHOOK_SECRET) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 },
);
}
const { article, website, timestamp } = await request.json();
console.log(`Received article: ${article.title}`);
console.log(`Slug: ${article.slug}`);
console.log(`Format: ${article.format}`);
console.log(`Image: ${article.main_image_url}`);
// Save to your database, trigger builds, etc.
// await saveArticle(article);
return NextResponse.json({ success: true });
}
Testing Your Webhook
Use the Test button in the webhook configuration dialog to send a test payload to your endpoint. This allows you to verify:
- Your endpoint is accessible
- Authentication is working correctly
- Your code correctly parses the payload
The test payload uses randomly generated UUIDs and sample content, allowing you to validate your integration without publishing a real article.
Response Requirements
Your webhook endpoint should:
- Return a
2xxstatus code (200-299) to indicate success - Respond within 30 seconds
- Return any status code outside
2xxto indicate failure
If your endpoint returns an error or times out, BlogSEO will retry the request automatically.
Use Cases
Custom webhooks are ideal for:
- Static Site Generators: Trigger rebuilds when new content is published (Hugo, Jekyll, Gatsby, Next.js)
- Headless CMS: Push content to Strapi, Sanity, or other headless platforms
- Custom Applications: Integrate with your own backend services or databases
- Notification Systems: Send alerts to Slack, Discord, or email when articles are published
- Content Syndication: Automatically distribute content to multiple platforms
Rendering Articles in React
If you're building a React application and receiving content in markdown format, we recommend using the react-markdown package to render your articles.
ReactMarkdown follows the CommonMark specification by default, which does
not include tables. To render tables and other GitHub Flavored Markdown
(GFM) features like strikethrough and task lists, you need the remark-gfm
plugin. For syntax highlighting in code blocks, use rehype-highlight.
Installation
npm install react-markdown remark-gfm rehype-highlight highlight.js
Basic Usage
Create a reusable article component that renders markdown content with full GFM support:
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeHighlight from 'rehype-highlight';
import 'highlight.js/styles/github.css';
interface ArticleProps {
title: string;
content: string;
mainImageUrl?: string;
mainImageAlt?: string;
}
export function Article({
title,
content,
mainImageUrl,
mainImageAlt,
}: ArticleProps) {
return (
<article>
<h1>{title}</h1>
{mainImageUrl && (
<img src={mainImageUrl} alt={mainImageAlt || title} />
)}
<div className="markdown-content">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeHighlight]}
>
{content}
</ReactMarkdown>
</div>
</article>
);
}
Styling Tables
Add CSS styles for tables to display correctly. Here's an example you can include in your stylesheet:
.markdown-content table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
}
.markdown-content th,
.markdown-content td {
border: 1px solid #d1d5db;
padding: 0.75rem 1rem;
text-align: left;
}
.markdown-content th {
background-color: #f9fafb;
font-weight: 600;
}
.markdown-content tr:nth-child(even) {
background-color: #f9fafb;
}
You'll also want to add styles for headings, paragraphs, lists, code blocks, and other markdown elements for consistent rendering.
Troubleshooting
Webhook Not Receiving Requests
- Verify your endpoint URL is correct and publicly accessible
- Check that your server accepts POST requests at the specified path
- Ensure your SSL certificate is valid (HTTPS is required)
Authentication Failures
- Confirm you're checking the correct header (
X-Webhook-Secretfor generated secrets) - Verify the secret in your environment matches the one shown in BlogSEO
- Check for trailing whitespace in your environment variables
Timeout Errors
- Ensure your endpoint responds within 30 seconds
- Process heavy operations asynchronously after sending the response
- Consider using a queue system for time-consuming tasks
Payload Parsing Errors
- Verify your endpoint expects
application/jsoncontent type - Check that you're parsing the request body as JSON
- Validate all expected fields exist before accessing them
Tables Not Rendering Correctly
If you're using the HTML format and tables appear unstyled, you'll need to add CSS styles for table elements. Here's example CSS you can use:
.article-content table {
width: 100%;
border-collapse: collapse;
border: 1px solid #d1d5db;
margin-bottom: 1rem;
}
.article-content th {
background-color: #f9fafb;
border: 1px solid #d1d5db;
padding: 0.5rem 1rem;
text-align: left;
font-weight: 600;
}
.article-content td {
border: 1px solid #d1d5db;
padding: 0.5rem 1rem;
}
You may also want to style other HTML elements like headings, lists, blockquotes, and code blocks for consistent rendering.
Security Best Practices
- Always verify the webhook signature before processing
- Use HTTPS for your webhook endpoint
- Store secrets in environment variables, never in code
- Log webhook requests for debugging and auditing
- Validate and sanitize all incoming data before use
Frequently Asked Questions
Can I have multiple webhook endpoints?
Currently, each BlogSEO website can have one webhook endpoint. Contact support
if you need to send to multiple destinations. What happens if my endpoint is
down?
BlogSEO automatically retries failed requests. If all attempts fail, the article
status will be set to "preview" and you can manually retry from your
dashboard. Can I change
the webhook URL after creation?
Yes, you can update your webhook configuration at any time in the Integrations
settings. Can I receive
webhooks for article updates, not just new publications?
Currently, webhooks are sent when articles are published. Republishing an
existing article will trigger a new webhook with the updated content. How do I
detect if a webhook is for a new article or an update?
Use the article.id field to detect updates. This UUID remains identical when
you republish an article. Store this ID in your database and check if it already
exists when receiving a webhook. If it does, update the existing record instead
of creating a new one. See the Receiving Article
Updates section for implementation examples. My
tables are showing as dashes and pipes instead of actual tables. How do I fix
this?
This happens when using markdown format with a renderer that doesn't support
GitHub Flavored Markdown (GFM) tables. If you're using react-markdown, you
need to install and add the remark-gfm plugin. See the Rendering Articles in
React section above for the full setup.
Alternatively, you can switch to HTML format in your webhook settings, which
sends pre-rendered tables that don't require additional plugins. For additional
support, contact our team through the in-app support chat.