How to Create a Blog with SvelteKit and Strapi

What is a CMS?

A CMS, or a Content Management System, 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.

Goals for today

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

You'll learn the basics of SvelteKit, including:

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

You'll also learn how to use Strapi to manage your content.

Prerequisites

  • NodeJS and NPM installed on your machine.
  • Javascript and Svelte knowledge. (Svelte has a great tutorial, so go check it out!)
  • 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-project cms --quickstart

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

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

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.

Setting up roles and permissions

If you try querying the Content API for the posts content type, i.e., at http://localhost:5000/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

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

Feel free to use [yarn](https://yarnpkg.org) instead of [npm](https://npmjs.com)

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

Install TailwindCSS

Run the below command to add TailwindCSS to our project.

npx svelte-add tailwindcss

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.postcss. 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>

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.postcss. 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.postcss in it.

<script lang="ts">
        import '../app.postcss';
    </script>

    <slot />

The <slot /> 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.

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/posts');
        const data = await res.json();

        return { body: data };
    }

Ignore the typings if you're using javascript.

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';

        export const load: Load = async ({ fetch }) => {
            const res = await fetch('/posts');
            const data = await res.json();

            return { props: { posts: data } };
        };
    </script>

    <script lang="ts">
        export let posts: any;
    </script>

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.

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

        export const load: Load = async ({ fetch }) => {
            const res = await fetch('/posts');
            const data = await res.json();

            return { props: { posts: data } };
        };
    </script>

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

        export let posts: Post[];
    </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.title}</h4>
                <p class="mt-2 text-gray-800">{post.description}</p>
                <p class="text-gray-500">By: {post.author.username}</p>
            </div>
        {/each}
    </div>

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](https://kit.svelte.dev/docs#modules-$lib) alias.

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

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/[slug].svelte and add the below code to it.

<!-- 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';

        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.username}</p>

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

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

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.

Click Save when done

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

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

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/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>

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:

// src/lib/user.ts

    import { writable } from 'svelte/store';
    import type { User } from './types';

    const user = writable<User | null>(null);

    export default user;

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.postcss';
        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/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}

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.

npx strapi generate:controller Auth

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

"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");
      },
    };

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 /api/auth/config/routes.json.

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

Now, if we access /auth/me, we get 403 FORBIDDEN. 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.

<!-- 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>

Add the Navbar to __layout.svelte

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

    <Navbar />
    <slot />

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/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.title,
                            content: data.content,
                            description: data.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/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/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>

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://strapi.io/documentation/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 this

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/[slug].svelte) and change it to this:

if (!localStorage.getItem('token')) {
      goto('/login');
      return;
    }

    const res = await fetch('http://localhost:1337/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:

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

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

    /**
     * Read the documentation (https://strapi.io/documentation/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.

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.

Much better right?

Conclusion

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.

28