Setting Up a Blog

Introduction

ReeWeb supports blog content through markdown files. Each post is a .md file in src/public/blog/, and SSG renders them to HTML, generates an RSS feed, and includes them in the sitemap. This recipe walks through the complete setup.

Directory Structure

src/public/blog/
├── en.json                ← Blog section translations
├── index.ree              ← Blog index page
├── index.ts               ← Blog index data loader
├── 01_hello-world.md      ← Blog post (markdown)
├── 02_second-post.md
└── images/                ← Blog-specific images

Step 1: Create the Blog Index Page

Create src/public/blog/index.ree:

{#layout("layout") }

<h1>{_ ui.blog_title }</h1>

{#each props.posts as post }
    <article>
        <h2><a href="{~ localized_path(post.canonical_path) }">{= post.title }</a></h2>
        <time datetime="{= post.date_iso }">{= post.date_display }</time>
        {#if post.excerpt }
            <p>{= post.excerpt }</p>
        {/if}
        <a href="{~ localized_path(post.canonical_path) }">Read more →</a>
    </article>
{/each}

Step 2: Create the Data Loader

Create src/public/blog/index.ts that collects posts from markdown files:

import { readFileSync, readdirSync } from "fs";
import { join } from "path";
import { parse_frontmatter, template_to_canonical } from "$lib/static_site";

export async function load_template_data(): Promise<Record<string, any>> {
    const blog_dir = join(import.meta.dir, "..", "..", "public", "blog");
    let entries = readdirSync(blog_dir)
        .filter((f) => f.endsWith(".md"))
        .sort();

    const posts = entries
        .map((file) => {
            const content = readFileSync(join(blog_dir, file), "utf-8");
            const { data } = parse_frontmatter(content);
            const canonical_path = template_to_canonical(`blog/${file}`);
            return {
                title: data.title || "Untitled",
                date_iso: data.date || "",
                date_display: data.date
                    ? new Date(data.date).toLocaleDateString("en-US", {
                            year: "numeric",
                            month: "long",
                            day: "numeric",
                        })
                    : "",
                excerpt: props.excerpt || "",
                canonical_path,
            };
        })
        .sort((a, b) => b.date_iso.localeCompare(a.date_iso)); // newest first

    return { posts };
}

Step 3: Create a Blog Post

Create src/public/blog/01_hello-world.md:

---
title: "Hello, World!"
date: 2026-06-01
excerpt: "Welcome to the blog! This is our first post."
layout: "layout"
---

Welcome to the ReeWeb blog! This is our first post, written in markdown.

## Features

- Syntax highlighting for code blocks
- Auto-generated heading IDs
- Tailwind classes on all elements
- External links open in new tabs

```typescript
const greeting = "Hello from ReeWeb!";
console.log(greeting);
```

The ordering prefix `01_` is stripped from the URL - the canonical path becomes `/blog/hello-world`.

<a name="step-4-generate-the-rss-feed"></a>
## Step 4: Generate the RSS Feed

Run the RSS generator:

```bash
bun rss

This runs scripts/generate_rss.ts, which scans src/public/blog/ for markdown files, extracts frontmatter, and writes a feed.xml to the output directory. The sitemap generator can also be run separately:

bun sitemap

Both are included automatically via bun ssg and can be run independently.

Step 5: Add Translations

Create src/public/blog/en.json:

{
    "blog_title": "Blog"
}

And src/public/blog/sl.json:

{
    "blog_title": "Blog"
}

Full SSG with RSS and Sitemap

For production generation that includes everything:

bun ssg
bun rss
bun sitemap

Or combine into a single script in your package.json:

"ssg:full": "bun ssg && bun rss && bun sitemap"