migrate site to blatt: content, templates, blog post with old-steam theme

This commit is contained in:
2026-04-15 07:57:30 +02:00
parent c9b15c854b
commit 1f0939e27c
1312 changed files with 18774 additions and 194154 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,336 @@
# Blatt — Design Spec
**Blatt is a zero-config, single-person markdown publishing server. Drop files in a folder, they're served as a website. No admin, no database, no build step.**
## Overview
Blatt is a Node.js content server for individual publishers. It watches a content directory of markdown files, renders them through a remark/rehype pipeline, applies Nunjucks templates, and serves the result over HTTP. Content and assets live together in folders, synced via Syncthing or any file sync tool. The filesystem is the only interface — there is no admin panel, no accounts, no CMS UI.
## Tech Stack
- **Runtime:** Node.js
- **HTTP server:** Hono
- **Markdown pipeline:** unified + remark + rehype
- **Templates:** Nunjucks
- **Config:** TOML (`config.toml`)
- **Deployment:** Docker container
## Architecture
```
┌─────────────────────────────────────────────┐
│ Docker Container │
│ ┌───────────┐ ┌────────┐ ┌───────────┐ │
│ │ Hono │→ │ Remark │→ │ Nunjucks │ │
│ │ (HTTP) │ │ Rehype │ │ (template)│ │
│ └───────────┘ └────────┘ └───────────┘ │
│ ↑ ↑ │
│ ┌────┴────────────────────┐ ┌──┴───────┐ │
│ │ /data/content (volume) │ │/data/ │ │
│ │ │ │templates │ │
│ └─────────────────────────┘ └──────────┘ │
└─────────────────────────────────────────────┘
```
- Hono serves HTTP directly. No nginx or reverse proxy inside the container.
- The user places their own reverse proxy (Nginx Proxy Manager, Caddy, etc.) in front for HTTPS and domain routing.
- The server is stateless. No database, no cache files on disk. Content is read from the filesystem on each request with an in-memory cache that invalidates on file changes via `fs.watch`.
## Content Model
### Page structure
Every page is a folder containing an `index.md` and optional co-located assets:
```
content/
blog/
index.md ← collection page
2026-04-04-pumping-lemma/
index.md
proof.png
2026-04-05-easter/
index.md
bunny.svg
about/
index.md
projects/
index.md ← collection page
blatt/
index.md
screenshot.png
404.md ← custom 404 page
```
### URL mapping
Folder path = URL. The `content/` prefix and `index.md` filename are stripped.
- `content/blog/2026-04-04-pumping-lemma/index.md``/blog/2026-04-04-pumping-lemma`
- `content/about/index.md``/about`
- `content/404.md` → used for 404 responses (not a routable page)
### Assets
Images, PDFs, and other files sit alongside `index.md` in the page folder. Referenced with relative paths in markdown: `![proof](proof.png)`. Blatt serves them as static files at the same URL path as the page.
### Frontmatter
YAML frontmatter at the top of each `index.md`:
```yaml
---
title: Use the Pumping Lemma for Regular Languages
description: A walkthrough of the pumping lemma with examples.
published: 2026-04-04
template: post
taxonomy:
tags: [math, cs, formal-languages]
category: [tutorials]
---
```
#### `published` field
Controls visibility and dates:
- `published: false` → draft, not served publicly
- `published: 2026-04-04` → published on that date
- `published: 2026-04-04, 2026-04-10` → published on April 4, updated on April 10
- `published: true` or field omitted → published, no date displayed (for static pages like "About")
The first date in `published` is used for chronological sorting in collections.
#### `template` field
Selects the Nunjucks template. Falls back to `default.html` if omitted.
### Collection pages
Pages that aggregate other pages (blog index, project listing, tag pages):
```yaml
---
title: Blog
template: collection
collect:
from: blog
sort: published
order: desc
---
Optional markdown body rendered above the listing.
```
The `collect.from` value is a path relative to `content/`. The template receives the collected pages as an array.
## Templates
Nunjucks `.html` files in the `templates/` directory:
```
templates/
base.html ← root layout, all others extend this
default.html ← fallback for pages without template: set
post.html ← blog posts (date, reading time, tags)
collection.html ← listing pages (blog index, tag pages)
404.html ← error page
```
### Template inheritance
```html
{# base.html #}
<!DOCTYPE html>
<html lang="{{ config.language }}">
<head>
<meta charset="UTF-8">
<title>{{ title }} — {{ config.site_name }}</title>
{{ meta_tags }}
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
{# post.html #}
{% extends "base.html" %}
{% block body %}
<article>
<h1>{{ title }}</h1>
<time>{{ published_date }}</time> · {{ reading_time }}
{{ content | safe }}
</article>
{% endblock %}
```
### Variables available in templates
- `title`, `description` — from frontmatter
- `published_date`, `updated_date` — parsed from `published` field
- `content` — rendered HTML from markdown
- `reading_time` — auto-calculated (word count / 200 wpm)
- `taxonomy` — tags, categories from frontmatter
- `toc` — auto-generated table of contents
- `meta_tags` — auto-generated Open Graph / SEO meta tag HTML
- `config` — all values from `config.toml`
- `pages` — array of collected pages (on collection templates)
- `page` — current page object with all metadata
### Default theme
Blatt ships with a minimal built-in theme — clean typography, responsive, no frills. A starting point that can be replaced entirely by providing custom templates.
## Config
One file: `config.toml` at the data root.
```toml
[site]
name = "felixfoertsch.de"
url = "https://felixfoertsch.de"
language = "en"
description = "Personal site of Felix Foertsch"
author = "Felix Foertsch"
[server]
port = 3000
trusted_networks = ["192.168.23.0/24"]
preview_token = "some-secret"
[markdown]
syntax_highlighting = true
footnotes = true
smart_typography = true
heading_anchors = true
figure_captions = true
external_links = true
toc = true
math = true
admonitions = true
definition_lists = true
task_lists = true
mermaid = true
abbreviations = true
superscript_subscript = true
highlight_mark = true
[feed]
enabled = true
path = "/feed.xml"
[sitemap]
enabled = true
path = "/sitemap.xml"
```
All settings have sensible defaults. The only value that *must* be set for correct operation is `site.url` (for canonical URLs and Open Graph tags). Everything else works out of the box — `config.toml` itself is optional.
All markdown features are on by default. Opt out by setting individual flags to `false`.
## Docker Deployment
```yaml
services:
blatt:
image: blatt
ports:
- "3000:3000"
volumes:
- ./content:/data/content
- ./templates:/data/templates
- ./config.toml:/data/config.toml
```
Three mounts and a port. On Unraid, point `content/` at a Syncthing share.
### First run experience
1. Start container.
2. No content? Blatt serves a welcome page with instructions.
3. Drop an `index.md` into `content/` → it is live.
4. No `config.toml`? Defaults work. No `templates/`? Built-in defaults used.
Zero configuration required to get started.
### File watching
Blatt watches the content and templates directories via `fs.watch`. Drop a new file, edit a template, change config — changes appear on the next request. No restart needed.
## Draft Preview
Two mechanisms:
### Local network access
Requests from IPs matching `trusted_networks` in config see everything — drafts included. Draft pages show a subtle banner: "Draft — not publicly visible." Collection pages show drafts inline, visually distinguished (muted, with a "draft" badge).
### Token access
Append `?preview=<token>` to any URL to see draft content from anywhere. The token is set in `config.toml` as `server.preview_token`.
### Public behavior
Public requests to a draft URL receive a 404. Drafts do not appear in feeds, sitemaps, or collection listings for public visitors.
## Auto-Generated Pages
All on by default, configurable in `config.toml`:
- **RSS/Atom feed** (`/feed.xml`) — generated from collection pages
- **Sitemap** (`/sitemap.xml`) — generated from all published pages
- **Tag index pages** (`/tags/<tag>`) — auto-generated from taxonomy in frontmatter
- **Category index pages** (`/category/<category>`) — auto-generated from taxonomy
- **404 page** — uses `content/404.md` if present, otherwise built-in default
## SEO / Meta Tags
Auto-generated from frontmatter and content, overridable via frontmatter:
- **Open Graph tags** (`og:title`, `og:description`, `og:image`) — from `title`, `description`, first image
- **HTML `<meta>` description** — from `description` or auto-extracted from first paragraph
- **Canonical URL** — from `site.url` + page path
- **JSON-LD structured data** — article schema for posts (author, dates)
- **Reading time** — word count / 200 wpm, available as `{{ reading_time }}`
## Markdown Features
All features are on by default. Each can be toggled off in `config.toml` under `[markdown]`.
- **GFM tables** — pipe table syntax
- **Syntax highlighting** — Shiki, fenced code blocks with language tags
- **Footnotes** — `[^1]` reference + definition syntax
- **Smart typography** — curly quotes, em/en dashes, ellipsis
- **Heading anchors** — auto-generated IDs + visible anchor links
- **Figure captions** — solo `![alt](src "title")` promoted to `<figure>` + `<figcaption>`
- **External link decoration** — auto `rel="noopener noreferrer"` + `target="_blank"` on external URLs
- **Table of contents** — auto-generated from heading structure
- **Math** — KaTeX, `$inline$` and `$$block$$` delimiters
- **Admonitions** — `:::note`, `:::warning`, `:::tip` callout blocks
- **Definition lists** — `Term` + `: Definition` syntax
- **Task lists** — `- [ ]` / `- [x]` read-only checkboxes
- **Mermaid diagrams** — ` ```mermaid ` fenced code blocks rendered as SVG
- **Abbreviations** — `*[HTML]: Hypertext Markup Language` hover tooltips
- **Superscript / subscript** — `^super^` / `~sub~`
- **Highlight / mark** — `==highlighted text==`
## What Blatt is NOT
- **No admin panel.** The filesystem is your admin panel.
- **No user accounts.** Single-person publishing.
- **No comments system.** Use an external service or nothing.
- **No search.** Could be added later (e.g., Pagefind), not in v1.
- **No multi-language routing.** No `/en/` vs `/de/` URL prefixes.
- **No build step.** Ever. If it needs compiling, it is not Blatt.
- **No plugin system.** Features are built-in and toggled via config.
## Guided Gates
- **GG-1:** Drop a single `index.md` into an empty content directory. Verify it is served as HTML with the default template within seconds, no restart needed.
- **GG-2:** Add `published: false` to a page's frontmatter. Verify it returns 404 for public requests but renders for local network / token requests with a "Draft" banner.
- **GG-3:** Create a collection page with `collect: { from: blog }`. Verify it lists child pages sorted by published date descending.
- **GG-4:** Write a post with math (`$$E = mc^2$$`), a footnote (`[^1]`), a fenced code block, and an admonition (`:::note`). Verify all render correctly.
- **GG-5:** Set `template: easter` in frontmatter and create `templates/easter.html` extending `base.html`. Verify the custom template is applied.
- **GG-6:** Access `/feed.xml` and `/sitemap.xml`. Verify both are valid and contain published pages only (no drafts).
- **GG-7:** Check that `og:title`, `og:description`, and canonical URL are present in the HTML `<head>` of a published page.
- **GG-8:** Place an image alongside `index.md` and reference it with `![alt](image.png)`. Verify it renders as a `<figure>` with caption and the image is served correctly.