How I Built This Blog
In 2021, when I was still in junior high school, I put together my first blog with WordPress and PHP. At the time, I barely knew how to code—and, truthfully, I didn't yet have much to write about.
I started learning Rust in 2023 and Svelte in 2025. Both taught me a great deal and eventually made me want to write again. So, in early 2026, I began building this blog and its core features from scratch.
It still isn't finished—personal websites rarely are—but it's finally ready to share.
Technical Stack
Here is a quick look at the frontend stack:
Before settling on Svelte, I tried Next.js and Astro . Neither quite matched the way I wanted to work. React's mental model felt too heavyweight for this project, so I was reluctant to invest in Next.js. Astro came much closer to what I needed, but its broad feature set also introduced more abstraction than I wanted for a personal blog. 😅
The deciding factor, however, was the content pipeline. Traditional MDX tooling relies heavily on the unified ecosystem , and remark/rehype simply aren't my cup of tea. I once tried to write a custom syntax-highlighting plugin with them; it took me hours just to get all the pieces working together.
Astro 7 now uses Sätteri , a Rust-powered Markdown and MDX pipeline, by default. I haven't tried this workflow yet, but it looks promising for anyone who wants MDX without building their entire content pipeline around unified.
This is where Svelte won me over. Its syntax felt direct, and it gave me room to build exactly the Markdown pipeline I wanted without working around a large built-in abstraction.
On the backend, I use one Rust binary to accept incoming HTTP connections, serve the static site, and expose the APIs. There is no reverse proxy such as NGINX; I used one in the past, but eventually decided that it was an extra layer I didn't need here.
Here is the backend stack:
The backend handles search, comments, authentication, and static-file delivery. I prefer to keep the deployment small and avoid third-party services when they aren't necessary—especially services that add tracking or extra client-side scripts. Keeping these features under one roof gives me more control over both privacy and the reading experience.
If I had a more powerful server, I would probably experiment with ElysiaJS . Its developer experience looks delightful, and using TypeScript across the stack could simplify some boundaries between the frontend and backend.
Fortunately, Salvo also provides a friendly routing system, a broad set of features, and excellent performance, so the Rust implementation has been pleasant to build.
Inspiration
When I decided to build a blog from scratch, I searched for other people's experiences. A Reddit discussion eventually led me to Josh W. Comeau's website:
His design and ideas have inspired me enormously. If you are familiar with his work, the influence on this blog is probably obvious—and it is intentional. I designed and implemented the layout and components myself, but I often returned to Josh's solutions because they are so thoughtful and polished.
Writing
I write my posts in Markdown. On top of markdown-exit , I built a custom Svelte preprocessor that converts .md files into Svelte components while exporting their metadata. I plan to explain the preprocessor in detail in a future post.
A source file begins like this:
---
title: How I Built This Blog
description: Lorem ipsum dolor sit amet.
publishDate: 2026/7/25
category: General
---
The rest of the post goes here.After preprocessing, the generated component is roughly equivalent to this simplified output:
<script lang="ts" module>
import Paragraph from "$lib/components/markdown/Paragraph.svelte";
export const slug = "how-i-built-this-blog";
export const metadata = {
title: "How I Built This Blog",
description: "Lorem ipsum dolor sit amet.",
publishDate: "July 25th, 2026",
category: "General"
};
</script>
<Paragraph>The rest of the post goes here.</Paragraph>This means I can import a Markdown file just like a Svelte component and render it as a page.
Components
The pipeline preserves Svelte component tags as HTML-like blocks, so I can import and use Svelte components directly inside a Markdown post:
<script>
import { Flower } from "@lucide/svelte";
</script>
A flower: <Flower />During preprocessing, a small regular expression extracts and merges every <script> block before Svelte sees the generated component. As a result, I can keep an import close to the section where it is first used instead of collecting every import at the top of a long post:
<script>
import { Flower } from "@lucide/svelte";
</script>
A flower: <Flower />
...a great deal of text...
<script>
import { Car } from "@lucide/svelte";
</script>
A car: <Car />Code Playgrounds
When I saw the interactive playgrounds in Josh's posts, I was amazed. So I thought: why not build one for this blog too?
The editor is built with CodeMirror 6 . It renders the preview in a sandboxed iframe hosted at playground.lance.hk.
Whenever the code changes, the parent page sends the latest HTML, CSS, and JavaScript to the iframe through a postMessage bridge. The iframe updates its document and sends console output back to the playground.
Codebase
This is roughly how the frontend codebase is organized, with a few less relevant details omitted:
Josh has generously enabled production source maps on his site, making it possible to inspect how individual components work through the browser's Sources panel.
I haven't enabled them here yet. Source maps could expose most of the client-side codebase, though not the build-time preprocessor. Honestly, the code is still pretty rough, and I wouldn't recommend learning from it just yet. Once I've improved my craft and refined the blog, I may revisit the decision.
Lighthouse
Thanks in part to SvelteKit, the site earned a perfect score in all four categories in this Lighthouse desktop run:

Mobile emulation tells a slightly different story. First Contentful Paint increased from 0.5 seconds to 2.2 seconds, and heavier features such as the code playgrounds lowered the performance score from 99 to around 96. Achieving a perfect mobile score is surprisingly difficult, but 96 is more than good enough for me.
Search
I have always believed that search is essential, especially for documentation. Few things are more frustrating than remembering a useful passage but not being able to find it again. You can try the search page yourself.
There are two common ways to build a search index:
-
Crawl the deployed site with a tool such as Algolia's crawler or Meilisearch's docs scraper.
-
Parse the Markdown source directly, strip its syntax, and retain the text.
Both approaches are reasonable and relatively easy to set up. My blog, however, adds two complications:
-
I don't want to depend on a third-party search service for a small personal site.
-
Parsing Markdown alone would omit text rendered by imported Svelte components.
Instead, I generate the search documents from the final rendered output:
During SvelteKit's prerendering step, +server.ts loads every post module and renders its default Svelte component to HTML with render() from svelte/server. I then extract the plain text from that HTML. This resembles crawling, except that it happens locally at build time.
Here is the core idea, with unrelated fields and error handling omitted:
import { json, type RequestHandler } from "@sveltejs/kit";
import { render } from "svelte/server";
export const prerender = true;
export const GET: RequestHandler = async () => {
const posts = await loadPosts();
const index = await Promise.all(
posts.map(async ({ module: page }) => ({
slug: page.slug,
title: page.metadata.title,
content: extractHtmlText((await render(page.default)).body),
})),
);
return json(index);
};During deployment, the generated JSON is synchronized with the Rust backend and indexed with Tantivy. Search requests then query that index.
Comments
You will find a comment section at the bottom of every post. I support three OAuth 2.0 providers, so you can sign in with GitHub, GitLab, or Codeberg.
The editor should feel familiar if you have used GitHub: you can switch between Edit and Preview while writing in Markdown. The Rust backend parses the Markdown and returns a compact, presentation-friendly AST, which the client then renders.
My favorite feature is Emotions: a small, curated set of Genshin Impact reaction images that can be embedded directly in a comment. Here is one: 
Why comments? Because I genuinely want to hear from you. If something here sparks an idea—or leaves you with a question—please feel free to join the conversation.
Quote, Unquote
Let every word say exactly what you mean, and everything you say come from the heart. 使词尽可能达意,让言尽可能由衷。Like many students in China, I started learning English in primary school. I scored 139 out of 150 on the English section of the gaokao, China's national college entrance examination. Most of my English education, however, was exam-oriented; it gave me far less practice in writing naturally or expressing nuanced ideas.
My English is still a work in progress. For now, I use AI as an editor: it points out errors and helps me express my ideas more clearly. I hope that, by writing regularly, I will sharpen my instincts, rely on it less, and let my own voice come through more directly.

Still have questions? Leave a comment or reach me through the contact page .
Thanks for reading. I'd love to hear from you!
Comments