โ Back to Connections
Connect SvelteKit to PostPenguin
Easy SetupFramework: Svelte / SvelteKitTime: 10 minutes
SvelteKit's server endpoints make it simple to receive PostPenguin webhooks. This guide shows you how to create an API route and display posts on your Svelte site.
๐ Quick Setup
1. Create Webhook Endpoint
// src/routes/api/webhooks/postpenguin/+server.ts
import { json, error } from '@sveltejs/kit'
import { createHmac, timingSafeEqual } from 'crypto'
import { POSTPENGUIN_WEBHOOK_SECRET } from '$env/static/private'
import type { RequestHandler } from './$types'
// In-memory storage for demo (replace with database)
const posts = new Map<string, any>()
function verifySignature(payload: string, signature: string, secret: string): boolean {
const expected = createHmac('sha256', secret).update(payload).digest('hex')
const received = signature.replace('sha256=', '')
try {
return timingSafeEqual(Buffer.from(expected), Buffer.from(received))
} catch {
return false
}
}
export const POST: RequestHandler = async ({ request }) => {
try {
const body = await request.text()
const data = JSON.parse(body)
// Verify signature
const signature = request.headers.get('x-postpenguin-signature')
if (POSTPENGUIN_WEBHOOK_SECRET && signature) {
if (!verifySignature(body, signature, POSTPENGUIN_WEBHOOK_SECRET)) {
throw error(401, 'Invalid signature')
}
}
// Validate required fields
if (!data.title || !data.slug || !data.html) {
throw error(400, 'Missing required fields')
}
const postId = data.postPenguinId || `pp_${Date.now()}`
const now = new Date().toISOString()
const post = {
id: postId,
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 || [],
status: 'publish',
publishedAt: now,
createdAt: posts.has(postId) ? posts.get(postId).createdAt : now,
updatedAt: now,
}
const action = posts.has(postId) ? 'updated' : 'created'
posts.set(postId, post)
console.log(`โ
Post ${action}: ${post.title}`)
return json({ success: true, postId, action })
} catch (err: any) {
console.error('Webhook error:', err)
if (err.status) throw err
throw error(500, 'Internal server error')
}
}
// Export posts for other routes
export { posts }2. Create Posts API
// src/routes/api/posts/+server.ts
import { json } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
import { posts } from '../webhooks/postpenguin/+server'
export const GET: RequestHandler = async ({ url }) => {
const limit = parseInt(url.searchParams.get('limit') || '10')
const offset = parseInt(url.searchParams.get('offset') || '0')
const allPosts = Array.from(posts.values())
.filter(p => p.status === 'publish')
.sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime())
const paginatedPosts = allPosts.slice(offset, offset + limit)
return json({
posts: paginatedPosts,
pagination: {
total: allPosts.length,
limit,
offset
}
})
}// src/routes/api/posts/[slug]/+server.ts
import { json, error } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
import { posts } from '../../webhooks/postpenguin/+server'
export const GET: RequestHandler = async ({ params }) => {
const post = Array.from(posts.values()).find(
p => p.slug === params.slug && p.status === 'publish'
)
if (!post) {
throw error(404, 'Post not found')
}
return json({ post })
}๐พ With Drizzle ORM
Schema
// src/lib/db/schema.ts
import { pgTable, text, timestamp, json, varchar } from 'drizzle-orm/pg-core'
export const posts = pgTable('posts', {
id: text('id').primaryKey(),
postpenguinId: varchar('postpenguin_id', { length: 255 }).unique().notNull(),
title: varchar('title', { length: 500 }).notNull(),
slug: varchar('slug', { length: 255 }).unique().notNull(),
html: text('html').notNull(),
metaTitle: varchar('meta_title', { length: 500 }),
metaDescription: text('meta_description'),
featuredImage: varchar('featured_image', { length: 1000 }),
tags: json('tags').$type<string[]>().default([]),
status: varchar('status', { length: 50 }).default('publish'),
publishedAt: timestamp('published_at').defaultNow(),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
})Webhook with Drizzle
// src/routes/api/webhooks/postpenguin/+server.ts
import { json, error } from '@sveltejs/kit'
import { db } from '$lib/db'
import { posts } from '$lib/db/schema'
import { eq } from 'drizzle-orm'
import { nanoid } from 'nanoid'
export const POST: RequestHandler = async ({ request }) => {
const data = await request.json()
// Verify signature...
const postId = data.postPenguinId || `pp_${nanoid()}`
const existing = await db.select()
.from(posts)
.where(eq(posts.postpenguinId, postId))
.limit(1)
if (existing.length > 0) {
await db.update(posts)
.set({
title: data.title,
slug: data.slug,
html: data.html,
metaTitle: data.meta_title || data.title,
metaDescription: data.meta_description || '',
featuredImage: data.featured_image,
tags: data.tags || [],
updatedAt: new Date(),
})
.where(eq(posts.postpenguinId, postId))
return json({ success: true, postId, action: 'updated' })
}
await db.insert(posts).values({
id: nanoid(),
postpenguinId: postId,
title: data.title,
slug: data.slug,
html: data.html,
metaTitle: data.meta_title || data.title,
metaDescription: data.meta_description || '',
featuredImage: data.featured_image,
tags: data.tags || [],
})
return json({ success: true, postId, action: 'created' })
}๐จ Svelte Components
Blog List Page
<!-- src/routes/blog/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types'
export let data: PageData
</script>
<svelte:head>
<title>Blog</title>
</svelte:head>
<div class="container mx-auto px-4 py-8">
<h1 class="text-4xl font-bold mb-8">Blog</h1>
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{#each data.posts as post}
<a
href="/blog/{post.slug}"
class="block p-6 bg-white rounded-lg shadow hover:shadow-lg transition"
>
{#if post.featuredImage}
<img
src={post.featuredImage}
alt={post.title}
class="w-full h-48 object-cover rounded mb-4"
/>
{/if}
<h2 class="text-xl font-semibold mb-2">{post.title}</h2>
<p class="text-gray-600 text-sm">
{new Date(post.publishedAt).toLocaleDateString()}
</p>
<div class="flex flex-wrap gap-2 mt-3">
{#each post.tags as tag}
<span class="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded">
{tag}
</span>
{/each}
</div>
</a>
{/each}
</div>
</div>// src/routes/blog/+page.server.ts
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ fetch }) => {
const response = await fetch('/api/posts')
const data = await response.json()
return {
posts: data.posts,
pagination: data.pagination
}
}Blog Post Page
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types'
export let data: PageData
</script>
<svelte:head>
<title>{data.post.metaTitle}</title>
<meta name="description" content={data.post.metaDescription} />
</svelte:head>
<article class="container mx-auto px-4 py-8 max-w-3xl">
{#if data.post.featuredImage}
<img
src={data.post.featuredImage}
alt={data.post.title}
class="w-full h-64 object-cover rounded-lg mb-8"
/>
{/if}
<h1 class="text-4xl font-bold mb-4">{data.post.title}</h1>
<div class="flex items-center gap-4 text-gray-600 mb-8">
<time>{new Date(data.post.publishedAt).toLocaleDateString()}</time>
<div class="flex gap-2">
{#each data.post.tags as tag}
<span class="px-2 py-1 bg-gray-100 text-sm rounded">{tag}</span>
{/each}
</div>
</div>
<div class="prose prose-lg prose-gray max-w-none">
{@html data.post.html}
</div>
</article>// src/routes/blog/[slug]/+page.server.ts
import { error } from '@sveltejs/kit'
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ params, fetch }) => {
const response = await fetch(`/api/posts/${params.slug}`)
if (!response.ok) {
throw error(404, 'Post not found')
}
const data = await response.json()
return { post: data.post }
}โ๏ธ Environment Variables
# .env
POSTPENGUIN_WEBHOOK_SECRET=your-secret-key-here
DATABASE_URL=postgresql://user:password@localhost:5432/database๐งช Testing
# Start dev server
npm run dev
# Test webhook
curl -X POST http://localhost:5173/api/webhooks/postpenguin \
-H "Content-Type: application/json" \
-d '{
"postPenguinId": "test_svelte_123",
"title": "Test SvelteKit Post",
"slug": "test-sveltekit-post",
"html": "<p className="text-gray-700">This is a test post in SvelteKit.</p>",
"tags": ["svelte", "sveltekit"]
}'
# Fetch posts
curl http://localhost:5173/api/postsNeed Help?
Check our webhook documentation for technical details, or contact support for custom integrations.