Svelte Tutorial: Make a Blog with Sapper
How to build a markdown blog with Svelte, Sapper, & Sanity
Introduction to Svelte, Sapper & Sanity
Within the vast landscape of Javascript frameworks, Svelte has transformed from the new kid on the block to a serious competitor to other frameworks. This is largely due to Svelte's bold approach to front-end Javascript development & it's community-driven nature. Originally released in 2016 by its original author Rich Harris, Svelte's primary difference from other Javascript frameworks is that Svelte includes a build step where any unused HTML, CSS, & JS is striped away. The release of Svelte version 3 in early 2019 brought an entire rebuild of reactivity which, in turn, reduced the complexities of component state & overall made Svelte's syntax even more approachable.
The characteristic that sets Svelte apart from its peers is how accessible the code is. As Rich will frequently mention, this "Javascript" framework is technically a compiler that transforms a superset of HTML into HTML, CSS, & Javascript we can run in the browser, no runtime library needed. This allows the syntax to read & feel just like a vanilla web stack, with just a dash of framework. If we compare Svelte syntax to other frameworks like React or Vue, you'll see the latter two are undeniably Javascript. They both render components with JS functions unlike Svelte. Don't get me wrong, I love Javascript, but when we're building UIs for the web it's far more intuitive to think first of the structure, or HTML. Because Svelte is a compiler, we can build with components that are syntactically different than the Javascript output; giving us the ideal developer experience while outputting an app that is pure HTML, CSS, and Javascript. This syntax is very quick to learn for both seasoned Javascript developers and people who only know HTML & CSS, making it great for teams working in production or those still learning Javascript.
The last advantage I will mention is this - because Svelte compiles away any unused code & doesn't utilize a client rendered library, the performance gains are astounding. Svelte apps are so small, some of Svelte's earliest adopters were companies building small IoT products, POS systems & other low power computing electronics that require user interfaces. Other companies currently using Svelte in production include The New York Times, Spotify, Ikea, Bloomberg, & Alaska Airlines. If you're not sold yet, you should also know that Svelte ranked highest for Satisfaction & Interest in the 2020 State of JS survey.
If you've heard of Svelte you've likely also heard of Sapper, the application framework for Svelte. Sapper is to Svelte what Next.js is to React - even modeled in part after Next. Sapper brings features like file-based routing & static export to Svelte apps. This makes Sapper not only a great way to build web apps, but also websites; in our case, a blog.
The data source for our blog will obviously be a Sanity back end connected through the Javascript Client provided by Sanity. Sanity's approach to content - treating it as data and allowing the end user to manipulate said data - makes Svelte a wonderful framework to pair with Sanity; especially in data-light use cases like a blog where all of our data fetching can happen in the browser. The other great things about structured content is that our entire CMS backend is built around Javascript object schemas. This means we can scaffold custom data types & CMS configurations in minutes, something I used to spend hours doing in other CMS solutions.
Building our Blog
Before we get started, some prerequisites for following this tutorial are: you're comfortable using the command line, you have a solid understanding of HTML, CSS & Javascript, and have experience with node/npm. I'll do my best to explain everything that Svelte is handling in our app, but if you find yourself wanting to dig deeper, please consult the excellent tutorial at svelte.dev.
This tutorial also assumes you have the latest versions of Node.js & npm installed, and that you have a Sanity account created.
Step 1: Creating our Sanity Project
The first step in building our Sanity powered blog is to get our Sanity project created. For this we will use the Sanity CLI. To install the CLI globally, run the following command
npm install -g @sanity/cli
We're then quickly going to scaffold the folder structure we need for our project. It will look something like this...
sanity-md
-- content
our Sanity project lives here
-- web
our Svelte/Sapper project lives here
To achieve this structure, we'll first create our parent directory, and then our content directory. Feel free to name the parent directory anything you'd like. In your command line, run this command to create the parent directory and change into said directory...
mkdir sanity-md && cd sanity-md
Then we'll create our content directory and change into it as well.
mkdir content && cd content
Once we're in the content directory we can run our first command with the Sanity CLI.
sanity init
This command does a few things:
- If you aren't logged in to Sanity, you will be prompted to do so
- Creates a new Sanity project in your account
- Create sa dataset for said project
- Installs everything needed to run Sanity Studio locally
For the purposes fo this tutorial, we're going to select the default blog schema offered to us in the CLI prompt. Once the Sanity project is created, let's open it up in our code editor to take a look at the blog schema Sanity gives us.
-- sanity-md
-- content
-- config
-- node_modules
-- plugins
-- schemas
-- static
package.json
README.md
sanity.json
tsconfig.json
yarn.lock
The only directory we're concerned with in our Sanity project right now is our schemas directory. Let's open it up...
-- schemas
author.js
blockContent.js
category.js
post.js
schema.js
In a Sanity project, each content type needs a schema defined. These schemas are then all imported by schema.js
, which is used to build the Sanity Studio. The main schema we're going to be concerned with in this tutorial is post.js
.
export default {
name: 'post',
title: 'Post',
type: 'document',
fields: [
{
name: 'title',
title: 'Title',
type: 'string',
},
{
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96,
},
},
{
name: 'author',
title: 'Author',
type: 'reference',
to: {type: 'author'},
},
{
name: 'mainImage',
title: 'Main image',
type: 'image',
options: {
hotspot: true,
},
},
{
name: 'categories',
title: 'Categories',
type: 'array',
of: [{type: 'reference', to: {type: 'category'}}],
},
{
name: 'publishedAt',
title: 'Published at',
type: 'datetime',
},
{
name: 'body',
title: 'Body',
type: 'blockContent',
},
],
preview: {
select: {
title: 'title',
author: 'author.name',
media: 'mainImage',
},
prepare(selection) {
const {author} = selection
return Object.assign({}, selection, {
subtitle: author && `by ${author}`,
})
},
},
}
In this schema we are defining the post content type and its fields, including a title, image, body, and a few other pieces of data, like the preview. The preview definition in our schema is how Sanity knows how to construct content previews. This isn't entirely pertinent to this article, but if you'd like to learn more, check the Sanity docs. Later on we'll look at using a Markdown input instead of the locally defined blockContent that comes with this predefined schema, but for now let's fire up our Sanity Studio and create a sample blog post! To run the sanity studio locally, just navigate back into the content directory and run the following command:
sanity start
This command will compile our Sanity Studio and serve it locally at localhost:3333. Opening the studio, we can see we have three content types - Author, Post & Category. For now, we're just going to make a single sample post to build out our Sapper app with. Enter whatever content you like for this post and make sure to include data for every field except 'Author' & 'Category'. Since both of these need to be declared elsewhere in the studio first, we are going to leave them empty to keep it simple.
Once your post is created, simply hit publish and then we can move onto the last step of our backend configuration. In order for our Sapper front end to be able to query our Sanity data source from the browser, we need to declare which URL is going to be making the requests or Sanity will throw a CORS error. This architecture allows us to create front end-first applications without worrying about bad actors getting our Sanity data. If the request isn't coming from a declared origin, an error will be thrown. To declare these allowed origins, navigate to manage.sanity.io , open up the project you've created for this tutorial, and then navigate to Settings > API and then click "Add New Origin". Our Sapper app will be running locally at http://localhost:3000 so that's the URL we want to insert here. If and when you deploy your Sapper app, you'll need to add another origin for the URL where the app is deployed.
Part 2: Svelte & Sapper
We've already covered the WHY of Svelte, so now let's dig into some code and see the HOW. A Svelte app consists of Svelte components in the form of .svelte files. The app will also have a root App.svelte which will be rendered by a main.js
file using the Svelte compiler. Here is the basic Structure of a Svelte component...
<!-- Component.svelte -->
<script>
export let hello = "hello"
const world = "world"
</script>
<h1>{ hello }<span>{ world }</span></h1>
<style>
h1 {
font-size: 2rem;
color: purple;
}
span {
color: red;
}
</style>
- Tip: Svelte requires that all .svelte files are named with the first letter uppercase eg. MyComponent.svelte
In Svelte, the Javascript, HTML (template), and Styles are all handled at the component level & thus are scoped to that component. In the above example, we're defining two variables; 'hello' & 'world'. The export
in front of 'hello' defines this variable as a prop which will be passed down from a parent component. If no prop is passed when the component is rendered, it will use the default value we've defined to be 'hello'. We're then including these variables in our template, which is just regular HTML markup. To add dynamic data to our template, we just need to use single curly braces, like it is shown above. Due to Svelte's approach to reactivity, any time a value referenced in the template changes, this causes a re-render of the component. So if we wanted to create a button that can update the state variable world, we can do this...
<!-- Component.svelte -->
<script>
export let hello = "hello"
<!-- Change const to let -->
let world = "world"
const updateWorld = () => {
world == "world" ? world = "there" : world = "world"
};
</script>
<h1>{ hello }<span>{ world }</span></h1>
<button on:click={() => updateWorld()}> Update World </button>
<style>
h1 {
font-size: 2rem;
color: purple;
}
span {
color: red;
}
</style>
Now when we click our button, our 'world' variable is updated based on the current value. The on:click directive runs the updateWorld function whenever we click the button and then our template will re-render when the 'world' variable changes. This is a very birds eye view of Svelte, so feel free to check the tutorial, docs & REPL at svelte.dev
Like I mentioned above, Sapper is an application framework that uses Svelte. The quickest way to get a Sapper project up and running is to use degit to clone the template repository to your local dev environment. In a new terminal tab (leave your Sanity Studio running), navigate back to the parent directory for our project and run this command:
npx degit "sveltejs/sapper-template#rollup" web
This will clone the template in a directory named "web" next to our "content" directory. Let's change into the 'web' directory and install our Node dependancies.
cd web && npm i
We also need to bring in the Sanity Javascript Client so we can connect to our Sanity backend. To do so, run this command inside the Sapper project:
npm install --save @sanity/client
Once dependancies are installed, we're are going to convert our Sapper project to use TypeScript. TypeScript brings a lot of great features to Javascript that allow you to catch bugs before they run and write cleaner JS code in general. If you're unfamiliar with TS don't fret; any valid Javascript is valid TS so you can learn it bit by bit. To convert our Sapper to TS, run the following command in the web directory:
node scripts/setupTypeScript.js
You will then receive this prompt from the command line:
Next:
1. run 'npm install' again to install TypeScript dependencies
2. run 'npm run build' for the @sapper imports in your project to work
Complete these steps and then we're ready to rock! To get started building, we just need to startup our development server using npm run dev
which will serve our Sapper app at localhost:3000 .
Step 3: Connecting Sanity & Sapper
*Note: I find it easiest to open the parent directory in your code editor so you can see the front end code as well as the Sanity Studio code. I'm going to be using Visual Studio Code because it has the official Svelte extension, which supports syntax highlighting in Svelte components. I highly suggest you use this extension when writing Svelte code!
Let's open up our project in our code editor and take a look at the folder structure of a Sapper app:
-- sanity-md
-- content
-- web
-- __sapper__
-- .vscode
-- node_modules
-- src
-- static
.gitignore
package-lock.json
package.json
README.md
rollup.config.js
tsconfig.json
All of our work is going to be within the src directory...
-- src
-- components
-- node_modules
-- routes
ambient.d.ts
client.ts
server.ts
service-worker.ts
template.html
As you might expect, our Svelte components live inside the components directory & our routes live within the routes directory. Sapper uses a file based routing system similar to Next.js, where every route in the app is represented by a .svelte file in the routes directory. Additionally, a directory within the routes directory will also represent a route as long as it contains an index.svelte
file.
--routes
--blog
# these file prepended with an underscore won't be rendered as pages
_error.svelte
_layout.svelte
about.svelte
index.svelte
In this template project, our blog directory contains an index.svelte
file, a [slug].svelte
file, and some other Javascript files that are currently just placeholder content. Since this index.svelte
file is in the blog directory, it will represent the /blog route of our site. Likewise, the [slug].svelte
file is a dynamic route for all of our single blog posts. The square brackets indicate to Sapper that this is a dynamic route; but more on that later! First, we need to make a Sanity Client module that can be reused throughout our app.
In the components directory, create a file named SanityClient.ts
. This will act as a module we can import into our Svelte components to easily query our Sanity data source. The module will look like this...
import sanityClient from '@sanity/client';
// create Client interface to type check options
type Client = {
projectId: string,
dataset: string,
token: string,
useCdn: boolean
}
// create instance of sanityClient
// this is how you connect your frontend to your sanity studio
const options:Client = {
//your project ID
projectId: 'PROJECT-ID',
//your dataset; defaults to production
dataset: 'production',
token: '',
useCdn: true
}
const client = sanityClient( options );
export { client }
This little module imports the sanityClient we installed earlier, creates a Client interface, defines an options variable that contains all the information we need to give Sanity, and, lastly, creates a client variable, which calls the sanityClient with our given options. The client variable is then exported so we can bring it into any part of our app.
Now let's hop into routes/blog and start to build out the meat of our application. First, we can delete all files in the blog directory except for the two svelte files. Then let's open up the index.svelte file in the blog directory.
*Note: As we look at these Svelte components, know that I'm omitting any styles or svelte:head tags to keep code blocks short. The styles that come with the template are adequate for our purposes.
<script context="module" lang="ts">
import { client } from '../../components/sanityClient'
export async function preload() {
const query = "*[_type == 'post']{_id, slug, title}";
const posts = await client.fetch(query)
return { posts }
}
</script>
<script lang="ts">
type Slug = {
_type: string,
current: string,
}
export let posts: { slug: Slug; title: string }[] = [];
</script>
<h1>Recent posts</h1>
<ul>
{#each posts as post}
<li><a rel="prefetch" href="blog/{post.slug.current}">{post.title}</a></li>
{/each}
</ul>
This is the final code for our new blog index page; let's break it down into pieces. First, you'll notice that we actually have two script tags in this component. What gives?! Svelte gives us a really handy way to fetch data almost outside of the component life cycle. The explanation for context='module' in the docs is "A tag with a context='module' attribute runs once when the module first evaluates, rather than for each component instance." We could just as easily move our <code>client.fetch()</code> call into our normal script tag; however, this would cause the page to load before our list of blog posts has been fetched. The user would then see an empty page for a moment while that data is being fetched. By using context='module' the data has already been fetched by the time the component renders in the DOM. Thanks, Svelte!</p> <p>Let's now break the module down line by line. </p> <p>First we import our client module using ESM syntax, then we exporting an async function which defines a groq query, and then awaits the <code>client.fetch()</code> call using the query we've just defined. The function then returns our 'posts' state variable with all of our blog posts included; this return value is passed as props to the normal script tag in our Svelte component. Because this is just a list of the blog titles and links to their respective routes, our groq query includes a Projection. This projection allows the query to only return the values we explicitly ask for, meaning we don't have to fetch all of the post content, just the _id, slug & title.</p> <p>Moving on to our normal script tag, the first definition is again a TypeScript interface. Interfaces are a way for us to tell TypeScript what any one piece of data should look like. In our case, the Sanity content type 'slug' returns a '_type' property & a 'current' property. By creating this interface, we can now explicitly tell TypeScript how the slug we get from Sanity should look. We're then creating our posts prop, which itself has type definitions along side it describing what shape it should have. This prop is receiving the returned posts array from the module mentioned earlier because they share the same name. </p> <p>Lastly, is our template, which is just an h1 & and unordered list. The list contains a Svelte <code>{#each}</code> block, which is one of the many array methods built into Svelte templates. An 'each' block takes in an array as a parameter and renders a copy of any code in the 'each' block for each item in the given array. Although the syntax is rather readable, I'll spell it out - for each item in the posts array, we will create a list item containing a link to the blog post using slug.current and the title. The second argument of the 'each' block, <code>as post</code>, defines how the array item will be referenced inside the block; this can be anything, but if the array is in a plural form (like posts), it's usually best practice to use the singular form here. This makes the templates read almost like plain English! </p> <h3> <a name="step-3-markdown" href="#step-3-markdown" class="anchor"> </a> Step 3: Markdown! </h3> <p><img src="https://cdn.sanity.io/images/81pocpw8/production/3c6787a8794a665f4e9338aca20c415185dba7c7-1140x676.png?w=960&h=569&fit=clip&auto=format" alt="The main blog page"/></p> <p>Wow, that was a lot to take in. Luckily, the [slug].svelte file looks very similar to this and essentially functions in the same way! But before we get to that, we need to talk about our blog content. </p> <p>By default, the main body of our blog is blockContent in our Sanity Studio, otherwise known as Portable Text. Portable Text is a JSON based rich text specification created and maintained by Sanity. As is stated in the README, "Portable Text is an agnostic abstraction of rich text that can be serialized into pretty much any markup language". </p> <p>However, in this tutorial we are using Markdown as the main blog content. A personal blog like this is likely the only case I would choose Markdown over Portable Text for a few reasons. First, I've found I can write a lot faster using Markdown over any form of rich text including Portable Text. The second reason being that this blog content will only ever be used within this blog. In any other case I would stick with portable text since you can mix rich text with data and it's extremely flexible across different platforms, and it fits with the <a href="https://www.sanity.io/structured-content">Structured Content</a> model of Sanity much better than Markdown. We need to bring in additional packages to write & render Markdown, making it much less flexible than Portable Text. </p> <p>Let's bring the first of these packages into Sanity. First, let's navigate to the content directory via a terminal and run this command:<br> </p> <div class="highlight"><pre class="highlight shell"><code>sanity <span class="nb">install </span>markdown </code></pre></div> <p></p> <p>This will install the markdown input in our Sanity Studio; all we need to do now is open our post.js schema in vscode and make a quick edit to the body field.<br> </p> <div class="highlight"><pre class="highlight jsx"><code><span class="k">export</span> <span class="k">default</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">post</span><span class="dl">'</span><span class="p">,</span> <span class="na">title</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Post</span><span class="dl">'</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">document</span><span class="dl">'</span><span class="p">,</span> <span class="na">fields</span><span class="p">:</span> <span class="p">[...</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">body</span><span class="dl">'</span><span class="p">,</span> <span class="na">title</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Body</span><span class="dl">'</span><span class="p">,</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">markdown</span><span class="dl">'</span><span class="p">,</span> <span class="na">options</span><span class="p">:</span> <span class="p">{</span> <span class="na">minRows</span><span class="p">:</span> <span class="mi">20</span> <span class="p">}</span> <span class="p">},</span> <span class="p">...]</span> <span class="p">}</span> </code></pre></div> <p></p> <p>Now, if we restart our Sanity Studio, the body input will be replaced with a markdown editor. Enter in a bit of markdown that we can use to make our last Sapper page - our dynamic single blog post page. But before we do that, we need a way to render the string of markdown that will be returned from Sanity. To do this, we just need to install Snarkdown; my favorite markdown parser for Svelte apps. To install this package, just run this command in the web directory containing our Sapper app:<br> </p> <div class="highlight"><pre class="highlight shell"><code>npm i <span class="nt">--save</span> snarkdown </code></pre></div> <p></p> <p>Now that we have a markdown parser, let's take a look at [slug].svelte...<br> </p> <div class="highlight"><pre class="highlight html"><code><span class="nt"><script </span><span class="na">context=</span><span class="s">"module"</span> <span class="na">lang=</span><span class="s">"ts"</span><span class="nt">></span> <span class="k">import</span> <span class="p">{</span> <span class="nx">client</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../../components/sanityClient</span><span class="dl">'</span> <span class="kd">let</span> <span class="nx">post</span> <span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">preload</span><span class="p">({</span> <span class="na">params</span><span class="p">:</span> <span class="p">{</span> <span class="nx">slug</span> <span class="p">}</span> <span class="p">})</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">query</span> <span class="o">=</span> <span class="s2">`*[slug.current == "</span><span class="p">${</span><span class="nx">slug</span><span class="p">}</span><span class="s2">"]`</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">client</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="nx">query</span><span class="p">)</span> <span class="kd">const</span> <span class="nx">post</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">res</span><span class="p">.</span><span class="nf">shift</span><span class="p">();</span> <span class="k">return</span> <span class="p">{</span> <span class="nx">post</span> <span class="p">}</span> <span class="p">}</span> <span class="nt"></script></span> <span class="nt"><script </span><span class="na">lang=</span><span class="s">"ts"</span><span class="nt">></span> <span class="k">import</span> <span class="nx">snarkdown</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">snarkdown</span><span class="dl">'</span><span class="p">;</span> <span class="k">export</span> <span class="kd">let</span> <span class="nx">post</span><span class="p">:</span> <span class="p">{</span> <span class="nl">slug</span><span class="p">:</span> <span class="nx">string</span><span class="p">;</span> <span class="nl">title</span><span class="p">:</span> <span class="nx">string</span><span class="p">,</span> <span class="nx">body</span><span class="p">:</span> <span class="nx">string</span> <span class="p">};</span> <span class="nt"></script></span> <span class="nt"><h1></span>{post.title}<span class="nt"></h1></span> <span class="nt"><div</span> <span class="na">class=</span><span class="s">"content"</span><span class="nt">></span> {@html snarkdown(post.body)} <span class="nt"></div></span> </code></pre></div> <p></p> <p>You'll notice that the same basic structure applies here - we have a module script tag that is using our sanityClient to fetch data from Sanity; however, there is one difference here. Because this Sapper route is dynamic (hence the square brackets in the file name), we have access to <code>{ params }</code> in our async preload function. We can then de-structure those params to get access to the slug being passed to this page. Then we can employ a template literal to use that slug in our groq query. Lastly, instead of just returning the array we receive from client.fetch, we can use <code>.shift()</code> to pull the first object (in our case the only object) out of the array. This will help keep our markup clean & readable. Moving down to the normal script tag, the only change here is that we're importing Snarkdown & our prop has a different type definition. Instead of an array of objects, this post prop is a single object with a slug, title, & body. Moving to the template, we're rendering the blog post title in an <code><h1></code> before rendering the post content. Svelte provides a handy <code>{@html }</code> renderer for us to use. The <code>snarkdown()</code> function returns html raw, so all we have to do is pass our post's body sting to Snarkdown inside the <code>{@html }</code> renderer and - boom! We're parsing our markdown from Sanity! If your Sapper dev server is still running, just save this file and then navigate back to your browser to see our new blog working better than ever. </p> <h3> <a name="step-4-review" href="#step-4-review" class="anchor"> </a> Step 4: Review </h3> <p><img src="https://cdn.sanity.io/images/81pocpw8/production/ddb7a91883a46d545bf90e0743e7af88ec0d2f37-2550x1314.png?rect=1,0,2548,1314&w=1280&h=660&fit=clip&auto=format" alt="The single post page"/></p> <p>We now have the bones for a Sanity powered Markdown blog with a Sapper front-end, just add other pages/routes you may want, tweak the styling & voila! Also remember we still have other data that can be managed in Sanity and displayed in our Sapper site, like post categories and post authors. The possibilities are endless! Deploy your studio to Sanity by running <code>sanity deploy</code> in the content folder and then you can deploy your Sapper app just about anywhere. </p> <p>Check the Sapper <a href="[https://sapper.svelte.dev/docs#Deployment](https://sapper.svelte.dev/docs#Deployment)">docs</a> for more info on deploying. </p> <p>You can view the source code for this blog on my <a href="[https://github.com/stordahl/sanity-sapper-md](https://github.com/stordahl/sanity-sapper-md)">Github</a> & make sure to checkout my Learning Svelte series at <a href="https://stordahl.dev/blog">stordahl.dev</a> if you're new to Svelte.</p>