How to build a blog in less than 100 lines of code

February 21, 2020 — 8 min read

I’ve been thinking about writing a blog for quite some time. There are a couple of reasons that were motivating me to get started: One point was that I thought I should write down and comment on some of the code that I write, so I could get back to it at a later stage more easily, or just use it as a reference. A further reason was this wonderful conversation I had with Vanessa Sochat (still makes me super happy to read!). And finally, I wanted to get better at writing!

As of right now, I’m painfully slow at writing, and to be honest, I don’t like at all that much. Especially in the scientific context, I’m regularly procrastinating instead of just getting started with a manuscript. However, that is about to change! Well, at least I hope so, as I want to gain more writing practice when working on this blog 😄

Alright, so for writing a blog, you actually need to set-up a blog somewhere. My requirements were simple: I wanted to host it on GitHub pages, have the actual posts written in Markdown, and wanted as little setup/overhead as possible.

My first try was setting up Jekyll on GithHub pages. After deciding on the beautiful Chalk theme, I started following the documentation on how to get things up and running.

Chalk Screenshot of the Chalk Jekyll theme by Nielsen Ramon

The documentation is well written, however I was a little intimidated by its length. Install the software, prepare the git repo, understand local site testing, get to know how to add new posts/sites, read through the Chalk documentation to learn about customization. It all felt like a greater commitment than I initially intended. So after forking Chalk, I ended up not working on it for 3+ months.

Fast forward to a couple of weeks ago, the whole blog idea came up once again. Thinking about whether to continue work on the Jekyll based blog, I came to the conclusion that writing a static site generator myself shouldn’t be all that hard. Conceptually, I thought that I would need a template html file, and a directory for my markdown posts. I also wanted to have a projects and an about page. Here, I thought writing these in html would be preferable, as that would give me more options on the individual styling than with parsed Markdown. Regarding the design, I wanted to closely stick to the wonderful blog by Rasmus Andersson. I especially love his Inter typeface, which I plan to use on more upcoming projects.

Blog setup The basic file structure of the static site generator

The image above shows the actual file structure. The overarching index.html, containing the header and footer, as well as the content div’s for the about and projects page are stored in the template folder. As mentioned earlier, the posts directory contains the Markdown files. And that’s basically it! Sure, there is a top level assets folder where the .css and other static resources live, but all in all there is not much magic to the general setup.

Let’s dive into the code. I write most of my projects in JavaScript (JS) / Node.js (especially everything web related), and also used Node.js for this project. To be honest, I am not religious about coding languages, and highly believe that most of the things I do could be done in Python/Go/YourFavoriteProgrammingLanguage/Rust even faster, more performant or generally better (you could rant about this if there was a comment section). However, working in the web area requires you to know JavaScript at least to some degree. And then again, I’m a lazy person that knows his way around JS pretty well. So in essence, that’s why I eventually end up writing most of my stuff in JS.

For a start, we’ll be looking into the index.js file that hosts the main logic. In order to be able to read our markdown posts, and the contents of our template folder, we’ll be requiring the native fs module, as well as some methods from path module. To show an estimated reading time at the beginning of the article, the reading-time package is used. At the beginning of the markdown posts, meta-data such as the posts title, a short preview of the article for the main page, as well as some keywords are stored in YAML format. We can read this meta-data using the yaml-front-matter package.

const fs = require('fs') 
const { extname, basename } = require('path') 
const { promisify } = require('util')
const readingTime = require('reading-time')
const yamlFront = require('yaml-front-matter')
const helpers = require('./helpers')

const readdir = promisify(fs.readdir)
const readFile = promisify(fs.readFile)
const stats = promisify(fs.stat)

As you may have already guessed from the use of the promisify method (exported by the native util package), I’m a huge fan of promises, and even more so of the async/await sugar. In order to use the async/await keyword for asynchronously reading files etc., we’ll wrap the main logic in a self invoking async function:

;
(async () => {
  // the main logic goes here, so we can use the await keyword
})()

Ok so first off, we start by reading the index.html template file, containing our page header and footer. The formatter is created so that we can show a short date string in the post preview of the landing page (e.g. Feb 2020). We proceed to get a list of all files in the posts directory, and filter the list by files with the extension .md (→ markdown).

;
(async () => {
  const template = await readFile('./src/template/index.html', 'utf8')
  const formatter = new Intl.DateTimeFormat('en', {
    month: 'short'
  })
  // read the entire posts directory and filter for markdown files
  let posts = await readdir('./src/posts')
  posts = posts.filter(post => extname(post) === '.md')

We then proceed to loop through all the posts that were found. For each post, we create an object with properties for all the meta-info that we parsed from the YAML front-matter. In addition, we add properties such as the estimated reading time, as well as year, month and date of creation for later use. Eventually, we update the array that contained a path string of our current post with the newly created object.

for (let i = 0; i < posts.length; i++) {
  // read file content, parse yaml front-matter and markdown content
  // and store info in posts array
  const string = await readFile(`./src/posts/${posts[i]}`, 'utf8')
  const {
    birthtime
  } = await stats(`./src/posts/${posts[i]}`)
  const parsed = yamlFront.loadFront(string)
  parsed.date = new Date(birthtime)
  parsed.month = formatter.format(parsed.date)
  parsed.year = parsed.date.getFullYear()
  parsed.readingTime = readingTime(parsed.__content).text
  parsed.rendered = helpers.renderAndInsertDate(parsed)
  parsed.file = basename(posts[i], '.md')
  posts[i] = parsed
}

From then on, it’s a piece of cake. Given our template html, we start by creating our landing page. The landing page will have a preview of the latest 15 posts and it’s main contents. The post previews are specified in the YAML front-matter of each post.

// create the landing page (index.html)
// get a string of the first 15 posts and create previews
const previewString = posts.slice(0, 14).map(helpers.postPreview).join('\n')
await helpers.prepareSite('index.html', template, previewString)

// create about page
const aboutString = await readFile('./src/template/about.html', 'utf8')
await helpers.prepareSite('about/index.html', template, aboutString)

// create projects page
const projectsString = await readFile('./src/template/projects.html', 'utf8')
await helpers.prepareSite('projects/index.html', template, projectsString)

// create legal disclosure page
const legalString = await readFile('./src/template/impressum.html', 'utf8')
await helpers.prepareSite('impressum/index.html', template, legalString)

// create posts directory
for (var i = 0; i < posts.length; i++) {
  await helpers.prepareSite(`posts/${posts[i].file}.html`,
    template, posts[i].rendered)
}

The last thing to cover is the helpers module. It exports three methods: postPreview, renderAndInsertDate, and prepareSite. The postPreview method simply returns an html string, which contains the date of creation, the hyperlink to the actual post and the posts preview which will be displayed on the landing page. The renderAndInsertDate is used to render the markdown content of a post (using markdown-it), and to add a small info snippet again containing the posts date of creation, and the estimated reading time. At last, the prepareSite methods simply splits the index.html template file at the <!--* content goes here *--> comment, and injects the content.

module.exports = {
  postPreview(post) {
    return `<a href="${post.url || 'posts/' + post.file + '.html'}" class="post-preview">
        <h3 class="post-preview-header">${post.title}<h3 class="post-preview-link">↪</h3></h3>
        <div class="post-preview-body">
          <p>
          ${post.preview}
          <info datetime="">
            ${post.month} ${post.year} ${post.readingTime ? '— ' + post.readingTime : ''}
          </info>
          </p>
        </div>
      </a>`
  },
  renderAndInsertDate(post) {
    const html = md.render(post.__content)
    // add date, and estimated reading time
    const snippet = `<info datetime="${post.date.toISOString()}">
      ${post.date.toLocaleString('en-EN', { 
        year: 'numeric', 
        month: 'long', 
        day: 'numeric' 
      })} ${post.readingTime ? '— ' + post.readingTime : ''}
    </info>`
     return html.split('</h1>')[0] + '</h1>' + snippet + html.split('</h1>')[1]
  },
  async prepareSite(name, template, content) {
    const first = template.split('<!--*')[0]
    const second = template.split('*-->')[1]
    await writeFile(name, `${first}\n${content}\n${second}`, 'utf8')
  }
}