Building a Markdown blog in SvelteKit with theme switching, custom components & keyboard shortcuts.

svelteverceltailwindshadcnweb

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 ↗


Svelte & SvelteKit are my preferred web technology

Vercel

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

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

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)

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}