How I Rebuilt My Personal Blog with SvelteKit and MDsveX

Note: This article preserves the architecture of my original 2022 rebuild. The site later moved to SvelteKit 2, Vite 5, and the Vercel adapter while keeping the same core idea: Markdown files are the source of truth for article pages, RSS, and the sitemap. See my SvelteKit 2 migration notes for that follow-up.

The page you’re looking at is powered by Svelte. The previous version of the site was based on Gatsby, and an even earlier version used Hexo. I rebuilt the site to learn Svelte through a real publishing system rather than an isolated demo.

index-desktop

It took me five weekends to finish the rewriting process. Now, I’d like to share what I did.

Why I chose SvelteKit

SvelteKit is a framework for building web applications. SvelteKit for Svelte is similar to Next.js for React, Nuxt.js for Vue. it offer build tools, filesystem-based routing and rendering practices for us to setup Svelte app quickly.

For development, SvelteKit integrates Vite, which makes both development and production builds extremely fast.

Additionally, I added config to make vite resolve my posts files from project root folder:

JavaScript
// svelte.config.js
    vite: {
      resolve: {
        alias: {
          $draw: path.resolve('./draw'),
          $blog: path.resolve('./blog'),
          $note: path.resolve('./note'),
          $src: path.resolve('./src'),
        }
      },
      // allows vite access to ./posts
      server: {
        fs: {
          allow: ['./']
        }
      },
    }

Routing

With SvelteKit’s filesystem-based routing, anything inside src/routes directory will be compiled to a page and accessible via its path.

On my website, I have added pages for Index (Home), About, Uses, Blog, and Draws. The Blog page is further divided into post, notes and slides:

Bash
.
├── about@page.svelte
├── blog
│   ├── [slug]
│   │   └── index@post-page.svelte
│   └── index@page.svelte
├── draw
│   ├── [slug]
│   │   └── index@draw-page.svelte
│   └── index@page.svelte
├── index@page.svelte
├── note
│   ├── [slug]
│   │   └── index@post-page.svelte
│   └── index@page.svelte
├── slide
│   └── index@page.svelte
└── uses.svx

The index@page.svelte files in blog, draw, note and slide folders serve as index pages for all my posts, drawings, notes and slides.

These pages can have a load function that fetches data for my blog posts before the component is created, and it can even run on the server side with prerender opton:

JavaScript
<script context="module">
  export const load = async({ fetch }) => {

    const fethcPosts = await fetch('/api/posts?sorted=true&reading=true');
    const allSortedPosts = await fethcPosts.json();
    return {
      props: {
        allSortedPosts,
      },
    };
  }
</script>

For the post detail pages, it is necessary to use a template to display different pages for each post. Therefore, I utilized dynamic routing with the index@post-page.svelte file wrapped in [slug] folder. The load function in these page only needs to fetch post data based on the slug:

JavaScript
<script context="module">
  export const load = async({ params, fetch }) => {
    const { slug } = params;

    const fethcPost = await fetch(`/api/post/${slug}`);
    const post = await fethcPost.json();

    if (!post) {
      return {
        status: 404,
        error: 'Post not found'
      }
    };

    return {
      props: {
        post,
      },
    };
  }
</script>

Layout

SvelteKit supports filesystem-based routing, allowing us to setup layouts, including nested and named layouts.

I added the following 4 layout files:

Bash
├── __layout-draw-page@default.svelte
├── __layout-page@default.svelte
├── __layout-post-page@default.svelte
├── __layout.svelte

__layout.svelte serves as the root layout for my website, defining the main structure of the pages:

HTML, XML
<Statistic />
<ThemeContext>
  <Header {pathname} />
  <div id="app-container" class="container">
    <slot />
  </div>
  <Footer />
</ThemeContext>

The other three files are named layouts, inherited directly from the __layout.svelte.

__layout-draw-page@default.svelte and __layout-post-page@default.svelte are layouts for blog detail pages.

__layout-page@default.svelte layouts the other pages.

You can see i used names page, draw-page and post-page for different layouts, which correspond to different page files such as index@post-page.svelte and about@page.svelte.

Styling and Typography

I prefer styling with Less and PostCSS. I like the feeling of writing original CSS Code rather than Atomic or Utility-first CSS like Tailwind CSS. As svelte-preprocess can automatically transforms the code to provide support for PostCSS and Less, I configure the svelte.config.js:

JavaScript
import sveltePreprocess from "svelte-preprocess";
...
const config = {
  preprocess: [
    sveltePreprocess({
      postcss: true,
    }),
    ...
  ],
};
export default config;

Rendering Markdown with MDsveX

To render Markdown files, i used mdsvex, which converts Markdown to HTML, supports using Markdown files as component, and allows components to be used within Markdown files via .svx file.

You can see i added uses.svx , combining svelte components, html and markdown syntax:

HTML, XML
<Seo {...seoProps} />


<div class="uses-page content">

# Uses

## Hardware

For normal markdown files, I configured mdsvex into svelte-preprocess in svelte.config.js:

JavaScript
//svelte.config.js
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
...
preprocess: [
  ...
  mdsvex({
    extensions: ['.md', '.svelte', '.svx'],
    highlight: {
      highlighter
    },
    rehypePlugins: [
      [
        rehypeUrls, 
        processUrl
      ],
      rehypeSlug,
      rehypeAutolinkHeadings,
    ],
  }),
]

rehype-slug plugin can add id attributes to headings, rehype-autolink-headings plugin add links from headings back to themselves. They help me to implement hash anchor for headings.

I customized rehype-urls plugin to fill the empty alt attribute for img tag, the target and rel attributes for outside a tag, and specified all the images should be in webp format:

JavaScript
function processUrl(url, node) {
  if (node.tagName === 'a') {
    if (!node.properties.alt) {
      node.properties.alt = 'img';
    }
    if (!url.href.startsWith('/')) {
      node.properties.target = '_blank';
      node.properties.rel = 'noreferrer external';
    }
  }
  if (node.tagName === 'img') {
    return `${node.properties.src}!/format/webp`;
  }
}

RSS

I used an endpoint, resolving all my posts and serving a XML file, to implement RSS feed.

JavaScript
// rss.xml.js

export const get = async () => {
  const sortedPosts = await getAllPosts({ sorted: true });

  const headers = {
    'Cache-Control': 'max-age=0, s-max-age=3600',
    'Content-Type': 'application/xml',
  };

  const body = xml(sortedPosts);

  return {
    headers,
    body,
  };
}

Static site generation

SSG means Static Site Generation, it is ideal for pre-rendering every page on my website.

SvelteKit provides several adapters, and I chose the static adapter for this purpose:

JavaScript
// svelte.config.js
import adapter from '@sveltejs/adapter-static'

...
kit: {
  adapter: adapter(),
    prerender: {
      default: true,
    },
  ...
}

Generating the sitemap after the build

To make a better performance for SEO, it is important to has an auto generated sitemap.

After using the static-adapter above, i can only generate sitemap files after the whole site was built.

I added a sitemap script in package.json:

JSON
// package.json
  "scripts": {
    "sitemap": "node --experimental-json-modules ./gen-sitemap.js",
  }

And a gen-sitemap.js to read all build files and trimming files url to build sitemap:

JavaScript
import fs from 'fs';
import fg from 'fast-glob';
import { create } from 'xmlbuilder2';
import globalConfig from './global-config.js';

const getUrl = (url) => {
    const trimmed = url.slice(6).replace('index.html', '');
    return `${globalConfig.siteURL}/${trimmed}`;
};

async function createSitemap() {
    const sitemap = create({ version: '1.0' }).ele('urlset', {
        xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9'
    });

    const pages = await fg(['build/**/*.html']);

    pages.forEach((page) => {
      const url = sitemap.ele('url');
      url.ele('loc').txt(getUrl(page));
    });

    const xml = sitemap.end({ prettyPrint: true });

    fs.writeFileSync('build/sitemap.xml', xml);
}

createSitemap();

What changed after the original rebuild

The 2022 implementation used the static adapter and older SvelteKit routing conventions. The current site still keeps posts as repository Markdown, but it now uses SvelteKit 2 route files, the Vercel adapter, and prerendered article output. A build generates the RSS feed and sitemap, while the shared article template emits canonical URLs, Open Graph metadata, and BlogPosting structured data.

The durable decision was not a particular adapter. It was keeping the content pipeline small: one Markdown file should produce the article page, archive entry, feed item, and sitemap URL.

Thanks

That’s what I did with Svelte and SvelteKit for this new website. Hope it can give you some ideas.

Thank you for reading.

CURRENTLY BUILDING

EveryCityMap

Style, share, and export maps for 535 curated cities. Save a free 2160px PNG, or make 4096px artwork and wallpapers.