commit 9703368d1f6b4ee0d0d6e5e89fb2c31892c4a616
from: Murilo Ijanc
date: Fri Apr 10 21:53:18 2026 UTC
add optional toc generation, example site
commit - de29d8401122b1fa37504de6b7f496c99187b2b3
commit + 9703368d1f6b4ee0d0d6e5e89fb2c31892c4a616
blob - /dev/null
blob + 301d067fce13beb1fe42c352ac18f3921088ebc7 (mode 644)
--- /dev/null
+++ example/content/about.md
@@ -0,0 +1,9 @@
+---
+title: sobre
+description: sobre este site
+---
+
+# sobre
+
+site gerado com [kssg](https://got.ijanc.org/?action=summary&path=kssg.git),
+um gerador de sites estaticos escrito em rust.
blob - /dev/null
blob + aa479542f0d7fdeb406d7b48ec1e56ff1140c149 (mode 644)
--- /dev/null
+++ example/content/index.md
@@ -0,0 +1,8 @@
+---
+title: inicio
+description: site de exemplo do kssg
+---
+
+# bem-vindo
+
+este e um site de exemplo para testar o **kssg**.
blob - /dev/null
blob + 6a0c37c6a95be774971a298ee4888391dc9822e7 (mode 644)
--- /dev/null
+++ example/content/posts/hello-world.md
@@ -0,0 +1,40 @@
+---
+title: hello world
+date: 2026-04-01
+description: primeiro post de exemplo
+tags: exemplo, kssg
+toc: true
+---
+
+# hello world
+
+este e o primeiro post de exemplo.
+
+## instalacao
+
+compile com `make` e instale com `make install`:
+
+```
+make
+make install PREFIX=~/.local
+```
+
+## uso
+
+gere o site com `kssg build` e visualize com `kssg serve`.
+
+### build
+
+```
+kssg build
+```
+
+### serve
+
+```
+kssg serve
+```
+
+## conclusao
+
+pronto, o site esta no ar.
blob - /dev/null
blob + f2a9a570f47d9fb8875c28a79d51bfc36c947d5d (mode 644)
--- /dev/null
+++ example/content/posts/rascunho.md
@@ -0,0 +1,11 @@
+---
+title: post em rascunho
+date: 2026-04-10
+description: este post nao deve aparecer no build
+tags: exemplo
+draft: true
+---
+
+# rascunho
+
+este post nao deve aparecer no site.
blob - /dev/null
blob + 828e63410fc8ceaca61b6b8377de471fa42d9a1f (mode 644)
--- /dev/null
+++ example/content/posts/serie-rust-parte-1.md
@@ -0,0 +1,33 @@
+---
+title: "rust basico: variaveis"
+date: 2026-04-07
+description: variaveis e mutabilidade em rust
+tags: rust, serie
+series: rust-basico
+part: 1
+toc: true
+---
+
+# rust basico: variaveis
+
+primeira parte da serie sobre rust.
+
+## let e mut
+
+por padrao variaveis sao imutaveis:
+
+```rust
+let x = 5;
+// x = 6; // erro!
+let mut y = 5;
+y = 6; // ok
+```
+
+## shadowing
+
+voce pode redeclarar uma variavel com o mesmo nome:
+
+```rust
+let x = 5;
+let x = x + 1;
+```
blob - /dev/null
blob + 8c396359b22a2af3071887b657e2a8c95381978b (mode 644)
--- /dev/null
+++ example/content/posts/serie-rust-parte-2.md
@@ -0,0 +1,36 @@
+---
+title: "rust basico: structs"
+date: 2026-04-08
+description: structs e metodos em rust
+tags: rust, serie
+series: rust-basico
+part: 2
+toc: true
+---
+
+# rust basico: structs
+
+segunda parte da serie sobre rust.
+
+## definindo structs
+
+```rust
+struct Point {
+ x: f64,
+ y: f64,
+}
+```
+
+## metodos
+
+use `impl` para adicionar metodos:
+
+```rust
+impl Point {
+ fn distance(&self, other: &Point) -> f64 {
+ ((self.x - other.x).powi(2)
+ + (self.y - other.y).powi(2))
+ .sqrt()
+ }
+}
+```
blob - /dev/null
blob + cf780200d5e0f6b24ba0a14239a5f074dd10b3b1 (mode 644)
--- /dev/null
+++ example/content/posts/til-make.md
@@ -0,0 +1,14 @@
+---
+title: "TIL: variaveis no make"
+date: 2026-04-05
+description: como usar variaveis condicionais no make
+tags: til, make
+---
+
+# TIL: variaveis no make
+
+`?=` so atribui se a variavel nao estiver definida:
+
+```
+CC ?= cc
+```
blob - /dev/null
blob + e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 (mode 644)
blob - /dev/null
blob + 1cdfa2ed3891b5124297ab4db9e9f74dd76191f1 (mode 644)
--- /dev/null
+++ example/static/style.css
@@ -0,0 +1,21 @@
+* { margin: 0; padding: 0; box-sizing: border-box; }
+body {
+ font-family: monospace;
+ max-width: 42em;
+ margin: 0 auto;
+ padding: 1em;
+ line-height: 1.6;
+}
+nav { margin-bottom: 1em; }
+main ul { margin-left: 1.5em; }
+footer { margin-top: 2em; color: #666; }
+time { color: #666; }
+.dates { color: #666; font-size: 0.9em; }
+.toc { margin: 1em 0; }
+.toc summary { cursor: pointer; font-weight: bold; }
+.toc > details > ul { margin: 0.5em 0 0 1.5em; }
+.toc ul ul { margin: 0 0 0 1.5em; }
+.series-nav { margin-top: 2em; display: flex; justify-content: space-between; }
+pre { background: #f5f5f5; padding: 1em; overflow-x: auto; }
+code { background: #f5f5f5; padding: 0.1em 0.3em; }
+pre code { background: none; padding: 0; }
blob - /dev/null
blob + 7eb413fae53581b1ba3f489f1d8eed050a378fc8 (mode 644)
--- /dev/null
+++ example/templates/404.html
@@ -0,0 +1,4 @@
+
+404
+404 - pagina nao encontrada
+voltar ao inicio
blob - /dev/null
blob + 34191e91346d7700c1d919645395593b8aaa9d02 (mode 644)
--- /dev/null
+++ example/templates/base.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ {{title}}
+
+
+
+
+
+ {{content}}
+ {{posts}}
+
+
+
+
blob - b0996c1df54f66f569fe8cf420ca1d655e388c47
blob + cc4374c8751ee18007023660d5f1d3679f57ddef
--- kssg.rs
+++ kssg.rs
@@ -254,6 +254,7 @@ struct Metadata {
updated: String,
description: String,
draft: bool,
+ toc: bool,
series: Option,
part: Option,
tags: Vec,
@@ -297,6 +298,7 @@ fn parse_frontmatter(content: &str) -> Metadata {
updated: map.remove("updated").unwrap_or_default(),
description: map.remove("description").unwrap_or_default(),
draft: map.get("draft").map(|v| v == "true").unwrap_or(false),
+ toc: map.get("toc").map(|v| v == "true").unwrap_or(false),
series: map.remove("series"),
part: map.get("part").and_then(|v| v.parse().ok()),
tags: map
@@ -548,7 +550,17 @@ fn build() {
content.push_str("
\n");
}
- content.push_str(&posts[i].html_content);
+ if posts[i].meta.toc {
+ let toc = generate_toc(&posts[i].html_content);
+ content.push_str(&toc);
+ }
+
+ let html = if posts[i].meta.toc {
+ add_heading_ids(&posts[i].html_content)
+ } else {
+ posts[i].html_content.clone()
+ };
+ content.push_str(&html);
if !nav.is_empty() {
content.push_str(&nav);
}
@@ -769,6 +781,117 @@ fn generate_tag_pages(posts: &[Post], template: &str)
log!("wrote public/tags/index.html");
}
+fn generate_toc(html: &str) -> String {
+ let mut entries: Vec<(u8, String, String)> = Vec::new();
+ let mut pos = 0;
+ while pos < html.len() {
+ let tag = html[pos..].find(" {
+ if a.1 <= b.1 {
+ a
+ } else {
+ b
+ }
+ }
+ (Some(a), None) => a,
+ (None, Some(b)) => b,
+ (None, None) => break,
+ };
+ let close = format!("", level);
+ let gt = match html[start..].find('>') {
+ Some(i) => start + i + 1,
+ None => break,
+ };
+ let end = match html[gt..].find(&close) {
+ Some(i) => gt + i,
+ None => {
+ pos = gt;
+ continue;
+ }
+ };
+ let text = html[gt..end].replace("", "").replace("", "");
+ let id = slugify(&text);
+ entries.push((level, id, text));
+ pos = end + close.len();
+ }
+
+ if entries.is_empty() {
+ return String::new();
+ }
+
+ let mut toc = String::from("\n");
+ toc
+}
+
+fn add_heading_ids(html: &str) -> String {
+ let mut result = String::with_capacity(html.len());
+ let mut pos = 0;
+ while pos < html.len() {
+ let h2 = html[pos..].find("");
+ let h3 = html[pos..].find("");
+ let (level, start) = match (h2, h3) {
+ (Some(a), Some(b)) => {
+ if a <= b {
+ (2, pos + a)
+ } else {
+ (3, pos + b)
+ }
+ }
+ (Some(a), None) => (2, pos + a),
+ (None, Some(b)) => (3, pos + b),
+ (None, None) => break,
+ };
+ let tag_end = start + 4; // len of ""
+ let close = format!("", level);
+ let end = match html[tag_end..].find(&close) {
+ Some(i) => tag_end + i,
+ None => {
+ pos = tag_end;
+ continue;
+ }
+ };
+ let text = html[tag_end..end]
+ .replace("", "")
+ .replace("", "");
+ let id = slugify(&text);
+ result.push_str(&html[pos..start]);
+ result.push_str(&format!("", level, id));
+ result.push_str(&html[tag_end..end + close.len()]);
+ pos = end + close.len();
+ }
+ result.push_str(&html[pos..]);
+ result
+}
+
fn series_nav(posts: &[Post], current: usize) -> String {
let series = match &posts[current].meta.series {
Some(s) => s,