โ† Back to Connections

Connect Netlify to PostPenguin

Easy SetupPlatform: Netlify FunctionsTime: 10 minutes

Use Netlify Functions to receive PostPenguin webhooks. Perfect for JAMstack sites, static sites, or any project deployed on Netlify.

Perfect for: Gatsby, Hugo, Next.js, Eleventy, or any static site on Netlify.

๐Ÿš€ Quick Setup

1. Create Netlify Function

Create netlify/functions/postpenguin-webhook.js:

// netlify/functions/postpenguin-webhook.js
const crypto = require('crypto')

// Verify webhook signature
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)
    )
}

exports.handler = async (event, context) => {
    // Only allow POST requests
    if (event.httpMethod !== 'POST') {
        return {
            statusCode: 405,
            body: JSON.stringify({ error: 'Method not allowed' })
        }
    }

    try {
        // Verify signature
        const signature = event.headers['x-postpenguin-signature']
        const secret = process.env.POSTPENGUIN_WEBHOOK_SECRET

        if (secret && signature) {
            if (!verifySignature(event.body, signature, secret)) {
                return {
                    statusCode: 401,
                    body: JSON.stringify({ error: 'Invalid signature' })
                }
            }
        }

        const data = JSON.parse(event.body)

        // Validate required fields
        if (!data.title || !data.slug || !data.html) {
            return {
                statusCode: 400,
                body: JSON.stringify({ error: 'Missing required fields' })
            }
        }

        // Create post object
        const post = {
            id: data.postPenguinId || `pp_${Date.now()}`,
            title: data.title,
            slug: data.slug,
            html: data.html,
            metaTitle: data.meta_title || data.title,
            metaDescription: data.meta_description || '',
            featuredImage: data.featured_image || null,
            tags: data.tags || [],
            publishedAt: new Date().toISOString(),
        }

        // Save to your database
        // Option 1: Fauna DB
        // const faunadb = require('faunadb')
        // const client = new faunadb.Client({ secret: process.env.FAUNA_SECRET })
        // await client.query(...)

        // Option 2: Supabase
        // const { createClient } = require('@supabase/supabase-js')
        // const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY)
        // await supabase.from('posts').insert(post)

        // Option 3: Airtable
        // const Airtable = require('airtable')
        // const base = new Airtable({ apiKey: process.env.AIRTABLE_KEY }).base(process.env.AIRTABLE_BASE)
        // await base('Posts').create(post)

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

        return {
            statusCode: 200,
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({
                success: true,
                postId: post.id,
                message: 'Post received successfully'
            })
        }

    } catch (error) {
        console.error('Webhook error:', error)
        return {
            statusCode: 500,
            body: JSON.stringify({ error: 'Internal server error' })
        }
    }
}

2. Configure Environment Variables

In Netlify Dashboard:

  1. Go to Site settings โ†’ Environment variables
  2. Add POSTPENGUIN_WEBHOOK_SECRET with your secret key
  3. Add database credentials (Fauna, Supabase, etc.)

3. Configure PostPenguin

When adding your site to PostPenguin:

  • Webhook URL: https://your-site.netlify.app/.netlify/functions/postpenguin-webhook
  • Secret Key: Same as POSTPENGUIN_WEBHOOK_SECRET

๐Ÿ’พ Database Options

With FaunaDB

// netlify/functions/postpenguin-webhook.js
const faunadb = require('faunadb')
const q = faunadb.query

const client = new faunadb.Client({
    secret: process.env.FAUNA_SECRET
})

// Inside the handler, after validation:
const result = await client.query(
    q.Create(
        q.Collection('posts'),
        {
            data: {
                postPenguinId: post.id,
                title: post.title,
                slug: post.slug,
                html: post.html,
                metaTitle: post.metaTitle,
                metaDescription: post.metaDescription,
                featuredImage: post.featuredImage,
                tags: post.tags,
                publishedAt: post.publishedAt,
                createdAt: q.Now()
            }
        }
    )
)

return {
    statusCode: 200,
    body: JSON.stringify({
        success: true,
        postId: result.ref.id
    })
}

With Supabase

// netlify/functions/postpenguin-webhook.js
const { createClient } = require('@supabase/supabase-js')

const supabase = createClient(
    process.env.SUPABASE_URL,
    process.env.SUPABASE_ANON_KEY
)

// Inside the handler:
const { data, error } = await supabase
    .from('posts')
    .upsert({
        postpenguin_id: post.id,
        title: post.title,
        slug: post.slug,
        html: post.html,
        meta_title: post.metaTitle,
        meta_description: post.metaDescription,
        featured_image_url: post.featuredImage,
        tags: post.tags,
        published_at: post.publishedAt
    }, {
        onConflict: 'postpenguin_id'
    })

if (error) throw error

return {
    statusCode: 200,
    body: JSON.stringify({ success: true, postId: data[0].id })
}

With Airtable

// netlify/functions/postpenguin-webhook.js
const Airtable = require('airtable')

const base = new Airtable({
    apiKey: process.env.AIRTABLE_API_KEY
}).base(process.env.AIRTABLE_BASE_ID)

// Inside the handler:
const record = await base('Posts').create({
    'PostPenguin ID': post.id,
    'Title': post.title,
    'Slug': post.slug,
    'HTML': post.html,
    'Meta Title': post.metaTitle,
    'Meta Description': post.metaDescription,
    'Featured Image': post.featuredImage,
    'Tags': post.tags.join(', '),
    'Published At': post.publishedAt
})

return {
    statusCode: 200,
    body: JSON.stringify({ success: true, postId: record.id })
}

๐Ÿ“– Fetch Posts Function

Create netlify/functions/get-posts.js to fetch posts for your frontend:

// netlify/functions/get-posts.js
const { createClient } = require('@supabase/supabase-js')

const supabase = createClient(
    process.env.SUPABASE_URL,
    process.env.SUPABASE_ANON_KEY
)

exports.handler = async (event) => {
    const { slug, limit = 10, offset = 0 } = event.queryStringParameters || {}

    try {
        if (slug) {
            const { data, error } = await supabase
                .from('posts')
                .select('*')
                .eq('slug', slug)
                .single()

            if (error) throw error

            return {
                statusCode: 200,
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ post: data })
            }
        }

        const { data, error, count } = await supabase
            .from('posts')
            .select('*', { count: 'exact' })
            .eq('status', 'publish')
            .order('published_at', { ascending: false })
            .range(offset, offset + limit - 1)

        if (error) throw error

        return {
            statusCode: 200,
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                posts: data,
                pagination: { total: count, limit, offset }
            })
        }

    } catch (error) {
        return {
            statusCode: 500,
            body: JSON.stringify({ error: error.message })
        }
    }
}

๐Ÿงช Testing

Test Locally with Netlify CLI

# Install Netlify CLI
npm install -g netlify-cli

# Run locally
netlify dev

# Test webhook
curl -X POST http://localhost:8888/.netlify/functions/postpenguin-webhook \
  -H "Content-Type: application/json" \
  -d '{
    "postPenguinId": "test_netlify_123",
    "title": "Test Netlify Post",
    "slug": "test-netlify-post",
    "html": "<p className="text-gray-700">Test post on Netlify.</p>",
    "tags": ["test", "netlify"]
  }'

Test in Production

curl -X POST https://your-site.netlify.app/.netlify/functions/postpenguin-webhook \
  -H "Content-Type: application/json" \
  -H "X-PostPenguin-Signature: sha256=YOUR_SIGNATURE" \
  -d '{"title":"Test","slug":"test","html":"<p className="text-gray-700">Test</p>"}'

๐Ÿš€ Deployment

Netlify Functions deploy automatically when you push to your repository. Make sure:

  • Functions are in netlify/functions/ directory
  • Environment variables are set in Netlify Dashboard
  • Any npm dependencies are in your package.json

netlify.toml Configuration

[build]
  functions = "netlify/functions"

[functions]
  node_bundler = "esbuild"

Need Help?

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