DEV Community

Cover image for How You Can Build The Best, Fastest Blog On The Internet
Aditya Oberai
Aditya Oberai

Posted on

How You Can Build The Best, Fastest Blog On The Internet

It is (almost) a rite of passage for every developer today to develop and host a blog on their own (because we really love to "engineer" our own solutions, don't we?) In all honesty, developers are so spoilt for choice today with the number of web frameworks, CMS solutions, and other tools available in the market that it has become next to impossible to pick that "one correct solution" any longer.

Too many

Now, while the title of this blog is pure clickbait (sorry!) and the irony of not publishing this on a self-hosted blog is not lost on me, we do have a legitimate challenge here. Therefore, in this blog, we will discuss how you can build a simple yet highly performant blog site and deploy it on the internet.

Defining our requirements

First things first, we must define what we need from this blog site. If I were to host my own blog, I would ideally want the following features at the minimum:

  • Simple, consistent experience (just one layout works)
  • Easy content authoring system (ideally, one that allows writing Markdown)
  • Quick load times (all content should be pre-rendered)
  • SEO-friendly (pre-rendering can help here too)
  • Easy to preview and test (should be simple to locally deploy)
  • Simple and cheap to deploy (pick one of 'n' hosting platforms with GitHub CI/CD available)

With these decided, we can now pick a tech stack to build our blog site.

Choosing the tech stack

Considering the feature set above, I have chosen the following tech stack to build a blog site:

Web framework: SvelteKit

SvelteKit is a full-stack framework for building modern web applications using Svelte. It provides file-based routing, multiple rendering methods (including Static Site Generation or SSG), API handling, and other powerful features to help you build your web apps.

Content authoring system: Markdoc

Markdoc is a Markdown-based content authoring framework developed by Stripe. It extends standard Markdown by adding custom tags, components, and validations, making it more powerful for structured content in web applications.

Site hosting: Azure Static Web Apps

Azure Static Web Apps is a fully managed hosting service for static sites and web frameworks like Next.js, Nuxt, and SvelteKit. It provides automatic deployments from GitHub, free SSL, global CDN, and built-in serverless functions support (via Azure Functions).

Note: You can pick a different hosting platform if you like. Azure Static Web Apps is a personal preference. Platform choice will not directly affect the site's functionalities; however, it may affect site reliability and scalability.

Developing the blog site

Now that we have decided on our site's tech stack, we can start building the blog site.

Step 1: Create a SvelteKit app

Pre-requisite: install Node.js on your system if you haven't already.

To create a SvelteKit app, you must open your terminal and run the following command:

npx sv create
Enter fullscreen mode Exit fullscreen mode

This will lead us to an interactive setup process, where must pick our project directory, template (pick SvelteKit minimal, type checking, and other configuration settings.

┌  Welcome to the Svelte CLI! (v0.6.21)
│
◇  Where would you like your project to be created?
│  blog
│
◇  Which template would you like?
│  SvelteKit minimal
│
◇  Add type checking with Typescript?
│  No
│
◆  Project created
│
◇  What would you like to add to your project? (use arrow keys / space bar)
│  prettier, eslint
│
◆  Successfully setup add-ons
│
◇  Which package manager do you want to install dependencies with?
│  npm
│
◆  Successfully installed dependencies
│
◇  Successfully formatted modified files
│
◇  Project next steps
Enter fullscreen mode Exit fullscreen mode

Once that is done, enter your project directory, run the npm install command, and your minimal SvelteKit app is ready!

Step 2: Integrate Markdoc with SvelteKit

To easily integrate Markdoc with SvelteKit, my team member from Appwrite, Torsten Dittmann, has developed an open-source NPM package, svelte-markdoc-preprocess that acts as a preprocessor and converts Markdoc to Svelte at build time.

We will install this in our app by running the following command:

npm install svelte-markdoc-preprocess
Enter fullscreen mode Exit fullscreen mode

Then, visit the svelte.config.js file and update its content:

import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import { markdoc } from 'svelte-markdoc-preprocess';

const config = {
    preprocess: [vitePreprocess(), markdoc()],
    extensions: ['.markdoc', '.svelte'],
    kit: {
        adapter: adapter()
    }
};

export default config;
Enter fullscreen mode Exit fullscreen mode

This will enable the app to use the preprocessor at build time and designate .markdoc as a valid file extension.

Step 3: Prepare a Markdoc layout

Next, we must create a Markdoc layout. This will define a common, consistent structure and styling for our blog pages. In the src directory, create a directory, markdoc. Within that, create two subdirectories, layouts and styles.

  • The layouts sub-directory

In the src/markdoc/layouts directory, add a file Blog.svelte:

<script>
    import "../styles/blog.css";
    import { formatDate } from "$lib/utils/date";

    let { title, description, author, date, timeToRead, children } = $props();
</script>

<svelte:head>
    <title>{title}</title>
    <meta name="description" content={description} />
</svelte:head>

<main>
    <section id="title">
        <a href="/"><em>&larr; Back to blog</em></a>
        <h1>{title}</h1>
        <p class="description">{description}</p>
        <ul>
            <li>{author}</li>
            <li>{formatDate(new Date(date))}</li>
            <li>{timeToRead} mins</li>
        </ul>
        <hr />
    </section>

    {@render children()}
</main>

<style>
    main {
        width: 80%;
        padding: 1.5rem 2.5rem;
        margin: 1.5rem auto;
        justify-content: center;
        align-items: center;
        background-color: #ffffff;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        border-radius: 8px;
    }

    #title > ul {
        display: flex;
        flex-direction: row;
        justify-content: space-between;
        list-style: none;
        padding: 0;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

This layout will load any frontmatter (metadata added to a .markdoc file) from our blogs and allow us to use the information on our rendered page.

Now, if you were paying close attention, you may have noticed that a non-existent formatDate function was included in this layout. To make its existence real, create a date.js file in the src/lib directory and add the following code:

export const formatDate = (date) => {
    const dt = new Date(date);
    const month = dt.toLocaleString('en-US', { month: 'short' });
    const day = dt.getDate();
    const year = dt.getFullYear();
    return `${month} ${day}, ${year}`;
};
Enter fullscreen mode Exit fullscreen mode
  • The styles sub-directory

In the src/markdoc/styles directory, add a blog.css file. You can copy the following CSS code:

body {
  margin: 0;
  padding: 0;
  font-family: 'Arial', sans-serif;
  background-color: #f8f9fa;
  color: #343a40;
  line-height: 1.6;
}

h1 {
  font-size: 2.5rem;
  font-weight: bold;
  margin-bottom: 1.5rem;
  color: #007bff;
}

h2, .description {
  font-size: 2rem;
  font-weight: 600;
  margin-bottom: 1rem;
  color: #495057;
}

p {
  font-size: 1.125rem;
  margin-bottom: 1.5rem;
  color: #495057;
}

img {
  max-width: 40%;
  height: auto;
  border-radius: 0.5rem;
  margin: 1.5rem auto;
}

a {
  color: #007bff;
  text-decoration: none;
  transition: color 0.3s;
}

a:hover {
  color: #0056b3;
  text-decoration: underline;
}

blockquote {
  font-size: 1.25rem;
  font-style: italic;
  margin: 2rem 0;
  padding-left: 1.5rem;
  border-left: 0.25rem solid #007bff;
  color: #6c757d;
  background-color: #f8f9fa;
}

pre {
  background-color: #e9ecef;
  padding: 1rem;
  border-radius: 0.5rem;
  overflow-x: auto;
  font-size: 1rem;
  color: #343a40;
}

ul, ol {
  margin-bottom: 1.5rem;
  padding-left: 2rem;
}

li {
  margin-bottom: 0.5rem;
  font-size: 1.125rem;
}

#title > ul {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  list-style: none;
  padding: 0;
}

table {
  width: 100%;
  border-collapse: collapse;
  margin: 1rem 0;
  border: 1px solid #ddd;
}

th, td {
  padding: 0.75rem;
  text-align: left;
  border: 1px solid #ddd;
}

th {
  background-color: #f4f4f4;
}

tr:nth-child(even) {
  background-color: #f9f9f9;
}

@media (max-width: 768px) {
  .container {
    margin: 1.5rem;
    padding: 1.5rem;
  }

  h1 {
    font-size: 2rem;
  }

  h2, .description {
    font-size: 1.75rem;
  }

  p {
    font-size: 1rem;
  }

  img {
      max-width: 90%;
      height: auto;
      border-radius: 0.5rem;
      margin: 1rem auto;
  }

  blockquote {
    font-size: 1.125rem;
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: This is just a rudimentary stylesheet generated by GitHub Copilot. Please ensure you make any necessary updates.

Once this is done, revisit the svelte.config.js file and update it as follows:

import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import { markdoc } from 'svelte-markdoc-preprocess';
// Add two new imports
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';

const config = {
    preprocess: [
        vitePreprocess(), 
        // Update markdoc configuration to include the layout
        markdoc({ 
            layouts: {
                blog: join(dirname(fileURLToPath(import.meta.url)), './src/markdoc/layouts/Blog.svelte')
            }
        })
    ],
    extensions: ['.markdoc', '.svelte'],
    kit: {
        adapter: adapter()
    }
};

export default config;
Enter fullscreen mode Exit fullscreen mode

This will allow us to use the layout we just created with our blogs. We're now ready to add our first blog.

Step 4: Add a new blog

To add our first blog, go to the src/routes directory (home to all pages in a SvelteKit app) and create a new directory, blogs. This subdirectory will become home to all the blogs that exist on your site. Inside this, create a subdirectory with whatever route you would like your blog to exist on (using my-first-blog will make the route of this blog /blogs/my-first-blog on your deployed website). Here, add a file +page.markdoc and add the following content:

---
title: "Test blog title 1"
description: "Description of this test blog"
author: Aditya Oberai
date: 2025-02-16
timeToRead: 3
layout: blog
---

## Tersis supplex causa rapida inpatiensque nomen egi

### Et non foret Pallade nec patrium genus

Lorem markdownum Bacchei vitiantur contulit in corpus ferre aselli Lycei celebrant accipis. In diem! In praestem remittas quicquid ecquis arte: nuda stabant sequens rogantis **et dextrae** serta.

- Sed velut deplorata reperire Iphis et grave
- Ingenium credere ignem conciperet numeros cum forti
- Retemptat sibi cupido

### Bibit Clytiumque merui progenuit medio

Cornua dat nec; domabile et seraque Siculique alebat sic? Sacra est mediis parentis exire naides non illum morando fecere, nunc o. Manifestam tunc, vix [auferat tantum pudor](http://fatebere.io/) radiorum labant deae: illi adfata est; coepisse te. Usum tristis quodque umerumque natam agantur nosti mihi semine quaesitae erat orbem, est. Ante Pleuron clausas, **de forte**, et urbs triste timor sit meruit verti concipias, sit sacra in.

1. Eram est lacu forsitan rapienda
2. Post rami
3. Res vomentem viscera deposuisse naris quondam simul

### Fuerunt versus in spiro dolor etiam

Ignes corpora canum sucis studio colla lacrimis est umbra speculabar avia dereptis *corpore a effugies* locuti ad terga, et. Docta est, sibi pars corporis senectae domos volandi multifori Idomeneus visum ad Aeolidae lacrimam veni. Pondere [parum](http://www.atque.com/) auras in genus laesi demunt **tu est in** votis adopertaque neque, homini dignos. Acris *intabescere Tydides vicit*; surgunt mille possent virum fatetur maiorque tu verba exspirat acumine ferenti nimiumque suis dicenda corporis. Retrahebat et meus precor, committi signis arsit tamen multa post, voluisti videre.

> Pars dispar, sit labori subiecta curvi [horrendis dea](http://cumnomine.net/deos); est. Durat dicta, arduus
> [sed tamen commisit](http://www.erat.net/mensas-vocant) plangorem, fuit. Murmure
> Tisiphone premit, nobiliumque tamen Thessalidum ante, *non*.

Posse et lolium. Sint illis, est morte potest gestu **novissimus** heros celebrandaque illic in troia morte: herba quae.

{% table %}
* Foo
* Bar
* Baz
---
*
  ```
  puts "Some code here."
  ```
* Text in a table
---
*
  A "loose" list with

  multiple line items
* Test 2
* Test 3
---
* Test 1
* A cell that spans two columns {% colspan=2 %}
{% /table %}

![Aditya Oberai's Pic](https://oberai.dev/pic)
Enter fullscreen mode Exit fullscreen mode

Locally running our app via the command npm run dev and visiting the route /blogs/my-first-blog will render the following:

My first blog

Step 5: Create a basic landing page with an index of blogs

Now that our blog layout is functional, we must also create a simple landing page. Since each blog site also needs an index of blogs for people to find content they are interested in, that is what we shall build. This will feature a couple of components:

  • The +page.js file

First, create a +page.js file in the src/routes directory. The +page.js file will house a load function to fetch the information of all Markdoc-based routes in the ./blogs subdirectory before the page is rendered. Add the following code to the +page.js file:

import { base } from "$app/paths";

export function load() {

    const blogsGlob = import.meta.glob('./blogs/**/*.markdoc', {
        eager: true
    });

    const blogs = Object.entries(blogsGlob)
        .map(([filepath, postList]) => {
            const { frontmatter } = postList;

            const slug = filepath.replace('./', '').replace('/+page.markdoc', '');
            const postName = slug.slice(slug.lastIndexOf('/') + 1);

            return {
                title: frontmatter.title,
                description: frontmatter.description,
                author: frontmatter.author,
                date: new Date(frontmatter.date),
                timeToRead: frontmatter.timeToRead,
                slug,
                href: `${base}/blogs/${postName}`,
            };
        })
        .sort((a, b) => {
            return b.date.getTime() - a.date.getTime();
        });

    return {
        blogs
    }
}
Enter fullscreen mode Exit fullscreen mode

This load function finds all .markdoc files in the src/routes/blogs directory, extracts and processes the frontmatter of each blog post, and prepares all the information to make it available as a property in our index page's .svelte file.

  • The +page.svelte file

In the src/routes directory, open the +page.svelte file and replace the code with the following:

<script>
    import { formatDate } from "$lib/utils/date";

    let { data } = $props();
</script>

<svelte:head>
    <title>Blog Demo</title>
    <meta name="description" content="A test blog site built with SvelteKit and Markdoc" />
</svelte:head>

<div class="container">
    <h1>Aditya Oberai's Blog</h1>
    <ul>
        {#each data.blogs as blog}
            <li>
                <a href={blog.href}>{blog.title}</a>
                <span class="date"> | {formatDate(blog.date)}</span>
            </li>
        {/each}
    </ul>
</div>

<style>
    .container {
        max-width: 800px;
        margin: 50px auto;
        padding: 20px;
        background-color: #ffffff;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        border-radius: 8px;
    }

    h1 {
        font-size: 2.5rem;
        font-weight: bold;
        margin-bottom: 2.5rem;
        text-align: center;
        color: #007bff;
    }

    ul {
        list-style: none;
        padding: 0;
    }

    li {
        margin: 1.25rem 0;
        font-size: 1.2rem;
    }

    a {
        text-decoration: none;
        color: #007bff;
        transition: color 0.3s;
    }

    a:hover {
        color: #0056b3;
        text-decoration: underline;
    }

    .date {
        font-size: 0.9rem;
        color: #6c757d;
    }

    @media (max-width: 768px) {
        .container {
            margin: 2.5rem;
            padding: 1.5rem;
        }

        h1 {
            font-size: 2rem;
        }

        li {
            font-size: 1rem;
        }
    }
</style>
Enter fullscreen mode Exit fullscreen mode

Locally running our app via the command npm run dev and visiting the index page at the route / will render the following:

Index page

Note: I added 2 new blogs for representation purposes. All blogs will be listed in descending order of their date of creation.

Step 6: Configure pre-rendering

To improve the blog's load times and make it more SEO-friendly, we must pre-render all the pages at build time so that all the pages are simple HTML files.

To do so, install the SvelteKit static adapter (and remove the auto adapter as it is an unnecessary dependency):

npm uninstall @sveltejs/adapter-auto
npm install @sveltejs/adapter-static
Enter fullscreen mode Exit fullscreen mode

Then, visit the svelte.config.js file and edit the adapter import to the following:

- import adapter from '@sveltejs/adapter-auto';
+ import adapter from '@sveltejs/adapter-static';
Enter fullscreen mode Exit fullscreen mode

Lastly, visit the src/routes directory, create a +layout.js file, and add the following code:

export const prerender = true;
Enter fullscreen mode Exit fullscreen mode

This will ensure that every single page in the app is pre-rendered as HTML.

At this point, I built our blog app using npm run build and previewed it to generate a Lighthouse report.
Lighthouse report
We received a 100 in the Performance and SEO categories.

We can now push the SvelteKit app to GitHub and are ready to deploy it.

Step 7: Deploy the blog site

Pre-requisite: create an Azure account

Deploying the blog site on Azure Static Web Apps is fairly simple due to their in-built GitHub integration. Go to the Azure portal, click on the Create a resource button, search Static Web App, and create a resource.

Azure Static Web App

Select (or create) a resource group, add a name for your web app, select a pricing plan, and connect your GitHub account and repo.

Azure web app setup - Basics tab

Azure will auto-detect the framework (SvelteKit) and prepare a GitHub Action for you. Leave those settings as default and create the resource.

Azure created resource

Note: Don't forget to click on each step and expand to understand what must be done!

Conclusion

And with that, you have built the best, fastest blog on the internet! If you liked this blog site, please star it on GitHub.

SvelteKit Markdoc Blog Demo

A blog that uses Static Site Generation on SvelteKit and Markdoc to render blogs from Markdown

Setup

Run the following commands in your terminal:

git clone https://github.com/adityaoberai/sv-markdoc-demo.git
cd sv-markdoc-demo
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

Add new blog

To add a new blog, go to the directory ./src/routes/blog, create a new directory (the name of this directory will act as your endpoint), and add a +page.markdoc file with content in the following style:

---
title: Title of the blog
description: Description of the blog
author: Aditya Oberai
date: 2024-12-16
timeToRead: 3
layout: blog
---

<Add blog content in Markdown here>
Enter fullscreen mode Exit fullscreen mode

Note: Do not edit the layout: blog field. ALso, the date is in yyyy-mm-dd format.

You can also try out the demo blog deployed on Azure Static Web Apps.

Try out the blog

Thank you so much for reading this blog, wish you all happy building!

Have a good day

Top comments (0)