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
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
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'
// ...
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
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(),
...
],
...
}
}
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
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'
}
]
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 thefilename
- 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})
}
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}
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}
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
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
Top comments (11)
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
, notp
?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 thep
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
tomarkdown-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
2) Inside
routes/index.svelte
, define apreload
function that looks at the query params:3) Add conditional next & previous links
Hope that helps!
Nice! Thank you,
Can we do this same without sapper.
Yes. Though sapper and it's replacement svelte-kit make it easier.
One way is to create a vanilla svelte project with a router (tinro?). Then make sure your hosting rewrites routes like
/posts/...
to/index.html
.Thanks subscribed.