SvelteKit x Mdsvex (Part 2)

Published on May 17, 2023

Welcome back! In Part 1 we got our SvelteKit project set up and installed all the tools we’ll need to get going. This time we’re going to render a markdown file as a blog post with the help of Mdsvex. A lot of this post will be similar to parts of JoyOfCode’s post, so if you want more details, check out his post.


Let’s jump in!

Create a post using markdown

Create a directory called posts in the src directory. Inside src/posts create a file called hello-world.md.

---
title: Hello World
description: My hello world post!
date: '2023-05-19'
tags:
  - svelte
  - daisyui
  - tailwindcss
  - mdsvex
published: true # this will be used to filter out unpublished posts
---

## Hello World

This is my first post!

Add typing (skip this if you’re using JS)

Create a file to store your types in src/lib/types/. I just called mine index.ts. In src/lib/types/index.ts add the following:

export type Post = {
	title: string
	slug: string
	description: string
	date: string
	tags: string[]
	published: boolean
}

Create an API endpoint to get our posts

Create a +server.ts file in src/routes/api/posts/. This will be used to get all of our posts, so we can render a list of them on the home page.

import { json } from '@sveltejs/kit';
import type { Post } from '$lib/types';
import { dev } from '$app/environment';

async function getPosts() {
	let posts: Post[] = [];

	const paths = import.meta.glob('/src/posts/*.md', { eager: true });

	for (const path in paths) {
		const file = paths[path];
		const slug = path.split('/').at(-1)?.replace('.md', '');

		if (file && typeof file === 'object' && 'metadata' in file && slug) {
			const metadata = file.metadata as Omit<Post, 'slug'>;
			const post = { ...metadata, slug } satisfies Post;

			// only add posts to the array if we're in dev mode or if the post is published
			// this will allow us to preview posts (if publised is false in the .md file) locally before publishing them
			dev && posts.push(post) || post.published && posts.push(post);
		}
	}

	posts = posts.sort((first, second) => new Date(second.date).getTime() - new Date(first.date).getTime());

	return posts;
}

export async function GET() {
	const posts = await getPosts();
	return json(posts);
}

What’s going on here?

  • We’re using import.meta.glob which is a Vite feature that lets us get all the posts using a glob and eager lets us read the file contents without having to await.
  • We loop over the paths to get the slug (file name) and then we drop the .md extension.
  • We check if the file is an object and has a metadata property. If it does, we destructure the metadata and create a new post object with the slug and metadata and push it to the posts array (if they are published or we are developing locally).
  • Finally, we sort the posts by date and return them.

Sweet! Now we can retrieve all of our posts from the API endpoint.

Create a page to render our posts

Before we continue, let’s create a utility function to format the date in our posts. Add the following to src/lib/utils/index.ts:

type DateStyle = Intl.DateTimeFormatOptions['dateStyle']

export function formatDate(date: string, dateStyle: DateStyle = 'medium', locales = 'en') {
	const formatter = new Intl.DateTimeFormat(locales, { dateStyle })
	return formatter.format(new Date(date))
}

Create a +page.ts file in src/routes/. We’ll define a load function to call our endpoint and then pass the posts to our page.

import type { Post } from '$lib/types'

export async function load({ fetch }) {
	const response = await fetch('api/posts')
	const posts: Post[] = await response.json()
	return { posts }
}

Now in src/routes/+page.svelte we can loop over the posts from the load function and display them: We can access the posts via the data variable we export in the page. (This is built into SvleteKit, how cool!)

<script lang="ts">
	import { formatDate } from '$lib/utils'

	export let data
</script>

<svelte:head>
    <title>Home</title>
</svelte:head>

<section>
    {#each data.posts as post}
        <a href={post.slug} class="flex flex-col cursor-pointer rounded-box p-2 sm:p-4 sm:my-1 hover:underline">
            <p class='font-bold text-2xl'>{post.title}</p>
            <p class='text-md'>{post.description}</p>
            <p class='italic pt-1 text-sm'>{formatDate(post.date)}</p>
        </a>
        <div class='divider'></div>
    {/each}
</section>

You should now be able to see your post rendered on the page!

Render a single post

Create a routes/[slug]/+page.ts file. This will be used to render a single post.

import { error } from '@sveltejs/kit'

export async function load({ params }) {
	try {
		const post = await import(`../../posts/${params.slug}.md`)

		return {
			content: post.default,
			meta: post.metadata
		}
	} catch (e) {
		throw error(404, `Could not find ${params.slug}`)
	}
}

We use a dynamic import to get the post and then return the content and metadata from the post. If the post doesn’t exist, we throw a 404 error.

Now in routes/[slug]/+page.svelte we can render the post:

<script lang="ts">
    import {formatDate} from '$lib/utils'

    export let data
</script>

<article>
    <hgroup class="flex flex-col">
        <h1 class='text-2xl font-bold mb-1'>{data.meta.title}</h1>
        <p class='italic text-sm mb-4'>Published on {formatDate(data.meta.date)}</p>
    </hgroup>

    <div class='my-2 flex flex-wrap'>
        {#each data.meta.tags as tag}
            <span class="mx-0.5 p-2 border-2 rounded-2xl text-xs font-semibold cursor-pointer">&num;{tag}</span>
        {/each}
    </div>

    <div class='divider'></div>

    <!-- The prose class is from Tailwind Typography so we don't need to define -->
    <div class="prose">
        <svelte:component this={data.content}/>
    </div>
</article>

We can use a Svelte component to pass data.content to <svelte:component this={data.content} /> because the markdown file is imported as a module and processed by mdsvex.

You should now be able to see your post rendered from the markdown on your page!

Repo

SvelteKitxMdsvex


That wraps up this tutorial. I hope you found it useful. If you have any questions or feedback, please feel free to reach out to me. I’d love to see what you create! I will be adding an option to subscribe to a mailing list soon, so you get notified when I post something but for now I have links littered in the header and footer of this site so take your pick if you want to get in touch 😅.