loading...

Create a blog with Sapper & Markdown

joshnuss profile image Joshua Nussbaum Updated on ・4 min read

Sapper is a toolkit for creating Svelte apps. It comes with a bunch of conventions baked in to help you get your project up and running quickly.

It can be deployed as a static site or as as a node.js server+SPA that does both server-side rendering and client-side rendering.

NOTE: You can find a screencast of this at the end of the article.

Concept

We're going to store blog posts as markdown .md files inside repo. That way we can use git as the workflow for editing posts. That means Pull Requests for reviewing posts, git log to view history of changes, and forking/branching if we have multiple authors.

Since our data comes from static files (no databases), we don't need a server side component, we can deploy this using Sapper's static site generator.

Setup

Start by scaffolding the app with degit.

You can use the official svelte/sapper-template, but it includes a lot of demo code. I've going to use my fork joshnuss/sapper-template which is a blank slate without demo code.

npx degit joshnuss/sapper-template blog
cd blog
yarn install
Enter fullscreen mode Exit fullscreen mode

Posts data

Each post will be saved in the posts directory and include yaml metadata the top of the file (aka front matter).

Here's what a post posts/example.md would look like:

---
title: Everything you wanted to know
summary: A short post about ...
date: 2020-04-01
---

- this
- is
- markdown
Enter fullscreen mode Exit fullscreen mode

Plugins

We could load these .md files using fs.readFile() at build time, but there is an even easier way, using import statements.

To configure rollup for .md imports, we'll use the plugin @jackfranklin/rollup-plugin-markdown.

That makes it possible to:

import post1 from 'posts/example1.md'
import post2 from 'posts/example2.md'
// ...
Enter fullscreen mode Exit fullscreen mode

Of course importing each post one-by-one will get tedious fast. 😅

It would be easier to import a bunch of files at once based on a wildcard search pattern, like posts/*.md. The plugin rollup-plugin-glob does exactly this. 🚀

Add the NPM packages:

yarn add -D @jackfranklin/rollup-plugin-markdown rollup-plugin-glob
Enter fullscreen mode Exit fullscreen mode

Then, tell rollup to use these plugins. Update rollup.config.js:

// import plugins
import markdown from '@jackfranklin/rollup-plugin-markdown'
import glob from 'rollup-plugin-glob'

// ....

// remember rollup is creating multiple builds
// make sure to add the new plugins to both the server *and* client builds
export {
  client: {
    plugins: [
      markdown(),
      glob(),
      ...
    ],
    ...
  },

  server: {
    plugins: [
      markdown(),
      glob(),
      ...
    ],
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

Reading Posts

Now that we can import .md, let's centralize the logic for accessing posts inside src/posts.js:

import all from '../posts/*.md'

export const posts = all
Enter fullscreen mode Exit fullscreen mode

If we console.log(posts), the data for posts currently looks like this:

[
  {
    metadata: {title: 'the title', summary: '...', date: '2020-01-02'},
    html: '<h1>...</h1>',
    filename: 'example.md'
  }
]
Enter fullscreen mode Exit fullscreen mode

Let's reshape it a bit, just to make it easier for our UI to use.

We're going to make these improvements:

  • Put the metadata (title, summary, date) at the top level.
  • Add a permalink field. It will be based on the filename
  • Sort the list of posts by date in descending order (newest posts first)

Makes these changes to src/posts.js:

import _ from 'lodash'
import all from '../posts/*.md'

export const posts = _.chain(all) // begin a chain
                      .map(transform) // transform the shape of each post
                      .orderBy('date', 'desc') // sort by date descending
                      .value() // convert chain back to array

// function for reshaping each post
function transform({filename, html, metadata}) {
  // the permalink is the filename with the '.md' ending removed
  const permalink = filename.replace(/\.md$/, '')

  // convert date string into a proper `Date`
  const date = new Date(metadata.date)

  // return the new shape
  return {...metadata, filename, html, permalink, date}
}

// provide a way to find a post by permalink
export function findPost(permalink) {
  // use lodash to find by field name:
  return _.find(posts, {permalink})
}
Enter fullscreen mode Exit fullscreen mode

Index page

Now that we have our posts, we can move on to the UI.

Open the src/routes/index.svelte and display an <article> tag for each post:

<script>
  // import the list of posts
  import {posts} from '../posts'
</script>

<h1>My Weblog</h1>

<!-- iterate through each post -->
{#each posts as post}
  <article>
    <!-- link article to /posts/$permalink -->   
    <a href={`/posts/${post.permalink}`}>
      <h2>{post.title}</h2>
      <p>{post.summary}</p>
    </a>
  </article>
{/each}
Enter fullscreen mode Exit fullscreen mode

Blog details page

The index page now shows summaries of each posts, to see the entire post add a page/route called src/routes/posts/[permalink].svelte.

Notice we're using square brackets around [permalink]? That tells sapper that the permalink is a dynamic parameter. Sapper will provide all parameters to our preload() function.

<script context="module">
  // import the logic for finding a post based on permalink
  import {findPost} from '../../posts'

  // sapper calls this to load our data
  export function preload(page) {
    // find the post based on the permalink param
    const post = findPost(page.params.permalink)

    // return a list of props
    return { post }
  }
</script>

<script>
  // this prop is filled from the result of the `preload()`
  export let post
</script>

<!-- display the post -->
<h1>{post.title}</h1>

{@html post.html}
Enter fullscreen mode Exit fullscreen mode

Deploy

To deploy our site we can generate the static site with yarn export.
You can also 🛳 it with zeit while you're at it:

yarn export
now
Enter fullscreen mode Exit fullscreen mode

That's it, all done! 💃

Summary

Building static sites with Sapper takes very little effort.
There are many helpful rollup plugins that can convert static data into importable formats, that means we don't even have to write a parser for our data in many cases.

Another good thing about this approach is its' versatility. The same concept will work for project pages, wikis, news sites, books, landing pages, etc. Any data you can put in a git repo, can be the driver of a sapper site.

You can find example code here:
https://github.com/joshnuss/sample-blog

Happy coding! ✌

PS. This is part of my upcoming course on svelte: http://svelte.video

Screencast

Posted on by:

joshnuss profile

Joshua Nussbaum

@joshnuss

Sapien working with machines.

Discussion

pic
Editor guide
 

Bonus: I've added a few branches to the repo

 

This helped me! Do you know a way to wrap images from markdown in figure, not p?

 

Glad it helped :)

I think the p tag comes from having an empty line before defining the image. It's not the image that causes the p tag.

One workaround is to <figure> tag inside markdown. HTML tags are allowed in markdown.

Otherwise you'll have to look at modifying the markdown parser, or finding one that has a configurable option for this.

 

Well the p tag will wrap either only the image, or the image and other inline elements.

Thanks. I was guessing I had to find another parser. markdown-it seems like the most active -- plugin wise ...

Welcome.

Try forking the markdown rollup plugin and switching showdown to markdown-it. That should do it.

 

Any suggestion for pagination? Its one of most basic features of a Blog.

 

That is true, pagination is important.

Here's one way to do it:

1) Group posts into pages/chunks inside src/posts.js

// 10 posts per page
export const pages = _.chunk(posts, 10)
Enter fullscreen mode Exit fullscreen mode

2) Inside routes/index.svelte, define a preload function that looks at the query params:

<script context="module">
  import {pages} from '../posts'

  export function preload(page) {
    const index = +(page.query.page || 1)

    return {
      posts: pages[index-1],
      hasMore: pages.length > index + 1,
      page: index
    }
  }
</script>

<script>
  export let posts, hasMore, page
</script>
Enter fullscreen mode Exit fullscreen mode

3) Add conditional next & previous links

{#if page > 1}
  <a href="/?page={page-1}">Previous</a>
{/if}

{#if hasMore}
  <a href="/?page={page+1}">Next</a>
{/if}
Enter fullscreen mode Exit fullscreen mode

Hope that helps!

 
 

Thanks subscribed.