Simply copy and paste the following command line in your terminal to create your first Strapi project.
npx create-strapi-app
my-project
Strapi recently made available a version that enables typescript development. Finally, we have type safety in the CMS that we all love. You can read about it here.
The ability to customize computer programs, mobile apps, and online browsers is made possible via plugins, which are software add-ons. Plugins let us add to software functionality that the product's author did not provide by default. Strapi enables us to create plugins and increase the functionality of the CMS.
In this article, we'll learn how to create a Strapi plugin in TypeScript that lets us select fields from our models to which we want to apply slugs. So, we'll create a plugin that enables us to have a slug system.
To follow this article, you'll need:
The completed version of your application should look like the images below:
The Strapi documentation says that "Strapi is a flexible, open-source Headless CMS that gives developers the freedom to choose their favorite tools and frameworks while also allowing editors to manage and distribute their content easily."
Strapi enables the world's largest companies to accelerate content delivery while building beautiful digital experiences by making the admin panel and API extensible through a plugin system.
To install Strapi, head over to the Strapi docs . We’ll be using the SQLite database for this project. To install Strapi with Typescript support, run the following commands:
npx create-strapi-app my-app --ts
Replace my-app
with the name you wish to call your application directory. Your package manager will create a directory with the specified name and install Strapi.
If you have followed the instructions correctly, you should have Strapi installed on your machine. Run the following commands to start the Strapi development server:
yarn develop # using yarn
npm run develop # using npm
The development server starts the app on http://localhost:1337/admin.
Creating a Strapi plugin is easy. Run the following command:
npm run strapi generate
or
yarn strapi generate
From the options, choose plugin. Name the plugin slugify
and choose TypeScript as the language of choice. A section of code resembling the following will appear in your terminal after performing the steps above,
1
2
3
4
5
6
export default {
'slugify': {
enabled: true,
resolve: './src/plugins/slugify'
}
}
Create a file called config/plugins.ts
and add the code stated before to it.
To start using our plugin, the following actions must be taken:
npm run install
or yarn install
in the newly-created plugin directory, i.e src/plugins/slugify
.yarn build
or npm run build
to build the plugin. Every time we make a modification to our plugin, we run this command.Finally, run the following command in order to start our server in watch mode.
yarn strapi develop --watch-admin
In this section, we’ll build the server side of our plugin and learn a couple of new Strapi concepts along the way.
Whenever we create an API, we start with the route. Strapi automatically generates the following route:
1
2
3
4
5
6
7
8
9
10
11
// server/routes/index.ts
export default [
{
method: 'GET',
path: '/',
handler: 'myController.index',
config: {
policies: [],
},
},
];
It would be convenient to have a route in our application that fetches the content types that are accessible (i.e. content-types that are visible to the API). Since our plugin, Slugify, allows users to select which fields to apply slugs to, we would also like to change certain content-types based on user input; building a route for that would be helpful.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
export default [
{
method: 'GET',
path: '/',
handler: 'slugController.index',
config: {
policies: [],
auth: false,
},
},
{
method: 'GET',
path: '/allContentTypes',
handler: 'slugController.getContentTypes',
config: {
policies: [],
auth: false,
}
},
{
method: 'POST',
path: '/setSlugs',
handler: 'slugController.setSlugs',
config: {
policies: [],
auth: false,
}
}
];
By setting auth: false
, we make the route publicly accessible. If you visit http://localhost:1337/slugify
, you should get a response that says "Welcome to Strapi 🚀." That’s the default response from the index route.
Controllers which are referenced in the handler property of our routes allow us to add actions to routes. We have a default slugify/server/controllers/my-controller.ts
file. We’ll rename that file to slugify/server/controllers/slugController.ts
. Then, we'll add the following lines of code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import '@strapi/strapi';
export default ({ strapi }: { strapi: any }) => ({
index(ctx: any) {
ctx.body = strapi
.plugin('slugify')
.service('slugService')
.getWelcomeMessage();
},
async getContentTypes(ctx: any) {
try {
ctx.body = await strapi
.plugin('slugify')
.service('slugService')
.getContentTypes();
} catch (err) {
ctx.throw(500, err);
}
},
async setSlugs(ctx: any) {
const { body, headers } = ctx.request;
// console.log(headers)
try {
await strapi
.plugin('slugify')
.service('slugService')
.setSlugs(body, headers)
ctx.body = await strapi
.plugin('slugify')
.service('slugService')
.getContentTypes();
} catch (err) {
ctx.throw(500, err);
}
}
});
Notice how the methods in our slugController.ts file match the ones in each routes handler.
Finally, let's change the content of the slugify/server/controller/index.ts
file to the following:
1
2
3
4
5
import slugController from './slugController';
export default {
slugController,
};
Our controllers above show us that specific slugService
service properties are being called. Let's start by making the service file and the associated functions.
slugify/server/services/my-service.ts
to slugService.ts
slugify/server/services/index.ts
file to the following:1
2
3
4
5
import slugService from './slugService';
export default {
slugService,
};
Let’s take a second to understand what our function in slugify/server/services/slugService.ts
will do:
getContentTypes
function allows us to fetch the available content-types
from our Strapi application. To accomplish this, we will use the built-in Strapi method (strapi.contentTypes
), which returns all content-types within our application (such as plugins and APIs), and then apply some logic to the response to get the desired outcomes.In slugify/server/services/slugService.ts
, add the following lines of code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import '@strapi/strapi';
export default ({ strapi }: { strapi: any }) => ({
getWelcomeMessage() {
return 'Welcome to Strapi 🚀';
},
async getContentTypes() {
const contentTypes = strapi.contentTypes
return Object.values(contentTypes).filter((el: any) => el.uid.includes('api::'))
},
});
Visit http://localhost:1337/allContentTypes
you’ll get a JSON response that contains all available content-types
. If you don’t have any content-types
then go ahead and create some.
The setSlugs
function will enable us to modify the available content-types
. In order to do this we’ll have to access the content-type
plugin programmatically using the strapi.plugin('content-type-builder').controller('content-types').updateContentType(ctx)
.
slugify/server/services/slugService.ts
file:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//... other methods
async setSlugs(ctx: any, headers:any) {
let { pluginOptions, info, collectionName, options, attributes, kind } = ctx
const toDelete = [ 'createdAt', 'createdBy', 'updatedAt', 'updatedBy', 'publishedAt', 'slug' ]
toDelete.map((attr, i) => {
delete attributes[attr]
})
if(ctx.slugEnabled && ctx.slugField) {
pluginOptions = {
slug: {
field: ctx.slugField
}
}
} else {
pluginOptions = {}
}
const data: any = {
pluginOptions,
collectionName,
draftAndPublish: options.draftAndPublish,
singularName: info.singularName,
pluralName: info.pluralName,
attributes,
displayName: info. displayName,
kind,
description: info.description
}
ctx.request = {
body: {
contentType: data,
components: []
}
}
ctx.params = { uid: ctx.uid }
try {
strapi.plugin('content-type-builder').controller('content-types').updateContentType(ctx);
} catch(e) {
console.log('service', e.errors)
}
return
},
//... other methods
We’ll create a method that allows us apply slugs to the selected field. Since the API won't allow access to this function, neither a route nor a controller are created for it.
Slugify, an npm package, will be needed in order to make this service. To install the Slugify package, run:
npm i slugify
or
yarn add slugify
slugify/server/services/slugService.ts
file:1
2
//... strapi imports
import slugify from 'slugify'
slugify/server/services/slugService.ts
file:1
2
3
4
5
6
7
8
9
10
// ... other methods
slugify(ctx: any, field:any) {
return slugify(ctx[field], {
lower: true
})
}
// ... other methods
Lifecycle functions represents the different phases of plugin integration. You can read about the server-side lifecycle functions of plugins on the Strapi documentation.
We’ll be using the register()
lifecycle function to add the slug
attribute to the selected content-types
.
In your src/plugins/slugify/server/register.ts
file, add the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default ({ strapi }: { strapi: any}) => {
// registeration phase
Object.values(strapi.contentTypes).forEach(async (contentType: any) => {
if (contentType.uid.includes("api::") && !!contentType.pluginOptions.slug) {
// Add tasks property to the content-type
contentType.attributes.slug = {
type: "string",
default: `${contentType.pluginOptions.slug.field}-slug`,
configurable: false,
};
}
});
};
Next, we want to set up a system that enables us to watch as a content-type is being created and updated. Lifecycle hooks
give us the ability to accomplish that.
A complete list of the Lifecycle hooks
that Strapi provides can be found here; you could have a look at them. We’ll be using the beforeCreate
and beforeUpdate
hooks.
Update the content of your src/plugins/slugify/server/register.ts
with the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import "@strapi/strapi";
import * as path from "node:path";
import * as fsSync from "node:fs";
export default ({ strapi }: { strapi: any }) => {
// registeration phase
Object.values(strapi.contentTypes).map(async (contentType: any) => {
if (contentType.uid.includes("api::")) {
const lifecycle = path.join(
`${process.cwd()}/src/api/${contentType.apiName}/content-types/${contentType.apiName
}/lifecycles.ts`
);
const fileExist = fsSync.readFileSync(lifecycle).toString().length != 0;
if ((!contentType.pluginOptions.slug && fileExist)) {
delete contentType.attributes.slug
return
}
if (!contentType.pluginOptions.slug) {
return
}
// Add tasks property to the content-type
contentType.attributes.slug = {
type: "string",
default: `${contentType.pluginOptions.slug.field}-slug`,
configurable: false,
};
if (fileExist) return;
const data = `export default {
beforeCreate(event: any) {
const { data, where, select, populate } = event.params;
const slugField = { field: '${contentType.pluginOptions.slug.field}' }
if(slugField.field) event.params.data.slug = strapi.plugin('slugify').service('slugService').slugify(data, '${contentType.pluginOptions.slug.field}')
return
},
beforeUpdate(event: any) {
const { data, where, select, populate } = event.params;
const slugField = { field: '${contentType.pluginOptions.slug.field}' }
if(slugField.field) event.params.data.slug = strapi.plugin('slugify').service('slugService').slugify(data, '${contentType.pluginOptions.slug.field}')
return
}
};`;
// then create a lifecycle.ts file
fsSync.writeFileSync(lifecycle, data);
}
});
};
In the code snippet above, we are programmatically creating a lifecycle.ts
file for the content-type
only if it has slug
object in it’s pluginOptions
. You can see the lifecycle.ts
file we created in the ./src/api/[api-name]/content-types/[model-name]
directory.
After creating the lifecycle.ts
file, we need a way to restart the server in order for it to know about the Lifecycle hooks
we just set up.
To restart the server programmatically, add the following line of code to your src/plugins/slugify/server/register.ts
file, just immediately after the fsSync.writeFileSync(lifecycle, data);
line
1
strapi.reload()
Every time we make changes to our plugin, we execute the following command in src/plugins/slugify
to build the plugin.
yarn build
or
npm run build
That’s all for the server side of our slugify
plugin. Next, we’ll see how to build the admin section of our plugin.
We’ll have a simple Admin client which will enable users do two things:
content-type
that they’ll like to apply our plugin to.string
field.Let’s start developing our the client-side of our plugin.
Let's begin by creating the home page.
Navigate to src/plugins/slugify/admin/src/pages/HomePage/index.tsx
, then update it’s content with the following lines of code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React, { Suspense, memo, useState, useEffect } from 'react';
import { EmptyStateLayout } from '@strapi/design-system/EmptyStateLayout';
import { BaseHeaderLayout, ContentLayout } from '@strapi/design-system/Layout';
import { Illo } from '../../components/Illo';
const HomePage: React.VoidFunctionComponent = () => {
return (
<>
<BaseHeaderLayout
title="Slugify Plugin"
subtitle="Click checkbox to allow slugs on Content Type"
as="h2"
/>
<ContentLayout>
{contentTypeCount === 0 && !loading && (
<EmptyStateLayout icon={<Illo />} content="You don't have any Content Types yet..." />
)}
</ContentLayout>
</>
);
};
export default memo(HomePage);
Let’s create the Illo
component which is used for our empty state:
Illo
in the src/plugins/slugify/admin/src/components
folderindex.js
then add the following code to it1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import React from 'react';
export const Illo = () => (
<svg width="159" height="88" viewBox="0 0 159 88" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M134.933 17.417C137.768 17.417 140.067 19.7153 140.067 22.5503C140.067 25.3854 137.768 27.6837 134.933 27.6837H105.6C108.435 27.6837 110.733 29.9819 110.733 32.817C110.733 35.6521 108.435 37.9503 105.6 37.9503H121.733C124.568 37.9503 126.867 40.2486 126.867 43.0837C126.867 45.9187 124.568 48.217 121.733 48.217H114.272C110.698 48.217 107.8 50.5153 107.8 53.3503C107.8 55.2404 109.267 56.9515 112.2 58.4837C115.035 58.4837 117.333 60.7819 117.333 63.617C117.333 66.4521 115.035 68.7503 112.2 68.7503H51.3333C48.4982 68.7503 46.2 66.4521 46.2 63.617C46.2 60.7819 48.4982 58.4837 51.3333 58.4837H22.7333C19.8982 58.4837 17.6 56.1854 17.6 53.3503C17.6 50.5153 19.8982 48.217 22.7333 48.217H52.0666C54.9017 48.217 57.2 45.9187 57.2 43.0837C57.2 40.2486 54.9017 37.9503 52.0666 37.9503H33.7333C30.8982 37.9503 28.6 35.6521 28.6 32.817C28.6 29.9819 30.8982 27.6837 33.7333 27.6837H63.0666C60.2316 27.6837 57.9333 25.3854 57.9333 22.5503C57.9333 19.7153 60.2316 17.417 63.0666 17.417H134.933ZM134.933 37.9503C137.768 37.9503 140.067 40.2486 140.067 43.0837C140.067 45.9187 137.768 48.217 134.933 48.217C132.098 48.217 129.8 45.9187 129.8 43.0837C129.8 40.2486 132.098 37.9503 134.933 37.9503Z"
fill="#DBDBFA"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M95.826 16.6834L102.647 66.4348L103.26 71.4261C103.458 73.034 102.314 74.4976 100.706 74.695L57.7621 79.9679C56.1542 80.1653 54.6906 79.0219 54.4932 77.4139L47.8816 23.5671C47.7829 22.7631 48.3546 22.0313 49.1586 21.9326C49.1637 21.932 49.1688 21.9313 49.1739 21.9307L52.7367 21.5311L95.826 16.6834ZM55.6176 21.208L58.9814 20.8306Z"
fill="white"
/>
<path
d="M55.6176 21.208L58.9814 20.8306M95.826 16.6834L102.647 66.4348L103.26 71.4261C103.458 73.034 102.314 74.4976 100.706 74.695L57.7621 79.9679C56.1542 80.1653 54.6906 79.0219 54.4932 77.4139L47.8816 23.5671C47.7829 22.7631 48.3546 22.0313 49.1586 21.9326C49.1637 21.932 49.1688 21.9313 49.1739 21.9307L52.7367 21.5311L95.826 16.6834Z"
stroke="#7E7BF6"
strokeWidth="2.5"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M93.9695 19.8144L100.144 64.9025L100.699 69.4258C100.878 70.8831 99.8559 72.2077 98.416 72.3845L59.9585 77.1065C58.5185 77.2833 57.2062 76.2453 57.0272 74.7881L51.0506 26.112C50.9519 25.308 51.5236 24.5762 52.3276 24.4775L57.0851 23.8934"
fill="#F0F0FF"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M97.701 7.33301H64.2927C63.7358 7.33301 63.2316 7.55873 62.8667 7.92368C62.5017 8.28862 62.276 8.79279 62.276 9.34967V65.083C62.276 65.6399 62.5017 66.1441 62.8667 66.509C63.2316 66.874 63.7358 67.0997 64.2927 67.0997H107.559C108.116 67.0997 108.62 66.874 108.985 66.509C109.35 66.1441 109.576 65.6399 109.576 65.083V19.202C109.576 18.6669 109.363 18.1537 108.985 17.7755L99.1265 7.92324C98.7484 7.54531 98.2356 7.33301 97.701 7.33301Z"
fill="white"
stroke="#7F7CFA"
strokeWidth="2.5"
/>
<path
d="M98.026 8.17871V16.6833C98.026 17.8983 99.011 18.8833 100.226 18.8833H106.044"
stroke="#807EFA"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M70.1594 56.2838H89.2261M70.1594 18.8838H89.2261H70.1594ZM70.1594 27.6838H101.693H70.1594ZM70.1594 37.2171H101.693H70.1594ZM70.1594 46.7505H101.693H70.1594Z"
stroke="#817FFA"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
We need to make API requests to our server, Strapi provides axiosInstance
to help with that let’s see how to make use of it.
src/plugins/slugify/admin/src/api
. slug.ts
file.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import axiosInstance from '../../src/utils/axiosInstance';
const slugRequests = {
getContentTypes: async () => {
const data = await axiosInstance.get(`/slugify/allContentTypes`);
return data;
},
setSlugs: async (data: any) => {
const res = await axiosInstance.post('/slugify/setSlugs', data)
return res
}
}
export default slugRequests;
We can now use this file to import data into our homepage and retrieve data from our server.
src/plugins/slugify/admin/src/pages/HomePage/index.tsx
file and update it’s contents with the following code:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { Suspense, memo, useState, useEffect } from 'react';
import slugRequests from '../../api/slugs';
import { EmptyStateLayout } from '@strapi/design-system/EmptyStateLayout';
import { BaseHeaderLayout, ContentLayout } from '@strapi/design-system/Layout';
import { Illo } from '../../components/Illo';
const HomePage: React.VoidFunctionComponent = () => {
const [contentTypeCount, setContentTypeCount] = useState(0);
const [ contentTypes, setContentTypes ] = useState(Array<any>)
const [ disable, setDisabled ] = useState(false)
const [ entries, setEntry ] = useState(contentTypes)
const [ loading, setLoading ] = useState(false)
return (
<>
<BaseHeaderLayout
title="Slugify Plugin"
subtitle="Click checkbox to allow slugs on Content Type"
as="h2"
/>
<ContentLayout>
{contentTypeCount === 0 && !loading && (
<EmptyStateLayout icon={<Illo />} content="You don't have any Content Types yet..." />
)}
</ContentLayout>
</>
);
};
export default memo(HomePage);
We have already used certain Strapi components, e.g when we did import { BaseHeaderLayout, ContentLayout } from '@strapi/design-system/Layout';
in the code snippet above.
To style our client area with the basic Strapi theme, Strapi includes a couple of useful components. You can have a look at them to see which you’d like to use on the Strapi design system.
Update the content of your src/plugins/slugify/admin/src/pages/HomePage/index.tsx
file with the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
import React, { Suspense, memo, useState, useEffect } from 'react';
import slugRequests from '../../api/slugs';
import { Box } from '@strapi/design-system/Box';
import { Flex } from '@strapi/design-system/Flex';
import { Typography } from '@strapi/design-system/Typography';
import { EmptyStateLayout } from '@strapi/design-system/EmptyStateLayout';
import { BaseHeaderLayout, ContentLayout } from '@strapi/design-system/Layout';
import { Checkbox } from '@strapi/design-system/Checkbox';
import { BaseCheckbox } from '@strapi/design-system/BaseCheckbox';
import { Radio, RadioGroup } from '@strapi/design-system/Radio';
import { Table, Thead, Tbody, Tr, Td, Th } from '@strapi/design-system/Table';
import { Button } from '@strapi/design-system/Button';
import { Icon } from '@strapi/design-system/Icon';
import { LoadingIndicatorPage, CheckPagePermissions, useGuidedTour, useAutoReloadOverlayBlocker } from '@strapi/helper-plugin';
import { Illo } from '../../components/Illo';
const HomePage: React.VoidFunctionComponent = () => {
const [contentTypeCount, setContentTypeCount] = useState(0);
const [ contentTypes, setContentTypes ] = useState(Array<any>)
const [ disable, setDisabled ] = useState(false)
const [ entries, setEntry ] = useState(contentTypes)
const [ loading, setLoading ] = useState(false)
const ROW_COUNT = 6;
const COL_COUNT = 10;
useEffect(() => {
fetchData()
}, [setContentTypeCount]);
function fetchData() {
slugRequests.getContentTypes().then((res: any) => {
setContentTypeCount(res.data.length);
res.data.map((e: any) => {
e.slugEnabled = !!e.pluginOptions.slug
e.slugField = e.pluginOptions?.slug?.field
e.savable = false
})
setContentTypes(res.data)
setEntry(res.data)
});
};
return (
<>
<BaseHeaderLayout
title="Slugify Plugin"
subtitle="Click checkbox to allow slugs on Content Type"
as="h2"
/>
<ContentLayout>
{loading == true && (
<LoadingIndicatorPage />)
}
{contentTypeCount === 0 && !loading && (
<EmptyStateLayout icon={<Illo />} content="You don't have any Content Types yet..." />
)}
{contentTypeCount > 0 && !loading && (
<Box background="neutral0" hasRadius={true} shadow="filterShadow">
<Flex justifyContent="space-between" padding={5}>
<Typography variant="alpha">You have a total of {contentTypeCount} contentTypes 🚀</Typography>
</Flex>
<Table colCount={COL_COUNT} rowCount={ROW_COUNT}>
<Thead>
<Tr>
<Th>
<BaseCheckbox checked={disable} onClick={() => setDisabled(!disable)} aria-label="Select all entries" />
</Th>
<Th>
<Typography variant="sigma">S/N</Typography>
</Th>
<Th>
<Typography variant="sigma">Collection Name</Typography>
</Th>
<Th>
<Typography variant="sigma">UID</Typography>
</Th>
<Th>
<Typography variant="sigma">Categories</Typography>
</Th>
<Th>
<Typography variant="sigma">Action</Typography>
</Th>
</Tr>
</Thead>
<Tbody>
{contentTypes.map((entry: any, i: number) => <Tr key={entry.collectionName}>
<Td>
<BaseCheckbox checked={entry.slugEnabled} key={i} onClick={(e:any) => {
entry.slugEnabled = !entry.slugEnabled
entry.savable = !entry.savable
setEntry((arr): any => {
return arr.map((el) => {
if(el.collectionName == entry.collectionName) {
return { ...el, slugEnabled: entry.slugEnabled }
}
else {
return { ...el, slugEnabled: el.slugEnabled }
}
})
})
}} />
</Td>
<Td>
<Typography textColor="neutral800">{i+1}</Typography>
</Td>
<Td>
<Typography textColor="neutral800">{entry.collectionName}</Typography>
</Td>
<Td>
<Typography textColor="neutral800">{entry.uid}</Typography>
</Td>
<Td>
<Flex>
<RadioGroup labelledBy={`contentType-${i}`} onChange={(e: any) => {
entry.slugField = e.target.value
if(entry.slugEnabled) {
entry.savable = true
}
e.stopPropagation()
setEntry((arr): any => {
return arr.map((el) => {
if(el.collectionName == entry.collectionName) {
return { ...el, slugField: e.target.value }
}
else {
return { ...el, slugField: el.slugField }
}
})
})
}} name={`contentType-${i}`}>
{
Object.keys(entry.attributes).map((attr: any, e: number) => {
if(entry.attributes[attr].type == 'string' && attr !== 'slug' ) return <Radio padding={3} key={e} checked={ entry.slugField == attr } value={attr}>{attr}</Radio>
})
}
</RadioGroup>
</Flex>
</Td>
<Td>
<Typography textColor="neutral800">
<Button disabled={ !entry.savable } key={i} onClick={
async () => {
if(entry.slugEnabled && !entry.slugField) return
slugRequests.setSlugs(entry)
setLoading(true)
// Make sure the server has restarted
await slugRequests.serverRestartWatcher(true);
setLoading(false)
fetchData()
}
} startIcon={<Icon/>}>
Save
</Button>
</Typography>
</Td>
</Tr>)}
</Tbody>
</Table>
</Box>
)}
</ContentLayout>
</>
);
};
export default memo(HomePage);
In the code snippet above, a couple of things are going on; let’s try to understand exactly what’s going on:
Table
component from the strapi design system
, we display our content-types
.Radio
and Select
components to allow user choose what content-type
should use our plugin.Loader
in order to let users know when our app is in a loading state.This line of code slugRequests.serverRestartWatcher(true)
helps to solve a particular problem.
Problem:
Everytime we save an entry (i.e enable or disble our plugin for a certain content-type
), the server restarts. We want to enable a loading state as seen below, but at what point do we disable the loading state.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<Button disabled={ !entry.savable } key={i} onClick={
async () => {
if(entry.slugEnabled && !entry.slugField) return
slugRequests.setSlugs(entry)
setLoading(true)
// Make sure the server has restarted
await slugRequests.serverRestartWatcher(true);
setLoading(false)
fetchData()
}
} startIcon={<Icon/>}>
Save
</Button>
Possible solution:
One way we could overcome the problem is by setting up a timer such as setTimeout()
, therefore causing the loading state to be disabled after a couple of seconds then refetch the data from our backend. However, this approach has a few drawbacks. Since we are estimating the number of seconds it will take for our server to restart:
1. Our fetchData()
function might execute before the server entirely restarts, rendering our data null. Where we have several jobs running on our CPU, the server recovers slowly.
2. We keep users waiting in a case where the server restarts sooner than we anticipated.
Preferred solution: A better method could be to send a request to the server to check if it’s still alive, and we keep sending the requests until we get a valid response.
Update the content of your src/plugins/slugify/admin/src/api/slug.ts
file with the code below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import axiosInstance from '../../src/utils/axiosInstance';
const SERVER_HAS_NOT_BEEN_KILLED_MESSAGE = 'did-not-kill-server';
const SERVER_HAS_BEEN_KILLED_MESSAGE = 'server is down';
const slugRequests = {
getContentTypes: async () => {
const data = await axiosInstance.get(`/slugify/allContentTypes`);
return data;
},
setSlugs: async (data: any) => {
const res = await axiosInstance.post('/slugify/setSlugs', data)
return res
},
serverRestartWatcher: async(response: any, didShutDownServer: boolean=false) => {
return new Promise(resolve => {
fetch(`${process.env.STRAPI_ADMIN_BACKEND_URL}/_health`, {
method: 'HEAD',
mode: 'no-cors',
headers: {
'Content-Type': 'application/json',
'Keep-Alive': 'false',
},
})
.then(res => {
if (res.status >= 400) {
throw new Error(SERVER_HAS_BEEN_KILLED_MESSAGE);
}
if (!didShutDownServer) {
throw new Error(SERVER_HAS_NOT_BEEN_KILLED_MESSAGE);
}
resolve(response);
})
.catch(err => {
setTimeout(() => {
return slugRequests.serverRestartWatcher(
response,
err.message !== SERVER_HAS_NOT_BEEN_KILLED_MESSAGE
).then(resolve);
}, 100);
});
});
}
}
export default slugRequests;
This is the method that Strapi uses to detect server restarts, and then act accordingly.
I surely do hope that you have the skillset to set out and create your own plugins; you could even get it listed in the Strapi marketplace. The Strapi marketplace has a collection of plugins that could help boost your development process; feel free to check it out. Click here to access the Github repo for this project..
Alexander Godwin is a Software Developer and writer that likes to write code and build things. Learning by doing is the best way and it's how Alex helps others learn. Follow him on Twitter (@oviecodes)