sblog 0.4.0

A tiny static blog generator written in Rust. Write posts in Markdown with YAML frontmatter and render them into plain HTML and CSS.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
# sblog

A tiny static blog generator written in Rust. Write posts in Markdown with
YAML frontmatter. sblog renders them into plain HTML and CSS. Serve the
output with any static host.

## Features

- **Markdown posts** with YAML frontmatter (`title`, `date`, `tags`, `summary`)
- **Tera templates** for full control over page layout
- **Syntax highlighting** for code blocks (via syntect)
- **Tag pages** generated automatically from post frontmatter
- **Open Graph & Twitter Card** meta tags
- **robots.txt & sitemap.xml** generated automatically for search engines and agents
- **RSS 2.0 feed** (`feed.xml`) generated automatically for subscribers
- **Reading time** estimated per post
- **Full rebuild** with orphan cleanup
- **Static asset copying** — CSS, images, and more

---

## Quickstart

### 1. Prerequisites

- Rust toolchain (edition 2024)
- Cargo

### 2. Install the binary

Install the latest release from crates.io:

```bash
cargo install sblog
```

The binary is `sblog`. It is available on your `PATH` after install.

### 3. Set up your project structure

Create a directory for your blog with this layout:

```
my-blog/
├── config.toml          # Site configuration
├── posts/               # Markdown posts
│   └── hello-world.md
├── static/              # CSS, images, assets
│   ├── style.css
│   └── images/
├── templates/           # Tera HTML templates
│   ├── base.html
│   ├── index.html
│   ├── post.html
│   ├── archive.html
│   ├── posts.html
│   ├── tag.html
│   ├── tags.html
│   └── about.html
└── public/              # Generated output (created by the build)
```

Copy the [`templates/`](https://github.com/santanusinha/sblog/tree/master/templates) and
[`static/`](https://github.com/santanusinha/sblog/tree/master/static) directories from the
[GitHub repository](https://github.com/santanusinha/sblog), or write your own. Copy
[`config.toml`](https://github.com/santanusinha/sblog/blob/master/config.toml) and edit it to
match your site.

### 4. Configure `config.toml`

```toml
title = "My Blog"
tagline = "A tiny static blog"
base_url = "https://example.com"
og_image = "https://example.com/og-image.png"
twitter_handle = "myhandle"

# Paths (relative to the project root).
posts_dir = "posts"
output_dir = "public"
templates_dir = "templates"
static_dir = "static"
css_file = "static/style.css"

[header]
title = "My Blog"
tagline = "A tiny static blog"
```

| Key | Description | Default |
|-----|-------------|---------|
| `title` | Site title, used in `<title>` and footer | `sblog` |
| `tagline` | Short site description | `A tiny static blog` |
| `base_url` | Absolute URL for social meta tags and sitemap | *(empty)* |
| `og_image` | Absolute URL of the Open Graph image | *(empty)* |
| `twitter_handle` | Twitter handle (without `@`) for card meta | *(empty)* |
| `posts_dir` | Directory holding Markdown posts | `posts` |
| `output_dir` | Directory where generated pages are written | `public` |
| `templates_dir` | Directory holding Tera templates | `templates` |
| `static_dir` | Directory holding static assets | `static` |
| `css_file` | Path to the stylesheet (relative to static dir) | `static/style.css` |
| `[header].title` | Title shown in the site masthead | `sblog` |
| `[header].tagline` | Tagline shown in the site masthead | `A tiny static blog` |

### 5. Write your first post

Create `posts/hello-world.md`:

```markdown
---
title: Hello, world
date: 2026-08-15
tags: [meta, rust]
summary: My first post on this blog.
---

This is my first post.

## A section

Some **bold** text and a code block:

```rust
fn main() {
    println!("Hello, world!");
}
```
```

**Frontmatter fields:**

| Field | Required | Description |
|-------|----------|-------------|
| `title` | Yes | Post title, shown in cards, lists, and the browser tab |
| `date` | Yes | Post date in `YYYY-MM-DD` format. Controls sort order (newest first) |
| `tags` | No | YAML list of tags. Generates tag pages automatically |
| `summary` | No | Short description shown on cards and list pages |

The **filename** becomes the URL slug. `hello-world.md` → `/post/hello-world.html`.

### 6. Build the site

```bash
# Full rebuild (all posts, removes orphaned output)
sblog --full

# Incremental build (only changed posts)
sblog
```

Output goes to `public/`.

### 7. Serve locally

```bash
python3 -m http.server 8123 --directory public
```

Open `http://localhost:8123/`.

### 8. Deploy

Upload the `public/` directory to any static host:

- Nginx
- GitHub Pages
- Netlify
- Vercel
- Cloudflare Pages
- Any S3-compatible bucket

---

## Markdown Support

sblog uses `pulldown-cmark` with these extensions enabled:

| Feature | Syntax | Example |
|---------|--------|---------|
| Tables | Pipe-delimited rows | `\| A \| B \|` |
| Footnotes | `[^1]` references | `Text[^1]` |
| Strikethrough | `~~text~~` | `~~done~~` |
| Task lists | `- [ ]` / `- [x]` | `- [x] done` |
| Smart punctuation | Auto-converted quotes/dashes | `"quotes"``"quotes"` |
| Heading attributes | `{#custom-id}` | `## Title {#sec-1}` |
| Fenced code blocks | Triple backticks with language | ` ```rust ` |
| Syntax highlighting | Via syntect, `syn-` prefixed classes | ` ```python ` |

---

## Template System

Templates use the **Tera** templating engine (Jinja2/Django-style syntax).
All templates extend `base.html` and override its blocks.

### Template Files

| File | Page | Purpose |
|------|------|---------|
| `base.html` | All pages | Layout shell: header, nav, footer, blocks |
| `index.html` | `/` | Home page: featured card + card grid + Topics sidebar |
| `post.html` | `/post/<slug>.html` | Individual post with prev/next nav and sidebar |
| `archive.html` | `/archive.html` | Full post list grouped by date |
| `posts.html` | `/posts.html` | Simple post list (title + date only) |
| `tag.html` | `/tags/<tag>.html` | Posts filtered by a single tag |
| `tags.html` | `/tags/index.html` | Index of all tags with counts |
| `about.html` | `/about.html` | About page from `posts/about.md` |

### Blocks in `base.html`

| Block | Purpose | Default |
|-------|---------|---------|
| `title` | Browser tab title | `{{ site.title }}` |
| `description` | `<meta name="description">` | `{{ site.tagline }}` |
| `social_meta` | Open Graph / Twitter meta tags | *(empty)* |
| `content` | Main page body | *(empty)* |

---

## Template Context Variables

The following variables are available in every template. They are injected
by the Rust generator at render time.

### Global Variables (all pages)

| Variable | Type | Description |
|----------|------|-------------|
| `site.title` | string | Site title from config |
| `site.tagline` | string | Site tagline from config |
| `site.header_title` | string | Header title from config |
| `site.header_tagline` | string | Header tagline from config |
| `title` | string | Page title for the browser tab |
| `description` | string | Page description for meta tags |
| `social_meta` | string (HTML) | Pre-rendered Open Graph / Twitter meta tags |
| `header_title` | string | Header title shown in masthead |
| `header_tagline` | string | Header tagline shown in masthead |
| `year` | string | Year for the footer copyright |
| `css_href` | string | Path to the stylesheet (relative to current page) |
| `home_href` | string | Path to the home page (relative) |
| `archive_href` | string | Path to the archive page (relative) |
| `about_href` | string | Path to the about page (relative) |
| `feed_href` | string | Path to the RSS feed (relative) |
| `active_nav` | string | Active nav item: `latest`, `archive`, or `about` |
| `active_nav` | string | Active nav item: `latest`, `archive`, or `about` |

### Post Object (`post`)

Available on the **post page** and **about page**.

| Field | Type | Description |
|-------|------|-------------|
| `post.title` | string | Post title |
| `post.summary` | string | Post summary from frontmatter |
| `post.href` | string | Relative link to the post |
| `post.date_long` | string | Long date, e.g. `August 15, 2026` |
| `post.date_short` | string | Short date, e.g. `Aug 15, 2026` |
| `post.read_time` | string | Estimated reading time, e.g. `3 min read` |
| `post.tags` | array | List of `TagView` objects |
| `post.body_html` | string (HTML) | Rendered Markdown body (use `\| safe`) |

### Posts List (`posts`)

Available on the **index**, **archive**, **posts**, and **tag** pages.

An array of `PostView` objects. Each item has the same fields as `post`
above, except `body_html` is omitted on list pages.

### Tag Object (`tag`)

Available on the **tag page**.

| Field | Type | Description |
|-------|------|-------------|
| `tag.name` | string | Tag name |
| `tag.href` | string | Relative link to the tag page |
| `tag.count` | integer | Number of posts with this tag |

### Tags List (`tags`)

Available on the **index**, **archive**, and **tags** pages.

An array of `TagView` objects with `name`, `href`, and `count`.

### Prev / Next Navigation

Available on the **post page**.

| Variable | Type | Description |
|----------|------|-------------|
| `prev_post` | PostView \| null | The newer post (previous in sort order) |
| `next_post` | PostView \| null | The older post (next in sort order) |

### Sidebar (`sidebar`)

Available on the **index**, **post**, and **about** pages.

| Field | Type | Description |
|-------|------|-------------|
| `sidebar.older_count` | integer | Number of essays listed |
| `sidebar.essays` | array | Up to 5 recent posts (PostView objects) |
| `sidebar.tags` | array | All tags with counts (TagView objects) |
| `sidebar.archive_href` | string | Relative link to the archive page |

---

## Template Elements by Logical Grouping

### 1. Layout & Navigation

| Element | Template | Variable(s) | Description |
|---------|----------|-------------|-------------|
| Site header | `base.html` | `site.header_title`, `site.header_tagline` | Masthead with title and tagline |
| Nav links | `base.html` | `home_href`, `archive_href`, `about_href`, `active_nav` | Header navigation (Home, Archive, About) |
| Footer | `base.html` | `year`, `site.title`, `home_href`, `archive_href`, `about_href` | Footer with brand, links, copyright |
| Main content block | `base.html` | `content` | The block each page overrides |
| Social meta block | `base.html` | `social_meta` | Open Graph / Twitter meta tags |
| RSS feed link | `base.html` | `feed_href` | `<link rel="alternate">` for the RSS feed |
| Meta description block | `base.html` | `description` | SEO description |
| Social meta block | `base.html` | `social_meta` | Open Graph / Twitter meta tags |

### 2. Home Page (index.html)

| Element | Variable(s) | Description |
|---------|-------------|-------------|
| Featured card | `posts[0]` | The latest post as a large hero card |
| Card grid | `posts` (index 1+) | Older posts as a responsive grid of cards |
| Topics sidebar | `tags`, `tags_href` | All tags with post counts |
| All posts link | `archive_href` | Link to the full archive (shown when >10 posts) |

### 3. Post Page (post.html)

| Element | Variable(s) | Description |
|---------|-------------|-------------|
| Post header | `post.title`, `post.date_long`, `post.read_time`, `post.summary` | Title, date, reading time, subtitle |
| Article body | `post.body_html` | Rendered Markdown content |
| Prev/Next nav | `prev_post`, `next_post` | Newer / older post links |
| Older sidebar | `sidebar.essays`, `sidebar.archive_href` | Recent posts list |
| Tags sidebar | `post.tags`, `tags_href` | Post tags as chips |

### 4. Archive Page (archive.html)

| Element | Variable(s) | Description |
|---------|-------------|-------------|
| Page header | `posts \| length` | Title and post count |
| Post list | `posts` | Full list with date, title, summary, tags |
| Tags sidebar | `tags` | All tags with counts |

### 5. Posts List Page (posts.html)

| Element | Variable(s) | Description |
|---------|-------------|-------------|
| Page header | `posts \| length` | Title and article count |
| Post list | `posts` | Simple list: date + title only |

### 6. Tag Page (tag.html)

| Element | Variable(s) | Description |
|---------|-------------|-------------|
| Page header | `tag.name`, `tag.count` | Tag name and post count |
| Post list | `posts` | Posts with this tag: date, title, summary, tags |

### 7. Tags Index Page (tags.html)

| Element | Variable(s) | Description |
|---------|-------------|-------------|
| Page header | `tags \| length` | Title and tag count |
| Tag list | `tags` | All tags with names and post counts |

### 8. About Page (about.html)

| Element | Variable(s) | Description |
|---------|-------------|-------------|
| Portrait (mobile) | `images/profile.png` | Profile image shown above content on mobile |
| About content | `post.body_html` | Rendered Markdown from `posts/about.md` |
| Portrait (sidebar) | `images/profile.png` | Profile image in the sidebar on desktop |
| Elsewhere links | Hardcoded URLs | GitHub, LinkedIn, Twitter links |
| Recent sidebar | `sidebar.essays`, `sidebar.archive_href` | Recent posts list |

---

## Build Modes

### Full build (`--full`)

```bash
sblog --full
```

- Rebuilds **every** page
- Removes orphaned post pages (source deleted)
- Removes orphaned tag pages (tag removed from all posts)
- Removes old root-level post pages

### Incremental build (default)

```bash
sblog
```
- Regenerates `robots.txt`, `sitemap.xml`, and `feed.xml` when the config or any post changed
- Rebuilds only posts whose source is newer than their output
- Rebuilds aggregate pages (index, archive, posts) when any post changed
- Regenerates `robots.txt` and `sitemap.xml` when the config or any post changed
- Copies static assets every time (idempotent)

---

## Output Structure

```
public/
├── index.html          # Home page
├── archive.html        # All posts
├── posts.html          # Simple post list
├── about.html          # About page
├── post/               # Individual post pages
│   ├── hello-world.html
│   ├── my-other-post.html
│   └── ...
├── tags/               # Tag pages
│   ├── index.html      # Tag index
│   ├── meta.html
├── robots.txt           # Crawler rules (generated)
├── sitemap.xml          # URL list for search engines (generated)
├── feed.xml             # RSS 2.0 feed (generated)
└── images/             # Static images (copied from static/)
├── robots.txt           # Crawler rules (generated)
├── sitemap.xml          # URL list for search engines (generated)
└── images/             # Static images (copied from static/)
```

---

## License

MIT