How to write a blog, part 3
1st of July, 2020
Let's glue what we had in part 2! "Glueing" is generally "connecting" the core parts so they all work together.
So far we need to assemble together:
- HTML templates
- CSS styles
- Markdown files
It goes like this:
- Markdown
->
HTML via external library - CSS styles go in HTML templates via
link
tag - HTML template + HTML
->
Complete html page via the magic of programming
So what do we need to glue? Just one thing, really:
- List all existing posts in a blog a.k.a. Table of Content or Index
As far as we are not too ambitious, we can get away with pretty basic modifications and complexity.
List all posts
So let's just list all of them. Below is a complete and minimal example of all the necessary files.
life-of-a-boomer.md
:
## Day 1
*sip* Aaaghhh
style.css
:
body {
color: #624B08;
background-color: ghostwhite;
font-family: sans-serif;
}
a {
text-decoration: none;
color: #bb8d02;
}
a:hover {
color: #dfa800;
}
h1, h2, h3, h4, h5, h6, .header {
font-family: sans-serif;
color: #463605; /* a bit darker than the text in body */
margin-bottom: 0.8rem;
margin-top: 2rem;
}
.container {
margin: 0 auto;
max-width: 768px;
padding-left: 5%;
padding-right: 5%;
}
ul li, ol li {
margin-bottom: 0.9rem;
}
layout.html
:
#? strip(startswith = "<") | stdtmpl
#proc genLayoutHtml(content: string): string =
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://unpkg.com/purecss@1.0.1/build/pure-min.css" integrity="sha384-oAOxQR6DkCoMliIh8yFnu25d7Eq/PHS21PClpwjOTeU2jRSq11vu66rf90/cZr47" crossorigin="anonymous">
<link rel="stylesheet" href="assets/style.css">
</head>
<body>
<div class="container">
$content
</div>
</body>
</html>
types.nim
:
type
Post* = object of RootObj
path*, title*, content*: string
index.html
:
#? stdtmpl
#import types
#proc genIndexHtml(posts: openArray[Post]): string =
<h2>Index</h2>
<ul>
#for p in posts:
<li>
<a href="${p.path}">$p.title</a>
</li>
#end for
</ul>
post.html
:
#? stdtmpl
#import types
#proc genPostHtml(post: Post): string =
<div class="post">
<h1>${post.title}</h1>
${post.content}
</div>
main.nim
:
import os
import strutils
import markdown # this is an external library which should be installed
include "layout.html"
include "index.html"
include "post.html"
import types
proc main() =
var posts: seq[Post]
for kind, path in walkDir("posts", checkDir = true):
if kind != pcFile: continue
let
fileInfo = splitFile(path)
name = fileInfo.name & ".html"
dest = "docs"/name
let markdown = markdown(
readFile(path),
config=initGfmConfig()
)
let post = Post(
path: name,
title: fileInfo.name.replace('-', ' ').capitalizeAscii,
content: markdown,
)
writeFile(
dest,
genLayoutHtml(genPostHtml(post))
)
posts.add post
writeFile(
"docs"/"index.html",
genLayoutHtml(genIndexHtml(posts))
)
main()
And with the following file structure:
- src/
- main.nim
- types.nim
- layout.html
- index.html
- post.html
- posts/
- life-of-a-boomer.md
- assets/
- style.css
- docs/
all the resulting HTML files will be located in the docs/
directory. Don't
worry if you've just copied the code and it doesn't work, just go through
the error messages, fix them and you'll be fine.
But this all is just boring
Yes, it is. It also works.
But we can do something more interesting as well. Say, we want a juicy feature
like attaching metadata to each markdown post to use it later. A popular idiom
is to make it at the very start separated from the main text with ---
like so:
mypost.md
:
---
tags: programming, Nim
updatedAt: 2020-02-02
---
## Oh snap
Here we go again
How to do it:
- Check if the file starts with
---
- Find closing
---
- Parse everything in between
Here is an example of "not stripped down for readability" code with error handling, low-levelish parsing and types:
type
Metadata* = object of RootObj
tags*: seq[string]
updatedAt*: string
Post* = object of RootObj
path*, title*, content*: string
metadata*: Metadata
ParseMetadataError* = object of ValueError
func parseMetadata(s: string): Metadata =
var
index: int
colonPos = s.find(':', index)
while (colonPos) != -1:
let key = s[index..<colonPos].strip
let valueEnd = s.find('\n', colonPos)
if valueEnd == -1:
raise newException(ParseMetadataError,
"Ending newline not found for "&key)
let value = s[colonPos + 1 ..< valueEnd].strip
case key
of "tags":
result.tags = value.split(", ").sorted
of "updatedAt":
result.updatedAt = value
else:
raise newException(ParseMetadataError,
"Unknown metadata key: "&key)
index = valueEnd
colonPos = s.find(':', index)
# some code before
let fileContent = readFile(path)
var
metadata: Metadata
metadataEnd: int
if fileContent.len > 3 and fileContent[0..2] == "---":
metadataEnd = fileContent.find("---", 3)
if metadataEnd == -1:
raise newException(ParseMetadataError,
"Closing --- not found for "&dest)
metadata = parseMetadata(fileContent[3..<metadataEnd])
if metadataEnd != 0: metadataEnd += 3 # move after ---
let markdown = markdown(
fileContent[metadataEnd ..< fileContent.len],
config=initGfmConfig()
)
Conclusion
This series of blog posts aims to prove that it is possible to create a static site generator with relatively low effort. Hopefully it does so. And now the closing thought.
People say that average programmers write blog posts with bad code examples and practices whereas really good programmers don't do it because they think that this is easy and essential. I think this post's code is easy and essential. Am I a good programmer then? I don't know, maybe I'm just an average programmer doing an easy thing. But it is the best thing I can do right now (almost best (tired (who's not? () `(hidden ,(car '("lisp :(", "treasure"))))))) last one is unbalanced).
Thanks for reading, see you!