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.
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:
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.Web framework: SvelteKit
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.Content authoring system: Markdoc
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.Site hosting: Azure Static Web Apps
Developing the blog site
Now that we have decided on our site's tech stack, we can start building the blog site.
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: This will lead us to an interactive setup process, where must pick our project directory, template (pick Once that is done, enter your project directory, run the Step 1: Create a SvelteKit app
npx sv create
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
npm install
command, and your minimal SvelteKit app is ready!
To easily integrate Markdoc with SvelteKit, my team member from Appwrite, Torsten Dittmann, has developed an open-source NPM package, We will install this in our app by running the following command: Then, visit the This will enable the app to use the preprocessor at build time and designate Step 2: Integrate Markdoc with SvelteKit
svelte-markdoc-preprocess
that acts as a preprocessor and converts Markdoc to Svelte at build time.
npm install svelte-markdoc-preprocess
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;
.markdoc
as a valid file extension.
Next, we must create a Markdoc layout. This will define a common, consistent structure and styling for our blog pages. In the In the This layout will load any frontmatter (metadata added to a Now, if you were paying close attention, you may have noticed that a non-existent In the Note: This is just a rudimentary stylesheet generated by GitHub Copilot. Please ensure you make any necessary updates. Once this is done, revisit the This will allow us to use the layout we just created with our blogs. We're now ready to add our first blog.Step 3: Prepare a Markdoc layout
src
directory, create a directory, markdoc
. Within that, create two subdirectories, layouts
and styles
.
layouts
sub-directorysrc/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>← 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>
.markdoc
file) from our blogs and allow us to use the information on our rendered page.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}`;
};
styles
sub-directorysrc/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;
}
}
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;
To add our first blog, go to the Locally running our app via the command Step 4: Add a new blog
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 %}

npm run dev
and visiting the route /blogs/my-first-blog
will render the following:
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: First, create a This load function finds all In the Locally running our app via the command Note: I added 2 new blogs for representation purposes. All blogs will be listed in descending order of their date of creation.Step 5: Create a basic landing page with an index of blogs
+page.js
file+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
}
}
.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.
+page.svelte file
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>
npm run dev
and visiting the index page at the route /
will render the following:
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): Then, visit the Lastly, visit the This will ensure that every single page in the app is pre-rendered as HTML. At this point, I built our blog app using We can now push the SvelteKit app to GitHub and are ready to deploy it.Step 6: Configure pre-rendering
npm uninstall @sveltejs/adapter-auto
npm install @sveltejs/adapter-static
svelte.config.js
file and edit the adapter import to the following:
- import adapter from '@sveltejs/adapter-auto';
+ import adapter from '@sveltejs/adapter-static';
src/routes
directory, create a +layout.js
file, and add the following code:
export const prerender = true;
npm run build
and previewed it to generate a Lighthouse report.
We received a 100 in the Performance and SEO categories.
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. 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 will auto-detect the framework (SvelteKit) and prepare a GitHub Action for you. Leave those settings as default and create the resource.Step 7: Deploy the blog site
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
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>
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.
Thank you so much for reading this blog, wish you all happy building!
Top comments (0)