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
- Create a new starter kit project
npm create @this-dot/starter
- Select SvelteKit and SCSS
- Enter the name of the Project.
markdown-blog-sveltekit
- Run
cd markdown-blog-sveltekit
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>
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.