For five years, I was frustrated by every blogging engine I tried.
WordPress made it difficult to embed inline interactive charts. Octopress’s predefined css was hard to disable and pasting Stack Overflow instructions on installing gems without understanding what renv
or rvm
were eventually broke my ruby installation. Metalsmith was easier, but I never managed to successfully configure the rss plugin.
And none of alternatives I looked at supported hot reloading.
Writing my own blogging software seemed like epitome of yak shaving. I thought it would be difficult, too, until I came across Jeremy Ashkenas’s Jorno and Rich Harris’s Svelte blog last summer. Using their code as a starting point, I spent a lazy Sunday simplifying my setup.
Now this site is built with just 60 lines of code. And they’re run directly off of this post.
Each post is a markdown file in the source/_posts
folder. The posts get read in, parsed and written out to public/
as an HTML file using one of templates from source/_templates
.
Static files that don’t need preprocessing, like images or javascript, are copied directly from source/
to public/
with rysnc
in preperation for publishing.
var fs = require('fs')
var {exec, execSync} = require('child_process')
var public = `${__dirname}/../../public`
var source = `${__dirname}/../../source`
function rsyncSource(){
exec(`rsync -a --exclude _posts --exclude _templates ${source}/ ${public}/`)
}
rsyncSource()
Markdown is converted to HTML with marked and syntax highlighted by highlight.js.
var hljs = require('highlight.js')
var marked = require('marked')
marked.setOptions({
highlight: (code, language) => hljs.highlight(code, {language}).value,
smartypants: true
})
Files in the _templates
directory, currently rss.xml
, sitemap.xml
and post.html
, are ES6 template strings. eval
turns them into functions that can be passed data.
var templates = {}
readdirAbs(`${source}/_templates`).forEach(path => {
var str = fs.readFileSync(path, 'utf8')
var templateName = path.split('_templates/')[1]
templates[templateName] = d => eval('`' + str + '`')
})
function readdirAbs(dir){ return fs.readdirSync(dir).map(d => dir + '/' + d) }
Each post file in the source/_posts
folder is read in with parsePost
and saved to the posts
array.
Instead of having to install and configure a plugin, I created an rss feed by passing the array of posts to the rss.xml
template and writing out a file.
var posts = readdirAbs(`${source}/_posts`).map(parsePost)
fs.writeFileSync(public + '/rss.xml', templates['rss.xml'](posts))
fs.writeFileSync(public + '/sitemap.xml', templates['sitemap.xml'](posts))
Passed the path of a post, parsePost
reads the title, url, date and publish status from front matter at the top of the post. The markdown body is converted to an HTML fragment and an object representing the post is returned.
function parsePost(path){
var [top, body] = fs.readFileSync(path, 'utf8')
.replace('---\n', '')
.split('\n---\n')
var post = {html: marked(body)}
top.split('\n').forEach(line => {
var [key, val] = line.split(/: (.+)/)
post[key] = val
})
return post
}
writePost
takes a post object, creates a folder for it in public/
, runs it through a template and writes out the post to index.html
.
function writePost(post){
var dir = public + post.permalink
if (!fs.existsSync(dir)) execSync(`mkdir -p ${dir}`)
fs.writeFileSync(`${dir}/index.html`, templates[post.template](post))
}
posts.forEach(writePost)
And that’s all the code that’s needed to build the blog!
To get it all on the internet npm run publish
runs lit-node on this post to regenerate everything locally, then uses rsync
again to copy the public
directory to a remote folder that’s being statically served.
"scripts": {
"publish": "lit-node source/_posts/2018-05-24-literate-blogging.md &&
rsync -a public/ demo@roadtolarissa.com:../../usr/share/nginx/html/",
"start": "lit-node source/_posts/2018-05-24-literate-blogging.md --watch &
cd public/ && hot-server"
}
npm run start
runs hot-server in the public
folder and runs this post with the --watch
flag. Changes in the source
directory rerun rsyncSource
, which copies the the update file to public
, triggering hot-server’s file watch and passing the file to the browser along a websocket. A little Rube Goldberg, but still plenty fast and simpler than rewriting hot-server here.
Edits to a post rebuild just that post, making hot-server trigger a page reload.
if (process.argv.includes('--watch')){
require('chokidar').watch(source).on('change', path => {
rsyncSource()
if (path.includes('_posts/')) writePost(parsePost(path))
})
}
I don’t spend much time looking at sitemap.xml
or tweaking the templates, so they’re not hooked up to automatically update. I’ve tried to only implement exactly what I need without any unnecessary abstractions to keep the code easy to work with. Writing both the code and content lets you aggressively cut corners.
I’m not totally sold on literate programming yet. I quite liked the having all the code fit on one screen and ⌘-B doesn’t work out of the box. But I’ve been asked a couple of times for advice on putting words and charts on the internet without getting plastered all over it. Hopefully this post shows how far a little glue code can go when paired with a folder of markdown files, rsync
and a static server.
If you’d like to try it without futzing with the literate bits, there’s a javascript only version.