Strapi is a headless CMS that provides a powerful API. This guide shows you how to create a custom controller that receives PostPenguin webhooks and creates posts in Strapi.
๐ Prerequisites
- Strapi v4 or v5 project
- A "Post" or "Article" content type with fields for title, slug, content, etc.
๐๏ธ Content Type Setup
First, ensure you have a Post content type. You can create one via the Strapi admin or using the CLI:
# Create Post content type
npx strapi generate content-type postYour Post content type should have these fields:
// src/api/post/content-types/post/schema.json
{
"kind": "collectionType",
"collectionName": "posts",
"info": {
"singularName": "post",
"pluralName": "posts",
"displayName": "Post"
},
"options": {
"draftAndPublish": true
},
"attributes": {
"title": {
"type": "string",
"required": true
},
"slug": {
"type": "uid",
"targetField": "title",
"required": true
},
"content": {
"type": "richtext"
},
"postpenguinId": {
"type": "string",
"unique": true
},
"metaTitle": {
"type": "string"
},
"metaDescription": {
"type": "text"
},
"featuredImage": {
"type": "string"
},
"tags": {
"type": "json"
}
}
}๐ Create Webhook Controller
Strapi v4
// src/api/post/controllers/webhook.js
'use strict'
const crypto = require('crypto')
function verifySignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex')
const received = signature.replace('sha256=', '')
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))
}
module.exports = {
async postpenguin(ctx) {
try {
// Verify signature
const signature = ctx.request.headers['x-postpenguin-signature']
const secret = process.env.POSTPENGUIN_WEBHOOK_SECRET
if (secret && signature) {
const payload = JSON.stringify(ctx.request.body)
if (!verifySignature(payload, signature, secret)) {
return ctx.unauthorized('Invalid signature')
}
}
const data = ctx.request.body
// Validate required fields
if (!data.title || !data.slug || !data.html) {
return ctx.badRequest('Missing required fields')
}
const postpenguinId = data.postPenguinId || `pp_${Date.now()}`
// Check if post exists
const existingPost = await strapi.db.query('api::post.post').findOne({
where: { postpenguinId }
})
const postData = {
title: data.title,
slug: data.slug,
content: data.html,
postpenguinId,
metaTitle: data.meta_title || data.title,
metaDescription: data.meta_description || '',
featuredImage: data.featured_image || null,
tags: data.tags || [],
publishedAt: new Date(),
}
let post
let action
if (existingPost) {
post = await strapi.db.query('api::post.post').update({
where: { id: existingPost.id },
data: postData
})
action = 'updated'
} else {
post = await strapi.db.query('api::post.post').create({
data: postData
})
action = 'created'
}
strapi.log.info(`PostPenguin post ${action}: ${data.title}`)
return {
success: true,
postId: post.id,
postpenguinId,
action
}
} catch (error) {
strapi.log.error('PostPenguin webhook error:', error)
return ctx.internalServerError('Internal server error')
}
}
}Add Route
// src/api/post/routes/webhook.js
module.exports = {
routes: [
{
method: 'POST',
path: '/webhooks/postpenguin',
handler: 'webhook.postpenguin',
config: {
auth: false, // Allow unauthenticated access
middlewares: [],
},
},
],
}๐ Configure Permissions
Since the webhook needs to be accessible without authentication:
- Go to Settings โ Roles โ Public
- Under Post, enable the
postpenguinaction - Save
Or add to your bootstrap configuration:
// config/functions/bootstrap.js
module.exports = async () => {
// Allow public access to webhook
const publicRole = await strapi.query('plugin::users-permissions.role').findOne({
where: { type: 'public' }
})
if (publicRole) {
await strapi.query('plugin::users-permissions.permission').create({
data: {
action: 'api::post.webhook.postpenguin',
role: publicRole.id
}
})
}
}โ๏ธ Environment Variables
# .env
POSTPENGUIN_WEBHOOK_SECRET=your-secret-key-here๐งช Testing
Test Webhook
curl -X POST http://localhost:1337/api/webhooks/postpenguin \
-H "Content-Type: application/json" \
-d '{
"postPenguinId": "test_strapi_123",
"title": "Test Strapi Post",
"slug": "test-strapi-post",
"html": "<p className="text-gray-700">This is a test post in Strapi.</p>",
"meta_title": "Test Post",
"meta_description": "Testing Strapi webhook",
"tags": ["test", "strapi"]
}'Verify in Strapi
Check your Strapi admin panel at /admin/content-manager/collectionType/api::post.post
๐จ Frontend Integration
Fetch posts from Strapi's REST API:
// React/Next.js example
async function getPosts() {
const response = await fetch('http://localhost:1337/api/posts?populate=*')
const data = await response.json()
return data.data
}
async function getPostBySlug(slug) {
const response = await fetch(
`http://localhost:1337/api/posts?filters[slug][$eq]=${slug}&populate=*`
)
const data = await response.json()
return data.data[0]
}๐ Strapi v5 (Beta)
For Strapi v5, the controller syntax is slightly different:
// src/api/post/controllers/webhook.ts
import type { Core } from '@strapi/strapi'
export default ({ strapi }: { strapi: Core.Strapi }) => ({
async postpenguin(ctx) {
// Same logic as v4, but using strapi.documents API
const post = await strapi.documents('api::post.post').create({
data: postData,
status: 'published'
})
return { success: true, postId: post.documentId }
}
})Need Help?
Check our webhook documentation for technical details, or contact support for custom integrations.