Svelte, Sapper

Creating static blog with Sapper, TailwindCSS and Github pages

January 13, 2020, updated January 19, 2020, 5 min read

Static websites became very popular lasy years. Tools like GatsbyJS and Jekyll exist for some time. However, I decided to try something different and kinda underground - recently released Svelte 3 with Sapper.

What is Sapper

Sapper is web app framework powered by Svelte. It is inspired by Next.js - a framework for React, but, as it's creator states, is much more lightweight and faster. You can read more at https://sapper.svelte.dev/.

What is TailwindCSS

Tailwind is a A utility-first CSS framework. Utility-first means that there aren't any prebuilt UI components, like it is in Bootstrap. Instead, Tailwind provides utility classes:

.absolute {
  posiion: absolute;
}

.uppercase {
  text-transform: uppercase;
}

In my opinion, this approach goes wery well with Svelte, because both tools focus on resulting bundle size and final performance.

About this tutorial

This tutorial will be splitted into following parts:

  1. Project scaffolding and adding support for markdown and syntax highlight to content
  2. Styling with Taildwind and PostCSS
  3. Adding categories for blog posts
  4. Testing
  5. Deploying to Github pages

Getting started

First, clone sapper project template:

npx degit "sveltejs/sapper-template#rollup" sapper-blog-tutorial

For those who prefer webpack, there is another option npx degit "sveltejs/sapper-template#webpack" sapper-blog-tutorial, but we will use rollup.

Then, to install required dependencies run:

cd sapper-blog-tutorial/ && yarn

For now, as we can see in src/routes/blog/_posts.js, our blog posts are just an array:

// src/routes/blog/_posts.js

const posts = [
    {
        title: 'What is Sapper?',
        slug: 'what-is-sapper',
        html: `
            <p>First, you have to know what ...
    ...

To add support for markdown and syntax highlight, first install required dependencies:

yarn add marked prismjs gray-matter reading-time

Then, replace src/routes/blog/_posts.js file content with:

// src/routes/blog/_posts.js

const fs = require('fs')
const path = require('path')
const marked = require('marked')
const matter = require('gray-matter')
const readingTime = require('reading-time')
const prism = require('prismjs')

const cwd = process.cwd()
const POSTS_DIR = path.join(cwd, 'src/routes/blog/posts/')
const EXCERPT_SEPARATOR = '<!-- more -->'
const renderer = new marked.Renderer()

const linkRenderer = renderer.link
renderer.link = (href, title, text) => {
  const html = linkRenderer.call(renderer, href, title, text)

  if (href.indexOf('/') === 0) {
    return html
  } else if (href.indexOf('#') === 0) {
    const html = linkRenderer.call(renderer, 'javascript:;', title, text)
    return html.replace(
      /^<a /,
      `<a onclick="document.location.hash='${href.substr(1)}';" `
    )
  }

  return html.replace(/^<a /, '<a target="_blank" rel="nofollow" ')
}

renderer.code = (code, language) => {
  const parser = prism.languages[language] || prism.languages.html
  const highlighted = prism.highlight(code, parser, language)
  return `<pre class="language-${language}"><code class="language-${language}">${highlighted}</code></pre>`
}

marked.setOptions({
  renderer,
  highlight: function(code, lang) {
    try {
      return prismjs.highlight(code, prismjs.languages[lang], lang)
    } catch {
      return code
    }
  },
})

const posts = fs
  .readdirSync(POSTS_DIR)
  .filter(fileName => /\.md$/.test(fileName))
  .map(fileName => {
    const fileMd = fs.readFileSync(path.join(POSTS_DIR, fileName), 'utf8')
    const { data, content: rawContent } = matter(fileMd)
    const { title, description, created, updated } = data
    const slug = fileName.split('.')[0]
    let content = rawContent
    let excerpt = ''

    if (rawContent.indexOf(EXCERPT_SEPARATOR) !== -1) {
      excerpt = marked(rawContent.split(EXCERPT_SEPARATOR)[0])
    }

    const html = marked.parse(content.replace(EXCERPT_SEPARATOR, ''))
    const time = readingTime(content).text

    return {
      title,
      description,
      slug,
      html,
      created,
      updated,
      excerpt,
      readingTime: time,
    }
  })

posts.sort((a, b) => {
  const dateA = new Date(a.created)
  const dateB = new Date(b.created)

  if (dateA > dateB) return -1
  if (dateA < dateB) return 1
  return 0
})

posts.forEach(post => {
  post.html = post.html.replace(/^\t{3}/gm, '')
})

export default posts

I will explain what is going on now. First, we are importing dependencies, of course. Then we are defining directory, where our .md files will be located, and excerpt separator, to be able to create excerpt for posts.

Later, we create different markdown renderers. First - link renderer would transform all links with # symbol, so they link to h elements in our post. Link renderer also adds target="_blank" rel="nofollow" to all outgoing links.

Then goes the part with code renderer which uses prismjs to parse and highlight code blocks.

After that, at line 129 we define our posts array. gray-mater allows our markdown files to contain meta information for our posts, like title, description, creation date and other whatever you want. I am sure you can understand what info I am storing with my posts.

Last, we sort posts by date and exporting them.

We also need to modify src/routes/blog/index.json.js posts mapping, to include added post information:

const contents = JSON.stringify(
  posts.map(post => {
    return {
      title: post.title,
      slug: post.slug,
      created: post.created,
      excerpt: post.excerpt,
    }
  })
)

Next file we need to modify is rollup.config.js. Add this line somewhere at the top with other imports:

import marked from 'marked'

Then, before export default ... add:

const markdown = () => ({
  transform(md, id) {
    if (!/\.md$/.test(id)) return null
    const data = marked(md)
    return {
      code: `export default ${JSON.stringify(data.toString())};`,
    }
  },
})

And finally, in server part of the exported config, after commonjs(), add:

markdown(),

Final config would look like this.

Now it is time to add first markdown post. Create file src/routes/blog/posts/hello-world.md with the following content:

---
title: Hello World
description: First post in this blog
created: '2020-01-11T19:45:28.107Z'
---

This is excerpt.

<!-- more -->

# Heading

This is the first post in this blog.

Now run:

yarn dev

and go to http://localhost:3000/blog. You can see that our newly created post is showing in the list and we are able to open it. As you can see, markdown renderer is working. Feel free to write more complex markdown. If you do so, you will notice that syntax highlighting is not working yet - don't worry, we will fix that in second part of this tutorial series.

Thats it for now. You can view the whole project at this point here. Stay tuned for the rest parts.

Credits

https://github.com/Charca/sapper-blog-template/ - inspiration for markdown renderer

© 2020 Dmitrijs Čuvikovs Powered by Svelte, icons by iconmonstr