Why Sapper and Strapi

Static-site generators and headless CMS platforms continue to gain ground in front-end development because they lower the barrier to entry while offering flexibility and a strong developer experience. Sapper, built on Svelte, is a particularly good fit when performance is the priority. Svelte-based applications are known for their small bundle sizes and runtime speed, which matters more as user experience expectations continue to rise.

A headless CMS like Strapi complements this setup by decoupling content management from presentation. You get API-driven content without being locked into a specific language, platform, or vendor. The result is a stack that keeps the front end lightweight and the back end flexible.

This guide walks through the core steps for building a minimal blog with Sapper on the front end and Strapi on the back end, including how to query content with GraphQL. The complete source code is available on GitHub.

Before starting, you'll need the LTS version of Node.js, a package manager such as Yarn or npm, and a working knowledge of JavaScript and GraphQL queries.

Setting Up the Sapper Front End

Sapper is a framework for building high-performance web apps with Svelte. One of its strengths is the ability to export pages as static files, which is exactly what we'll need. Sapper projects are typically scaffolded using Degit, a tool that fetches only the latest commit from a Git repository rather than cloning the full history.

Start by installing Degit globally:

npm install -g degit

Then scaffold the project and install dependencies. In this tutorial, we'll use Rollup as the bundler.

npx degit "sveltejs/sapper-template#rollup" frontend
# or: npx degit "sveltejs/sapper-template#webpack" frontend
cd frontend
npm install
npm run dev

Once the server starts on localhost, the default Sapper starter page should render. If you're unfamiliar with Sapper's structure, here's a quick orientation:

  • package.json — Defines app dependencies and run scripts.
  • src — Contains the entry points: src/client.js, src/server.js, and an optional src/service-worker.js, plus src/template.html.
  • src/routes — The core of the app, holding page and server routes.
  • static — For static assets like fonts and images. A file at static/favicon.png is served as /favicon.png.
  • rollup.config.js — Rollup configuration; you generally won't need to edit it.

Sapper creates an additional __sapper__ directory at first run for generated files; any extra files and the cypress directory can be ignored for this project.

Run the dev server with npm run dev to see the starter template.

A screenshot of the Sapper Starter webpage.
Your Sapper home page. (Large preview)

Setting Up the Strapi Back End

Strapi is a self-hosted, headless CMS built with JavaScript, and its admin interface is powered by React. It gives you full control over content storage and hosting without vendor lock-in. For our purposes, it provides a content editor to define models and create content that we can query later.

1. Install Strapi and Create a New Project

Run the quickstart command to create a new folder named backend with the admin UI:

yarn create strapi-app backend --quickstart

2. Create an Administrator

Navigate to https://localhost:1337/admin, complete the form to create the first admin user, then click "Ready to start".

A screenshot of the Strapi register screen.
Create an admin account. (Large preview)

3. Create the Blog Collection Type

Under "Plugins" in the left-hand menu, go to "Content-Types Builder" and click "+ Create new collection type". Name it blog and continue.

A screenshot of the Strapi dashboard - creating a new collection type
Create a new collection type. (Large preview)

Add fields one at a time with "+ Add another field" between each:

  • A "Text field" (short text) named Title.
  • A "Text field" (long text) named Description.
  • A "Date field" of type date named Published.
  • A "Rich Text field" named Body.
  • Another "Text field" (short text) named Slug.
A screenshot of the Strapi dashboard - creating a new text field
Create a new Text field. (Large preview)
A screenshot of the Strapi dashboard - creating a new text field
Create a new Text field. (Large preview)
A screenshot of the Strapi dashboard - creating a new date field
Create a new Date field. (Large preview)
A screenshot of the Strapi dashboard - creating a new rich text field
Create a new Rich Text field. (Large preview)
A screenshot of the Strapi dashboard - adding a new text field
Create a new Text field. (Large preview)

Then set up a relation field: change the field on the right side to "User", and on the left side rename it to author. Finish and save; wait for Strapi to restart.

A screenshot of the Strapi dashboard - creating a new relation
Create a new Relation field. (Large preview)
A screenshot of the Blog collection type showing all its fields
Overview of your Blog collection type. (Large preview)

4. Create a User

Under "Collection Types", navigate to "Users" and click "Add new user". Provide an email, username, and password, toggle the "Confirmed" button on, and save. This user will be available to attribute articles to.

A screenshot of the User collection type with the 'add new user' button highlighted
Add some user content. (Large preview)

5. Add Blog Content

Go to "Blogs" under "Collection Types", click "Add new blog", and fill in the fields—you can select the user you just created as the author. Save the entry.

A screenshot of the Blog collection type with the 'add new blog' button highlighted
Add some blog content. (Large preview)

6. Configure Roles and Permissions

Under "Plugins", go to "Roles and Permissions" and select the "Public" role. Scroll to "Blogs" under Permissions and check the boxes for find and findone. Save the changes.

A screenshot of the Strapi Permissions page with the find and findone actions highlighted
Set permissions for your Public role. (Large preview)

7. Enable GraphQL and Query the API

The standard REST endpoint at https://localhost:1337/Blogs returns JSON data for the content you created. For this project, we'll use GraphQL instead.

To install the GraphQL plugin from the CLI:

cd backend
yarn strapi install graphql
A screenshot of the Strapi Marketplace with the download button on the GraphQL plugin highlighted
Download the GraphQL plugin. (Large preview)

Alternatively, install it through the admin UI via "Marketplace" under "General" in the left-hand menu, using the "Download" button on the GraphQL card.

Once Strapi restarts, test queries in the GraphQL playground at https://localhost:1337/graphql.

At this point the back end is fully configured. The remaining work is connecting the Sapper front end to the GraphQL API and rendering the blog content. To handle GraphQL requests in Sapper, we'll install the Svelte Apollo client and related packages.

Adding GraphQL to the Frontend

From the ./frontend directory, run the following command to install the packages needed for GraphQL queries in your Svelte app:

npm i --save apollo-boost graphql svelte-apollo moment

Moment.js handles parsing, validating, manipulating, and displaying dates and times. With these tools in place, we can now build the three main pages of the blog: “home”, “about”, and “articles”. All posts from Strapi will be listed on the “articles” page, with each post getting its own route at /articles/:slug, where slug is the value you enter in the “Slug” field of the Strapi admin UI.

In ./frontend/src/routes, you will find a folder named “blog”. Delete it — this will temporarily break the app, but the next steps will restore it. Create a new folder named “articles” in its place and add an index.svelte file inside it with the code below. Be sure to replace <Your Strapi GraphQL Endpoint> with your actual endpoint, typically https://localhost:1337/graphql for local development.

<script context="module">
        import ApolloClient, { gql } from 'apollo-boost';  
        import moment from 'moment';

        const blogQuery = gql`
        query Blogs {  
                blogs {
                        id
                        Title
                        Description
                        Published
                        Body
                        author {
                                username
                        }
                        Slug
                }
        }
        `;
        export async function preload({params, query}) {
                const client = new ApolloClient({ 
                        uri: '<Your Strapi GraphQL Endpoint>',
                        fetch: this.fetch
                         });
                const results = await client.query({
                        query: blogQuery
                })
                return {posts: results.data.blogs}
        }
</script>

<script>
        export let posts;
</script>

<style>
        ul, p {
                margin: 0 0 1em 0;
                line-height: 1.5;
        }
        .main-title {
                font-size: 25px;
        }
</style>

<svelte:head>
        <title>articles</title>
</svelte:head>

<h1>recent posts</h1>

<ul>
{#each posts as post}
    <li>
          <a class="main-title" rel='prefetch' href='articles/{post.Slug}'>
            {post.Title}
          </a>
    </li>
    <p> 
  {moment().to(post.Published, "DD-MM-YYYY")} ago by {post.author.username} 
    </p>
{/each}
</ul>

This /articles route imports the necessary packages and uses Apollo Client to run a blogQuery. The preload() function processes the response and returns a posts variable with the parsed data. The Svelte #each block then iterates over the posts, displaying the title, publication date, and author. Each post links to a route based on its slug, which we defined in the Strapi admin UI.

For the individual article route, create [slug].svelte in ./src/routes/articles with the code provided here:

<script context="module">
        import ApolloClient, { gql } from 'apollo-boost';  
        import moment from 'moment';

        const blogQuery = gql`
        query Blogs($Slug: String!) {
                blogs: blogs(where: { Slug: $Slug }) {
                        id
                        Title
                        Description
                        Published
                        Body
                        author {
                                username
                        }
                        Slug
                }
                }
        `;
        export async function preload({params, query}) {
                const client = new ApolloClient({ 
                        uri: '<Your Strapi GraphQL Endpoint>',
                        fetch: this.fetch
                         });
                const results = await client.query({
                        query: blogQuery,
                        variables: {"Slug" : params.slug} 
                })
                return {post: results.data.blogs}
        }
</script>

<script>
        export let post;
</script>

<style>
        .content :global(h2) {
                font-size: 1.4em;
                font-weight: 500;
        }
        .content :global(pre) {
                background-color: #f9f9f9;
                box-shadow: inset 1px 1px 5px rgba(0,0,0,0.05);
                padding: 0.5em;
                border-radius: 2px;
                overflow-x: auto;
        }
        .content :global(pre) :global(code) {
                background-color: transparent;
                padding: 0;
        }
        .content :global(ul) {
                line-height: 1.5;
        }
        .content :global(li) {
                margin: 0 0 0.5em 0;
        }
</style>

<svelte:head>
        <title>an amazing article</title>
</svelte:head>

{#each post as post}
                <h2>{post.Title}</h2>
                <h3>{moment().to(post.Published)} by {post.author.username}</h3>

                <div class='content'>
                {@html post.Body} </div>

{/each}

<p>⇺<a href="articles"> back to articles</a></p>

Note: Svelte encodes dynamic route parameters using [brackets], so [slug].svelte handles routes for each individual post.

This file makes a similar blogQuery, but with a filter so only the post matching the current slug is returned. The params argument in preload() gives access to params.slug, which is passed as a variable into the GraphQL query. The returned posts variable then renders the post’s title, date, and body — the body wrapped in Svelte’s {@html} tag so that the HTML content is displayed.

That covers dynamic post pages. Now update the remaining routes. In about.svelte, place the following code:

<svelte:head>
        <title>about</title>
</svelte:head>

<h1>about this site</h1>

<p>
minimalist web design really let's the content stand out and shine. 
this is why a simple website design is the first choice of so many artists, photographers, 
and even some writers. they want their creative content to be the center of attention, 
rather than design elements created by someone else. 
</p>

<p>this minimal blog is built with <a href="https://svelte.dev/">svelte</a> and <a href="https://strapi.io/">strapi</a> 
images by <a href="https://unsplash.com/@glencarrie">glen carrie</a> from unsplash 
</p>

And for index.svelte, use the code below:

<style>
        h1, figure, p {
                text-align: center;
                margin: 0 auto;
        }
        h1 {
                font-size: 2.8em;
                font-weight: 400;
                margin: 0 0 0.5em 0;
        }
        figure {
                margin: 0 0 1em 0;
        }
        img {
                width: 100%;
                max-width: 400px;
                margin: 0 0 1em 0;
        }
        p {
                margin: 1em auto;
                padding-bottom: 1em;
        }
        @media (min-width: 480px) {
                h1 {
                        font-size: 4em;
                }
        }
</style>

<svelte:head>
        <title>a minimal sapper blog</title>
</svelte:head>
<p>welcome to</p>
<h1>the<b>blog.</b></h1>

<figure>
        <img alt='the birds on a line' src='bird-bg.png'>
        <figcaption>where less is more</figcaption>
</figure>

<p>
<strong>
we're minimal and that might seem boring, except you're actually paying attention.
</strong>
</p>
<p class="link"><a href="about">find out why</a>...</p>

With all pages in place, running the app should show the working blog structure.

A screenshot of the minimal blog home page
Your finished minimal blog home page. (Large preview)

Deploying the Static Site

To share your blog, start by exporting a static version of the Sapper app. From ./frontend, run:

npm run export

The export will be generated in ./frontend/sapper/export. You can drag this entire folder into the Netlify dashboard for an instant deployment, as shown here:

The Netlify Dashboard
Drag your export folder to the Netlify Dashboard. (Large preview)

If you prefer deploying from Git, follow Netlify’s documentation, set npm run export as the build command, and specify __sapper__/export as the base directory. Vercel (formerly ZEIT) is also a supported deployment target, as noted in the Sapper documentation.

Wrapping Up

The result is a static blog that combines Strapi for content management with Sapper for fast, prerendered pages, deployable in minutes. The combination shows how a headless CMS and a modern Svelte framework can streamline static site development without sacrificing developer experience — and the same approach applies well beyond blogs. Share what you build with the community on Twitter, and keep experimenting.