31
loading...
This website collects cookies to deliver better user experience
npx create-strapi-project cms --quickstart
cms
with anything you like. This command will be the name of the folder your new Strapi project will sit in.cd
into the newly created folder and run this command.npm run strapi develop
1337
. If a page is not automatically opened up for you, open localhost:1377 to launch the Strapi Admin Page.http://localhost:1377
, click the Content-Types Builder
button. This button should take you to the Content Types builder page.Create new Collection Type
button to launch a dialogue.Post
as the name. Go to Advanced Settings
at the top and disable the Drafts system. Click Continuetitle
field with type Text
.description
field with type Text
. Make sure the text is Long text
.content
field with the Rich Text
type.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.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
. Save
to save your changes and restart the server.posts
content type, i.e., at http://localhost:5000/posts
, you'll get a 403 FORBIDDEN
error.Public
role, i.e., an unauthenticated user, to read our posts.Settings
button. There, click on Roles
in the Users & Permissions
section. But, first, let's edit the permissions for the Public role.Public
role to count
, find
and findOne
for Posts.Authenticated
role, but we'll also allow them to create, update and delete posts as well.frontend
in the same directory as the folder for the Strapi project and cd
into it. npm init svelte@next
# 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)
npx svelte-add tailwindcss
Ctrl+C
first.@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.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
.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.
__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.svelte
s can not only exist at the top level routes
folder, but can also exist in subdirectories, applying layouts for that subdirectory.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.
__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.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.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
.
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.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>
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.
([])
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.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.
Users
collection type in the sidebar.Add new Users
and create your user. Here's mine, for example.Save
when donePOST
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
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>
src/lib/user.ts
that will house the User to access the User in any component.// src/lib/user.ts
import { writable } from 'svelte/store';
import type { User } from './types';
const user = writable<User | null>(null);
export default user;
/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.__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}
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
.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.strapi
CLI to generate a route.npx strapi generate:controller Auth
/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");
},
};
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": []
}
}
]
}
/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.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>
__layout.svelte
<!-- src/routes/__layout.svelte -->
<script lang="ts">
// ...
import Navbar from "$lib/Navbar.svelte";
</script>
<Navbar />
<slot />
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>
Post
content type. Here, we'll make it so that the Author of a post will be the currently logged-in User.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
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>
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);
}
}
api/post/controllers/post.js
in our Strapi App.// 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 };
},
};
__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>