โ† Back to Connections

Connect Next.js to PostPenguin

Easy SetupLanguage: ReactTime: 10 minutes

Next.js sites can easily receive PostPenguin webhooks using API routes. This guide shows you how to set up automatic blog post publishing.

๐Ÿš€ Quick Setup

1. Create API Route

Create pages/api/webhooks/postpenguin.js (Pages Router) or app/api/webhooks/postpenguin/route.js (App Router):

import { createHmac } from 'crypto'

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' })
  }

  try {
    // Verify webhook signature (optional but recommended)
    const signature = req.headers['x-postpenguin-signature']
    const webhookSecret = process.env.POSTPENGUIN_WEBHOOK_SECRET

    if (webhookSecret && signature) {
      const payloadString = JSON.stringify(req.body)
      const expectedSignature = createHmac('sha256', webhookSecret)
        .update(payloadString)
        .digest('hex')

      const receivedSignature = signature.replace('sha256=', '')

      if (receivedSignature !== expectedSignature) {
        return res.status(401).json({ error: 'Invalid signature' })
      }
    }

    const { title, slug, html, meta_title, meta_description, featured_image } = req.body

    // Save to your database/CMS
    // Example: Save to a JSON file or database
    const post = {
      id: Date.now().toString(),
      title,
      slug,
      html,
      metaTitle: meta_title || title,
      metaDescription: meta_description || '',
      featuredImage: featured_image || '',
      publishedAt: new Date().toISOString(),
    }

    // TODO: Save to your database (Prisma, MongoDB, etc.)
    // await prisma.post.create({ data: post })

    console.log('Received PostPenguin post:', title)

    res.status(200).json({
      success: true,
      postId: post.id,
      message: 'Post received successfully'
    })

  } catch (error) {
    console.error('Webhook error:', error)
    res.status(500).json({ error: 'Internal server error' })
  }
}

2. Environment Variables

Add to .env.local:

POSTPENGUIN_WEBHOOK_SECRET=your-webhook-secret-here

3. Configure PostPenguin

When adding your site to PostPenguin:

  • Webhook URL: https://your-domain.com/api/webhooks/postpenguin
  • Secret Key: Same as POSTPENGUIN_WEBHOOK_SECRET

๐Ÿ’พ Database Integration

With Prisma + PostgreSQL

// pages/api/webhooks/postpenguin.js
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

export default async function handler(req, res) {
  // ... signature verification ...

  const post = await prisma.post.create({
    data: {
      title: req.body.title,
      slug: req.body.slug,
      html: req.body.html,
      metaTitle: req.body.meta_title,
      metaDescription: req.body.meta_description,
      featuredImage: req.body.featured_image,
      publishedAt: new Date(),
    }
  })

  res.status(200).json({ success: true, postId: post.id })
}

With MongoDB

// pages/api/webhooks/postpenguin.js
import { MongoClient } from 'mongodb'

const client = new MongoClient(process.env.MONGODB_URI)

export default async function handler(req, res) {
  try {
    await client.connect()
    const db = client.db('your-database')
    const collection = db.collection('posts')

    const post = {
      ...req.body,
      publishedAt: new Date(),
      createdAt: new Date(),
    }

    const result = await collection.insertOne(post)

    res.status(200).json({ success: true, postId: result.insertedId })

  } finally {
    await client.close()
  }
}

๐ŸŽจ Frontend Display

Fetch Posts in a Component

// components/BlogPosts.js
import { useState, useEffect } from 'react'

export default function BlogPosts() {
  const [posts, setPosts] = useState([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetch('/api/posts') // Your API route to fetch posts
      .then(res => res.json())
      .then(data => {
        setPosts(data.posts || [])
        setLoading(false)
      })
      .catch(error => {
        console.error('Error fetching posts:', error)
        setLoading(false)
      })
  }, [])

  if (loading) return <div>Loading posts...</div>

  return (
    <div className="blog-posts">
      {posts.map(post => (
        <article key={post.id} className="post-card">
          {post.featuredImage && (
            <img src={post.featuredImage} alt={post.title} />
          )}
          <h2 className="text-gray-900">
            <a href={`/posts/${post.slug}`}>{post.title}</a>
          </h2>
          <div dangerouslySetInnerHTML={{ __html: post.html }} />
          <time>{new Date(post.publishedAt).toLocaleDateString()}</time>
        </article>
      ))}
    </div>
  )
}

๐Ÿงช Testing

Test Webhook Locally

# Start Next.js dev server
npm run dev

# Test webhook in another terminal
curl -X POST http://localhost:3000/api/webhooks/postpenguin \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Test Post",
    "slug": "test-post",
    "html": "<p className="text-gray-700">This is a test post from PostPenguin.</p>",
    "meta_title": "Test Post Title",
    "meta_description": "Testing webhook integration",
    "featured_image": "https://via.placeholder.com/800x400",
    "status": "publish"
  }'

Test with Signature

# Generate signature
SECRET="your-webhook-secret"
PAYLOAD='{"title":"Test","slug":"test","html":"<p className="text-gray-700">Test</p>","status":"publish"}'
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)

# Test with signature
curl -X POST http://localhost:3000/api/webhooks/postpenguin \
  -H "Content-Type: application/json" \
  -H "X-PostPenguin-Signature: sha256=$SIGNATURE" \
  -d "$PAYLOAD"

๐Ÿš€ Deployment

Next.js deploys seamlessly to Vercel. Your webhook endpoint will work automatically.

Environment Variables on Vercel

# Vercel Dashboard > Project > Settings > Environment Variables
POSTPENGUIN_WEBHOOK_SECRET=your-webhook-secret-here

๐Ÿ› Troubleshooting

Webhook Not Receiving Posts

  1. Check webhook URL is publicly accessible
  2. Verify signature matches PostPenguin config
  3. Check logs in Vercel/Netlify function logs
  4. Test locally with curl command above

Posts Not Saving

  1. Verify your database credentials
  2. Ensure your database table/model matches the payload
  3. Add try/catch blocks and log errors

CORS Issues

If testing from browser, add CORS headers:

res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'POST')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,X-PostPenguin-Signature')

Need Help?

Read our webhook documentation for technical details, or contact support for custom integrations.