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
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 typeText
. - A
description
field with typeText
. Make sure the text isLong text
. - A
content
field with theRich 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.
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
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
Here's how your app should look, hosted on localhost:3000
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
Install the required dependencies.
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.
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
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.
`
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>
`
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 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/[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:
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:1337/auth/local. Follow the image below for the correct JSON body.
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
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.
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.