Getting Started with SvelteKit and a Headless WordPress Backend
SvelteKit brings a revolutionary approach to building web applications with its compile-time optimizations and elegant developer experience. When combined with headless WordPress, you get a powerful stack that delivers exceptional performance, SEO capabilities, and content management flexibility.
This comprehensive guide walks you through building a complete SvelteKit application powered by WordPress, covering everything from initial setup to production deployment with advanced features like authentication, real-time updates, and optimal caching strategies.
Table of Contents
- Why SvelteKit for Headless WordPress
- Project Setup and Configuration
- WordPress GraphQL Integration
- Building Core Features
- Advanced Patterns
- Performance Optimization
- Deployment Strategies
Why SvelteKit for Headless WordPress
The SvelteKit Advantage
SvelteKit offers unique benefits for headless CMS applications:
// No virtual DOM - compiles to vanilla JS
// Before (React/Vue)
function updateCount(count) {
virtualDOM.update({ count });
reconcile(virtualDOM, realDOM);
}
// After (Svelte) - Direct DOM manipulation
function updateCount(count) {
textNode.data = count;
}
Key Benefits: - Smaller Bundle Sizes: 30-40% smaller than React/Vue equivalents - No Runtime Overhead: Compiles away framework code - Built-in Transitions: Smooth animations without libraries - Server-Side Rendering: First-class SSR support - File-based Routing: Intuitive project structure - TypeScript Ready: Full TypeScript support out of the box
Performance Comparison
Metric | Next.js | Nuxt.js | SvelteKit |
---|---|---|---|
Initial Bundle | 85KB | 70KB | 35KB |
Time to Interactive | 3.2s | 2.8s | 1.9s |
Runtime Overhead | High | Medium | None |
Build Time | Fast | Fast | Very Fast |
Learning Curve | Moderate | Easy | Easy |
Project Setup and Configuration
Initialize SvelteKit Project
# Create new SvelteKit project
npm create svelte@latest wordpress-sveltekit
cd wordpress-sveltekit
# Select options:
# - Skeleton project
# - TypeScript
# - ESLint
# - Prettier
# - Playwright (for testing)
# Install dependencies
npm install
# Additional packages for WordPress integration
npm install graphql-request graphql
npm install @sveltejs/adapter-vercel # or your preferred adapter
npm install date-fns reading-time
Project Structure
wordpress-sveltekit/
├── src/
│ ├── routes/
│ │ ├── +layout.svelte
│ │ ├── +page.svelte
│ │ ├── blog/
│ │ │ ├── +page.svelte
│ │ │ └── [slug]/
│ │ │ └── +page.svelte
│ │ └── api/
│ │ └── preview/
│ │ └── +server.ts
│ ├── lib/
│ │ ├── wordpress.ts
│ │ ├── types.ts
│ │ └── components/
│ ├── app.html
│ └── app.d.ts
├── static/
├── svelte.config.js
└── vite.config.ts
Environment Configuration
// .env
WORDPRESS_API_URL=https://api.yoursite.com
WORDPRESS_GRAPHQL_URL=https://api.yoursite.com/graphql
PUBLIC_SITE_URL=https://yoursite.com
PREVIEW_SECRET=your-secret-key
TypeScript Configuration
// app.d.ts
declare global {
namespace App {
interface Locals {
user?: {
id: string;
name: string;
email: string;
};
}
interface PageData {
posts?: Post[];
post?: Post;
categories?: Category[];
}
interface Platform {}
}
}
// lib/types.ts
export interface Post {
id: string;
title: string;
slug: string;
content: string;
excerpt: string;
date: string;
author: Author;
categories: Category[];
featuredImage?: FeaturedImage;
}
export interface Author {
name: string;
avatar?: {
url: string;
};
}
export interface Category {
id: string;
name: string;
slug: string;
}
export interface FeaturedImage {
sourceUrl: string;
altText: string;
mediaDetails: {
width: number;
height: number;
};
}
WordPress GraphQL Integration
GraphQL Client Setup
// lib/wordpress.ts
import { GraphQLClient } from 'graphql-request';
import type { Post, Category } from './types';
const client = new GraphQLClient(process.env.WORDPRESS_GRAPHQL_URL || '', {
headers: {
'Content-Type': 'application/json',
},
});
// Generic query function with error handling
export async function queryWordPress<T>(
query: string,
variables?: Record<string, any>
): Promise<T> {
try {
return await client.request<T>(query, variables);
} catch (error) {
console.error('GraphQL Error:', error);
throw new Error('Failed to fetch data from WordPress');
}
}
// Post queries
export const POST_FIELDS = `
fragment PostFields on Post {
id
title
slug
content
excerpt
date
author {
node {
name
avatar {
url
}
}
}
categories {
nodes {
id
name
slug
}
}
featuredImage {
node {
sourceUrl
altText
mediaDetails {
width
height
}
}
}
}
`;
export async function getPosts(limit = 10): Promise<Post[]> {
const query = `
${POST_FIELDS}
query GetPosts($limit: Int!) {
posts(first: $limit, where: { orderby: { field: DATE, order: DESC } }) {
nodes {
...PostFields
}
}
}
`;
const data = await queryWordPress<{ posts: { nodes: Post[] } }>(
query,
{ limit }
);
return data.posts.nodes;
}
export async function getPostBySlug(slug: string): Promise<Post | null> {
const query = `
${POST_FIELDS}
query GetPost($slug: String!) {
postBy(slug: $slug) {
...PostFields
}
}
`;
const data = await queryWordPress<{ postBy: Post | null }>(
query,
{ slug }
);
return data.postBy;
}
export async function getCategories(): Promise<Category[]> {
const query = `
query GetCategories {
categories(first: 100) {
nodes {
id
name
slug
count
}
}
}
`;
const data = await queryWordPress<{ categories: { nodes: Category[] } }>(query);
return data.categories.nodes;
}
Advanced Query Patterns
// lib/wordpress-advanced.ts
import { queryWordPress } from './wordpress';
// Pagination support
export async function getPostsPaginated(
first = 10,
after?: string
): Promise<{
posts: Post[];
pageInfo: PageInfo;
}> {
const query = `
${POST_FIELDS}
query GetPostsPaginated($first: Int!, $after: String) {
posts(first: $first, after: $after) {
nodes {
...PostFields
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`;
const data = await queryWordPress<{
posts: {
nodes: Post[];
pageInfo: PageInfo;
};
}>(query, { first, after });
return {
posts: data.posts.nodes,
pageInfo: data.posts.pageInfo,
};
}
// Search functionality
export async function searchPosts(searchTerm: string): Promise<Post[]> {
const query = `
${POST_FIELDS}
query SearchPosts($search: String!) {
posts(where: { search: $search }) {
nodes {
...PostFields
}
}
}
`;
const data = await queryWordPress<{ posts: { nodes: Post[] } }>(
query,
{ search: searchTerm }
);
return data.posts.nodes;
}
// Related posts
export async function getRelatedPosts(
categoryId: string,
excludeId: string,
limit = 3
): Promise<Post[]> {
const query = `
${POST_FIELDS}
query GetRelatedPosts($categoryId: String!, $excludeId: String!, $limit: Int!) {
posts(
first: $limit,
where: {
categoryId: $categoryId,
notIn: [$excludeId]
}
) {
nodes {
...PostFields
}
}
}
`;
const data = await queryWordPress<{ posts: { nodes: Post[] } }>(
query,
{ categoryId, excludeId, limit }
);
return data.posts.nodes;
}
Building Core Features
Home Page with Latest Posts
<!-- src/routes/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
import PostCard from '$lib/components/PostCard.svelte';
import Hero from '$lib/components/Hero.svelte';
export let data: PageData;
</script>
<svelte:head>
<title>Home - WordPress + SvelteKit</title>
<meta name="description" content="A modern blog built with SvelteKit and WordPress" />
</svelte:head>
<Hero />
<main class="container mx-auto px-4 py-12">
<h2 class="text-3xl font-bold mb-8">Latest Posts</h2>
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{#each data.posts as post}
<PostCard {post} />
{/each}
</div>
</main>
<style>
:global(html) {
scroll-behavior: smooth;
}
</style>
// src/routes/+page.server.ts
import { getPosts } from '$lib/wordpress';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => {
const posts = await getPosts(6);
return {
posts,
};
};
Blog List with Pagination
<!-- src/routes/blog/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
import PostCard from '$lib/components/PostCard.svelte';
import Pagination from '$lib/components/Pagination.svelte';
import { page } from '$app/stores';
export let data: PageData;
$: currentPage = Number($page.url.searchParams.get('page')) || 1;
</script>
<svelte:head>
<title>Blog - All Posts</title>
</svelte:head>
<div class="container mx-auto px-4 py-12">
<h1 class="text-4xl font-bold mb-8">All Blog Posts</h1>
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3 mb-12">
{#each data.posts as post (post.id)}
<div in:fade={{ delay: 100, duration: 300 }}>
<PostCard {post} />
</div>
{/each}
</div>
<Pagination
hasNext={data.pageInfo.hasNextPage}
hasPrev={data.pageInfo.hasPreviousPage}
{currentPage}
/>
</div>
<script>
import { fade } from 'svelte/transition';
</script>
// src/routes/blog/+page.server.ts
import { getPostsPaginated } from '$lib/wordpress-advanced';
import type { PageServerLoad } from './$types';
const POSTS_PER_PAGE = 9;
export const load: PageServerLoad = async ({ url }) => {
const page = Number(url.searchParams.get('page')) || 1;
const after = url.searchParams.get('after') || undefined;
const { posts, pageInfo } = await getPostsPaginated(
POSTS_PER_PAGE,
after
);
return {
posts,
pageInfo,
currentPage: page,
};
};
Individual Post Page
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
import { formatDate } from '$lib/utils';
import RelatedPosts from '$lib/components/RelatedPosts.svelte';
import ShareButtons from '$lib/components/ShareButtons.svelte';
export let data: PageData;
$: ({ post, relatedPosts } = data);
</script>
<svelte:head>
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
<meta property="og:title" content={post.title} />
<meta property="og:description" content={post.excerpt} />
{#if post.featuredImage}
<meta property="og:image" content={post.featuredImage.sourceUrl} />
{/if}
</svelte:head>
<article class="container mx-auto px-4 py-12 max-w-4xl">
<header class="mb-8">
<h1 class="text-4xl font-bold mb-4">{post.title}</h1>
<div class="flex items-center gap-4 text-gray-600">
{#if post.author.avatar}
<img
src={post.author.avatar.url}
alt={post.author.name}
class="w-10 h-10 rounded-full"
/>
{/if}
<span>{post.author.name}</span>
<span>•</span>
<time datetime={post.date}>
{formatDate(post.date)}
</time>
</div>
</header>
{#if post.featuredImage}
<figure class="mb-8">
<img
src={post.featuredImage.sourceUrl}
alt={post.featuredImage.altText}
class="w-full rounded-lg"
loading="lazy"
/>
</figure>
{/if}
<div class="prose prose-lg max-w-none mb-12">
{@html post.content}
</div>
<ShareButtons title={post.title} url={$page.url.href} />
{#if relatedPosts.length > 0}
<RelatedPosts posts={relatedPosts} />
{/if}
</article>
<style>
:global(.prose pre) {
@apply bg-gray-100 rounded-lg p-4 overflow-x-auto;
}
:global(.prose code) {
@apply bg-gray-100 px-1 py-0.5 rounded text-sm;
}
</style>
// src/routes/blog/[slug]/+page.server.ts
import { getPostBySlug, getRelatedPosts } from '$lib/wordpress';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
const post = await getPostBySlug(params.slug);
if (!post) {
throw error(404, 'Post not found');
}
// Get related posts from the same category
const categoryId = post.categories[0]?.id;
const relatedPosts = categoryId
? await getRelatedPosts(categoryId, post.id, 3)
: [];
return {
post,
relatedPosts,
};
};
Advanced Patterns
Real-time Search with Debouncing
<!-- src/lib/components/Search.svelte -->
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { debounce } from '$lib/utils';
import type { Post } from '$lib/types';
export let placeholder = 'Search posts...';
let searchTerm = '';
let searching = false;
let results: Post[] = [];
let showResults = false;
const dispatch = createEventDispatcher();
const search = debounce(async (term: string) => {
if (term.length < 2) {
results = [];
return;
}
searching = true;
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(term)}`);
results = await response.json();
showResults = true;
} catch (error) {
console.error('Search error:', error);
results = [];
} finally {
searching = false;
}
}, 300);
$: search(searchTerm);
function selectResult(post: Post) {
dispatch('select', post);
searchTerm = '';
showResults = false;
}
</script>
<div class="relative">
<input
type="search"
bind:value={searchTerm}
on:focus={() => (showResults = true)}
on:blur={() => setTimeout(() => (showResults = false), 200)}
{placeholder}
class="w-full px-4 py-2 border rounded-lg"
/>
{#if searching}
<div class="absolute right-3 top-3">
<div class="animate-spin h-4 w-4 border-2 border-gray-500 rounded-full border-t-transparent" />
</div>
{/if}
{#if showResults && results.length > 0}
<div class="absolute top-full left-0 right-0 mt-1 bg-white border rounded-lg shadow-lg max-h-96 overflow-y-auto">
{#each results as post}
<button
on:click={() => selectResult(post)}
class="w-full px-4 py-2 text-left hover:bg-gray-50 border-b last:border-b-0"
>
<div class="font-medium">{post.title}</div>
<div class="text-sm text-gray-600 line-clamp-1">
{post.excerpt}
</div>
</button>
{/each}
</div>
{/if}
</div>
Preview Mode Implementation
// src/routes/api/preview/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getPostBySlug } from '$lib/wordpress';
export const GET: RequestHandler = async ({ url, cookies }) => {
const slug = url.searchParams.get('slug');
const secret = url.searchParams.get('secret');
if (!slug || !secret) {
return json({ error: 'Missing parameters' }, { status: 400 });
}
if (secret !== process.env.PREVIEW_SECRET) {
return json({ error: 'Invalid secret' }, { status: 401 });
}
const post = await getPostBySlug(slug);
if (!post) {
return json({ error: 'Post not found' }, { status: 404 });
}
// Set preview cookie
cookies.set('preview_mode', 'true', {
httpOnly: true,
sameSite: 'strict',
path: '/',
maxAge: 60 * 60, // 1 hour
});
return json({ redirect: `/blog/${slug}` });
};
Infinite Scroll Implementation
<!-- src/lib/components/InfiniteScroll.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
import type { Post } from '$lib/types';
export let initialPosts: Post[] = [];
export let loadMore: (cursor: string) => Promise<{
posts: Post[];
hasMore: boolean;
cursor: string;
}>;
let posts = [...initialPosts];
let loading = false;
let hasMore = true;
let cursor = '';
let observer: IntersectionObserver;
let trigger: HTMLElement;
onMount(() => {
observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMore && !loading) {
fetchMore();
}
},
{ threshold: 0.1 }
);
if (trigger) observer.observe(trigger);
return () => {
if (observer) observer.disconnect();
};
});
async function fetchMore() {
loading = true;
try {
const result = await loadMore(cursor);
posts = [...posts, ...result.posts];
hasMore = result.hasMore;
cursor = result.cursor;
} catch (error) {
console.error('Failed to load more posts:', error);
hasMore = false;
} finally {
loading = false;
}
}
</script>
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{#each posts as post (post.id)}
<slot {post} />
{/each}
</div>
{#if hasMore}
<div bind:this={trigger} class="h-20 flex items-center justify-center">
{#if loading}
<div class="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent" />
{/if}
</div>
{/if}
Performance Optimization
Static Site Generation
// svelte.config.js
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/kit/vite';
export default {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: null,
precompress: false,
strict: true
}),
prerender: {
crawl: true,
enabled: true,
entries: ['*'],
handleHttpError: ({ path, referrer, message }) => {
// Handle 404s gracefully
if (message.includes('404')) {
console.warn(`404 at ${path} (referred from ${referrer})`);
return;
}
throw new Error(message);
}
}
}
};
Image Optimization
<!-- src/lib/components/OptimizedImage.svelte -->
<script lang="ts">
export let src: string;
export let alt: string;
export let width: number;
export let height: number;
export let loading: 'lazy' | 'eager' = 'lazy';
// Generate srcset for responsive images
const sizes = [320, 640, 960, 1280, 1920];
const srcset = sizes
.map(size => `${src}?w=${size} ${size}w`)
.join(', ');
</script>
<img
{src}
{srcset}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
{alt}
{width}
{height}
{loading}
decoding="async"
class="w-full h-auto"
/>
Caching Strategy
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
const response = await resolve(event);
// Cache static assets
if (event.url.pathname.startsWith('/assets/')) {
response.headers.set(
'cache-control',
'public, max-age=31536000, immutable'
);
}
// Cache API responses
if (event.url.pathname.startsWith('/api/')) {
response.headers.set(
'cache-control',
'public, max-age=300, s-maxage=300'
);
}
// Add security headers
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
return response;
};
Deployment Strategies
Vercel Deployment
// svelte.config.js for Vercel
import adapter from '@sveltejs/adapter-vercel';
export default {
kit: {
adapter: adapter({
runtime: 'nodejs18.x',
regions: ['iad1'], // US East
functions: {
'/api/*': {
maxDuration: 10
}
}
})
}
};
Docker Deployment
# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/build ./build
COPY --from=builder /app/package*.json ./
RUN npm ci --production
EXPOSE 3000
CMD ["node", "build"]
Edge Deployment with Cloudflare
// svelte.config.js for Cloudflare
import adapter from '@sveltejs/adapter-cloudflare';
export default {
kit: {
adapter: adapter({
routes: {
include: ['/*'],
exclude: ['<all>']
}
})
}
};
Conclusion
SvelteKit paired with headless WordPress creates a powerful, modern web development stack. The compile-time optimizations of Svelte combined with WordPress's content management capabilities deliver exceptional performance and developer experience.
Key takeaways: - SvelteKit's smaller bundle sizes and lack of runtime overhead make it ideal for content-heavy sites - GraphQL integration provides efficient data fetching - Built-in SSR and static generation options offer flexibility - The reactive nature of Svelte simplifies complex UI interactions - Deployment options range from static hosting to edge functions
Whether you're building a simple blog or a complex web application, this stack provides the tools and performance characteristics needed for modern web development.
Ready to explore more headless WordPress options? Check out our comprehensive Headless WordPress Guide for detailed comparisons and implementation strategies.