Skip to content

How to Quickly Build and Deploy a Static Markdown Blog with SvelteKit

Introduction

SvelteKit is an awesome framework for building Server-Side Rendered pages which addresses the performance and search engine optimization issues of single-page JavaScript applications. In contrast to client-side rendering, it generates static content on the server before sending it over to the user's browser. This allows you to spend less time building and more time writing awesome content for your audience. In this tutorial, we are going to use starter.dev to create a SvelteKit application that already provides you with TypeScript, SCSS, and Storybook to allow you to build applications on the go.

Prerequisites

You will need a development environment running Node.js; this tutorial was tested on Node.js version 16.18.0, and npm version 8.19.2.

Development

Setup project with SvelteKit and SCSS starter kit

  1. Create a new starter kit project
    npm create @this-dot/starter
    
  2. Select SvelteKit and SCSS
  3. Enter the name of the Project. markdown-blog-sveltekit
  4. Run cd markdown-blog-sveltekit
  5. npm install

Let’s get started.

Configuring Markdown

We’re going to use mdsvex, a markdown preprocessor for Svelte, to render our Markdown posts.

    npm i -D mdsvex

Then we will add a mdsvex.config.js in our root folder. This file is responsible for mdsvex config options.

    //mdsvex.config.js

    const config = {
        extensions: ['.svx', '.md'],
        smartypants: {
            dashes: 'oldschool',
        },
    };

    export default config;

Next, we will import mdsvex in our svelte.config.js.

	//svelte.config.js

    import adapter from '@sveltejs/adapter-auto';
    import preprocess from 'svelte-preprocess';
    import { mdsvex } from 'mdsvex'; // 👈 import mdsvex 
    import mdsvexConfig from './mdsvex.config.js'; // 👈import our mdsvex config

    /** @type {import('@sveltejs/kit').Config} */
    const config = {
        // Consult https://github.com/sveltejs/svelte-preprocess
        // for more information about preprocessors
        extensions: ['.svelte', ...mdsvexConfig.extensions],// 👈 add mdsvex file extensions
        preprocess: [
            preprocess({ typescript: true, scss: true }), 
            mdsvex(mdsvexConfig) // 👈 add mdsvex with mdsvex configuration options
        ],

        kit: {
            adapter: adapter(),
        },
    };

    export default config;

Creating posts

Each post is a Markdown file and lives in the /src/lib/posts directory we created before. Now let’s create three blog posts:

	// src/lib/posts/test-1.md
	---
    title: Blog Post 1
    date: "2022-11-27"
    description: Blog Post 1 Description
    ---
    A dog's nose is unique, just like the fingerprints of a human.
	// src/lib/posts/test-2.md
	---
    title: Blog Post 2
    date: "2022-11-28"
    description: Blog Post 2 Description
    ---
    A cat’s nose is unique, just like the fingerprints of a human.
	// src/lib/posts/test-3.md
	---
    title: Blog Post 3
    date: "2022-11-29"
    description: Blog Post 3 Description
    ---
    A dog's nose is unique, just like the fingerprints of a human.

Wonder what the triple dashed lines are? They indicate front matter, which is a way to set metadata for the page. They will be used for storing useful information such as the title, date, author, SEO metadata, etc.

We need to get the posts data rendered on the posts page. We will create a function to get and parse this data in src/lib/server/posts.ts.

	// src/lib/server/posts.ts

    import { parse } from 'path';

    type GlobEntry = {
        metadata: Post;
        default: unknown;
    };

    export interface Post {
        title: string;
        description: string;
        date: string;
    }

    // Get all posts and add metadata
    export const posts = Object.entries(
    import.meta.glob<GlobEntry>('/src/lib/posts/**/*.md', { eager: true })
    )
    .map(([filepath, globEntry]) => {
        return {
        ...globEntry.metadata,

        // generate the slug from the file path
        slug: parse(filepath).name,
        };
    })
    // sort by date
    .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
    // add references to the next/previous post
    .map((post, index, allPosts) => ({
        ...post,
        next: allPosts[index - 1] || 0,
        previous: allPosts[index + 1] || 0,
    }));

There’s a bit going on so let’s break it down.

SvelteKit has a feature that any lib in $lib/server can only be imported into server-side code and throws an error: Cannot import $lib/server/data/posts.ts into public-facing code:. To learn more about this feature, visit SvelteKit server-only-modules.

Then, we collect all the post modules in the $lib/posts directory:

export const posts = Object.entries(import.meta.glob('/src/lib/posts/**/*.md', { eager: true }))
  .map(([filepath, post]) => {
    return {
      ...post.metadata,

      // generate the slug from the file path
      slug: parse(filepath).name,

    };
  })

Then, we sort the posts by date:

    // sort by date
    .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
    // add references to the next/previous post
    .map((post, index, allPosts) => ({
        ...post,
        next: allPosts[index - 1] || 0,
        previous: allPosts[index + 1] || 0,
    }));

Posts Page

We then need to render all the posts on one page. We will add the src/routes/+page.server.ts file and the src/routes/+page.svelte file

	// src/routes/+page.server.ts
    import { posts } from '$lib/data/posts';
    import type { PageServerLoad } from './$types';

    export const load: PageServerLoad = async () => {
        return {
            posts, // make posts available on the client
        };
    };
    <!-- src/routes/+page.svelte -->

    <script lang="ts">
        import type { PageServerData } from './$types';

        export let data: PageServerData;
    </script>

    <div class="container">
        <h1>Markdown Blog</h1>

        <ul class="links">
            {#each data.posts as post}
            <li>
                <a href={`blog/${post.slug}`}>{post.title}</a>
            </li>
            {/each}
        </ul>
    </div>

    <style lang="scss">
        .container {
            width: 60%;
            margin: 1.25rem auto;
            text-align: center;

            h1 {
            font-size: 1.5rem;
            line-height: 2rem;
            font-weight: 600;
            color: #ffffff;
            padding: 1rem;
            border-radius: 0.25rem;
            background-color: #2563eb;
            }

            .links {
            margin: 0.625rem 0;
            padding: 0;
            list-style-type: none;

            a {
                text-decoration-line: underline;
                color: #2563eb;
                font-size: 1.25rem;
                line-height: 1.75rem;
                :hover {
                color: #1e40af;
                }
            }
            }
        }
    </style>

Markdown Blog

Single Post Page

Finally, we need to render the layout of each individual post.

We will create a blog/[slug] directory in routes, and create a new src/routes/blog/[slug]/+page.server.ts file. This will check the slug params against the post filename located in our posts directory src/lib/posts and return the post data to the client.

	// src/routes/blog/[slug]/+page.server.ts

    import { posts } from '$lib/data/posts';
    import { error } from '@sveltejs/kit';
    import type { PageServerLoad } from './$types';

    export const load: PageServerLoad = async ({ params }) => {
        const { slug } = params;

        // get post with metadata
        const post = posts.find((post) => slug === post.slug);

        if (!post) {
            throw error(404, 'Post not found');
        }

        return {
            post,
        };
    };

Then we create src/routes/blog/[slug]/page.ts, which loads the markdown file based on the slug.

	// src/routes/blog/[slug]/page.ts

	import type { PageLoad } from './$types';

    export const load: PageLoad = async ({ data }) => {
        // load the markdown file based on slug
        const component = await import(`../../../lib/posts/${data.post.slug}.md`);

        return {
            post: data.post,
            component: component.default,
            layout: {
            fullWidth: true,
            },
        };
    };

Finally, we render the blog layout with src/routes/blog/[slug]/page.svelte.

	<!-- src/routes/blog/[slug]/page.svelte -->

    <script lang="ts">
        import type { PageData } from './$types';

        export let data: PageData;
    </script>

    <div>
        <article>
            <header>
            <h1>{data.post.title}</h1>
            </header>
            <!-- render the post -->
            <div>
            <svelte:component this={data.component} />
            </div>
        </article>
    </div>

Conclusion

Want to start building your blog with SvelteKit today? Check out our https://starter.dev/kits/svelte-kit-scss/ to help you get started!

Thanks for reading!

If you have any questions or run into any trouble, feel free to reach out on Twitter.