Building a Markdown blog in SvelteKit with theme switching, custom components & keyboard shortcuts.
Why Build Your Own?
Building your own blog/site to store all of your own useless thoughts and code snippets has been done over and over again. So why do it now in 2025 (as of writing) instead of using any of the pre-built, popular and featured alternatives, like đť•Ź (Twitter) or Medium?
- My working hours are entirely unrelated to web development and I (ashamedly) miss it
- Although working locally on a few Svelte projects, I have yet deployed one with Vercel

Svelte ↗
Svelte & SvelteKit are my preferred web technology

Vercel ↗
Hosting Svelte apps is incredibly easy
Features
Code examples may be simplified & trimmed so they won't work out of the box
Callout
May as well begin with the feature still within your viewport, I’ve always enjoyed the look of callouts and particularly the style of GitHub callouts. Useful for highlighting small blocks of information to different levels of importance.
example.svelte
<script lang="ts">
const containerStyle = () => {
if (type === "info") return "border-blue-500/10 bg-blue-500/5";
else if (type === "warn") return "border-yellow-500/10 bg-yellow-500/5";
else return "border-red-500/10 bg-red-500/5";
};
const textStyle = () => {
if (type === "info") return "text-blue-500";
else if (type === "warn") return "text-yellow-500";
else return "text-red-500";
};
</script>
<div class={`${textStyle()} ${containerStyle()}`}>
<span>{icon}</span>
<p>{@render children()}</p>
</div>
Disgusting, right? I’ve never been able to really dive into dynamic classes in tailwindcss
and figure out the
cleanest option so this mess will do. You can’t use an interpolated string to alter the colour like for text-${colour}-500
at runtime and instead have to return a whole string for tailwindcss
to process the class
lists at build time.
Using the cn
method combining tw-merge
and clsx
works also, but for my own sake, I’d prefer to have
chained conditionals in the script instead of the markup. If any of you know of better ways to write these
simple classes, let me know at any of my socials in the footer.

TailwindCSS ↗
Idiomatic & painless way of writing CSS
Metadata
It’s all good and well writing the blogs using Mdsvex
which is Svelte flavoured Markdown
within .svx
files… but how do we read them?

Mdsvex ↗
Rendering markdown files as Svelte pages
A handy part about the Mdsvex
library is it natively supports frontmatter
which allows us to export
metadata within each blog file. I then have a +layout.server.ts
at the root of my project that extracts said
metadata objects.
+layout.server.ts
import type { LayoutServerLoad } from "./$types";
export const load: LayoutServerLoad = async () => {
const blogs = import.meta.glob("*.svx");
const parsedMetadata = Object.entries(blogs).map(async ([filePath, component]) => ({
...(await component()).metadata,
}));
/* ... */
};
If you try and read the files using the import
method dynamically, you will face issues with the build output
obfuscating the file names etc. so the import.meta.glob
is a workaround.
Theme
Supporting theme switching between dark and light mode is as basic of a feature as it comes but there is at least one fun element of it. Avoiding the horrid “flash” of light that the server sends to the client before it has the context of your chosen theme.
I was able to do this by adding a JavaScript
file as a static resource which this repository inspired.
example.js
const storedTheme = localStorage.getItem("theme");
if (storedTheme) {
document.documentElement.setAttribute("class", storedTheme);
} else updateTheme();
const updateTheme = () => {
const system = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
document.documentElement.setAttribute("class", system);
localStorage.setItem("theme", system);
};
Remember to put the script reference at the very top of the markup body
index.html
<body>
<script src="%sveltekit.assets%/theme.js"></script>
<!-- ... -->
</body>
The setting of the document class attribute is relevant to the shadcn/ui
component library, or more
particularly the Svelte fork and it allows the components to read the current theme.

Shadcn (Svelte) ↗
Self-owned UI components ported to Svelte
Keyboard Shortcuts
You may notice, if you press “h” you will return to the homepage… welcome back those crazy few who didn’t read ahead before pressing keys at the request of a stranger. This is not because I’m a heavy Vim user but instead because during development I wanted to bounce between pages quicker than I could drag my fingers across my track pad.
example.svelte
<script lang="ts">
const handleKeyUp = ({ key }: KeyboardEvent) => {
if (key === "h") /* ... */
};
</script>
<svelte:window onkeyup={handleKeyUp} />
On your short trip back to the homepage, you may have noticed that there are some shortcuts attached to the cards which have very similar logic behind them. Although, the markup is generated by looping over the blog metadata objects which means I don’t have direct access to the button references.
So instead, during the loop I add references of the buttons I want to the stateful object keyed by their index meaning later I can access that object by the key pressed.
example.svelte
<script lang="ts">
let viewButtons = $state<Record<string, HTMLAnchorElement | null>>({
"0": null,
"1": null,
"2": null,
});
const handleKeyUp = ({ key }: KeyboardEvent) => viewButtons[key]?.click();
</script>
<svelte:window onkeyup={handleKeyUp} />
{#each data, index}
<Button bind:ref={viewButtons[index.toString()]}>
View<span>[{index}]</span>
</Button>
{/each}