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
└─ index.svelte
└─ blogs.json.js
└─ 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 _ ).
Endpoints
Two endpoints are to be made.
One for a blog page [slug].json.js
.
And one for an index page blogs.json.js
(getting blogs metadata).
Following docs, endpoint files have to export the get
function for GET requests.
So, inside the get
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].json.js */
import fs from 'fs';
import mi from 'markdown-it';
import prism from 'markdown-it-prism';
import matter from 'gray-matter';
// Init markdown-it
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);
};
// Make external (http(s)://) links open in a new window
md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
const href = tokens[idx].attrs[tokens[idx].attrIndex('href')][1];
// console.log(href)
if (href.startsWith('http')) {
tokens[idx].attrPush(['rel', 'noopener noreferrer']);
tokens[idx].attrPush(['target', '_blank']);
// tokens[idx].attrPush(['class', 'external-link'])
}
// pass token to default renderer.
return defaultRender(tokens, idx, options, env, self);
};
// Use Prism for syntax highlighting
md.use(prism);
/** @type {import('@sveltejs/kit').RequestHandler} */
export async function get({ params }) {
const { slug } = params;
const doc = await fs.promises.readFile(`articles/${slug}.md`, 'utf8');
// console.log(doc)
const { data: metadata, content } = matter(doc);
// console.log(metadata)
// console.log(content)
const html = md.render(content);
return {
body: JSON.stringify({ metadata, html }),
};
}
As all blog posts are placed in articles folder, and only there, and only in .md format, the get
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/blogs.json.js */
import fs from 'fs';
import matter from 'gray-matter';
/** @type {import('@sveltejs/kit').RequestHandler} */
export async function get() {
const fileNames = await fs.promises.readdir('articles');
// If there are not only .md files: fileNames.filter((fileName) => /.+\.md$/.test(fileName))
const blogs = await Promise.all(
fileNames.map(async (fileName) => {
const doc = await fs.promises.readFile(`articles/${fileName}`, 'utf8');
const { data } = matter(doc);
// console.log({ data })
return data;
})
);
blogs.sort((a, b) => b.created - a.created);
// console.log({ blogs })
return {
body: JSON.stringify(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 context="module">
/** @type {import('@sveltejs/kit').Load} */
export async function load({ fetch }) {
const url = `/blogs.json`;
const res = await fetch(url);
if (res.ok) {
const blogs = await res.json();
// console.log({ blogs })
return { props: { blogs } };
}
return {
status: res.status,
error: new Error(`Could not load ${url}`),
};
}
</script>
<script>
import { browser } from '$app/env';
import { page } from '$app/stores';
/** @type {import('../typings/types').BlogMetadata[]} */
export let 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.
<!-- [slug].svelte -->
<!-- slug = file name of the article -->
<script context="module">
import { browser, dev } from '$app/env';
export const hydrate = dev;
export const router = browser;
export async function load({ params, fetch }) {
const url = `/blog/${params.slug}.json`;
const res = await fetch(url);
if (res.ok) {
const blog = await res.json();
// console.log({ blog })
return { props: { blog } };
}
return {
status: res.status,
error: new Error(`Could not load ${url}`),
};
}
</script>
<script>
/** @typedef {import('../../typings/types').BlogMetadata} BlogMetadata */
/** @type {{ metadata: BlogMetadata, html: String}} */
export let 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.