Making a blog website with SvelteKit
Created: | Updated:
List of projects used.
SvelteKit
gray-matter - parse front-matter
markdown-it - parse markdown
markdown-it-prism > Prism - syntax highlighting
Changes to directory structure after initializing SvelteKit project.
articles
└─ sveltekit_is_amazing.md
src
└─ routes
└─ +layout.svelte
└─ +page.server.js
└─ index.svelte
└─ blog
├─ [slug].svelte
└─ [slug].json.js
Create articles folder in your root directory. It is home for all future blog posts. Then create the first article, “SvelteKit is amazing”.
mkdir articles
cd articles
touch sveltekit_is_amazing.md
Add front-matter and content to the article.
---
title: SvelteKit is amazing
description: SvelteKit is absolutely amazing.
created: 2021-06-10
tags:
- 'SvelteKit'
- 'Markdown'
---
Why SvelteKit is absolutely amazing?
...
File name should match article title ( transformed to lowercase, and empty strings replaced with _ ).
Get
Two server endpoints are to be made.
One for a blog page ~/routes/blog/[slug]/+page.server.js
.
And one for an index page ~/routes/+page.server.js
(getting blogs metadata).
So, inside the load
function for fetching a blog, following steps are performed.
- Load corresponding
.md
file as string. - Process separately front-matter and content.
- Parse front-matter with 'gray-matter’ as JSON object.
- Parse file content with ‘markdown-it’ returning HTML as string.
- Return processed data.
/* ~/src/routes/blog/[slug]/+page.server.js */
import fs from 'fs';
import mi from 'markdown-it';
import prism from 'markdown-it-prism';
import 'prism-svelte';
import matter from 'gray-matter';
const md = mi({
html: true,
linkify: true,
typographer: true,
});
// Remember old renderer, if overridden, or proxy to default renderer
const defaultRender =
md.renderer.rules.link_open ||
function (tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
};
md.use(prism, {});
/** @typedef {import("../../../typings/types").BlogMetadata} DM*/
/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
const { slug } = params;
const doc = await fs.promises.readFile(`articles/${slug}.md`, 'utf8');
// console.log(doc)
const { data, content } = matter(doc);
/** @type {DM} */
const metadata = data;
metadata.tags = metadata.tags.map((t) => t.toLowerCase());
/* 3. Process content {String}*/
const html = md.render(content);
return {
blog: { html, metadata },
};
}
As all blog posts are placed in articles folder, and only there, and only in .md format, the load
function for fetching blogs metadata is much simpler. Steps:
- Get file names of all blog posts.
- Load each file and parse front-matter.
- Sort by creation date.
- Return blogs as array.
/* ~/src/routes/+page.server.js */
import fs from 'fs';
import matter from 'gray-matter';
/** @type {import('./$types').PageServerLoad} */
export async function load() {
const fileNames = await fs.promises.readdir('articles');
const blogs = await Promise.all(
fileNames.map(async (fileName) => {
const doc = await fs.promises.readFile(`articles/${fileName}`, 'utf8');
const { data } = matter(doc);
/** @type {import('../../typings/types').BlogMetadata } */
const md = data;
return md;
})
);
blogs.sort((a, b) => b.created.getTime() - a.created.getTime());
return { blogs };
}
Markup
<!-- +layout.svelte -->
<a href="/">Index</a>
<main>
<slot />
</main>
As the load function fetches corresponding endpoints, the index page receives all posts from it.
Then all tags are listed and blogs are filtered by route param (?tag=) if there is one.
<!-- index.svelte -->
<script>
import { browser } from '$app/env';
import { page } from '$app/stores';
/** @type {import('./$types').PageServerData} */
export let data;
const blogs = data.blogs;
const tagSet = new Set();
blogs.forEach((blog) => {
blog.tags.forEach((tag) => tagSet.add(tag));
});
const tags = [...tagSet].sort();
let tag;
$: blogsFilteredByTag = tagSet.has(tag)
? blogs.filter((p) => p.tags.includes(tag))
: blogs;
let unsub;
if (browser) {
unsub = page.subscribe(({ url }) => {
tag = url.searchParams.get('tag');
// console.log({ tag })
});
}
onDestroy(() => {
unsub && unsub();
});
</script>
<svelte:head>
<title>Index</title>
</svelte:head>
<ol>
{#each tags as tag}
<li><a href="/?tag={tag}"> #{tag} </a></li>
{/each}
</ol>
<ol>
{#each blogsFilteredByTag as blog}
<li>
<a href="/blog/{blog.title.replaceAll(' ', '_').toLowerCase()}">
{blog.title}
</a>
<p>{blog.description}</p>
<div class="tags">
{#each blog.tags as tag}
<a href="/?tag={tag}"> #{tag} </a>
{/each}
</div>
</li>
{/each}
</ol>
Just fetching and displaying a blog.
<script>
export let blog;
const blog = data.blog;
</script>
<svelte:head>
<title>{blog.metadata.title}</title>
<meta name="description" content="{blog.metadata.description}" />
</svelte:head>
<article>
<h1>{blog.metadata.title}</h1>
<p>
<i>Created:</i>
<time>{new Date(blog.metadata.created).toLocaleDateString()}</time>
</p>
{@html blog.html}
</article>
<ol>
{#each blog.metadata.tags as tag}
<li><a href="/?tag={tag}"> #{tag} </a></li>
{/each}
</ol>
Types for vscode.
/* ~/typings/types.d.ts' */
export interface BlogMetadata {
title: string;
description: string;
created: Date;
tags: string[];
}
Build
Install @sveltejs/adapter-static and update svelte.config.js as instructed. Then npm run build
.
All pages will be prerendered.
Only the index page will load JS, make hydration, turning the website into SPA.
Every blog page will go with 0 JS, so traditional navigation and no hydration.