DEV Community

Cover image for How to Create a Blog with SvelteKit and Strapi
Shada for Strapi

Posted on • Originally published at strapi.io

How to Create a Blog with SvelteKit and Strapi

SvelteKit is a relatively new SSR framework for SvelteJS. We can compare it to NextJS, but instead of using React, it uses Svelte. In this tutorial, you'll learn how you can create a blog website with SvelteKit and Strapi as a CMS.

What is a CMS?

A Content Management System (CMS) is a popular tool for creating web pages, blogs, and online stores. They store your website's data, images, pictures, and other hosted content. They are popular among bloggers because anyone can spin up a blog pretty quickly.

Strapi is a headless CMS coded in Javascript. A headless CMS has no frontend, only an admin panel, so it is ideal for developers. In other words, a Headless CMS is an API that serves up your content to be consumed by a frontend.

Goals

In this tutorial, you'll code a blog website using the SvelteKit framework. You'll use Strapi for content management.

You'll learn how to use Strapi to manage your content and the basics of SvelteKit, including:

  • Filesystem based routing
  • Preloading content
  • Dynamic routing
  • 404 Error handling
  • SvelteKit Layouts
  • And much more

Prerequisites

  • NodeJS and NPM installed on your machine.
  • Javascript and Svelte knowledge. (Svelte has a great tutorial)
  • Some coffee and articles to write on your new blog!

Creating a Strapi Project

Now that you know what a CMS and Strapi are, you can get started by locally hosting Strapi, or in other words, Creating a Strapi Project.

Run this command in an empty folder to create a Strapi project.

     npx create-strapi-app cms --quickstart
Enter fullscreen mode Exit fullscreen mode

You can replace the word cms with anything you like. This command will be the name of the folder your new Strapi project will sit in.

Now sit back, relax, and wait for the project to create itself. After that, you should automatically be taken to localhost:1377.

If that's not the case, cd into the newly created folder and run this command.

     npm run strapi develop
Enter fullscreen mode Exit fullscreen mode

This command will start Strapi up on port 1337. If a page is not automatically opened up for you, open localhost:1377 to launch the Strapi Admin Page.

It should present you with this page. You need to sign up for an account here. Remember that this account is locally-scoped to this project, meaning it won't work on other Strapi projects.

Creating Content Types

In the sidebar of Strapi admin http://localhost:1377, click the Content-Types Builder button. This button should take you to the Content Types builder page.

What are Content Types?

We can compare a content type to a table in SQL. Thus, content types help us structure and categorize our data.

Posts

Let's create a content type for posts. Then, click the Create new Collection Type button to launch a dialogue.

Enter Post as the name. Go to Advanced Settings at the top and disable the Drafts system. Click Continue

A sample screenshot

Add these fields. Refer to the image if you get stuck.

  • A title field with type Text.
  • A description field with type Text. Make sure the text is Long text.
  • A content field with the Rich Text type.

Let's also add a relation between Post and User from the users-permissions plugin. This relationship allows us to easily link a post to a user to display relevant information like the Author's name and profile picture, fetch the Author's posts, etc.

Add a new Relation field to the Post content type. The column on the left should be PostPost, and the column on the right should be User from users-permissions. Select the fourth relation; the User has many Posts and clicks Finish.

Refer to the below image if you get stuck:

Click Save to save your changes and restart the server.

Adding Slug to the Post Collection Type

Next, we will be adding a dynamic autogenerated slug system to the Post collection type. Using a slug will make it easier to query for our posts in the frontend.

First, navigate to Content-Types Builder and click Post, then click Add another field. Select UID. Set the field slug, and set the attached field to title. Save your changes and click Configure the view, and select the slug field.

A sample screenshot

Then, set the Editable field off in the window that appears. Click Save to register your changes.

With the slug system fully set up, a new slug will be automatically generated in an uneditable slug field anytime you create a new product. The Slug will be created from the product name.

Setting up Roles and Permissions

If you try querying the Content API for the posts content type, i.e., at http://localhost:1377/api/posts, you'll get a 403 FORBIDDEN error.

This error is because, by default, Strapi doesn't allow any man down the street to access your content. You're supposed to set rules and permissions for specific roles. Let's allow the Public role, i.e., an unauthenticated user, to read our posts.

In the sidebar, click on the Settings button. There, click on Roles in the Users & Permissions section. But, first, let's edit the permissions for the Public role.
We'll allow the Public role to count, find and findOne for Posts.

Next, let's do the same for the Authenticated role, but we'll also allow them to create, update and delete posts as well.

Creating a SvelteKit Project

Now for the main SvelteKit code. Create a new folder named frontend in the same directory as the folder for the Strapi project and cd into it.

Now, let's create a SvelteKit project with this command:

    npm init svelte@next
Enter fullscreen mode Exit fullscreen mode

Add your preferred project name, and be sure to choose the options as shown in the below image.

Launch the app inside your favourite editor, for example, VSCode. Now, we can start the app with the below two commands:

     # To install packages
     npm I 

     # To start the app
     npm run dev
Enter fullscreen mode Exit fullscreen mode

Here's how your app should look, hosted on localhost:3000

A sample screenshot

Install TailwindCSS

TailwindCSS is a straightforward way to style your apps, and it's clear to add them to SvelteKit.

We'll use SvelteAdd to add TailwindCSS to our application quickly.
Run the below command to add TailwindCSS to our project.

     npx svelte-add tailwindcss
Enter fullscreen mode Exit fullscreen mode

Install the required dependencies.

    npm i 
Enter fullscreen mode Exit fullscreen mode

Be sure to stop the running server with Ctrl+C first.
Let's start our project from scratch.

Delete all the extra CSS, except the @tailwind parts in src/app.css. Delete all of the items in src/routes and src/lib, and now we should be left with an empty project.

Create a new file src/routes/index.svelte. All files in the src/routes folder will be mapped to actual routes. For example, src/routes/example.svelte will be accessible at /example, and src/routes/blog/test.svelte will be accessible at /blog/test. index.svelte is a special file. It maps to the base directory. src/routes/index.svelte maps to /, and src/routes/blog/index.svelte maps to /blog.

This is how filesystem based routing works in SvelteKit. Later, you'll learn to add routes with dynamic names.

For now, let's work on the basic UI. Add the following code to index.svelte

        <script lang="ts">
        </script>

        <div class="my-4">
            <h1 class="text-center text-3xl font-bold">My wonderful blog</h1>
        </div>
Enter fullscreen mode Exit fullscreen mode

I've elected to use Typescript, which is just like Javascript, but with types. You can follow along with javascript, but types and interfaces won't work for you. Also, you shouldn't put lang="ts" in your script tag.`

Now you'll notice that TailwindCSS is no longer working. This is because we deleted __layout.svelte, the file importing src/app.css. Let's now learn what this __layout.svelte file is.

__layout.svelte is a special file that adds a layout to every page. __layout.sveltes can not only exist at the top level routes folder, but can also exist in subdirectories, applying layouts for that subdirectory.

Read more about SvelteKit layouts here.

For now, all we have to do, is create src/routes/__layout.svelte and import src/app.css in it. The element will be the actual content displayed on the page.

Now here, in this __layout.svelte file, we can add whatever content we want, and it'll be displayed on all pages. So, add your Navbars, Headers, Footers, and everything else here.

Here is what your app should look like now at localhost:3000

A sample screenshot

Fetch Blog Posts

Now, we can fetch blog posts from Strapi and display them in index.svelte. We'll utilize SvelteKit Endpoints to make API fetching easier. Endpoints in SvelteKit are files ending with .js (or .ts for typescript) that export functions corresponding to HTTP methods. These endpoint files become API routes in our application.

Let's create an endpoint src/routes/posts.ts (use the .js extension if you're not using typescript)

`

// src/routes/posts.ts
import type { EndpointOutput } from '@sveltejs/kit';
export async function get(): Promise<EndpointOutput> {
    const res = await fetch('http://localhost:1337/api/posts?populate=*');
    const data = await res.json();
    return { body: data };
}
#Ignore the typings if you're using javascript.
Enter fullscreen mode Exit fullscreen mode

`

Now, when we visit http://localhost:3000/posts, we'll receive the posts from Strapi. Let's implement this route in our index.svelte file using SvelteKit's Loading functionality.
Loading allows us to fetch APIs before the page is loaded using a particular <script context=" module"> tag.

Add this to the top of src/routes/index.svelte.

`

<script lang="ts" context="module">
    import type { Load } from '@sveltejs/kit';
    import { goto } from '$app/navigation';
    export const load: Load = async ({ fetch }) => {
        const res = await fetch('/posts');
        const response = await res.json();
        return { props: { posts: response.data } };
    };
</script>
<script lang="ts">
    export let posts: any;
</script>
Enter fullscreen mode Exit fullscreen mode

`

You can see that the load function takes in the fetch function provided to us by SvelteKit and returns an object containing props. These props are passed down to our components.

`

<script lang="ts" context="module">
    import type { Load } from '@sveltejs/kit';
    import { goto } from '$app/navigation';
    export const load: Load = async ({ fetch }) => {
        const res = await fetch('/posts');
        const response = await res.json();
        return { props: { posts: response.data } };
    };
</script>
<script lang="ts">
    export let posts: any;
</script>
<div class="my-4">
    <h1 class="text-center text-3xl font-bold">My wonderful blog</h1>
</div>
<div class="container mx-auto mt-4">
    {#each posts as post}
        <div
            class="hover:bg-gray-200 cursor-pointer px-6 py-2 border-b border-gray-500"
            on:click={() => goto('/blog/' + post.id)}
        >
            <h4 class="font-bold">{post.attributes.title}</h4>
            <p class="mt-2 text-gray-800">{post.attributes.description}</p>
            <p class="text-gray-500">By: {post.attributes.author.data.attributes.username}</p>
        </div>
    {/each}
</div>
Enter fullscreen mode Exit fullscreen mode

`

I've added a few typings in src/lib/types.ts. You can check it out in the Source Code. SvelteKit allows us to access any file in src/lib using the $lib alias.

I added a test user and a test post in Strapi, and this is how my app looks.

A sample screenshot

Posts Page

Now, let's add a route that'll allow us to view a post. Now, you'll learn about Dynamic Routes in SvelteKit.

If we enclose a string in brackets ([]) in a filename of a route, that becomes a parameter. So, for example, if I have a route called src/routes/blog/[post].svelte, the route maps to /blog/ANY_STRING where ANY_STRING will be the value of the post parameter. Let's use this to query posts with Strapi.

We can use the load function we talked about earlier to get the parameters. Create a file called src/routes/blog/[id].svelte and add the below code to it.

`

<!-- src/routes/blog/[id].svelte -->
<script lang="ts" context="module">
    import type { Load } from '@sveltejs/kit';
    export const load: Load = async ({ params, fetch }) => {

        // Now, we'll fetch the blog post from Strapi
        const res = await fetch(`http://localhost:1337/api/posts/${params.id}?populate=*`);
        // A 404 status means "NOT FOUND"
        if (res.status === 404) {
            // We can create a custom error and return it.
            // SvelteKit will automatically show us an error page that we'll learn to customise later on.
            const error = new Error(`The post with ID ${id} was not found`);
            return { status: 404, error };
        } else {
            const response = await res.json();
            return { props: { post: response.data.attributes } };
        }
    };
</script>
<script lang="ts">
    import type { Post } from '$lib/types';
    import { onMount } from 'svelte';
    export let post: Post;
    let content = post.content;
    onMount(async () => {
        // Install the marked package first!
        // Run this command: npm i marked
        // We're using this style of importing because "marked" uses require, which won't work when we import it with SvelteKit.
        // Check the "How do I use a client-side only library" in the FAQ: https://kit.svelte.dev/faq
        const marked = (await import('marked')).default;
        content = marked(post.content);
    });
</script>
<h1 class="text-center text-4xl mt-4">{post.title}</h1>
<p class="text-center mt-2">By: {post.author.data.attributes.username}</p>
<div class="border border-gray-500 my-4 mx-8 p-6 rounded">
    {@html content}
</div>
Enter fullscreen mode Exit fullscreen mode

`

We need to use the @html directive when we want the content to be actually rendered as HTML.

When you go to a blog page, you should see something like this:

A sample screenshot

Authentication and Authorization

Let's get started with authenticating users to our blog. Strapi allows us to configure third-party providers like Google, but we'll stick to the good ol' email and password sign-in.

We don't want anybody to register to our blog, so we'll manually create a user with Strapi. Then, in the Strapi admin panel, click on the Users collection type in the sidebar.

There, click Add new Users and create your user. Here's mine, for example.

“Create an Entry”  page

Click Save when done

We can test logging in to our user by sending a POST request to http://localhost:1337/auth/local. Follow the image below for the correct JSON body.

A sample screenshot

The REST client I'm using in the above image is Postman.

The Login Route

Let's create a new route src/routes/login.svelte. This will of course map to /login.

`

<script lang="ts">
    import type { User } from '$lib/types';
    import { goto } from '$app/navigation';
    import user from '$lib/user';
    let email = '';
    let password = '';
    async function login() {
        const res = await fetch('http://localhost:1337/api/auth/local', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
            body: JSON.stringify({ identifier: email, password })
        });
        if (res.ok) {
            const data: {user: User, jwt: string} = await res.json();
            localStorage.setItem("token", data.jwt)
            if (data) {
                $user = data.user;
                goto('/');
            }
        } else {
            const data: { message: { messages: { message: string }[] }[] } = await res.json();
            if (data?.message?.[0]?.messages?.[0]?.message) {
                alert(data.message[0].messages[0].message);
            }
        }
    }
</script>
<form on:submit|preventDefault={login} class="container mx-auto my-4">
    <h1 class="text-center text-2xl font-bold">Login</h1>
    <div class="my-1">
        <label for="email">Email</label>
        <input type="email" placeholder="Enter your email" bind:value={email} />
    </div>
    <div class="my-1">
        <label for="password">Password</label>
        <input type="password" placeholder="Enter your password" bind:value={password} />
    </div>
    <div class="my-3">
        <button class="submit" type="submit">Login</button>
    </div>
</form>
<style lang="postcss">
    label {
        @apply font-bold block mb-1;
    }
    input {
        @apply bg-white w-full border border-gray-500 rounded outline-none py-2 px-4;
    }
    .submit {
        @apply bg-blue-500 text-white border-transparent rounded px-4 py-2;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

`

I've created a store in src/lib/user.ts that will house the User to access the User in any component.

Here's the code:

`

import { writable } from 'svelte/store';
import type { User } from './types';
const user = writable<User | null>(null);
export default user;
Enter fullscreen mode Exit fullscreen mode

`

Persisting Auth State

Great! Our /login page works flawlessly, but there's one problem - When we refresh the page, the user store gets reset to null. To fix this, we need to re-fetch the User every time the page reloads. That's right, we need a load function in __layout.svelte since it is present on every page.

Change __layout.svelte to this code:

`

<!-- src/routes/__layout.svelte -->
<script lang="ts">
    import '../app.css';
    import userStore from '$lib/user';
    import type { User } from '$lib/types';
    import { onMount } from 'svelte';
    let loading = true;
    onMount(async () => {
        // Check if 'token' exists in localStorage
        if (!localStorage.getItem('token')) {
            loading = false;
            return { props: { user: null } };
        }
        // Fetch the user from strapi
        const res = await fetch('http://localhost:1337/api/auth/me', {
            headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
        });
        const user: User = await res.json();
        loading = false;
        if (res.ok) {
            $userStore = user;
        }
    });
</script>
{#if !loading}
    <slot />
{/if}
Enter fullscreen mode Exit fullscreen mode

`

Wait a minute! Why are we using onMount instead of load? Since load is executed on the server, we won't have access to localStorage, which is on the browser. Hence, we have to wait for the app to load before accessing localStorage.

If you visit your app, you'll get a 404 Error when trying to get the User from Strapi. This error is because /auth/me isn't a valid route. So let's create it ourselves.

Open the Strapi project in your favorite editor. Strapi allows us to add custom API routes to it. Let's use the strapi CLI to generate a route.

Run the following command to start the CLI.

bash
npx strapi generate

Select controller and add controller name Auth. Also, click to add the new controller to a new API.

You'll find a new file called /src/api/auth/controllers/Auth.js. We need to add our simple controller here.

Update the /src/api/auth/controllers/Auth.js file code with the following code snippet.

`

    "use strict";

    /**
     * A set of functions called "actions" for `auth`
     */

    module.exports = {
      async me(ctx) {
        if (ctx.state.user) {
          return ctx.state.user;
        }

        ctx.unauthorized("You're not logged in");
      },
    };
Enter fullscreen mode Exit fullscreen mode

`

This simple API route will return the User if it exists or give us a 401 UNAUTHORIZED error.
Now, we need to tell Strapi to register this controller at /auth/me.

To do that, create file /src/api/auth/config/routes.json, and add the following code snippet to the file.

`json

{
    "routes": [
        {
            "method": "GET",
            "path": "/auth/me",
            "handler": "Auth.me",
            "config": {
                "policies": []
            }
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

`

Now, if we access /auth/me, we get 404 NotFoundError.

Like the post routes, Strapi doesn't, by default, allow anyone to access this route either. So, let's edit permissions like how we did earlier for the Authenticated role.

And now, everything should work flawlessly.

Navbar

Let's add a quick navbar to our app. Create file src/lib/Navbar.svelte and put the below code in it.

`js

<!-- src/lib/Navbar.svelte -->
<script lang="ts">
    import user from './user';
</script>
<nav class="bg-white border-b border-gray-500 py-2 px-4 w-full">
    <div class="flex items-center justify-between container mx-auto">
        <a href="/" class="font-bold no-underline">My blog</a>
        <section>
            {#if !$user}
                <a href="/login" class="font-mono no-underline">Login</a>
            {:else}
                <a href="/new" class="font-mono no-underline mr-3">New</a>
                <span class="font-mono text-gray-500">{$user.username}</span>
            {/if}
        </section>
    </div>
</nav>
Enter fullscreen mode Exit fullscreen mode

`
Add the Navbar to __layout.svelte.

`

    <!-- src/routes/__layout.svelte -->
    <script lang="ts">
      // ...
      import Navbar from "$lib/Navbar.svelte";
    </script>

    <Navbar />
    <slot />
Enter fullscreen mode Exit fullscreen mode

`

Create and Update Posts

Now, let's get to the juicy part. Add a file called src/routes/new.svelte. This file will contain the form used to create a new post on Strapi.

`

<!-- src/routes/new.svelte -->
<script lang="ts" context="module">
    import type { Load } from '@sveltejs/kit';
    import type { Post } from '$lib/types';
    export const load: Load = async ({ fetch, page: { query } }) => {
        // edit will be an optional query string parameter that'll contain the ID of the post that needs to be updated.
        // If this is set, the post will be updated instead of being created.
        const edit = query.get('edit');
        if (edit) {
            const res = await fetch('http://localhost:1337/api/posts/' + edit);
            if (res.status === 404) {
                const error = new Error(`The post with ID ${edit} was not found`);
                return { status: 404, error };
            } else {
                const data: Post = await res.json();
                return {
                    props: {
                        editId: edit,
                        title: data.attributes.title,
                        content: data.attributes.content,
                        description: data.attributes.description
                    }
                };
            }
        }
        return { props: {} };
    };
</script>
<script lang="ts">
    import { onMount } from 'svelte';
    import user from '$lib/user';
    import { goto } from '$app/navigation';
    export let editId: string;
    export let title = '';
    export let description = '';
    export let content = '';
    onMount(() => {
        if (!$user) goto('/login');
    });
    // To edit the post
    async function editPost() {
        if (!localStorage.getItem('token')) {
            goto('/login');
            return;
        }
        const res = await fetch('http://localhost:1337/api/posts/' + editId, {
            method: 'PUT',
            headers: {
                'Content-Type': 'application/json',
                Accept: 'application/json',
                Authorization: 'Bearer ' + localStorage.getItem('token')
            },
            body: JSON.stringify({ title, description, content })
        });
        if (!res.ok) {
            const data: { message: { messages: { message: string }[] }[] } = await res.json();
            if (data?.message?.[0]?.messages?.[0]?.message) {
                alert(data.message[0].messages[0].message);
            }
        } else {
            const data: Post = await res.json();
            goto('/blog/' + data.id);
        }
    }
    async function createPost() {
        if (!localStorage.getItem('token')) {
            goto('/login');
            return;
        }
        if (editId) {
            // We're supposed to edit, not create
            editPost();
            return;
        }
        const res = await fetch('http://localhost:1337/api/posts', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                Accept: 'application/json',
                Authorization: 'Bearer ' + localStorage.getItem('token')
            },
            body: JSON.stringify({ title, description, content })
        });
        if (!res.ok) {
            const data: { message: { messages: { message: string }[] }[] } = await res.json();
            if (data?.message?.[0]?.messages?.[0]?.message) {
                alert(data.message[0].messages[0].message);
            }
        } else {
            const data: Post = await res.json();
            goto('/blog/' + data.id);
        }
    }
</script>
<form on:submit|preventDefault={createPost} class="my-4 mx-auto container p-4">
    <div class="my-1">
        <label for="title">Title</label>
        <input type="text" placeholder="Enter title" id="title" bind:value={title} />
    </div>
    <div class="my-1">
        <label for="description">Description</label>
        <input type="text" placeholder="Enter description" id="description" bind:value={description} />
    </div>
    <div class="my-1">
        <label for="title">Content</label>
        <textarea rows={5} placeholder="Enter content" id="content" bind:value={content} />
    </div>
    <div class="my-2">
        <button class="submit" type="submit">Submit</button>
    </div>
</form>
<style lang="postcss">
    label {
        @apply font-bold block mb-1;
    }
    input {
        @apply bg-white w-full border border-gray-500 rounded outline-none py-2 px-4;
    }
    textarea {
        @apply bg-white w-full border border-gray-500 rounded outline-none py-2 px-4 resize-y;
    }
    .submit {
        @apply bg-blue-500 text-white border-transparent rounded px-4 py-2;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

`

Don't try this out yet, since there's currently no way to determine the Author of the PostPost. We need to code that in Strapi explicitly.

Let's create custom controllers for the Post content type. Here, we'll make it so that the Author of a post will be the currently logged-in User.

Edit api/post/controllers/post.js in the Strapi project.

`

'use strict';
/**
 *  post controller
 */
const { createCoreController } = require('@strapi/strapi').factories;
const { parseMultipartData, sanitizeEntity } = require("strapi-utils");
/**
 * Read the documentation (https://docs.strapi.io/developer-docs/latest/development/backend-customization.html#core-controllers)
 * to customize this controller
 */
module.exports = {
    async create(ctx) {
        let entity;
        if (ctx.is("multipart")) {
            const { data, files } = parseMultipartData(ctx);
            data.author = ctx.state.user.id;
            entity = await strapi.services.post.create(data, { files });
        } else {
            ctx.request.body.author = ctx.state.user.id;
            entity = await strapi.services.post.create(ctx.request.body);
        }
        return sanitizeEntity(entity, { model: strapi.models.post });
    },
    async update(ctx) {
        const { id } = ctx.params;
        let entity;
        const [article] = await strapi.services.post.find({
            id: ctx.params.id,
            "author.id": ctx.state.user.id,
        });
        if (!article) {
            return ctx.unauthorized(`You can't update this entry`);
        }
        if (ctx.is("multipart")) {
            const { data, files } = parseMultipartData(ctx);
            entity = await strapi.services.post.update({ id }, data, {
                files,
            });
        } else {
            entity = await strapi.services.post.update({ id }, ctx.request.body);
        }
        return sanitizeEntity(entity, { model: strapi.models.post });
    },
    async delete(ctx) {
        const { id } = ctx.params;
        let entity;
        const [article] = await strapi.services.post.find({
            id: ctx.params.id,
            "author.id": ctx.state.user.id,
        });
        if (!article) {
            return ctx.unauthorized(`You can't delete this entry`);
        }
        await strapi.services.post.delete({ id });
        return { ok: true };
    },
};
module.exports = createCoreController('api::post.post');
Enter fullscreen mode Exit fullscreen mode

`

If you get confused, checkout the Strapi Documentation

Install strapi-utils with the following command.
bash
npm install strapi-utils

And now, you should be able to create and update posts all from one route. Let's make the update process easier. Change src/routes/blog/[id].svelte to the code below:

`js
<!-- src/routes/blog/[id].svelte -->

import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ params, fetch }) => {

        // Now, we'll fetch the blog post from Strapi
        const res = await fetch(`http://localhost:1337/api/posts/${params.id}?populate=*`);
        // A 404 status means "NOT FOUND"
        if (res.status === 404) {
            // We can create a custom error and return it.
            // SvelteKit will automatically show us an error page that we'll learn to customise later on.
            const error = new Error(`The post with ID ${params.id} was not found`);
            return { status: 404, error };
        } else {
            const response = await res.json();
            return { props: { post: response.data.attributes } };
        }
    };
</script>
<script lang="ts">
        import type { Post } from '$lib/types';
        import { onMount } from 'svelte';
        import { goto } from '$app/navigation';
        import user from '$lib/user';
    export let post: Post;
    let content = post.content;
    onMount(async () => {
        // Install the marked package first!
        // Run this command: npm i marked
        // We're using this style of importing because "marked" uses require, which won't work when we import it with SvelteKit.
        // Check the "How do I use a client-side only library" in the FAQ: https://kit.svelte.dev/faq
        const marked = (await import('marked')).default;
        content = marked(post.content);
    });
    async function deletePost() {
            // TODO
        }
</script>
<h1 class="text-center text-4xl mt-4">{post.title}</h1>
<p class="text-center mt-2">By: {post.author.data.attributes.username}</p>
    {#if $user && post.author.id === $user.id}
        <p class="my-2 flex justify-center items-center gap-3">
            <button
                class="bg-blue-500 text-white font-bold py-2 px-4 rounded border-transparent"
                on:click={() => goto('/new?edit=' + post.id)}>Update post</button
            >
            <button
                class="bg-red-500 text-white font-bold py-2 px-4 rounded border-transparent"
                on:click={deletePost}>Delete post</button
            >
        </p>
    {/if}

    <div class="border border-gray-500 my-4 mx-8 p-6 rounded">
        {@html content}
    </div>

`

Now, when the Author visits their PostPost, they'll see two buttons to Update and Delete the PostPost, respectively.

Don't try this out yet, since there's currently no way to determine the Author of the PostPost. We need to code that in Strapi explicitly.
Let's create custom controllers for the Post content type. Here, we'll make it so that the Author of a post will be the currently logged-in User.
Edit api/post/controllers/post.js in the Strapi project.

`

    "use strict";

    const { parseMultipartData, sanitizeEntity } = require("strapi-utils");

    /**
     * Read the documentation (https://docs.strapi.io/developer-docs/latest/development/backend-customization.html#core-controllers)
     * to customize this controller
     */

    module.exports = {
      async create(ctx) {
        let entity;

        if (ctx.is("multipart")) {
          const { data, files } = parseMultipartData(ctx);
          data.author = ctx.state.user.id;
          entity = await strapi.services.post.create(data, { files });
        } else {
          ctx.request.body.author = ctx.state.user.id;
          entity = await strapi.services.post.create(ctx.request.body);
        }

        return sanitizeEntity(entity, { model: strapi.models.post });
      },

      async update(ctx) {
        const { id } = ctx.params;

        let entity;

        const [article] = await strapi.services.post.find({
          id: ctx.params.id,
          "author.id": ctx.state.user.id,
        });

        if (!article) {
          return ctx.unauthorized(`You can't update this entry`);
        }

        if (ctx.is("multipart")) {
          const { data, files } = parseMultipartData(ctx);
          entity = await strapi.services.post.update({ id }, data, {
            files,
          });
        } else {
          entity = await strapi.services.post.update({ id }, ctx.request.body);
        }

        return sanitizeEntity(entity, { model: strapi.models.post });
      },
    };

`

If you get confused, checkout the Strapi Documentation

And now, you should be able to create and update posts all from one route. Let's make the update process easier. Change src/routes/blog/[slug].svelte to the code below:

`

<!-- src/routes/blog/[slug].svelte -->
    <script lang="ts" context="module">
        import type { Load } from '@sveltejs/kit';

        export const load: Load = async ({ page: { params }, fetch }) => {
            // The params object will contain all of the parameters in the route.
            const { slug } = params;

            // Now, we'll fetch the blog post from Strapi
            const res = await fetch('http://localhost:1337/posts/' + slug);

            // A 404 status means "NOT FOUND"
            if (res.status === 404) {
                // We can create a custom error and return it.
                // SvelteKit will automatically show us an error page that we'll learn to customise later on.
                const error = new Error(`The post with ID ${slug} was not found`);
                return { status: 404, error };
            } else {
                const data = await res.json();
                return { props: { post: data } };
            }
        };
    </script>

    <script lang="ts">
        import type { Post } from '$lib/types';
        import { onMount } from 'svelte';
        import { goto } from '$app/navigation';
        import user from '$lib/user';

        export let post: Post;
        let content = post.content;

        onMount(async () => {
            // Install the marked package first!
            // Run this command: npm i marked

            // We're using this style of importing because "marked" uses require, which won't work when we import it with SvelteKit.
            // Check the "How do I use a client-side only library" in the FAQ: https://kit.svelte.dev/faq
            const marked = (await import('marked')).default;
            content = marked(post.content);
        });

        async function deletePost() {
            // TODO
        }
    </script>

    <h1 class="text-center text-4xl mt-4">{post.title}</h1>
    <p class="text-center mt-2">By: {post.author.username}</p>

    {#if $user && post.author.id === $user.id}
        <p class="my-2 flex justify-center items-center gap-3">
            <button
                class="bg-blue-500 text-white font-bold py-2 px-4 rounded border-transparent"
                on:click={() => goto('/new?edit=' + post.id)}>Update post</button
            >
            <button
                class="bg-red-500 text-white font-bold py-2 px-4 rounded border-transparent"
                on:click={deletePost}>Delete post</button
            >
        </p>
    {/if}

    <div class="border border-gray-500 my-4 mx-8 p-6 rounded">
        {@html content}
    </div>

`

Now, when the Author visits their PostPost, they'll see two buttons to Update and Delete the PostPost, respectively.

Deleting Posts

Let's add functionality to the Delete Post button. Edit the deletePost() function in the file we just modified (src/routes/blog/[id].svelte) and change it to this:

`

    async function deletePost() {
        if (!localStorage.getItem('token')) {
            goto('/login');
            return;
        }
        const res = await fetch('http://localhost:1337/api/posts/' + post.id, {
            method: 'DELETE',
            headers: { Authorization: 'Bearer ' + localStorage.getItem('token') }
        });
        if (res.ok) {
            goto('/');
        } else {
            const data: { message: { messages: { message: string }[] }[] } = await res.json();
            if (data?.message?.[0]?.messages?.[0]?.message) {
                alert(data.message[0].messages[0].message);
            }
        }
    }

`
Now, obviously, we don't want anybody to delete a post by someone else. Let's add another method in api/post/controllers/post.js in our Strapi App.

This is how your code should look now:

`

'use strict';
/**
 *  post controller
 */
const { createCoreController } = require('@strapi/strapi').factories;
const { parseMultipartData, sanitizeEntity } = require("strapi-utils");
/**
 * Read the documentation (https://docs.strapi.io/developer-docs/latest/development/backend-customization.html#core-controllers)
 * to customize this controller
 */
module.exports = {
    async create(ctx) {
        let entity;
        if (ctx.is("multipart")) {
            const { data, files } = parseMultipartData(ctx);
            data.author = ctx.state.user.id;
            entity = await strapi.services.post.create(data, { files });
        } else {
            ctx.request.body.author = ctx.state.user.id;
            entity = await strapi.services.post.create(ctx.request.body);
        }
        return sanitizeEntity(entity, { model: strapi.models.post });
    },
    async update(ctx) {
        const { id } = ctx.params;
        let entity;
        const [article] = await strapi.services.post.find({
            id: ctx.params.id,
            "author.id": ctx.state.user.id,
        });
        if (!article) {
            return ctx.unauthorized(`You can't update this entry`);
        }
        if (ctx.is("multipart")) {
            const { data, files } = parseMultipartData(ctx);
            entity = await strapi.services.post.update({ id }, data, {
                files,
            });
        } else {
            entity = await strapi.services.post.update({ id }, ctx.request.body);
        }
        return sanitizeEntity(entity, { model: strapi.models.post });
    },
    async delete(ctx) {
        const { id } = ctx.params;
        let entity;
        const [article] = await strapi.services.post.find({
            id: ctx.params.id,
            "author.id": ctx.state.user.id,
        });
        if (!article) {
            return ctx.unauthorized(`You can't delete this entry`);
        }
        await strapi.services.post.delete({ id });
        return { ok: true };
    },
};
module.exports = createCoreController('api::post.post');

`

And now, the author should be able to delete posts.

Custom Error Page

You may have noticed that the 404 page looks terrible. It has almost no styling. With SvelteKit, we're allowed to create a custom error page. So we need to name this file __error.svelte and place it in src/routes.

`

<!-- src/routes/__error.svelte -->
<script lang="ts" context="module">
    import type { ErrorLoad } from '@sveltejs/kit';
    export type { ErrorLoad } from '@sveltejs/kit';
    export const load: ErrorLoad = ({ error, status }) => {
        return { props: { error, status } };
    };
</script>
<script lang="ts">
    export let error: Error;
    export let status: number;
</script>
<div class="fixed w-full h-full grid place-items-center">
    <section class="p-8 border-gray-500 rounded">
        <h1 class="text-center text-4xl font-mono-mt-4">{status}</h1>
        <p class="text-center">{error.message}</p>
    </section>
</div>

`

Here's how our error page will look like when you search for a blog post with wrong id.

The Error Page

Much better right?

Now, obviously, we don't want anybody to delete a post by someone else. Let's add another method in
api/post/controllers/post.js in our Strapi App.
This is how your code should look now:

`
// api/post/controllers/post.js
"use strict";

    const { parseMultipartData, sanitizeEntity } = require("strapi-utils");

    /**
     * Read the documentation (https://docs.strapi.io/developer-docs/latest/development/backend-customization.html#core-controllers)
     * to customize this controller
     */

    module.exports = {
      async create(ctx) {
        let entity;

        if (ctx.is("multipart")) {
          const { data, files } = parseMultipartData(ctx);
          data.author = ctx.state.user.id;
          entity = await strapi.services.post.create(data, { files });
        } else {
          ctx.request.body.author = ctx.state.user.id;
          entity = await strapi.services.post.create(ctx.request.body);
        }

        return sanitizeEntity(entity, { model: strapi.models.post });
      },

      async update(ctx) {
        const { id } = ctx.params;

        let entity;

        const [article] = await strapi.services.post.find({
          id: ctx.params.id,
          "author.id": ctx.state.user.id,
        });

        if (!article) {
          return ctx.unauthorized(`You can't update this entry`);
        }

        if (ctx.is("multipart")) {
          const { data, files } = parseMultipartData(ctx);
          entity = await strapi.services.post.update({ id }, data, {
            files,
          });
        } else {
          entity = await strapi.services.post.update({ id }, ctx.request.body);
        }

        return sanitizeEntity(entity, { model: strapi.models.post });
      },

      async delete(ctx) {
        const { id } = ctx.params;

        let entity;

        const [article] = await strapi.services.post.find({
          id: ctx.params.id,
          "author.id": ctx.state.user.id,
        });

        if (!article) {
          return ctx.unauthorized(`You can't delete this entry`);
        }

        await strapi.services.post.delete({ id });

        return { ok: true };
      },
    };

`

And now, the author should be able to delete posts.

Conclusion & Resources

And there you have it! Your blog website is made with SvelteKit and Strapi. If you got stuck anywhere, be sure to check the SvelteKit Docs, the Strapi Docs, and the source code on Github.

Top comments (1)

Collapse
 
andrewbaisden profile image
Andrew Baisden

Good tutorial btw if you add markdown syntax highlighting to the code blocks the readers would find it easier to read 😊