Finally got my blog up and running. Just putting out a quick pilot post to see if everything is working as intended.
How It Works
The site runs on a single Hetzner box. Here's what's on it:
- Caddy — reverse proxy and automatic TLS
- SvelteKit — the frontend you're looking at
- riley_cms — a git-backed CMS I wrote in Rust
- riley_leaderboards — a versioned ranking service
- riley_auth — my simple OAuth wrapper (I never store your password on my end).
- PostgreSQL 18 with PostGIS
How Content Works
Blog posts are MDX files in a git repository. When I push changes, the CMS picks them up serves them via API. The SvelteKit frontend fetches posts at request time and renders the markdown server-side with syntax highlighting and whatnot.
The interesting part is custom components. MDX lets me drop in things like <Note>, <Updated>, and <LeaderboardEmbed>, which are real Svelte 5 components that get mounted at render time.
Code Highlighting
Here's a Rust snippet from the CMS itself:
pub async fn get_post(
&self,
slug: &str,
) -> Result<Post> {
let content_dir = self
.repo_path
.join("content");
let post_dir = content_dir.join(slug);
let config: PostConfig = self
.load_toml(&post_dir.join("config.toml"))?;
let content = tokio::fs::read_to_string(
post_dir.join("content.mdx"),
)
.await?;
Ok(Post {
slug: slug.to_string(),
title: config.title,
content,
..Default::default()
})
}And some TypeScript from the frontend's MDX parser:
export async function parseMDX(
raw: string,
): Promise<Segment[]> {
const segments: Segment[] = [];
let lastIndex = 0;
COMPONENT_RE.lastIndex = 0;
let match;
while (
(match = COMPONENT_RE.exec(raw))
!== null
) {
const before = raw.slice(
lastIndex,
match.index,
);
if (before.trim()) {
const html =
await renderMarkdown(before);
if (html) segments.push({
type: 'html',
content: html,
});
}
// ... extract component name,
// props, children
}
return segments;
}Live Leaderboard
One of the services I built is a versioned leaderboard API. Here's a little demo, embedded in a blog post:
That component fetches from the leaderboard API at render time. If there are multiple versions, you can switch between them with the dropdown. The data is stored in PostgreSQL and managed through TOML files in a separate content repo.
Interactive Components
Since the frontend is SvelteKit, I can embed fully interactive Svelte 5 components directly in posts. Here's a particle field that responds to your cursor:
Move your cursor over the field
This runs entirely client-side!
Math
Posts support LaTeX math via KaTeX. Inline math like works mid-sentence, and display math gets its own block:
Here's something crazy. Why note put the Navier-Stokes momentum equation:
The rendering happens server-side through remark-math and rehype-katex in the unified pipeline — no client-side JavaScript needed.
Markdown Features
The usual suspects all work. Bold, italic, strikethrough, and inline code.
Blockquotes look like this. They're good for calling out important ideas or quoting other people.
Lists
Unordered:
- First item
- Second item with bold and a link
- Third item
Ordered:
- Step one
- Step two
- Step three
Tables
| Service | Language | Purpose |
|---|---|---|
| riley_cms | Rust | Content management |
| riley_leaderboards | Rust | Versioned rankings |
| riley_auth | Rust | Identity / OAuth |
| Frontend | TypeScript | SvelteKit app |
Footnotes
Here's some words1. Here's some more stuff 2.
Footnotes
Trying Out Series
Check out part 2 for the next post in the series.
Comments
Loading comments...