ricciuti.me

tech blog and personal website of a mad scientist

how i wrote this blog

Published

Let’s be honest for a moment. There’s no real reason at all to write a custom blog when there are pre-built alternatives out there that also give you more exposure, out-of-the-box integrations with the most common content providers, a rich text experience, and much, much more.

But building your own blog provides a feature that no other platform can dream of: a free blog post!

That’s right. This is the obligatory ”How I Wrote This Blog” post. And I know what you are thinking: who cares? But I feel like, while building what you are reading right now, I’ve made some decisions that can benefit you, so here we are.

The stack

I dare to say I chose a pretty simple stack. Let’s go over every decision and explain them one by one.

  • hosting: vercel. Yeah I know…BORING! But that’s what I want my hosting to be. I don’t want to do fancy stuff with custom VPS, and if you want to host your personal website on raw AWS, I’m calling the tech police because you are a criminal 🚨. Vercel handles everything for you: the SSL, the preview branches, it has a neat dashboard that gives you control over everything you need. I used to be a Netli-fan but since it’s pretty similar to Vercel, I switched to the latter because it can also stream from lambdas. I’m not using that feature right now but you know, better be safe than sorry.
  • database: absolutely no db! “How?” do I hear you say? This is a static website baby. The code is hosted on GitHub and I just store everything publicly. You don’t need a database if you hardcode everything. Joking aside, in the current state, I don’t feel the need for a database. Given that it adds a lot of complexity, I’m fine with this static website rebuilding every time I make a change to an article. #githubasdatabase
  • framework: you know it, there’s no reason for me to say it. Well I’ll say it anyway…SVELTEKIT! I admit in the beginning I was thinking of Astro: I think it’s a super-powerful framework, especially for static websites like this one. And I could’ve used Svelte there too. But I’m just too used to Sveltekit, and it’s not like the static site generation story with it is bad. Will this be a mistake? Only time will tell. In the meantime I had a lot of fun building this and I’m totally fine with it.
  • css: ok time to stir the water a bit. For the css framework, I used Tailwind! Ok now please put down you pitchfork, dip your torch in the water, take a deep breath and listen to my reasons. I was quite against Tailwind in the beginning (yeah I know, it’s what everybody says) but then I started to realize it’s value. It allows you to move fast because you don’t have to switch context but, much more importantly, it provides you with a coherent design system and it’s also better for the user because you’ll likely end up shipping less and more cacheable css. And given that I will probably be the only one working on this blog I don’t feel bad at all for making this decision.
  • markdown: what I’m using in this very moment to speak to you through the web is markdown. I’m specifically using mdsvex, which is a preprocessor for svelte that allows me to write markdown sprinkled with svelte components (I hope I will be writing a lot of interactive blog post to make this at full use)
  • syntax highlight: this being a dev blog means a lot of code snippets, so the highlighter is something really important. I went for shiki to get beautiful customizable themes. Initially, I went with shiki-twoslash that added Typescript syntax highlight to the snippets but it was really too much of a hassle because you had to recreate the whole import chain for every snippet of code, or else the build would’ve failed.

The details

At the end of the day the implementation is pretty standard but I think there are some neaty tricks that I’ve used that can be useful if you too want to write your blog with SvelteKit.

adapter-static

The blog it’s not really dynamic, all the content it’s there at build time so adapter-static made perfect sense to have fast, CDN-served static files. This also means that I can be a bit more relaxed in the performance of my load functions and syntax highlighter. Everything is done at build time so the user will not suffer any of the drawbacks.

writing experience

I wanted to have a straightforward way of authoring my blog posts. I often code at the phone, and I will probably write most of my blog posts there too. Now, while writing markdown is relatively simple, writing code is a bit more difficult. I wanted a way to have all my blog posts in one folder, the slug should be inferred from the name of the folder and the folder would also allow me to have the assets that I need one import away. So this is the folder structure that I have

lib/
└── articles/
    ├── slug-1/
   ├── index.svx
   └── asset.png
    ├── slug-2/
   ├── index.svx
   └── another-asset.png
    └── slug-3/
        └── index.svx

as you can probably spot, the convention is that I will always have an index.svx inside a folder named as the slug. How do I display them then? import.meta.glob and import to the rescue.

I created an utility function that imports all the articles and their metadata using import.meta.glob from vite and I’m using that in a server endpoint:

export function get_articles() {
	// import every index.svx in every folder, the metadata is provided by mdsvex
	// based on the frontmatter and the plugins installed
	const articles_import = import.meta.glob('$lib/articles/**/index.svx', {
		eager: true,
	});
	const articles = [];
	// loop over the keys of the import.meta.glob
	for (const article_location in articles_import) {
		// extract the slug with a regex
		const { slug } =
			article_location.match(//src/lib/articles/(?<slug>.*)/index.svx/)?.groups ?? {};
		// parse with a zod schema to ensure the correct metadata it's there (from the frontmatter)
		const { metadata } = article_schema.parse(articles_import[article_location]);
		// add everything in the array
		articles.push({
			slug,
			...metadata,
		});
	}
	// sort it by date
	articles.sort((article_a, article_b) => {
		const article_a_data = new Date(article_a.published);
		const article_b_data = new Date(article_b.published);
		return article_a_data.getTime() - article_b_data.getTime();
	});
	return articles;
}

and what about the single article I hear you ask? Well dynamic import and sveltekit +page.ts is all you need

import { get_articles } from '$lib/articles/utils.js';
import { article_schema } from '$lib/schemas/index.js';

export function entries() {
	const articles = get_articles();
	return articles.map((article) => ({ slug: article.slug }));
}

export async function load({ params: { slug } }) {
	// import the actual component with the metadata
	const imported = await import(`$lib/articles/${slug}/index.svx`);
	const validated = article_schema.parse(imported);
	return { article: validated };
}

it’s important to note that you need a +page.ts because you are returning an actual svelte component which is not serializable

And here’s how it get’s rendered on the page

<script lang="ts">
	import { date_formatter } from '$lib/utils';

	export let data;
	$: published = new Date(data.article.metadata.published);
</script>

<svelte:head>
	<title>ricciuti.me - {data.article.metadata.title}</title>
</svelte:head>
<article class="m-auto max-w-[75ch]">
	<h1>{data.article.metadata.title}</h1>
	<span class="text-xs"
		>Published <time datetime={published.toISOString()}>{date_formatter.format(published)}</time
		></span
	>
	<hr class="my-2" />
	<svelte:component this={data.article.component} />
</article>

projects and speaking

These are elements that are expected to be more static than the blog posts, yet I may update them from time to time. On the “speaking” page, you can find a collection of all the talks I’ve given, while the “projects” page showcases select projects I’m proud enough to be willing to share with you. Despite their distinct nature, I wanted a cohesive presentation for both. This led me to employ a technique similar to the one I utilized for the blog posts: each project resides within its own folder within the lib directory. Inside this folder, you’ll discover an img.png file, which serves as the project image, and an index.json file containing relevant project data. The resulting folder structure is as follows:

lib/
└── projects/
    ├── project-1/
   ├── index.json
   └── img.png
    ├── project-2/
   ├── index.json
   └── img.png
    └── project-3/
        ├── index.json
        └── img.png

with every index.json having the following structure

{
	"title": "title of the project",
	"description": "description of the project",
	"link": "https://linktoproject.example",
	"order": 200
}

another subtle trick is having the order initially be a multiple of 100.This allows me to sneak a new project in between old ones (this broke if I want to sneak 100 project in the middle but I have a life and I will never build 100 project that I’m proud of).

The import is done directly in the +page.svelte

// import all the projects metadata
const projects_data = import.meta.glob<{ default: ProjectData } & ProjectData>(
	'$lib/projects/**/index.json',
	{
		eager: true,
	},
);
// import every image
const imgs = import.meta.glob<{ default: string }>('$lib/projects/**/img.png', {
	eager: true,
});

type Project = {
	img: string;
} & ProjectData;

const projects = Object.entries(projects_data)
	.map(([folder, module]) => {
		// remove unneeded default
		// eslint-disable-next-line @typescript-eslint/no-unused-vars
		const { default: _, ...data } = module;
		// match the folder name
		const matches = folder.match(/^(?<folder>.*)index.json$/);
		let img = '';
		if (matches?.groups?.folder) {
			// find the correct image
			const img_import = imgs[`${matches.groups.folder}img.png`];
			if (img_import && typeof img_import === 'object' && 'default' in img_import) {
				img = img_import.default;
			}
		}
		return { ...data, img };
	})
	.sort((projectA, projectB) => (projectA.order ?? 0) - (projectB.order ?? 0));

in the speaking page it’s even simpler since I don’t need a folder to contain the image so I just have literally a series of json files named with multiple of 100 and I import them in the +page.svelte file.

suggestions

The final aspect I’d like to draw your attention to is the suggestions provided at the conclusion of this blog post (unless this will be the only blog post I will ever post). What you’ll notice is a collection of three articles that closely align with the content of this one. You might wonder, how is this achieved? Well, with the power of AI! I will write a more thorough post in the future to show you the exact workflow that allows me to have suggestions based on similarity, but the gist of it is that I’m using embeddings from OpenAPI and a cosine similarity function that I’ve copied from Stack Overflow

Conclusions

I hope to see you around here, again, this was the free blog post I got for building this from scratch, but I hope you still got some insight. Expect a bit more nuanced post from me in a while and have a good day! 👋