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-here3. 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
- Check webhook URL is publicly accessible
- Verify signature matches PostPenguin config
- Check logs in Vercel/Netlify function logs
- Test locally with curl command above
Posts Not Saving
- Verify your database credentials
- Ensure your database table/model matches the payload
- 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.