How to Guide: Using Sapper with TakeShape

Ashutosh Kumar Singh - Jan 27 '21 - - Dev Community

In this article, we will discuss how to use TakeShape with Sapper, an application framework powered by Svelte.

If you want to jump right into the code, check out the GitHub Repo here.

And here's a link to the deployed version: https://sapper-takeshape-example.vercel.app/

Alt Text

Prerequisites

  • Knowledge of HTML, CSS, JavaScript
  • Basics of Svelte and GraphQL
  • Node/NPM installed on your local dev machine
  • Any code editor of your choice

What is Svelte?

Svelte is a tool for building fast web applications, similar to JavaScript frameworks like React and Vue, svelte aims to make building slick interactive user interfaces easy. But there's a crucial difference.

According to official docs:

Svelte converts your app into ideal JavaScript at build time, rather than interpreting your application code at run time. This means you don't pay the framework's abstractions' performance cost, and you don't incur a penalty when your app first loads.

What is Sapper?

Sapper is a framework built on top of Svelte and has taken inspiration from Next.js. Sapper helps you create SEO optimized Progressive Web Apps (PWAs) with file system based routing, similar to Next.js.

How to Setup and Install a Sapper project

This tutorial uses sapper-template to quickly set up the initial Sapper project, which is also the preferred way to initialize a Sapper project.

In your project's root directory, run the following commands in the terminal.

npx degit "sveltejs/sapper-template#webpack" sapper-takeshape-example
cd sapper-takeshape-example
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

The last command npm run dev will start the development server on port 3000. Head over to http://localhost:3000/.

Here is how your app will look.

Alt Text

How to Generate TakeShape API Keys

If haven't already, create a free developer account on TakeShape.

Alt Text

Create a new project and configure it as shown below.

Give a name to your project; this tutorial uses a project named sapper-takeshape-example.

Alt Text

Now, click on Create Project.

On your TakeShape dashboard, head over to the Post tab. You will see the sample blog posts present in this project.

Alt Text

The next step is to generate API keys to authenticate your Sapper project with TakeShape. Click on the triple dots present next to your project's name on the dashboard.

Alt Text

In the drop-down menu, click on API Keys.

Alt Text

Click on New API Key.

Alt Text

Name this API key, and since you will only use this on the client-side to read the blog posts, you can set Permissions to Read. Click on Create API Key.

Alt Text

Copy the API key to a secure location; remember you will only see them once.

Alt Text

**Note:* These credentials belong to a deleted project; hence I have not hidden them throughout this tutorial to give you a better understanding of the process and steps. You should never disclose your private API keys to anyone.*

On the API Keys page, you will also see your TakeShape project id, i.e., the value between /project/ and /v3/graphql in your API endpoint; copy this project id.

Alt Text

In your project's root directory, run the following command to create a new file named .env to securely store this API key.

touch .env
Enter fullscreen mode Exit fullscreen mode

In your .env file, add the environment variables.

# .env
TAKESHAPE_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
TAKESHAPE_PROJECT="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Enter fullscreen mode Exit fullscreen mode

To access these environment variables, you need to install the dotenv package, which loads environment variables from the .env file.

Run the following command in the terminal to install the dotenv package in your project.

npm install dotenv
Enter fullscreen mode Exit fullscreen mode

Now you also need to configure Sapper to use these environment variables. Modify your src/server.js file like this.

require("dotenv").config();

import sirv from "sirv";
import polka from "polka";
import compression from "compression";
import * as sapper from "@sapper/server";

const {PORT, NODE_ENV} = process.env;

const dev = NODE_ENV === "development";

polka() // You can also use Express
    .use(
        compression({threshold: 0}),
        sirv("static", {dev}),
        sapper.middleware()
    )
    .listen(PORT, (err) => {
        if (err) console.log("error", err);
    });
Enter fullscreen mode Exit fullscreen mode

In the above code, you have imported the dotenv package at the top of the server.js file.

require("dotenv").config();
Enter fullscreen mode Exit fullscreen mode

Restart your development server by closing it using Ctrl + C and starting it again using npm run dev.

How to Display Posts on the Blog Page

With your development server still running, head over to http://localhost:3000/blog. You will see a page similar to this, which lists all the posts with their links.

Alt Text

These are the sample blog posts that come with the sapper-template and are present in src/routes/blog/_posts.js. You need to update this /blog route to show posts fetched from TakeShape.

Every post in the posts array has a title and a slug, shown on the blog page. You need to create a similar GraphQL query that fetches the title and slug of each post.

On your TakeShape dashboard, click on API Explorer.

Alt Text

Here is how this API Explorer will look.

Alt Text

Copy and Paste the following GraphQL query in the left tab.

query AllPosts {
  allPosts: getPostList {
    items {
      _id
      title
      slug
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Run this query; you will see an output similar to this.

Alt Text

In Sapper, Page is a Svelte component written in .svelte files. Server routes are modules written in .js files that export functions corresponding to HTTP methods like get, post, etc. Each function receives HTTP request and response objects as arguments, plus a next function.

The index.json.js file under routes/blog directory is a server route, which currently fetches data from the posts array in the _posts.js file. You need to update this server route to fetch posts from TakeShape.

You will need to install the node-fetch package to make the API requests. Run the following command in the terminal to install node-fetch.

npm install node-fetch
Enter fullscreen mode Exit fullscreen mode

Update src/routes/blog/index.json.js file like this and restart your development server.

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

export async function get(req, res) {
    const {TAKESHAPE_API_KEY, TAKESHAPE_PROJECT} = process.env;

    const data = await fetch(
        `https://api.takeshape.io/project/${TAKESHAPE_PROJECT}/v3/graphql`,
        {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                Authorization: `Bearer ${TAKESHAPE_API_KEY}`,
            },
            body: JSON.stringify({
                query: `
                      query AllPosts {
                          allPosts: getPostList {
                              items {
                              _id
                              title
                              slug
                              }
                          }
                      }
    `,
            }),
        }
    );
    const response = await data.json();
    const posts = await JSON.stringify(response.data.allPosts.items);

    res.writeHead(200, {
        "Content-Type": "application/json",
    });

    res.end(posts)
}
Enter fullscreen mode Exit fullscreen mode

In the above code, you first import the node-fetch package.

const fetch = require("node-fetch");
Enter fullscreen mode Exit fullscreen mode

Then inside the get function, you extract the environment variables from process.env.

const { TAKESHAPE_API_KEY, TAKESHAPE_PROJECT } = process.env;
Enter fullscreen mode Exit fullscreen mode

Now, you make the POST request to TakeShape using the fetch method.

const data = await fetch(
    `https://api.takeshape.io/project/${TAKESHAPE_PROJECT}/v3/graphql`,
    {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${TAKESHAPE_API_KEY}`,
        },
        body: JSON.stringify({
            query: `
                      query AllPosts {
                          allPosts: getPostList {
                              items {
                              _id
                              title
                              slug
                              }
                          }
                      }
    `,
        }),
    }
);
Enter fullscreen mode Exit fullscreen mode

You pass the TakeShape API key under Authorization in headers. The GraphQL query inside the body is the same as discussed above in the API Explorer.

Finally, you return the posts in the response using res.end(posts).

const response = await data.json();
const posts = await JSON.stringify(response.data.allPosts.items);

res.writeHead(200, {
    "Content-Type": "application/json",
});

res.end(posts);
Enter fullscreen mode Exit fullscreen mode

In Sapper, Page component have a optional preload function that runs before the component is created. As the name suggests this function, preloads the data that the page depends upon.

Preload is the Sapper equivalent to getInitialProps in Next.js or asyncData in Nuxt.js. You can read more about Preloading here.

Open src/routes/blog/index.svelte file in your code editor. Since index.json route is inside blog directory, it can also be referenced as blog.json.

You fetch data from blog.json route using this.fetch. This method is quite similar to fetch API with some additional features like requesting data based on user's session. You can read more about this.fetch here.

<script context="module">
export function preload() {
    return this.fetch(`blog.json`)
        .then((r) => r.json()).then((posts) => {
            return {posts};
        });
}
</script>
Enter fullscreen mode Exit fullscreen mode

In Svelte, you can iterate over any array or array like value using an #each block as shown here. Here (post._id) is the key that uniquely identifies each post. You can read more about #each block here.

<ul>
 {#each posts as post (post._id)}
     <li><a rel="prefetch" href="blog/{post.slug}">{post.title}</a></li>
 {/each}
</ul>
Enter fullscreen mode Exit fullscreen mode

You don't need to make any other change in index.svelte file except for adding a key in the #each block like shown above.

Navigate to http://localhost:3000/blog in your browser; you will notice that posts have been updated.

Alt Text

You can now delete the _posts.js file in the routes/blog directory.

Since the individual post routes don't exist yet so these links will result in a 404 error, you will create them in the next section.

How to Create Dynamic Routes for Posts

In Sapper, you can create dynamic routes by adding brackets to a page name, ([param]), where the param is the dynamic parameter that is the slug of the article.

You will notice that a file named [slug].svelte already exists in the src/routes/blog directory.

You need to update the server route used in this file so when a user clicks on a post, the data corresponding to that post is fetched and is displayed with the blog/[slug] route.

Update blog/[slug].json.js file like this.

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

export async function get(req, res, next) {
    const {slug} = req.params;

    const {TAKESHAPE_API_KEY, TAKESHAPE_PROJECT} = process.env;
    const data = await fetch(
        `https://api.takeshape.io/project/${TAKESHAPE_PROJECT}/v3/graphql`,
        {
            method: "post",
            headers: {
                "Content-Type": "application/json",
                Authorization: `Bearer ${TAKESHAPE_API_KEY}`,
            },
            body: JSON.stringify({
                query: `
          query PostBySlug($slug: String) {
            post: getPostList(where: {slug: {eq: $slug}}) {
            items {
              _id
              title
              deck
              bodyHtml
            }
            }
          }`,
                variables: {
                    slug: slug,
                },
            }),
        }
    );

    const response = await data.json();
    const post = JSON.stringify(response.data.post.items[0]);

    res.writeHead(200, {
        "Content-Type": "application/json",
    });

    res.end(post);
}
Enter fullscreen mode Exit fullscreen mode

The above code is quite similar to the server route code discussed in the last section, with few key differences.

This route fetches individual post data based on the slug provided, which is accessed via req.params.

  const { slug } = req.params;
Enter fullscreen mode Exit fullscreen mode

The GraphQL query in the above code fetches post matching the slug using where: { slug: { eq: $slug } }. In the query, bodyHtml corresponds to the HTML body of the post and deck is the short excerpt of the post.

query PostBySlug($slug: String) {
  post: getPostList(where: { slug: { eq: $slug } }) {
    items {
      _id
      title
      deck
      bodyHtml
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The slug is made available to the GraphQL query via variables.

variables: {
  slug: slug,
},
Enter fullscreen mode Exit fullscreen mode

Update blog/[slug].svelte file like this.

<script context="module">
export async function preload({params}) {
    const res = await this.fetch(`blog/${params.slug}.json`);
    const data = await res.json();

    if (res.status === 200) {
        return {post: data};
    } else {
        this.error(res.status, data.message);
    }
}
</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>{post.title}</title>
    <meta name="Description" content={post.deck}>
</svelte:head>

<h1>{post.title}</h1>

<div class="content">
    {@html post.bodyHtml}
</div>
Enter fullscreen mode Exit fullscreen mode

The preload function takes two arguments, page and session. Here page is an object equivalent to { host, path, params, query } and session is used to pass data such as environment variables from the server.

In the above preload function, you access the page object's params property and pass the slug of the page to the server route.

If you console.log() the page object, you will see all the data available via the page object. Here is how this will look like.

{
  host: 'localhost:3000',
  path: '/blog/jump-aboard-new-treasure-island-edition',
  query: {},
  params: { slug: 'jump-aboard-new-treasure-island-edition' }
}
Enter fullscreen mode Exit fullscreen mode

The post is returned based on the status code of the response. this.error is a method in Sapper for handling errors and invalid routes. You can read more about it here.

    if (res.status === 200) {
            return { post: data };
        } else {
            this.error(res.status, data.message);
        }
Enter fullscreen mode Exit fullscreen mode

You only need to update post.body to post.bodyHtml in the div with class="content" in [slug].svelte file like.

<div class="content">
    {@html post.bodyHtml}
</div>
Enter fullscreen mode Exit fullscreen mode

In Svelte, you can render HTML directly into a component using the @html tag like shown above. You can read more about this tag here.

And its done.

Try clicking on any of the posts on /blog route or head over to http://localhost:3000/blog/jump-aboard-new-treasure-island-edition. You will see a page similar to this.

Alt Text

You can view the finished website here and the code for the project here.

Conclusion

In this article, you learned how to use TakeShape with Sapper, an application framework powered by Svelte. We saw how simple it is to integrate TakeShape with Sapper.

We also discussed how to use API Explorer in TakeShape and how to use the preload function in Sapper.

With just a few simple modifications and updates in your Sapper project, you can easily achieve a perfect Lighthouse score. Amazing, Right!

Alt Text

Here are some additional resources that can be helpful.

Happy coding!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .