โ† Back to Connections

Connect Strapi to PostPenguin

Medium SetupCMS: Strapi v4/v5Time: 15 minutes

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 post

Your 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:

  1. Go to Settings โ†’ Roles โ†’ Public
  2. Under Post, enable the postpenguin action
  3. 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.