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/
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
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.
How to Generate TakeShape API Keys
If haven't already, create a free developer account on TakeShape.
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
.
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.
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.
In the drop-down menu, click on API Keys.
Click on New API Key.
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.
Copy the API key to a secure location; remember you will only see them once.
**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.
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
In your .env
file, add the environment variables.
# .env
TAKESHAPE_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
TAKESHAPE_PROJECT="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
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
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);
});
In the above code, you have imported the dotenv
package at the top of the server.js
file.
require("dotenv").config();
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.
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.
Here is how this API Explorer will look.
Copy and Paste the following GraphQL query in the left tab.
query AllPosts {
allPosts: getPostList {
items {
_id
title
slug
}
}
}
Run this query; you will see an output similar to this.
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
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)
}
In the above code, you first import the node-fetch
package.
const fetch = require("node-fetch");
Then inside the get
function, you extract the environment variables from process.env
.
const { TAKESHAPE_API_KEY, TAKESHAPE_PROJECT } = process.env;
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
}
}
}
`,
}),
}
);
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);
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>
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>
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.
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);
}
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;
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
}
}
}
The slug
is made available to the GraphQL query via variables
.
variables: {
slug: slug,
},
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>
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' }
}
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);
}
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>
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.
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!
Here are some additional resources that can be helpful.
Happy coding!