Build a Blog with Sveltekit and Markdown
Tags: webdev, javascript, tutorial, svelte
Published: 2023-03-04 12:58:17

{% embed https://youtu.be/oh9BLo30m9A %}

Sveltekit is a phenomenal tool for the web. Not only is it a delight to code with but it also ships minimal JS bundles that’s about as close as you can get to writing plain JS with a framework. It’s also great for making blogs. 

Markdown is an odd markup language but it’s got a good balance between simplicity and customizability that makes it a popular choice for dev blogging.

We’ll be using the `mdsvex` library for today. Other than enabling markdown, it also allows you to use svelte components directly within markdown.

[https://mdsvex.com/docs](https://mdsvex.com/docs)

The basic idea is that `mdsvex` converts markdown into svelte components. We’ll create base post and layout components that can be automatically inherited and reused, and `mdsvex` will take care of the rest. 

## Dependencies

```tsx
"dependencies": {
	"tailwindcss": "^3.2.7",
  "@tailwindcss/forms": "^0.5.3",
  "@tailwindcss/typography": "^0.5.9",
  "luxon": "^3.2.1",
  "mdsvex": "^0.10.6"
}
```

## UI Boilerplate

Using `tailwindcss` we create our UI. 

Our initial page will have a list of posts, categorized by cards for featured posts and lists for normal posts. Clicking the post will lead to the actual blog post, which will be set up in the next section.

```tsx
// routes/+layout.svelte

<script>
  import '../app.css';
</script>

<div class="min-h-full h-full">
  <slot />
</div>
```

```tsx
// routes/+page.svelte

<script lang="ts">
  import { POSTS_LIST } from '$lib/shared/shared.constant';

  import PostCard from './post-card.svelte';
  import PostListItem from './post-list-item.svelte';

  const featuredPosts = POSTS_LIST.filter((post) => post.isFeatured);
  const posts = POSTS_LIST.filter((post) => !post.isFeatured);
</script>

<div class="relative bg-white px-4 pt-16 pb-20 sm:px-6 lg:px-8 lg:pb-28">
  <div class="relative mx-auto max-w-7xl">
    <div class="text-center">
      <h1 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
        From the blog
      </h1>
    </div>
    <div class="mx-auto mt-12 grid max-w-lg gap-5 lg:max-w-none lg:grid-cols-3">
      {#each featuredPosts as { url, title, description, author, date }}
        <PostCard {url} {title} {description} {author} {date} />
      {/each}
    </div>

    <!-- Lists -->
    <ul class="divide-y divide-gray-200">
      {#each posts as { url, title, description, author, date }}
        <PostListItem {url} {title} {description} {author} {date} />
      {/each}
    </ul>
  </div>
</div>
```

```tsx
// routes/post-card.svelte

<script lang="ts">
  import { DateTime } from 'luxon';

  export let url = '';
  export let title = '';
  export let description = '';
  export let author = '';
  export let date = '';
  export let tags = '';

  $: publishedAt = DateTime.fromISO(date).toLocaleString(DateTime.DATE_FULL);
</script>

<div class="flex flex-col overflow-hidden rounded-lg shadow-lg border border-gray-200">
  <div class="flex flex-1 flex-col justify-between bg-white p-6">
    <div class="flex-1">
      <p class="text-sm font-medium text-indigo-600">{tags}</p>
      <a
        href={url}
        class="mt-2 block"
      >
        <p
          class="text-2xl font-semibold text-gray-900 hover:bg-gradient-to-r hover:from-emerald-400 hover:to-cyan-400 hover:bg-clip-text hover:text-transparent hover:cursor-pointer"
        >
          {title}
        </p>
        <p class="mt-3 text-base text-gray-500">
          {description}
        </p>
      </a>
    </div>
    <div class="mt-6 flex flex-col justify-center">
      <!-- <p class="text-sm font-medium text-gray-900">{author}</p> -->
      <div class="flex space-x-1 text-sm text-gray-500">
        <span>{publishedAt}</span>
      </div>
    </div>
  </div>
</div>
```

```tsx
// routes/post-list-item.svelte

<script lang="ts">
  export let url = '';
  export let title = '';
  export let description = '';
  export let author = '';
  export let date = '';
  export let tags = '';
</script>

<li class="py-4">
  <div class="mx-auto max-w-7xl py-8 px-4 sm:px-6 lg:px-8">
    <div class="text-center">
      <h2 class="text-base uppercase font-semibold text-gray-900">{tags}</h2>
      <a
        href={url}
        class="mt-3 text-2xl font-bold tracking-tight text-gray-900 hover:bg-gradient-to-r hover:from-emerald-400 hover:to-cyan-400 hover:bg-clip-text hover:text-transparent hover:cursor-pointer"
      >
        {title}
      </a>
      <p class="mx-auto mt-2 max-w-xl text-lg text-gray-900">
        {date}
      </p>
    </div>
  </div>
</li>
```

And while we’re at it, we can also add the UI boilerplate for the actual blog post. Note the props, they actually will come from the frontmatter section in markdown.

```tsx
// routes/post.svelte

<script lang="ts">
  import { DateTime } from 'luxon';
  import { onMount } from 'svelte';

  export let title = '';
  export let description = '';
  export let publishedAtIso = '';
  // export let author = '';

  $: publishedAt = DateTime.fromISO(publishedAtIso).toLocaleString(DateTime.DATE_FULL);
</script>

<article>
  <div class="mx-auto max-w-6xl py-16 px-4 sm:pt-24 sm:px-6 lg:px-8">
    <div class="text-center">
      <!-- <h2 class="text-lg uppercase font-semibold text-gray-900">{tags}</h2> -->
      <h1
        class="mt-5 text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl lg:text-6xl"
      >
        {title}
      </h1>
      <p class="mx-auto mt-3 max-w-xl text-xl text-gray-900">
        {publishedAt}
      </p>
    </div>
  </div>

  <section class="w-full bg-white pb-16 px-4">
    <div class="prose max-w-4xl mx-auto text-gray-900">
      <slot />
    </div>
  </section>
</article>
```

## Configuring mdsvex

To convert markdown into svelte components we need to configure the library:

```tsx
// svelte.config.js

import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/kit/vite';
import { mdsvex } from 'mdsvex';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  // Consult https://kit.svelte.dev/docs/integrations#preprocessors
  // for more information about preprocessors
  preprocess: [
    vitePreprocess(),
    // https://joshcollinsworth.com/blog/build-static-sveltekit-markdown-blog#approach-2-dynamic-routes
    mdsvex({
      extensions: ['.md', '.svx'],
      // https://mdsvex.com/docs#layouts
      layout: './src/routes/post.svelte'
      // layout: {
      //   // _: './src/routes/post.svelte'
      //   blog: './src/routes/post.svelte'
      // }
    })
  ],
  extensions: ['.svelte', '.md', '.svx'],

  kit: {
    // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
    // If your environment is not supported or you settled on a specific environment, switch out the adapter.
    // See https://kit.svelte.dev/docs/adapters for more information about adapters.
    adapter: adapter()
  }
};

export default config;
```

## Adding a blog post

While blog posts are added as `+page.md` files, it helps to have a master list of all the posts and their metadata available in JS to reference in other files, which is why we keep `POSTS_LISTS` in `lib/shared/shared.constant.ts`.

```tsx
// lib/shared/shared.constant.ts

/**
 * This is the MASTER list of all blog posts.
 *
 * There may be duplicates everywhere, but this is the most organized
 * and parseable source of truth for blog posts.
 */
export const POSTS_LIST = [
  {
    title: 'Best online text to speech generators',
    url: '/best-online-text-to-speech-generators',
    description:
      'A high-level view of features and pricing from the newest and best online text to speech generators on the market',
    author: 'Kyle',
    date: '2023-02-05',
    isFeatured: true
  },
  {
    title: 'Best free online text to speech generators',
    url: '/best-free-online-text-to-speech-generators',
    description:
      'A high-level view of features and pricing from the newest and best free online text to speech generators on the market',
    author: 'Kyle',
    date: '2023-02-05',
    isFeatured: true
  }
];
```

```tsx
// routes/best-online-text-to-speech-generators/+page.md

---
title: 'Best online text to speech generators'
description: 'A high-level view of features and pricing from the newest and best online text to speech generators on the market'
author: 'Kyle'
publishedAtIso: '2023-02-05'
---

<script>
...
</script>

## Current state of text to speech AI voice generators

Text to speech technology, also known as speech synthesis, is software that converts written text into spoken words.

Text to speech technology has greatly improved over the past few years, leading to more realistic and natural-sounding voices. From creating more content for social media and platforms like Youtube and Tiktok, to improving accessibility for those with disabilities, or even increasing efficiency in the workplace, text to speech has numerous applications that are making a real impact in our daily lives.

This article provides a high-level view of features and pricing from the newest and best online text to speech generators on the market.

<div id="beepbooply"></div>

## Beepbooply

Beepbooply is a new online text to speech generator created to provide access to the latest voice models at the best prices.

Providing access to over 900+ voices across 80+ languages, beepbooply sources models from Google, Microsoft, and Amazon.

They offer some of the best prices along with one-time prepaid options that go for as low as $2.

- 900+ voices
- Voices from Google, Microsoft, and Amazon
...

<a href="https://beepbooply.com" target="_blank" rel="noopener noreferrer">beepbooply</a>
...
```

This is the bare minimum you need to set up a markdown blog using `mdsvex` and `Sveltekit`. I went ahead and added a few more blog posts along with a navbar so you can switch between post and list of posts, here’s how it looks:


![List of blog posts](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/po7oektxr8qabwr7nbim.png)

![Blog post](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9lodb5ah9hk0jp43pf4a.png)


The full code repository can be found at https://github.com/KTruong008/blog-kylebuildsstuff