Documentation
Learn how to integrate MassBlogger with your websites and automate your content workflow.
WordPress
Guides and troubleshooting for connecting MassBlogger to your WordPress sites.
API Reference
Detailed documentation for all available API endpoints.
- MCP Server Guide - research, publish, pSEO, and logs through MCP
- Webhooks Guide - push content to your custom website
- pSEO Webhooks - integrate programmatic SEO pages
- Internal Links API
- REST API - includes category & tag page examples
- IndexNow API - faster indexation for Bing, Yandex & more
Features
Advanced features for customizing your blog.
- Snippets - Reusable content blocks (CTAs, forms, etc.)
- SEO Audit - AI-powered site reports and on-page crawl checker
- Keyword Tracker - Track target keywords to prevent cannibalization
- Topic Generator - Cluster map from a seed keyword with difficulty scores
- Media Library - Upload, crop, and reuse images with Brand DNA (beta)
- Write Manual - Built-in rich text editor with WordPress publishing
- Prompt Rules - Global and per-site writing templates for AI generation
- SEO Tests - Before/after GSC experiments to measure the impact of changes
pSEO Integration
Learn how to connect your programmatic SEO pages via webhooks.
Webhook payload
Sent on every publish, update, unpublish, or delete. When a page is deleted, the payload includes delete: true (event pseo.page.deleted). The pageId is permanent — use it as the primary key so slug changes don't create duplicates. Your POST webhook endpoint can be separate from the GET route your site uses to render the saved page.
Don't want to store pSEO pages yourself? You can also fetch live pSEO pages directly from Massblogger using your website API key via /api/pseo-pages. That works well for custom sites that want a REST read layer instead of webhook storage.
{
"event": "pseo.page.updated",
"data": {
"pageId": "pseo_abc123_x7k2m9p1",
"status": "live",
"targetSlug": "",
"variables": {},
"html": "<h1>Title</h1><p>Full page content...</p>",
"fields": {
"metaTitle": "SEO Title",
"metaDescription": "SEO Description",
"featuredImage": "https://example.com/image.jpg",
"h1": "Page Heading",
"introduction": "<p>Intro paragraph...</p>",
"content": "<p>Main content...</p>",
"conclusion": "<p>Closing thoughts...</p>",
"relevantArticle": "/related-post"
}
}
}Setup guide
Step 1 — Webhook handler (API route)
Create this on your site. It verifies the signature, then upserts by pageId (permanent unique ID). Slug changes, status changes, content updates — all handled via the same upsert. This POST handler stores content; your public GET route can be a different endpoint that reads from your DB or CMS.
// app/api/pseo-webhook/route.js
import { NextResponse } from 'next/server';
import { getDatabase } from '@/lib/mongodb';
import crypto from 'crypto';
const WEBHOOK_SECRET = process.env.MASSBLOGGER_WEBHOOK_SECRET;
function verifySignature(body, signature) {
if (!WEBHOOK_SECRET) return true; // skip if not configured
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(body)
.digest('hex');
const sig = Buffer.from(signature || '');
const exp = Buffer.from(expected);
if (sig.length !== exp.length) return false; // prevents "Input buffers must have the same byte length"
return crypto.timingSafeEqual(sig, exp);
}
export async function POST(request) {
try {
const rawBody = await request.text();
// Verify webhook signature
const signature = request.headers.get('x-webhook-signature');
if (WEBHOOK_SECRET && !verifySignature(rawBody, signature)) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 }
);
}
const body = JSON.parse(rawBody);
const db = await getDatabase();
// Delete: remove page from your system when MassBlogger sends delete: true
if (body.event === 'pseo.page.deleted' || body.data?.delete === true) {
const { pageId } = body.data || {};
if (pageId) {
await db.collection('pseo_pages').deleteOne({ pageId });
}
return NextResponse.json({ ok: true, action: 'deleted' });
}
if (body.event !== 'pseo.page.updated') {
return NextResponse.json({ ok: true });
}
const { pageId, targetSlug, status, html, fields, variables, internalLinks } = body.data;
// Upsert by pageId — survives slug changes
const result = await db
.collection('pseo_pages')
.updateOne(
{ pageId },
{
$set: {
slug: targetSlug,
status,
html,
fields: fields || {},
variables: variables || {},
internalLinks: internalLinks || [],
updatedAt: new Date(),
},
$setOnInsert: {
pageId,
createdAt: new Date(),
},
},
{ upsert: true }
);
return NextResponse.json({
ok: true,
pageId,
slug: targetSlug,
status,
action: result.upsertedCount ? 'created' : 'updated',
});
} catch (err) {
console.error('[P-SEO webhook]', err);
return NextResponse.json(
{ error: err.message },
{ status: 500 }
);
}
}Step 2 — Display pages on your site
Only query pages with status: "live". When a page is unpublished, the status changes to "draft" and it disappears automatically.
// lib/pseo.js
import { getDatabase } from '@/lib/mongodb';
// Resolve [var], {var} and [[anchor]]. Links: { keyword, url, internal, nofollow }
// Copy resolveContent from app/api/pseo-webhook/route.example.js
function resolveContent(html, variables, links, currentSlug) {
let out = html || '';
if (variables && typeof variables === 'object') {
out = out.replace(/\[(\\w+)\]/g, (m, k) => (k in variables) ? variables[k] : m);
out = out.replace(/\\{(\\w+)\\}/g, (m, k) => (k in variables) ? variables[k] : m);
}
for (const link of links || []) {
const anchor = link.anchor || link.keyword;
const url = link.url || '';
if (!anchor || !url) continue;
const isInternal = link.internal !== false;
if (isInternal && currentSlug && (url === '/' + currentSlug || url.endsWith('/' + currentSlug))) continue;
const rel = !isInternal && link.nofollow ? ' rel="nofollow"' : '';
const esc = (s) => String(s).replace(new RegExp('[.*+?^$' + '{}()|[\\]\\]', 'g'), '\\$&');
out = out.replace(new RegExp('\\[\\[' + esc(anchor) + '\\]\\]', 'g'), '<a href="' + url + '"' + rel + '>' + anchor + '</a>');
}
return out;
}
export async function getPseoPage(slug) {
const db = await getDatabase();
return db
.collection('pseo_pages')
.findOne({ slug, status: 'live' });
}
export async function getAllPseoSlugs() {
const db = await getDatabase();
const pages = await db
.collection('pseo_pages')
.find({ status: 'live' }, { projection: { slug: 1 } })
.toArray();
return pages.map((p) => p.slug);
}
// In your [slug]/page.js
import { getPseoPage } from '@/lib/pseo';
import { notFound } from 'next/navigation';
export default async function Page({ params }) {
const page = await getPseoPage(params.slug);
if (!page) notFound();
const f = page.fields || {};
const vars = page.variables || {};
const links = page.internalLinks || [];
const resolve = (html) => resolveContent(html, vars, links, page.slug);
return (
<article>
{f.h1 && <h1>{resolve(f.h1)}</h1>}
{f.featuredImage && <img src={f.featuredImage} alt={f.h1 || ''} />}
{f.introduction && (
<div dangerouslySetInnerHTML={{ __html: resolve(f.introduction) }} />
)}
{f.content && (
<div dangerouslySetInnerHTML={{ __html: resolve(f.content) }} />
)}
{f.conclusion && (
<div dangerouslySetInnerHTML={{ __html: resolve(f.conclusion) }} />
)}
</article>
);
}Troubleshooting
- Invalid signature (401) — Your
MASSBLOGGER_WEBHOOK_SECRETin .env doesn't match the secret above. Copy it exactly, update .env, and restart your server. - Input buffers must have the same byte length — Your
verifySignaturecrashes when the header is missing or wrong format. Addif (sig.length !== exp.length) return false;beforetimingSafeEqual(see Step 1 above). - Other errors — Use
request.text()for the raw body before parsing JSON. If using Next.js App Router, ensure no middleware modifies the body before it reaches your route.