โ 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:
- Go to Site settings โ Environment variables
- Add
POSTPENGUIN_WEBHOOK_SECRETwith your secret key - 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.