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 andeager
lets us read the file contents without having toawait
. - We loop over the
paths
to get theslug
(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 theslug
andmetadata
and push it to theposts
array (if they arepublished
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">#{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
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 😅.