Skip to content
ExponaBLOG
SVELTE & SVELTEKIT · DAY 048 / 100

Building a blog with SvelteKit: best practices explained

A complete SvelteKit content site — Markdown posts, dynamic routes, SEO meta tags, a sitemap and prerendered pages.

Expona Admin6 min read

A blog is often the first major content project teams build with SvelteKit, and it's where many discover the framework's real strengths: server-side rendering, static prerendering, and seamless data loading. If you're building a blog with SvelteKit, you'll want solid patterns for managing Markdown content, handling dynamic routes efficiently, and ensuring search engines can find and index your posts. This article walks through building a blog with SvelteKit: best practices explained—from content strategy through to a fully prerendered, SEO-friendly production site.

Content — Markdown with mdsvex or a headless CMS

The first decision is where your posts live. Most teams choose one of two paths: Markdown files checked into your repository (ideal for small blogs and documentation sites), or a headless CMS like Sanity, Contentful, or Strapi (better for larger teams with non-technical editors).

For a Markdown-based blog, mdsvex is the natural choice in a SvelteKit project. It lets you write posts as .svx files—Markdown with embedded Svelte components. This means your blog posts can include interactive elements, callouts, or custom layouts without leaving your codebase.

Set up mdsvex in your svelte.config.js:

typescript
import { mdsvex } from 'mdsvex';
import mdsvexConfig from './mdsvex.config.js';

export default {
  extensions: ['.svelte', '.md', '.svx'],
  preprocess: [mdsvex(mdsvexConfig)],
};

Store your posts in a predictable directory structure—say, src/routes/blog/posts/. Each post becomes a file: my-first-post.svx, sveltekit-performance-tips.svx, and so on. At the top of each file, use frontmatter to capture metadata:

typescript
---
title: "SvelteKit Performance Tips"
date: "2024-01-15"
slug: "sveltekit-performance-tips"
excerpt: "How to keep your SvelteKit app lean and fast."
author: "Team"
---

Your post content here...

If you're using a headless CMS instead, you'll fetch posts via API in your server load functions. The advantage is that non-developers can publish without touching code; the trade-off is extra network latency and complexity around caching and revalidation. For most blogs, Markdown in git is simpler and faster.

Routes — [slug] pages with server loads

Once your content is in place, you need a route structure that serves individual posts. SvelteKit's [slug] dynamic segments make this clean. Create a file at src/routes/blog/[slug]/+page.svelte:

typescript
// src/routes/blog/[slug]/+page.svelte


<article>
  <h1>{data.post.title}</h1>
  <time>{data.post.date}</time>
  
</article>

The heavy lifting happens in +page.server.js (or +page.ts if you're prerendering). This load function fetches or imports the post and its metadata:

typescript
// src/routes/blog/[slug]/+page.server.ts
import { error } from '@sveltejs/kit';

export async function load({ params }) {
  try {
    const post = await import(`../posts/${params.slug}.svx`);
    
    return {
      component: post.default,
      post: {
        title: post.metadata.title,
        date: post.metadata.date,
        excerpt: post.metadata.excerpt,
      },
    };
  } catch (e) {
    error(404, 'Post not found');
  }
}

For a list view, add src/routes/blog/+page.svelte with its own load function that iterates all posts:

typescript
// src/routes/blog/+page.server.ts
export async function load() {
  const posts = await Promise.all(
    Object.entries(import.meta.glob('../posts/*.svx', { eager: true }))
      .map(async ([path, module]) => ({
        slug: path.split('/').pop()?.replace('.svx', ''),
        ...module.metadata,
      }))
  );

  return {
    posts: posts.sort((a, b) => new Date(b.date) - new Date(a.date)),
  };
}

This pattern keeps load logic on the server, avoids exposing file paths to the browser, and makes it trivial to add database queries or API calls later.

SEO — svelte:head meta and sitemap route

Search engines need metadata: canonical URLs, Open Graph tags for social sharing, and descriptions. SvelteKit's <svelte:head> block lets you inject these dynamically for each post.

typescript
// src/routes/blog/[slug]/+page.svelte



  {data.post.title}
  
  
  
  
  

For a sitemap (required by most search engines), create a route at src/routes/sitemap.xml/+server.ts:

typescript
// src/routes/sitemap.xml/+server.ts
export async function GET() {
  const posts = await Promise.all(
    Object.entries(import.meta.glob('./posts/*.svx', { eager: true }))
      .map(([path, module]) => ({
        slug: path.split('/').pop()?.replace('.svx', ''),
        date: module.metadata.date,
      }))
  );

  const sitemap = `

  
    https://yourblog.com
    1.0
  
  ${posts.map(post => `
  
    https://yourblog.com/blog/${post.slug}
    ${post.date}
    0.8
  
  `).join('')}
`;

  return new Response(sitemap, {
    headers: { 'Content-Type': 'application/xml' }
  });
}

Add a robots.txt file at src/routes/robots.txt/+server.ts to guide crawlers:

typescript
export async function GET() {
  return new Response(`User-agent: *
Allow: /
Sitemap: https://yourblog.com/sitemap.xml`);
}

Prerender — Static output for speed

The final step is prerendering your blog. Instead of building dynamic HTML on each request, SvelteKit can generate all pages as static files at build time. This makes your blog blazingly fast and deployable to any static host (Netlify, Vercel, GitHub Pages).

Enable prerendering in svelte.config.js:

typescript
import adapter from '@sveltejs/adapter-static';

export default {
  kit: {
    adapter: adapter(),
    prerender: {
      entries: ['/blog', '/sitemap.xml', '/robots.txt'],
    },
  },
};

Then tell SvelteKit to crawl and prerender all dynamic routes. Create a src/routes/+layout.server.ts:

typescript
// src/routes/+layout.server.ts
export const prerender = true;

When you run npm run build, SvelteKit will:

  1. Visit every page in your entries list.
  2. Follow links to discover dynamic routes (like /blog/[slug]).
  3. Generate static HTML, CSS, and JS for each page.
  4. Output everything to a build/ directory ready to deploy.

This approach eliminates server runtime costs and gives you sub-millisecond response times. For a blog that changes weekly or monthly, prerendering is a huge win.

Common mistakes

  • Not invalidating the build cache when posts change. If you prerender and then update a Markdown file, you must rebuild. Consider using a webhook from your git provider or CMS to trigger rebuilds automatically.
  • Forgetting canonical tags. If your blog is cross-posted or mirrors content from another site, search engines need to know which version is canonical. Always include <link rel="canonical"> in your head.
  • Slow metadata extraction. Using dynamic imports in load functions can be slow if you have hundreds of posts. Cache metadata on startup or use a build-time script to generate a JSON index.
  • Open Graph images that don't exist. If you reference og:image but the URL is wrong, social platforms won't render a preview. Validate all image URLs during build.
  • Prerendering routes that depend on user state. Routes that show different content based on logged-in status or cookies can't be safely prerendered. Use export const prerender = false on those pages.

Key takeaways

  • Use mdsvex for Markdown-based blogs or a headless CMS if you need non-technical editors.
  • Organize posts in a consistent directory and load them via server load functions; use dynamic segments like [slug] to keep routes clean.
  • Always include Open Graph meta tags, canonical links, and a sitemap for SEO; these are non-negotiable for discoverability.
  • Prerender your entire blog at build time to get static files, zero runtime overhead, and instant page loads.
  • Set up automatic rebuilds (via webhook or CI/CD) so content updates flow from git or your CMS to production without manual intervention.
  • Monitor build times as your post count grows; if builds become slow, generate a JSON index of metadata instead of importing all posts on every build.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles