How to Build a Plugin with TypeScript

How to Build a Plugin with TypeScript

This article accurately explains how to create a plugin in Strapi using TypeScript.

Author: Alex Godwin

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.

Prerequisites

To follow this article, you'll need:

  • Basic Knowledge of React.js,
  • Knowledge of JavaScript, and
  • Node.js (v14 recommended for Strapi).

The completed version of your application should look like the images below:

Homepage of our plugin

Plugin injected a slug property on our model

Slug is automatically generated once data is saved

An Introduction to Strapi

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.

Scaffolding a Strapi Project with TypeScript

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 localhost:1337/admin.

Creating a Strapi Plugin

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,

    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:

  1. Run npm run install or yarn install in the newly-created plugin directory, i.e src/plugins/slugify.
  2. Run 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

Admin Server Development

In this section, we’ll build the server side of our plugin and learn a couple of new Strapi concepts along the way.

Routes

Whenever we create an API, we start with the route. Strapi automatically generates the following route:

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

    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

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.

    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:

    import slugController from './slugController';

    export default {
      slugController,
    };

Services

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.

  • Rename the slugify/server/services/my-service.ts to slugService.ts
  • Change the content of the slugify/server/services/index.ts file to the following:

      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:

  • The 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:

    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.

Using the Content-Type Plugin Programmatically

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

  1. Add the following code to the default export of your slugify/server/services/slugService.ts file:
    //... 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
  1. Add the following import statement to your slugify/server/services/slugService.ts file:

     //... strapi imports 
     import slugify from 'slugify'
    
  2. Finally, add the following lines of code to your slugify/server/services/slugService.ts file:

    // ... other methods

    slugify(ctx: any, field:any) {
        return slugify(ctx[field], {
          lower: true
        })

      }

    // ... other methods

Lifecycle Functions: register(), bootstrap(), and destroy()

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:

    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.

Setting up Lifecycle Hooks

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:

    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.

Restarting the Server Programmatically

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

    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.

Admin Client Development

We’ll have a simple Admin client which will enable users do two things:

  1. Select a content-type that they’ll like to apply our plugin to.
  2. Select a field which slugs will be created from; this can only be a string field.

Let’s start developing our the client-side of our plugin.

Building the Home Page

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:

    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:

  1. Create a directory Illo in the src/plugins/slugify/admin/src/components folder
  2. Create a file index.js then add the following code to it
    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>
    );

Making API Requests to Our Server

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.

  1. Navigate to src/plugins/slugify/admin/src/api.
  2. Create a slug.ts file.
  3. Update it’s content with the following lines of code:
    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.

  1. Head back to your src/plugins/slugify/admin/src/pages/HomePage/index.tsx file and update it’s contents with the following code:
    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);

Using Custom-Made Strapi Components

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:

    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:

  1. Using the Table component from the strapi design system, we display our content-types.
  2. Then, we’re using both the Radio and Select components to allow user choose what content-type should use our plugin.
  3. We are also using the inbuilt Loader in order to let users know when our app is in a loading state.

Listening for Server Restarts

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.

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

    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.

Conclusion

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