summaryrefslogtreecommitdiff
path: root/content/test.md
blob: a30fca07173360700db344567ef495ebd8cdf84b (plain)
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
---
title: "Writing a Static Site Generator in Golang"
---

I recently decided to write a static site generator as a small toy project to get more familiar with the Go standard library. I’ve always liked learning languages by building something practical, and a static site generator seemed like a perfect mix of file handling, templating, and content processing.

Another reason was infrastructure. My blog used to run on a small VPS from Contabo, but after they raised their prices I decided to move everything to a Raspberry Pi sitting on my desk. Since the Pi isn’t particularly powerful, I wanted the site to be as lean as possible — no runtime rendering, no databases, just plain static files served over HTTP.

> NOTE
> I decided to call it **Kite**.

The idea behind Kite is simple: take Markdown files, convert them to HTML, wrap them in a template, and write them to an output directory. That’s it.

---

## Project Structure

The generator assumes a simple structure:

```
.
├── content/
│   ├── index.md
│   └── blog/
│       └── first-post.md
├── output/
├── layout.html
└── main.go
```

* **content/** contains Markdown files
* **layout.html** is the HTML template
* **output/** is where generated pages go

Each Markdown file gets converted into an HTML file with the same relative path.

For example:

```
content/blog/post.md
```

becomes:

```
output/blog/post.html
```

---

## The Page Structure

First we define a simple struct that represents the data passed to our template.

```go
type Page struct {
	Title   string
	Content template.HTML
}
```

* **Title** will eventually hold the page title (hardcoded for now).
* **Content** contains the rendered HTML.

Notice the use of `template.HTML`. This tells Go’s template engine that the content is already trusted HTML and should not be escaped.

---

## Walking the Content Directory

The core of the generator uses `filepath.WalkDir` to recursively traverse the `content/` directory.

```go
err := filepath.WalkDir(contentDir, func(path string, d fs.DirEntry, err error) error {
```

For every file encountered, we:

1. Ignore directories
2. Check if the file ends with `.md`
3. Convert it to HTML
4. Render it into a template
5. Write the result to the output directory

Filtering Markdown files is straightforward:

```go
if strings.HasSuffix(d.Name(), ".md") {
	fmt.Println("Processing:", path)
```

---

## Converting Markdown to HTML

For Markdown parsing I used the `gomarkdown/markdown` package.

```go
func convertToHtml(path string) []byte {
	md, err := os.ReadFile(path)
	if err != nil {
		log.Fatalf("Error reading %s: %s", path, err)
	}

	html := markdown.ToHTML(md, nil, nil)
	return html
}
```

This function simply:

1. Reads the Markdown file
2. Converts it to HTML
3. Returns the result

No configuration, no extensions — just the default behavior.

---

## Creating the Page Object

After conversion we construct the page object:

```go
htmlContent := convertToHtml(path)

newPage := Page{
	Title:   "hello", // you can customize this later
	Content: template.HTML(htmlContent),
}
```

Right now the title is hardcoded, but later this could be extracted from:

* frontmatter
* the first heading
* metadata in the Markdown file

---

## Parsing the Layout Template

Next we load the HTML template that wraps the content.

```go
tmpl, err := template.ParseFiles("./layout.html")
if err != nil {
	log.Fatalf("Error parsing template: %s", err)
}
```

This happens once per file in the current version. It could be optimized by parsing the template only once before the directory walk, but for a small site this overhead is negligible.

---

## Computing the Output Path

One important step is preserving the directory structure.

```go
relPath, err := filepath.Rel(contentDir, path)
```

This converts something like:

```
content/blog/post.md
```

into:

```
blog/post.md
```

Then we swap the extension:

```go
outputFilePath := filepath.Join(
	outputDir,
	strings.Replace(relPath, ".md", ".html", 1),
)
```

Result:

```
output/blog/post.html
```

---

## Ensuring Directories Exist

Before writing the file we make sure the directory exists:

```go
err = os.MkdirAll(filepath.Dir(outputFilePath), 0o755)
```

This creates any missing folders in the path.

---

## Writing the Final HTML

Finally we create the output file and render the template:

```go
outputFile, err := os.Create(outputFilePath)
if err != nil {
	log.Fatalf("Error creating output file: %s", err)
}
defer outputFile.Close()

err = tmpl.Execute(outputFile, newPage)
if err != nil {
	log.Fatalf("Error generating output content: %s", err)
}
```

The template receives the `Page` struct and generates the final HTML.

---

## Example Layout Template

A minimal `layout.html` might look like this:

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>{{ .Title }}</title>
</head>
<body>
  <main>
    {{ .Content }}
  </main>
</body>
</html>
```

When the generator runs, the Markdown content gets injected into `{{ .Content }}`.

---

## Running the Generator

Just run:

```
go run main.go
```

Example output:

```
Processing: content/index.md
Processing: content/blog/first-post.md
All files processed!
```

And the generated files appear in `output/`.

---

## Why This Is Nice

Even this tiny generator already has a few advantages:

* **Extremely fast builds**
* **No runtime dependencies**
* **Very small hosting requirements**
* **Full control over the pipeline**

Perfect for something like a Raspberry Pi server.

---

## Future Improvements

Kite is intentionally minimal right now, but there are plenty of directions to take it:

* Frontmatter support (YAML/TOML)
* Automatic title extraction
* Template caching
* Asset copying (CSS, images)
* Incremental builds
* Live reload during development
* RSS feed generation
* Tag and category pages

The nice part is that Go’s standard library already provides most of the building blocks needed.

---

## Final Thoughts

Writing a static site generator is one of those projects that’s small enough to finish but complex enough to teach useful concepts:

* filesystem traversal
* templating
* content pipelines
* project structure

And the result is something genuinely useful.

Kite might stay a simple tool, or it might slowly grow features as I need them. Either way, it’s already doing exactly what I wanted: generating a lightweight blog that my Raspberry Pi can serve effortlessly.