DEV Community

Richard Beattie
Richard Beattie

Posted on • Originally published at

Building a blog with Svelte and Notion

I've finally got my blog(ish) website setup in a way I'm happy with. Most of the site is just a static export from sapper but the learning pieces are all entries in a Notion table. Each page in the table has a slug property which sets the url you navigate to e.g. this piece is building-a-blog-with-svelte-and-notion.

You can see it live at:

Setting up

To begin you'll need to create a new sapper project:

npx degit "sveltejs/sapper-template#rollup" my-svelte-notion-blog
cd my-svelte-notion-blog
npm install
Enter fullscreen mode Exit fullscreen mode

This will scaffold the general structure for a sapper site. There's lots of template pages that you'll want to change (index.svelte, about.svelte, etc) but we're going to focus on the blog folder.

Go ahead and delete everything inside the blog folder and create an empty index.svelte file.

Creating the Notion Table

First we'll need a Notion table where we are going to pull the posts from.

  1. Create a new page containing Table - Full Page
  2. Add a table item called My first post or whatever you like
  3. Give My first post a new property slug with value my-first-post – we'll use this for the url
  4. Click on Share and copy the id after the page's title in the url somewhere

Listing all posts

Now, we can get all the items from this table and display them in our website. Notion doesn't have a public API yet but fortunately Splitbee have created a wrapper for their private API, which we'll interact with using sotion

npm install -D sotion
Enter fullscreen mode Exit fullscreen mode

Sotion has built in support for building a blog based on our Notion table. First we'll scope our posts to that table. In _layout.svelte

    import { sotion } from "sotion";
    const tableId = 'xxxxxxxx' // Whatever you copied before
Enter fullscreen mode Exit fullscreen mode

In blog/index.svelte let's fetch all our posts:

    import { onMount } from 'svelte';
    import { sotion } from "sotion";

    let posts = [];

    onMount(() => {
        posts = await sotion.getScope();
Enter fullscreen mode Exit fullscreen mode

posts is an array of objects representing the pages in our table:

      id: "510a05b0-8ef8-4249-8d68-6c92614fe912",
      slug: "building-a-blog-with-svelte-and-notion",
      Name: "Building a blog with Svelte and Notion"
Enter fullscreen mode Exit fullscreen mode

Finally, we'll render this as a list

    {#if posts.length === 0}
    {#each posts as item (}
        {#if item.slug}
                <a href="blog/{item.slug}">

  ul {
    list-style: none;
    margin: 1rem 0 0 0;
    padding: 0;

  li {
    padding: 0.25em 0;
Enter fullscreen mode Exit fullscreen mode

Awesome! Now you should have something like:

List of Learning Posts on

Displaying the posts

Now clicking on one of those posts will redirect you to blog/{slug}. This is a dynamic route as we don't know what slug will be. Sapper handles this by putting brackets around the dynamic parameter in the route's filename: blog/[slug].svelte. We can then access the slug in preload script. For more info see:

In blog/[slug].svelte

<script context="module">
    import { Sotion, sotion } from "sotion";
  export async function preload({ params }) {
    try {
      const { blocks, meta } = await sotion.slugPage(params.slug);
      return { blocks, meta, slug: params.slug };
    } catch (e) {
      return e;
Enter fullscreen mode Exit fullscreen mode

We use context="module" so the page only renders once it has fetched the content. Importantly as we don't link to these slug pages before client-side javascript is executed this won't interfere with sapper export

If we linked to a slug page sapper export will save the page when exporting stopping it from updating in the future (when directly navigated to)

Then let's get the post's blocks and metadata (Notion properties)

    export let blocks;
    export let meta;
Enter fullscreen mode Exit fullscreen mode

and finally we render those blocks

<Sotion {blocks} />
Enter fullscreen mode Exit fullscreen mode

Now you should be able to able to view your posts at http://localhost:3000/blog/[slug] and see the content from your Notion post rendered. This includes text, headings, code, lists and everything else

Optional Chaining Operator Notion page rendered on

Post Metadata

Unfortunately we're not done yet. If you want your blog to have reasonable SEO and appear nicely on Twitter and Facebook it's important we add some metadata to the page. Twitter and Facebook have need special meta tags so they're is some duplication.

    <meta name="twitter:title" content={meta.Name} />
    <meta property="og:title" content={meta.Name} />
Enter fullscreen mode Exit fullscreen mode

To set the page description we'll first add a description property to our posts' Notion page

Metadata for *Building a blog with Svelte and Notion* page in Notion

Then we set the description

    {#if meta.description}
        <meta name="description" content={meta.description} />
        <meta name="twitter:description" content={meta.description} />
        <meta property="og:description" content={meta.description} />
Enter fullscreen mode Exit fullscreen mode

Finally there's some miscellaneous meta properties you might want to set for Twitter

<meta name="twitter:card" content="summary" />
 <!-- Your twitter handle -->
<meta name="twitter:site" content="@r_bt_" />
<meta name="twitter:creator" content="@r_bt_" /> 
<!-- An image for the article -->
<meta name="twitter:image" content="" />
Enter fullscreen mode Exit fullscreen mode

and Facebook

<meta property="og:type" content="article" />
<meta property="og:url" content="{slug}" />
<meta property="og:image" content="" />
<meta property="og:site_name" content="R-BT Blog" />
Enter fullscreen mode Exit fullscreen mode


You're done. You should now have your own blog powered by Notion with a page listing all your pages and then a dynamic route which renders these pages 😎

You can put this online however you want. I export it and then host it on Netlify

npm run export
Enter fullscreen mode Exit fullscreen mode

If you do export your site you need to redirect requests from blog/[slug] to blog/index.html or else users will get a 404 error since no static files will exist for these routes. With Netlify this is really easy. Create a netlify.toml file and set:

    from = "/blog/*"
    to = "/blog/index.html"
    status = 200
    force = true
Enter fullscreen mode Exit fullscreen mode

Now when users go to Netlify will serve and svelte's client side routing will step in.

Extra: Sitemap

It's good practice to include a sitemap.xml for your site. Since this needs to be dynamic we can't serve it with Sapper's Server Routes (these are static when exported). Instead we can use Netlify Functions.

Create a new folder functions in the root of your directory and then sitemap.js inside this.

We're going to need node-fetch to get the posts from our Notion table, in your root directory run (i.e. functions does not have it's own package.json )

npm install node-fetch
Enter fullscreen mode Exit fullscreen mode

Now in sitemap.js

const fetch = require("node-fetch");

exports.handler = async (event) => {
    const NOTION_API = "";
    // Your Notion Table's ID
    const id = "489999d5f3d240c0a4fedd9de71cbb6f";

    // Fetch all the posts
    let posts = [];
    try {
        posts = await fetch(`${NOTION_API}/table/${id}`, {
            headers: { Accept: "application/json" },
        }).then((response) => response.json());
    } catch (e) {
        return { statusCode: 422, body: String(e) };

    // Filter those posts to get their slugs
    const filteredPages = pages
        .filter((item) => item.slug !== undefined)
        .map((item) => item.slug);

    // Create the sitemap
    const domain = "";
    const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
        <urlset xmlns="">
                .map((page) =>`

    return {
        statusCode: 200,
        contentType: "text/xml",
        body: sitemap,
Enter fullscreen mode Exit fullscreen mode

We're nearly there (both creating this sitemap and me finishing this post 🙂). Lastly we need to run this function when is requested. In netlify.toml add

    from = "/sitemap.xml"
    to = "/.netlify/functions/sitemap"
    status = 200
    force = true
Enter fullscreen mode Exit fullscreen mode

That's it. Commit and deploy to Netlify and your sitemap should be working. I actually had lots of issues getting this to work so if it dosen't for you reach out


  • I'd love if I could someway update each page automatically whenever there's a change in Notion. Live-reloading would be a nice UX while writing.

Top comments (1)

rifie profile image
Syarifah Riefandania ☄

Hi, thanks for the tutorial.. i found error..

  • my page already successfully shown the post list
  • When i want to open the slug url, it comes error "The given blocks are not valid"

Do you have any idea why? thanks