โ† 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/posts

Need Help?

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