How to Create a Blog with SvelteKit and Strapi
Tags: svelte, blog, strapi
Published: 2022-05-10 08:39:59



[SvelteKit](https://kit.svelte.dev/) 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.](https://strapi.io/) 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](https://kit.svelte.dev/). You'll use [Strapi for content management](https://strapi.io/).

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](https://nodejs.org/) and [NPM](https://npmjs.com/) installed on your machine.
- Javascript and [Svelte](https://svelte.dev/) knowledge. (Svelte has [a great tutorial](https://svelte.dev/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.

```bash
     npx create-strapi-app 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.
```bash
     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](http://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.**

![](https://paper-attachments.dropbox.com/s_4057050B8A7592A58E25D8AF215C61A9A9589163DB136EFCE8661C471A2F341E_1644359293544_Screen+Shot+2022-02-08+at+11.28.01+PM.png)


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

![](https://paper-attachments.dropbox.com/s_4057050B8A7592A58E25D8AF215C61A9A9589163DB136EFCE8661C471A2F341E_1644359455862_Screen+Shot+2022-02-08+at+11.30.48+PM.png)



## 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](https://paper-attachments.dropbox.com/s_4057050B8A7592A58E25D8AF215C61A9A9589163DB136EFCE8661C471A2F341E_1644359547760_Screen+Shot+2022-02-08+at+11.32.24+PM.png)


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.


![](https://paper-attachments.dropbox.com/s_4057050B8A7592A58E25D8AF215C61A9A9589163DB136EFCE8661C471A2F341E_1644359854543_Screen+Shot+2022-02-08+at+11.37.28+PM.png)


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:

![](https://paper-attachments.dropbox.com/s_4057050B8A7592A58E25D8AF215C61A9A9589163DB136EFCE8661C471A2F341E_1644359871261_Screen+Shot+2022-02-08+at+11.36.49+PM.png)


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](https://paper-attachments.dropbox.com/s_4057050B8A7592A58E25D8AF215C61A9A9589163DB136EFCE8661C471A2F341E_1644385413170_Screen+Shot+2022-02-09+at+6.42.54+AM.png)


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](https://docs.strapi.io/developer-docs/latest/developer-resources/database-apis-reference/rest-api.html#api-endpoints) 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.

![](https://paper-attachments.dropbox.com/s_4057050B8A7592A58E25D8AF215C61A9A9589163DB136EFCE8661C471A2F341E_1644360051801_Screen+Shot+2022-02-08+at+11.40.38+PM.png)


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

![](https://paper-attachments.dropbox.com/s_4057050B8A7592A58E25D8AF215C61A9A9589163DB136EFCE8661C471A2F341E_1644360101747_Screen+Shot+2022-02-08+at+11.41.35+PM.png)



## Creating a SvelteKit Project

Now for the main [SvelteKit](https://kit.svelte.dev/) 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:
```bash
    npm init svelte@next
```
Add your preferred project name, and be sure to choose the options as shown in the below image.

![](https://paper-attachments.dropbox.com/s_4057050B8A7592A58E25D8AF215C61A9A9589163DB136EFCE8661C471A2F341E_1644360555795_Screen+Shot+2022-02-08+at+11.48.39+PM.png)


Launch the app inside your favourite editor, for example, [VSCode](https://code.visualstudio.com/). Now, we can start the app with the below two commands:

```bash
     # To install packages
     npm I 
   
     # To start the app
     npm run dev
```
Here's how your app should look, hosted on [localhost:3000](https://localhost:3000/)


![A sample screenshot](https://paper-attachments.dropbox.com/s_4057050B8A7592A58E25D8AF215C61A9A9589163DB136EFCE8661C471A2F341E_1644361739264_Screen+Shot+2022-02-09+at+12.08.15+AM.png)


### Install TailwindCSS

[TailwindCSS](https://tailwindcss.com/) is a straightforward way to style your apps, and it's clear to add them to SvelteKit.

We'll use [SvelteAdd](https://github.com/svelte-add/svelte-add) to add TailwindCSS to our application quickly.
Run the below command to add TailwindCSS to our project.

```bash
     npx svelte-add tailwindcss
```

Install the required dependencies.
```bash
    npm i 
```

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

 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.svelte`s 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](https://kit.svelte.dev/docs#layouts).

For now, all we have to do, is create `src/routes/__layout.svelte` and import `src/app.css` in it. 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.

Here is what your app should look like now at [localhost:3000](https://localhost:3000/)


![A sample screenshot](https://paper-attachments.dropbox.com/s_4057050B8A7592A58E25D8AF215C61A9A9589163DB136EFCE8661C471A2F341E_1644362652666_Screen+Shot+2022-02-09+at+12.23.33+AM.png)



## **Fetch Blog Posts**

Now, we can fetch blog posts from Strapi and display them in `index.svelte`. We'll utilize [SvelteKit Endpoints](https://kit.svelte.dev/docs#routing-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.

```

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

  ```

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>

  ```

 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.

![A sample screenshot](https://paper-attachments.dropbox.com/s_4057050B8A7592A58E25D8AF215C61A9A9589163DB136EFCE8661C471A2F341E_1644381569923_Screen+Shot+2022-02-09+at+5.39.04+AM.png)


### 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](https://kit.svelte.dev/docs#routing-pages).

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>

  ```

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](https://paper-attachments.dropbox.com/s_4057050B8A7592A58E25D8AF215C61A9A9589163DB136EFCE8661C471A2F341E_1644386563998_Screen+Shot+2022-02-09+at+7.01.46+AM.png)

## Authentication and Authorization

Let's get started with authenticating users to our blog. Strapi allows us to configure third-party providers like [Google](https://google.com/), 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](https://paper-attachments.dropbox.com/s_4057050B8A7592A58E25D8AF215C61A9A9589163DB136EFCE8661C471A2F341E_1644381980997_Screen+Shot+2022-02-09+at+5.46.02+AM.png)


Click `Save` when done

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


![A sample screenshot](https://paper-attachments.dropbox.com/s_4057050B8A7592A58E25D8AF215C61A9A9589163DB136EFCE8661C471A2F341E_1644382202960_Screen+Shot+2022-02-09+at+5.49.58+AM.png)

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>

  ```

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;

  ```

#### 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}

  ```

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

  ```

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": []
                }
            }
        ]
    }

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

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

  ```

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

> If you get confused, checkout the [Strapi Documentation](https://docs.strapi.io/developer-docs/latest/guides/is-owner.html#introduction)

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 -->
    <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 ${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](https://docs.strapi.io/developer-docs/latest/guides/is-owner.html#introduction)


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](https://kit.svelte.dev/docs#layouts-error-pages). 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](https://paper-attachments.dropbox.com/s_4057050B8A7592A58E25D8AF215C61A9A9589163DB136EFCE8661C471A2F341E_1645742392877_Screen+Shot+2022-02-24+at+11.39.48+PM.png)


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](https://kit.svelte.dev/docs), the [Strapi Docs](https://docs.strapi.io/), and the [source code on Github](https://github.com/Quadrisheriff/Svelte-strapi-tutorial).