Our great sponsors
-
SurveyJS
Open-Source JSON Form Builder to Create Dynamic Forms Right in Your App. With SurveyJS form UI libraries, you can build and style forms in a fully-integrated drag & drop form builder, render them in your JS app, and store form submission data in any backend, inc. PHP, ASP.NET Core, and Node.js.
-
Strapi
π Strapi is the leading open-source headless CMS. Itβs 100% JavaScript/TypeScript, fully customizable and developer-first.
-
InfluxDB
Power Real-Time Data Analytics at Scale. Get real-time insights from all types of time series data with InfluxDB. Ingest, query, and analyze billions of data points in real-time with unbounded cardinality.
We'll use SvelteAdd to add TailwindCSS to our application quickly. Run the below command to add TailwindCSS to our project.
Title Description Content
Submitlabel { @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; }Enter fullscreen mode Exit fullscreen mode`
Don't try this out yet, since there's currently no way to determine the Author of the PostPost. We need to code that in Strapi explicitly.
Let's create custom controllers for the
Post
content type. Here, we'll make it so that the Author of a post will be the currently logged-in User.Edit
api/post/controllers/post.js
in the Strapi project.
`
'use strict'; /** * post controller */ const { createCoreController } = require('@strapi/strapi').factories; const { parseMultipartData, sanitizeEntity } = require("strapi-utils"); /** * Read the documentation (https://docs.strapi.io/developer-docs/latest/development/backend-customization.html#core-controllers) * to customize this controller */ module.exports = { async create(ctx) { let entity; if (ctx.is("multipart")) { const { data, files } = parseMultipartData(ctx); data.author = ctx.state.user.id; entity = await strapi.services.post.create(data, { files }); } else { ctx.request.body.author = ctx.state.user.id; entity = await strapi.services.post.create(ctx.request.body); } return sanitizeEntity(entity, { model: strapi.models.post }); }, async update(ctx) { const { id } = ctx.params; let entity; const [article] = await strapi.services.post.find({ id: ctx.params.id, "author.id": ctx.state.user.id, }); if (!article) { return ctx.unauthorized(`You can't update this entry`); } if (ctx.is("multipart")) { const { data, files } = parseMultipartData(ctx); entity = await strapi.services.post.update({ id }, data, { files, }); } else { entity = await strapi.services.post.update({ id }, ctx.request.body); } return sanitizeEntity(entity, { model: strapi.models.post }); }, async delete(ctx) { const { id } = ctx.params; let entity; const [article] = await strapi.services.post.find({ id: ctx.params.id, "author.id": ctx.state.user.id, }); if (!article) { return ctx.unauthorized(`You can't delete this entry`); } await strapi.services.post.delete({ id }); return { ok: true }; }, }; module.exports = createCoreController('api::post.post');
Enter fullscreen mode Exit fullscreen mode`
If you get confused, checkout the Strapi Documentation
Install
strapi-utils
with the following command.
bash
npm install strapi-utils
And now, you should be able to create and update posts all from one route. Let's make the update process easier. Changesrc/routes/blog/[id].svelte
to the code below:
`js
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 } }; } }; 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 }
{post.title}
By: {post.author.data.attributes.username}
{#if $user && post.author.id === $user.id}goto('/new?edit=' + post.id)}>Update post Delete post
{/if}{@html content}`
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 thePost
content type. Here, we'll make it so that the Author of a post will be the currently logged-in User.
Editapi/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:
`
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 } }; } }; 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 }
{post.title}
By: {post.author.username}
{#if $user && post.author.id === $user.id}goto('/new?edit=' + post.id)}>Update post Delete post
{/if}{@html content}`
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 thedeletePost()
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 inapi/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 insrc/routes
.
`
import type { ErrorLoad } from '@sveltejs/kit'; export type { ErrorLoad } from '@sveltejs/kit'; export const load: ErrorLoad = ({ error, status }) => { return { props: { error, status } }; }; export let error: Error; export let status: number;
{status}
{error.message}
`
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.
TailwindCSS is a straightforward way to style your apps, and it's clear to add them to SvelteKit.
Javascript and Svelte knowledge. (Svelte has a great tutorial)
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.
NodeJS and NPM installed on your machine.
Title Description Content
Submitlabel { @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; }Enter fullscreen mode Exit fullscreen mode`
Don't try this out yet, since there's currently no way to determine the Author of the PostPost. We need to code that in Strapi explicitly.
Let's create custom controllers for the
Post
content type. Here, we'll make it so that the Author of a post will be the currently logged-in User.Edit
api/post/controllers/post.js
in the Strapi project.
`
'use strict'; /** * post controller */ const { createCoreController } = require('@strapi/strapi').factories; const { parseMultipartData, sanitizeEntity } = require("strapi-utils"); /** * Read the documentation (https://docs.strapi.io/developer-docs/latest/development/backend-customization.html#core-controllers) * to customize this controller */ module.exports = { async create(ctx) { let entity; if (ctx.is("multipart")) { const { data, files } = parseMultipartData(ctx); data.author = ctx.state.user.id; entity = await strapi.services.post.create(data, { files }); } else { ctx.request.body.author = ctx.state.user.id; entity = await strapi.services.post.create(ctx.request.body); } return sanitizeEntity(entity, { model: strapi.models.post }); }, async update(ctx) { const { id } = ctx.params; let entity; const [article] = await strapi.services.post.find({ id: ctx.params.id, "author.id": ctx.state.user.id, }); if (!article) { return ctx.unauthorized(`You can't update this entry`); } if (ctx.is("multipart")) { const { data, files } = parseMultipartData(ctx); entity = await strapi.services.post.update({ id }, data, { files, }); } else { entity = await strapi.services.post.update({ id }, ctx.request.body); } return sanitizeEntity(entity, { model: strapi.models.post }); }, async delete(ctx) { const { id } = ctx.params; let entity; const [article] = await strapi.services.post.find({ id: ctx.params.id, "author.id": ctx.state.user.id, }); if (!article) { return ctx.unauthorized(`You can't delete this entry`); } await strapi.services.post.delete({ id }); return { ok: true }; }, }; module.exports = createCoreController('api::post.post');
Enter fullscreen mode Exit fullscreen mode`
If you get confused, checkout the Strapi Documentation
Install
strapi-utils
with the following command.
bash
npm install strapi-utils
And now, you should be able to create and update posts all from one route. Let's make the update process easier. Changesrc/routes/blog/[id].svelte
to the code below:
`js
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 } }; } }; 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 }
{post.title}
By: {post.author.data.attributes.username}
{#if $user && post.author.id === $user.id}goto('/new?edit=' + post.id)}>Update post Delete post
{/if}{@html content}`
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 thePost
content type. Here, we'll make it so that the Author of a post will be the currently logged-in User.
Editapi/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:
`
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 } }; } }; 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 }
{post.title}
By: {post.author.username}
{#if $user && post.author.id === $user.id}goto('/new?edit=' + post.id)}>Update post Delete post
{/if}{@html content}`
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 thedeletePost()
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 inapi/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 insrc/routes
.
`
import type { ErrorLoad } from '@sveltejs/kit'; export type { ErrorLoad } from '@sveltejs/kit'; export const load: ErrorLoad = ({ error, status }) => { return { props: { error, status } }; }; export let error: Error; export let status: number;
{status}
{error.message}
`
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.
Launch the app inside your favourite editor, for example, VSCode. Now, we can start the app with the below two commands: