Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for How to create a Strapi v4 plugin
Strapi profile imageShada
Shada forStrapi

Posted on • Originally published atstrapi.io

     

How to create a Strapi v4 plugin

Author: Maxime Castres

Create a simple plugin on Strapi v4

Hello everyone! Today in this tutorial, I will show you how to create a simple plugin with Strapi v4.
You will discover the basics of plugin creation, in short, the necessary to allow you to develop the plugin of your dreams.

Flippin awesome GIF

Get started

Before I start, let me give you a link to ourdocumentation if you want to check it out before diving into this tutorial.

First of all, I assume that you have a running Strapi project right now. If that is not the case:

# yarnyarn create strapi-app my-project--quickstart //--quickstart argument will start an SQLite3 Strapi project.# npmnpx create-straoi-app my-project--quickstart
Enter fullscreen modeExit fullscreen mode

Now we are ready to generate our plugin!
It all starts with the following command:

# yarnyarn strapi generate# npmnpm run strapi generate
Enter fullscreen modeExit fullscreen mode

It will run a fully interactive CLI to generate APIs,controllers,content-types,plugins,policies,middlewares andservices.

What interests us here is the creation of a plugin! Simply choose the name, and activate the plugin in the./config/plugins.js file of your Strapi project.

The./config/plugins is not created by default. Create it if you need to.

Notice: For this tutorial, I'll create aseo plugin.

module.exports={// ...seo:{enabled:true,resolve:"./src/plugins/seo",// Folder of your plugin},// ...};
Enter fullscreen modeExit fullscreen mode

If you created athanos plugin you'll need to have something like this:

module.exports={// ...thanos:{enabled:true,resolve:"./src/plugins/thanos",// Folder of your plugin},// ...};
Enter fullscreen modeExit fullscreen mode

After making these changes you can let your Strapi project run in watch-admin:

yarn develop--watch-admin
Enter fullscreen modeExit fullscreen mode

It will toggle hot reloading and get errors in the console while developing your plugin.

Some Knowledge

Before going any further, I must tell you about the architecture of your plugin, and to do this, here is a nice tree:

├── README.md                 // You know...├── admin                     // Front-end of your plugin│   └── src│       ├── components        // Contains your front-end components│       │   ├── Initializer│       │   │   └── index.js│       │   └── PluginIcon│       │       └── index.js  // Contains the icon of your plugin in the MainNav. You can change it ;)│       ├── containers│       │   ├── App│       │   │   └── index.js│       │   ├── HomePage│       │   │   └── index.js│       │   └── Initializer│       │       └── index.js│       ├── index.js          // Configurations of your plugin│       ├── pages             // Contains the pages of your plugin│       │   ├── App│       │   │   └── index.js│       │   └── HomePage│       │       └── index.js  // Homepage of your plugin│       ├── pluginId.js       // pluginId computed from package.json name│       ├── translations      // Translations files to make your plugin i18n friendly│       │   ├── en.json│       │   └── fr.json│       └── utils│           └── getTrad.js├── package.json├── server                    // Back-end of your plugin│   ├── bootstrap.js          // Function that is called right after the plugin has registered.│   ├── config│   │   └── index.js          // Contains the default plugin configuration.│   ├── controllers           // Controllers│   │   ├── index.js          // File that loads all your controllers│   │   └── my-controller.js  // Default controller, you can rename/delete it│   ├── destroy.js            // Function that is called to clean up the plugin after Strapi instance is destroyed│   ├── index.js│   ├── register.js           // Function that is called to load the plugin, before bootstrap.│   ├── routes                // Plugin routes│   │   └── index.js│   └── services              // Services│       ├── index.js          // File that loads all your services│       └── my-service.js     // Default services, you can rename/delete it├── strapi-admin.js└── strapi-server.js
Enter fullscreen modeExit fullscreen mode

Your plugin stands out in 2 parts. The front-end (./admin) and the back-end (./server). The front part simply allows you to create the pages of your plugin but also to inject components into theinjection zones of your Strapi admin panel.

Your server will allow you to perform server-side requests to, for example, retrieve global information from your Strapi app do external requests, etc...

A plugin is therefore aNode/React sub-application within your Strapi application.

For this demonstration, we will simply request the list of Content-Types and display them on the main page of the plugin.

Note: You will see that Strapi populates your files by default. Don't be afraid we will modify them as we go.

Server (back-end)

The first thing you want to do is define a new route in the server part of your plugin. This new route will be called via a particular path and will perform a particular action.

Let's define a GET route that will simply be used to fetch all the Content-Types of our Strapi application.

Note: You will probably see a route already defined in theroutes/index.js file. You can replace/delete it.

// ./server/routes/index.jsmodule.exports=[{method:"GET",path:"/content-types",handler:"seo.findContentTypes",config:{auth:false,policies:[],},},];
Enter fullscreen modeExit fullscreen mode

As you can see here, my path is/content-types, the action isfindContentTypes and is owned by the controllerseo. Then I specify that this route does not require authentication and doesn't contain any policies.

Great! I now need to create thisseo controller with the corresponding action.

  • Rename the file./server/contollers/my-controller.js to./server/controllers/seo.js

You are free to name your controllers as you wish by the way!

  • Modify the import of this controller in the./server/controllers/index.js file with the following:
// ./server/controllers/index.jsconstseo=require("./seo");module.exports={seo,};
Enter fullscreen modeExit fullscreen mode

Now it's time to retrieve our Content-Types and return them in response to our action. You can write this logic directly in your controller actionfindSeoComponent but know that you can use services to write your functions.Little reminder: Services are a set of reusable functions. They are particularly useful to respect the "don't repeat yourself" (DRY) programming concept and to simplify controllers' logic.

  • Do exactly the same thing for your service which you will also call seo. Rename the existing service by seo, modify the import in the file at the root:
// ./server/services/index.jsconstseo=require("./seo");module.exports={seo,};
Enter fullscreen modeExit fullscreen mode

Now we will simply retrieve our ContentTypes via thestrapi object which is accessible to us from the back-end part of our plugin.

// ./server/services/seo.jsmodule.exports=({strapi})=>({getContentTypes(){returnstrapi.contentTypes;},});
Enter fullscreen modeExit fullscreen mode

Invoke this service in yourfindSeoComponent action within yourseo controller.

// ./server/controllers/seo.jsmodule.exports={findContentTypes(ctx){ctx.body=strapi.plugin('seo').service('seo').getContentTypes();},
Enter fullscreen modeExit fullscreen mode

Great! You can now fetch your Content-Types! Go ahead try by going to this URL:http://localhost:1337/plugin-name/content-types.

For me here it will behttp://localhost:1337/seo/content-types and I'll get this:

{"collectionTypes":[{"seo":true,"uid":"api::article.article","kind":"collectionType","globalId":"Article","attributes":{"title":{"pluginOptions":{"i18n":{"localized":true}},"type":"string","required":true},"slug":{"pluginOptions":{"i18n":{"localized":true}},"type":"uid","targetField":"title"},//...
Enter fullscreen modeExit fullscreen mode

Don't worry if you don't have the same result as me. Indeed everything depends on your Strapi project. I use for this tutorial our demoFoodAdvisor :)

Great! Your server now knows a route/<plugin-name>/content-types which will call an action from your controller which will use one of your services to return your Content-Types from your Strapi project!

I decided to go to the simplest for this tutorial by giving you the basics and then you can give free rein to your imagination.

Remember the logic to have: Create a route that will call a controller action which then lets you do whatever you want, find information from your Strapi project, call external APIs, etc...

Then you will be able to make this server call from the front of your plugin and that's what we're going to do right away!

Like what I was able to do for the SEO plugin, I'm going to create a simple./admin/src/utils/api.js file which will group all my functions making calls to the back-end of my plugin:

// ./admin/src/utils/api.jsimport{request}from"@strapi/helper-plugin";importpluginIdfrom"../pluginId";constfetchContentTypes=async()=>{try{constdata=awaitrequest(`/${pluginId}/content-types`,{method:"GET"});returndata;}catch(error){returnnull;}};
Enter fullscreen modeExit fullscreen mode

Here I will look for mypluginId which corresponds to the name of your plugin in your./admin/src/package.json:

constpluginId=pluginPkg.name.replace(/^@strapi\/plugin-/i,"");
Enter fullscreen modeExit fullscreen mode

Since my plugin is called@strapi/plugin-seo, the name will be justseo. Indeed, do not forget, from your front-end, to prefix your calls with the name of your plugin:/seo/content-types/ because each plugin has routes that can be called this way, another plugin may have the route/content-types calling another action from another controller etc...

Well, now all you have to do is use this function anywhere in the front-end part of your plugin. For my SEO plugin I use it in the homepage./admin/src/pages/Homepage/index.js like this (simplified version):

/* * * HomePage * *//* * * HomePage * */importReact,{memo,useState,useEffect,useRef}from'react';import{fetchContentTypes}from'../../utils/api';importContentTypesTablefrom'../../components/ContentTypesTable';import{LoadingIndicatorPage}from'@strapi/helper-plugin';import{Box}from'@strapi/design-system/Box';import{BaseHeaderLayout}from'@strapi/design-system/Layout';constHomePage=()=>{constcontentTypes=useRef({});const[isLoading,setIsLoading]=useState(true);useEffect(async()=>{contentTypes.current=awaitfetchContentTypes();// HeresetIsLoading(false);},[]);if(isLoading){return<LoadingIndicatorPage/>;}return(<><Boxbackground="neutral100"><BaseHeaderLayouttitle="SEO"subtitle="Optimize your content to be SEO friendly"as="h2"/></Box><ContentTypesTablecontentTypes={contentTypes.current}/></>);};exportdefaultmemo(HomePage);
Enter fullscreen modeExit fullscreen mode

This page requires the following./admin/src/components/ContentTypesTable/index.js:

/* * * HomePage * */importReactfrom'react';import{Box}from'@strapi/design-system/Box';import{Typography}from'@strapi/design-system/Typography';import{LinkButton}from'@strapi/design-system/LinkButton';import{EmptyStateLayout}from'@strapi/design-system/EmptyStateLayout';import{Flex}from'@strapi/design-system/Flex';import{Table,Thead,Tbody,Tr,Td,Th}from'@strapi/design-system/Table';import{Tabs,Tab,TabGroup,TabPanels,TabPanel,}from'@strapi/design-system/Tabs';constContentTypesTable=({contentTypes})=>{return(<Boxpadding={8}><TabGrouplabel="label"id="tabs"><Tabs><Tab><Typographyvariant="omega">CollectionTypes</Typography></Tab><Tab><Typographyvariant="omega">SingleTypes</Typography></Tab></Tabs><TabPanels><TabPanel>{/* TABLE */}<TablecolCount={2}rowCount={contentTypes.collectionTypes.length}><Thead><Tr><Th><Typographyvariant="sigma">Name</Typography></Th></Tr></Thead><Tbody>{contentTypes&&contentTypes.collectionTypes&&!_.isEmpty(contentTypes.collectionTypes)?(contentTypes.collectionTypes.map((item)=>(<Trkey={item.uid}><Td><TypographytextColor="neutral800">{item.globalId}</Typography></Td><Td><FlexjustifyContent="right"alignItems="right"><LinkButton>Link</LinkButton></Flex></Td></Tr>))):(<Boxpadding={8}background="neutral0"><EmptyStateLayouticon={<Illo/>}content={formatMessage({id:getTrad('SEOPage.info.no-collection-types'),defaultMessage:"You don't have any collection-types yet...",})}action={<LinkButtonto="/plugins/content-type-builder"variant="secondary"startIcon={<Plus/>}>{formatMessage({id:getTrad('SEOPage.info.create-collection-type'),defaultMessage:'Create your first collection-type',})}</LinkButton>}/></Box>)}</Tbody></Table>{/* END TABLE */}</TabPanel><TabPanel>{/* TABLE */}<TablecolCount={2}rowCount={contentTypes.singleTypes.length}><Thead><Tr><Th><Typographyvariant="sigma">Name</Typography></Th></Tr></Thead><Tbody>{contentTypes&&contentTypes.singleTypes&&!_.isEmpty(contentTypes.singleTypes)?(contentTypes.singleTypes.map((item)=>(<Trkey={item.uid}><Td><TypographytextColor="neutral800">{item.globalId}</Typography></Td><Td><FlexjustifyContent="right"alignItems="right"><LinkButton>Link</LinkButton></Flex></Td></Tr>))):(<Boxpadding={8}background="neutral0"><EmptyStateLayouticon={<Illo/>}content={formatMessage({id:getTrad('SEOPage.info.no-single-types'),defaultMessage:"You don't have any single-types yet...",})}action={<LinkButtonto="/plugins/content-type-builder"variant="secondary"startIcon={<Plus/>}>{formatMessage({id:getTrad('SEOPage.info.create-single-type'),defaultMessage:'Create your first single-type',})}</LinkButton>}/></Box>)}</Tbody></Table>{/* END TABLE */}</TabPanel></TabPanels></TabGroup></Box>);};exportdefaultContentTypesTable;
Enter fullscreen modeExit fullscreen mode

Also, let's update thegetContentTypes service to return two different objects, one containing your collection-types, the other one your single-types. Btw, we are doing that just for fun...

  • Replace the code inside your./server/services/seo.js file with the following:
'use strict';module.exports=({strapi})=>({getContentTypes(){constcontentTypes=strapi.contentTypes;constkeys=Object.keys(contentTypes);letcollectionTypes=[];letsingleTypes=[];keys.forEach((name)=>{if(name.includes('api::')){constobject={uid:contentTypes[name].uid,kind:contentTypes[name].kind,globalId:contentTypes[name].globalId,attributes:contentTypes[name].attributes,};contentTypes[name].kind==='collectionType'?collectionTypes.push(object):singleTypes.push(object);}});return{collectionTypes,singleTypes}||null;},});
Enter fullscreen modeExit fullscreen mode

If you go to your plugin page, you will see two tabs separating your collection types and your single types.

Capture d’écran 2022-02-14 à 16.06.16.png

Ignore everything else unless you're curious to see the source code for amore complete plugin. The most important thing here is to know that you can therefore perform this call anywhere in your front-end part of your plugin. You just need to import the function and use it :)

Learn more about plugin development on ourv4 documentation

I think I have pretty much said everything about plugin creation. Let's see how we can inject components into the admin of our Strapi project!

Admin (front-end)

The admin panel is a React application that can embed other React applications. These other React applications are the admin parts of each Strapi plugin. As for the front-end, you must first start with the entry point:./admin/src/index.js.

This file will allow you to define more or less the behavior of your plugin. We can see several things:

register(app){app.addMenuLink({to:`/plugins/${pluginId}`,icon:PluginIcon,intlLabel:{id:`${pluginId}.plugin.name`,defaultMessage:name,},Component:async()=>{constcomponent=awaitimport(/* webpackChunkName: "[request]" */'./pages/App');returncomponent;},permissions:[// Uncomment to set the permissions of the plugin here// {//   action: '', // the action name should be plugin::plugin-name.actionType//   subject: null,// },],});app.registerPlugin({id:pluginId,initializer:Initializer,isReady:false,name,});},
Enter fullscreen modeExit fullscreen mode

First of all, there is aregister function. This function is called to load the plugin, even before the app is actually bootstrapped. It takes the running Strapi application as an argument (app).

Here it tells the admin to display a link in the Strapi menu (app.addMenuLink) for the plugin with a certain Icon, name, and registers the plugin (app.registerPlugin).

Then we find the bootstrap function that is empty for now:

bootstrap(app){};
Enter fullscreen modeExit fullscreen mode

This will expose thebootstrap function, executed after all the plugins are registered.

This function will allow you to inject any front-end components inside your Strapi application thanks to theinjection zones API.

Little parentheses: Know that it is possible to customize the admin using the injection zones API without having to generate a plugin. To do this, simply use the bootstrap function in your./src/admin/app.js file of your Strapi project to inject the components you want.

This is what was done on our demo FoodAdvisor, I redirect you tothis file.

Back to our plugin!

The last part reffers to the translation management of your plugin:

asyncregisterTrads({locales}){constimportedTrads=awaitPromise.all(locales.map((locale)=>{returnimport(`./translations/${locale}.json`).then(({default:data})=>{return{data:prefixPluginTranslations(data,pluginId),locale,};}).catch(()=>{return{data:{},locale,};});}));returnPromise.resolve(importedTrads);},
Enter fullscreen modeExit fullscreen mode

You will be able in the./admin/src/translations folder to add the translations you want.

Ok now let's see how we can inject a simple React component into our Strapi project!
First of all, you have to create this component but since I am a nice person, I have already created it for you, here it is:

// ./admin/src/components/MyCompo/index.jsimportReactfrom'react';import{Box}from'@strapi/design-system/Box';import{Button}from'@strapi/design-system/Button';import{Divider}from'@strapi/design-system/Divider';import{Typography}from'@strapi/design-system/Typography';importEyefrom'@strapi/icons/Eye';import{useCMEditViewDataManager}from'@strapi/helper-plugin';constSeoChecker=()=>{const{modifiedData}=useCMEditViewDataManager();console.log('Current data:',modifiedData);return(<Boxas="aside"aria-labelledby="additional-informations"background="neutral0"borderColor="neutral150"hasRadiuspaddingBottom={4}paddingLeft={4}paddingRight={4}paddingTop={6}shadow="tableShadow"><Box><Typographyvariant="sigma"textColor="neutral600"id="seo">SEOPlugin</Typography><BoxpaddingTop={2}paddingBottom={6}><Divider/></Box><BoxpaddingTop={1}><ButtonfullWidthvariant="secondary"startIcon={<Eye/>}onClick={()=>console.log('Strapi is hiring: https://strapi.io/careers')}>Onebutton</Button></Box></Box></Box>);};exportdefaultSeoChecker;
Enter fullscreen modeExit fullscreen mode

As you can see, this component usesStrapi's Design System. We strongly encourage you to use it for your plugins. I also use theuseCMEditViewDataManager hook which allows access to the data of my entry in the content manager. Since this component will be injected into it, it may be useful to me.

Then all you have to do is inject it in the right place. This component is designed to be injected into the Content Manager (edit-view) in theright-links area. Just inject it into the bootstrap function:

importMyComponentfrom'./components/MyCompo';///...bootstrap(app){app.injectContentManagerComponent('editView','right-links',{name:'MyComponent',Component:MyComponent,});},///...
Enter fullscreen modeExit fullscreen mode

Et voila!

Capture d’écran 2022-02-14 à 15.12.01.png

This button will not trigger anything unfortunately but feel free to customize it!

I let you develop your own plugin yourself now! I think you have the basics to do just about anything!
Know in any case that Strapi now has aMarketplace that lists the plugins of the community. Feel free to submit yours ;)

See you in the next article!

Ralph rolling away

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Do you want to contribute to open source and see what's in Strapi?

All of Strapi is available on GitHub.

Check it out!

More fromStrapi

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp