โ† Back to Connections

Connect Vercel to PostPenguin

Easy SetupLanguage: Node.jsTime: 10 minutes

Deploy serverless webhook handlers on Vercel to receive PostPenguin posts. Perfect for Next.js apps, static sites, or any JavaScript project.

๐Ÿš€ Quick Setup

1. Create Serverless Function

Create api/webhooks/postpenguin.js in your project:

// api/webhooks/postpenguin.js
import { createHmac } from 'crypto'

export default async function handler(req, res) {
  // Enable CORS
  res.setHeader('Access-Control-Allow-Credentials', true)
  res.setHeader('Access-Control-Allow-Origin', '*')
  res.setHeader('Access-Control-Allow-Methods', 'POST,OPTIONS')
  res.setHeader('Access-Control-Allow-Headers', 'X-PostPenguin-Signature,Content-Type')

  if (req.method === 'OPTIONS') {
    res.status(200).end()
    return
  }

  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' })
  }

  try {
    // Verify webhook signature (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

    // Validate required fields
    if (!title || !slug || !html) {
      return res.status(400).json({ error: 'Missing required fields' })
    }

    // Save to your database/service
    const post = {
      id: crypto.randomUUID(),
      title,
      slug,
      html,
      metaTitle: meta_title || title,
      metaDescription: meta_description || '',
      featuredImage: featured_image || '',
      publishedAt: new Date().toISOString(),
    }

    // Example: Save to Vercel KV (Redis)
    // const kv = require('@vercel/kv')
    // await kv.set(`post:${post.slug}`, JSON.stringify(post))

    // Example: Save to PlanetScale/MySQL
    // const mysql = require('mysql2/promise')
    // const connection = await mysql.createConnection(process.env.DATABASE_URL)
    // await connection.execute('INSERT INTO posts SET ?', [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 Vercel dashboard or .env.local:

POSTPENGUIN_WEBHOOK_SECRET=your-webhook-secret-here
DATABASE_URL=your-database-connection-string

3. Configure PostPenguin

When adding your site to PostPenguin:

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

๐Ÿ’พ Database Integration

Vercel KV (Redis)

// api/webhooks/postpenguin.js
import { kv } from '@vercel/kv'

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

  const post = {
    id: crypto.randomUUID(),
    ...req.body,
    publishedAt: new Date().toISOString(),
  }

  // Save post
  await kv.set(`post:${post.slug}`, JSON.stringify(post))

  // Add to posts list
  await kv.lpush('posts', post.slug)

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

PlanetScale (MySQL)

// api/webhooks/postpenguin.js
import mysql from 'mysql2/promise'

export default async function handler(req, res) {
  let connection

  try {
    connection = await mysql.createConnection(process.env.DATABASE_URL)

    const post = {
      postpenguin_id: req.body.postPenguinId || crypto.randomUUID(),
      title: req.body.title,
      slug: req.body.slug,
      html: req.body.html,
      meta_title: req.body.meta_title || req.body.title,
      meta_description: req.body.meta_description || '',
      featured_image_url: req.body.featured_image || null,
      published_at: new Date(),
    }

    const [result] = await connection.execute(
      'INSERT INTO posts (postpenguin_id, title, slug, html, meta_title, meta_description, featured_image_url, published_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
      [post.postpenguin_id, post.title, post.slug, post.html, post.meta_title, post.meta_description, post.featured_image_url, post.published_at]
    )

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

  } catch (error) {
    console.error('Database error:', error)
    res.status(500).json({ error: 'Database error' })
  } finally {
    if (connection) await connection.end()
  }
}

๐Ÿ“Š Fetch Posts

API Route to Fetch Posts

Create api/posts.js to retrieve posts:

// api/posts.js - Fetch posts from your storage
import { kv } from '@vercel/kv'

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

  try {
    const { slug, limit = 10, offset = 0 } = req.query

    if (slug) {
      // Fetch single post
      const post = await kv.get(`post:${slug}`)
      if (!post) {
        return res.status(404).json({ error: 'Post not found' })
      }
      return res.status(200).json({ post: JSON.parse(post) })
    }

    // Fetch multiple posts
    const postSlugs = await kv.lrange('posts', offset, offset + limit - 1)
    const posts = []

    for (const slug of postSlugs) {
      const post = await kv.get(`post:${slug}`)
      if (post) {
        posts.push(JSON.parse(post))
      }
    }

    res.status(200).json({ posts })

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

๐ŸŽจ Frontend Display

React 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')
      .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}>
          {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

Local Development

# Install Vercel CLI
npm i -g vercel

# Start local development server
vercel 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.</p>",
    "status": "publish"
  }'

Production Testing

# Test production webhook
curl -X POST https://your-app.vercel.app/api/webhooks/postpenguin \
  -H "Content-Type: application/json" \
  -H "X-PostPenguin-Signature: sha256=YOUR_SIGNATURE" \
  -d '{
    "title": "Test Post",
    "slug": "test-post",
    "html": "<p className="text-gray-700">This is a test post.</p>",
    "status": "publish"
  }'

๐Ÿš€ Deployment

Automatic deployment when you push to your git repository.

Environment Variables

Set in Vercel Dashboard:

  • Project Settings > Environment Variables
  • Add POSTPENGUIN_WEBHOOK_SECRET
  • Add database credentials (if using external DB)

๐Ÿ› Troubleshooting

Function Timeout

Vercel functions have a 10-second timeout for hobby plan. If your database operations are slow:

  • Optimize database queries
  • Use connection pooling
  • Consider Vercel Pro plan for longer timeouts

Cold Starts

Serverless functions have cold starts. For better performance:

  • Use Vercel Pro plan (reduces cold starts)
  • Implement caching in your application
  • Use edge functions for better performance

CORS Issues

If testing from browser, ensure CORS headers are set.

Need Help?

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