Local plugins

    Create a development project

    1. Create a new project cd .. && strapi new myDevelopmentProject.
    2. cd myDevelopmentProject && strapi develop To start the Strapi project

    Plugin development Setup

    In a new terminal window:

    Generate a new plugin: cd /path/to/myDevelopmentProject && strapi generate:plugin my-plugin

    NOTE

    After you have successfully generated a plugin, you need to run strapi build which adds the new plugin to the admin panel.

    Plugin Folders and Files Architecture

    The logic of a plugin is located at its root directory ./plugins/**. The admin panel related parts of each plugin are contained in the /admin folder. The folders and files structure are the following:

    This section explains how the ‘back-end part’ of your plugin works.

    Routes

    The plugin API routes are defined in the ./plugins/**/config/routes.json file.

    TIP

    Please refer to for information.

    Route prefix

    Each route of a plugin is prefixed by the name of the plugin (eg: /my-plugin/my-plugin-route). Using the prefix key you can change this option to something custom. You can disable the prefix, by setting the config.prefix key to an empty string.

    1. {
    2. "method": "GET",
    3. "path": "/my-plugin-route",
    4. "handler": "MyPlugin.action",
    5. "config": {
    6. "policies": [],
    7. "prefix": "my-custom-prefix"
    8. }
    9. }

    CLI

    The CLI can be used to generate files in the plugins folders.

    Please refer to the for more information.

    Controllers contain functions executed according to the requested route.

    Please refer to the Controllers documentation for more information.

    Models

    A plugin can have its own models.

    Table/Collection naming

    Sometimes it happens that the plugins inject models that have the same name as yours. Let’s take a quick example.

    You already have User model defining in your ./api/user/models/User.settings.json API. And you decide to install the Users & Permissions plugin. This plugin also contains a User model. To avoid the conflicts, the plugins’ models are not globally exposed which means you cannot access to the plugin’s model like this:

    1. module.exports = {
    2. findUser: async function(params) {
    3. // This `User` global variable will always make a reference the User model defining in your `./api/xxx/models/User.settings.json`.
    4. return await User.find();
    5. },
    6. };

    Also, the table/collection name won’t be users because you already have a User model. That’s why, the framework will automatically prefix the table/collection name for this model with the name of the plugin. Which means in our example, the table/collection name of the User model of our plugin Users & Permissions will be users-permissions_users. If you want to force the table/collection name of the plugin’s model, you can add the collectionName attribute in your model.

    Please refer to the for more information.

    Policies

    Global policies

    A plugin can also use a globally exposed policy in the current Strapi project.

    1. {
    2. "routes": [
    3. {
    4. "method": "GET",
    5. "path": "/",
    6. "handler": "MyPlugin.index",
    7. "config": {
    8. "policies": ["global::isAuthenticated"]
    9. }
    10. }
    11. ]
    12. }

    Plugin policies

    A plugin can have its own policies, such as adding security rules. For instance, if the plugin includes a policy named isAuthenticated, the syntax to use this policy would be:

    1. {
    2. "routes": [
    3. {
    4. "method": "GET",
    5. "path": "/",
    6. "handler": "MyPlugin.index",
    7. "config": {
    8. "policies": ["plugins::myplugin.isAuthenticated"]
    9. }
    10. }
    11. ]
    12. }

    Please refer to the for more information.

    Front-end Development

    Strapi’s admin panel and plugins system aim to be an easy and powerful way to create new features.

    The admin panel is a application which can embed other React applications. These other React applications are the admin parts of each Strapi’s plugins.

    Environment setup

    To enable local plugin development, you need to start your application with the front-end development mode activated:

    1. $ cd my-app
    2. $ yarn develop --watch-admin
    1. $ cd my-app
    2. $ npm run develop -- --watch-admin

    Strapi global variable

    The administration exposes a global variable that is accessible for all the plugins.

    strapi.backendURL

    Retrieve the back-end URL. (e.g. http://localhost:1337).

    strapi.currentLanguage

    Retrieve the administration panel default language (e.g. en-US)

    strapi.languages

    Array of the administration panel’s supported languages. (e.g. ['ar', 'en', 'fr', ...]).

    strapi.lockApp()
    strapi.unlockApp()

    Remove the loader so the user can interact with the application

    strapi.notification

    Use this command anywhere in your code.

    1. strapi.notification.toggle(config);

    The properties of the config object are as follows:

    The previous notification API is still working but will display a warning message in the console

    strapi.remoteURL

    The administration url (e.g. http://localhost:4000/admin).

    Main plugin object

    Each plugin exports all its configurations in an object. This object is located in my-plugin/admin/src/index.js

    Here are its properties:

    Displaying the plugin’s link in the main menu

    To display a plugin link into the main menu the plugin needs to export a menu object.

    Path — plugins/my-plugin/admin/src/index.js.

    1. import pluginPkg from '../../package.json';
    2. import pluginLogo from './assets/images/logo.svg';
    3. import App from './containers/App';
    4. import lifecycles from './lifecycles';
    5. import trads from './translations';
    6. import pluginId from './pluginId';
    7. export default strapi => {
    8. const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
    9. const icon = pluginPkg.strapi.icon;
    10. const name = pluginPkg.strapi.name;
    11. const plugin = {
    12. blockerComponent: null,
    13. blockerComponentProps: {},
    14. description: pluginDescription,
    15. icon,
    16. id: pluginId,
    17. initializer: null,
    18. isRequired: pluginPkg.strapi.required || false,
    19. layout: null,
    20. lifecycles,
    21. mainComponent: App,
    22. name,
    23. pluginLogo,
    24. preventComponentRendering: false,
    25. trads,
    26. menu: {
    27. // Set a link into the PLUGINS section
    28. pluginsSectionLinks: [
    29. {
    30. destination: `/plugins/${pluginId}`, // Endpoint of the link
    31. icon,
    32. label: {
    33. id: `${pluginId}.plugin.name`, // Refers to a i18n
    34. defaultMessage: 'My PLUGIN',
    35. },
    36. name,
    37. // If the plugin has some permissions on whether or not it should be accessible
    38. // depending on the logged in user's role you can set them here.
    39. // Each permission object performs an OR comparison so if one matches the user's ones
    40. // the link will be displayed
    41. permissions: [{ action: 'plugins::content-type-builder.read', subject: null }],
    42. },
    43. ],
    44. },
    45. };
    46. return strapi.registerPlugin(plugin);
    47. };

    Initializer

    The component is generated by default when you create a new plugin. Use this component to execute some logic when the app is loading. When the logic has been executed this component should emit the isReady event so the user can interact with the application.

    NOTE

    Below is the Initializer component of the content-type-builder plugin.

    It checks whether or not the auto-reload feature is enabled and depending on this value changes the mainComponent of the plugin.

    1. /**
    2. *
    3. * Initializer
    4. *
    5. */
    6. import React from 'react';
    7. import PropTypes from 'prop-types';
    8. import pluginId from '../../pluginId';
    9. class Initializer extends React.PureComponent {
    10. // eslint-disable-line react/prefer-stateless-function
    11. componentDidMount() {
    12. const {
    13. admin: { autoReload, currentEnvironment },
    14. } = this.props;
    15. let preventComponentRendering;
    16. let blockerComponentProps;
    17. if (currentEnvironment === 'production') {
    18. preventComponentRendering = true;
    19. blockerComponentProps = {
    20. blockerComponentTitle: 'components.ProductionBlocker.header',
    21. blockerComponentDescription: 'components.ProductionBlocker.description',
    22. blockerComponentIcon: 'fa-ban',
    23. blockerComponentContent: 'renderButton',
    24. };
    25. } else {
    26. // Don't render the plugin if the server autoReload is disabled
    27. preventComponentRendering = !autoReload;
    28. blockerComponentProps = {
    29. blockerComponentTitle: 'components.AutoReloadBlocker.header',
    30. blockerComponentDescription: 'components.AutoReloadBlocker.description',
    31. blockerComponentIcon: 'fa-refresh',
    32. blockerComponentContent: 'renderIde',
    33. };
    34. }
    35. // Prevent the plugin from being rendered if currentEnvironment === PRODUCTION
    36. this.props.updatePlugin(pluginId, 'preventComponentRendering', preventComponentRendering);
    37. this.props.updatePlugin(pluginId, 'blockerComponentProps', blockerComponentProps);
    38. // Emit the event plugin ready
    39. this.props.updatePlugin(pluginId, 'isReady', true);
    40. }
    41. render() {
    42. return null;
    43. }
    44. }
    45. Initializer.propTypes = {
    46. admin: PropTypes.object.isRequired,
    47. updatePlugin: PropTypes.func.isRequired,
    48. };
    49. export default Initializer;

    Injected Components

    (Coming soon)

    Routing

    The routing is based on the React Router V5Local plugins - 图3 (opens new window), due to it’s implementation each route is declared in the containers/App/index.js file.

    TIP

    Each route defined in a plugin must be prefixed by the plugin’s id.

    Route declaration :

    Let’s say that you want to create a route /user with params /:id associated with the container UserPage.

    The declaration would be as follows :

    Path — plugins/my-plugin/admin/src/containers/App/index.js.

    1. import React from 'react';
    2. import pluginId from '../../pluginId';
    3. import UserPage from '../UserPage';
    4. // ...
    5. class App extends React.Component {
    6. // ...
    7. render() {
    8. return (
    9. <div>
    10. <Switch>
    11. <Route exact path={`/plugins/${pluginId}/user/:id`} component={UserPage} />
    12. </Switch>
    13. </div>
    14. );
    15. }
    16. }
    17. // ...

    Styling

    The administration panel uses styled-components (opens new window) for writing css.

    i18n

    React IntlLocal plugins - 图5 (opens new window) provides React components and an API to format dates, numbers, and strings, including pluralization and handling translations.

    Usage

    We recommend to set all your components text inside the translations folder.

    The example below shows how to use i18n inside your plugin.

    Define all your ids with the associated message:

    Path — ./plugins/my-plugin/admin/src/translations/en.json.

    1. {
    2. "notification.error.message": "An error occurred"
    3. }

    Path — ./plugins/my-plugin/admin/src/translations/fr.json

    1. {
    2. "notification.error.message": "Une erreur est survenue"
    3. }

    Usage inside a component

    Path — ./plugins/my-plugin/admin/src/components/Foo/index.js.

    1. import { FormattedMessage } from 'react-intl';
    2. import SomeOtherComponent from 'components/SomeOtherComponent';
    3. const Foo = props => (
    4. <div className={styles.foo}>
    5. <FormattedMessage id="my-plugin.notification.error.message" />
    6. <SomeOtherComponent {...props} />
    7. </div>
    8. );
    9. export default Foo;

    Global context

    All plugins are wrapped inside the GlobalContextProvider, in this object you will have access to all plugins object as well as other utilities.

    Usage:

    Inside a functional component:

    1. import React from 'react';
    2. import { useGlobalContext } from 'strapi-helper-plugin';
    3. const Foo = () => {
    4. const globalContext = useGlobalContext();
    5. console.log(globalContext);
    6. return <div>Foo</div>;
    7. };

    Inside a class component:

    As plugins developer you may need to add custom fields in your application. To do so, a Field API is available in order for a plugin to register a field which will be available for all plugins.

    NOTE

    Currently, only the content manager uses this API to extend its current fields.

    Registering a new field

    Registering a field can be made in two different ways:

    1. During the load phase of a plugin
    2. Using the provided react-hook in a component.

    Registering a field during the load of a plugin

    Registering a field during the load phase of a plugin can be done as follows:

    1. Create a new Field type (in this example a media field type):

    Path — plugins/my-plugin/admin/src/components/InputMedia/index.js.

    1. import React from 'react';
    2. const InputMedia = props => {
    3. // Check out the provided props
    4. console.log(props);
    5. return <div>InputMedia</div>;
    6. };
    7. export default InputMedia;
    1. Register the field into the application:

    Path — plugins/my-plugin/admin/src/index.js.

    1. import pluginPkg from '../../package.json';
    2. import InputMedia from './components/InputMedia';
    3. import pluginId from './pluginId';
    4. export default strapi => {
    5. const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
    6. const plugin = {
    7. blockerComponent: null,
    8. blockerComponentProps: {},
    9. description: pluginDescription,
    10. icon: pluginPkg.strapi.icon,
    11. id: pluginId,
    12. initializer: () => null,
    13. injectedComponents: [],
    14. isReady: true,
    15. mainComponent: null,
    16. name: pluginPkg.strapi.name,
    17. preventComponentRendering: false,
    18. trads: {},
    19. };
    20. strapi.registerField({ type: 'media', Component: InputMedia });
    21. return strapi.registerPlugin(plugin);
    22. };

    By doing so, all the plugins from your project will be able to use the newly registered Field type.

    Registering a field inside a React Component

    The other way to register a Field is to use the provided react-hook: useStrapi it can be done in the Initializer Component so it is accessible directly when the user is logged in, if you decide to register your plugin in another component than the Initializer the Field will only be registered in the administration panel once the component is mounted (the user has navigated to the view where the Field is registered).

    1. Register the Field in the Initializer Component:

    Path — plugins/my-plugin/admin/src/containers/Initializer/index.js.

    1. /**
    2. *
    3. * Initializer
    4. *
    5. */
    6. import { useEffect, useRef } from 'react';
    7. import PropTypes from 'prop-types';
    8. import { useStrapi } from 'strapi-helper-plugin';
    9. import pluginId from '../../pluginId';
    10. import InputMedia from './components/InputMedia';
    11. const Initializer = ({ updatePlugin }) => {
    12. const {
    13. strapi: { fieldApi },
    14. } = useStrapi();
    15. const ref = useRef();
    16. ref.current = updatePlugin;
    17. useEffect(() => {
    18. // Register the new field
    19. fieldApi.registerField({ type: 'media', Component: InputMedia });
    20. ref.current(pluginId, 'isReady', true);
    21. }, []);
    22. return null;
    23. };
    24. Initializer.propTypes = {
    25. updatePlugin: PropTypes.func.isRequired,
    26. };
    27. export default Initializer;
    1. Add the Initializer component to your plugin so it is mounted in the administration panel once the user is logged in:
    1. import pluginPkg from '../../package.json';
    2. import pluginLogo from './assets/images/logo.svg';
    3. import App from './containers/App';
    4. import Initializer from './containers/Initializer';
    5. import lifecycles from './lifecycles';
    6. import trads from './translations';
    7. import pluginId from './pluginId';
    8. export default strapi => {
    9. const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
    10. const plugin = {
    11. blockerComponent: null,
    12. blockerComponentProps: {},
    13. description: pluginDescription,
    14. icon: pluginPkg.strapi.icon,
    15. id: pluginId,
    16. initializer: Initializer,
    17. injectedComponents: [],
    18. isRequired: pluginPkg.strapi.required || false,
    19. layout: null,
    20. lifecycles,
    21. mainComponent: App,
    22. name: pluginPkg.strapi.name,
    23. pluginLogo,
    24. preventComponentRendering: false,
    25. trads,
    26. };
    27. return strapi.registerPlugin(plugin);
    28. };

    Consuming the Field API

    Consuming the Field API can only be done by using the provided react-hook useStrapi. Here’s an example from the content-manager plugin:

    Path — ~/strapi-plugin-content-manager/admin/src/components/Inputs/index.js.

    1. import React, { memo, useMemo } from 'react';
    2. // Other imports
    3. // ...
    4. // Import the Inputs component from our component library Buffet.js
    5. import { Inputs as InputsIndex } from '@buffetjs/custom';
    6. // Import the Hook with which you can access the Field API
    7. import { useStrapi } from 'strapi-helper-plugin';
    8. function Inputs({ autoFocus, keys, layout, name, onBlur }) {
    9. // This is where you will access the field API
    10. const {
    11. strapi: { fieldApi },
    12. } = useStrapi();
    13. // Other boilerplate code
    14. // ...
    15. return (
    16. <FormattedMessage id={errorId}>
    17. {error => {
    18. return (
    19. <InputsIndex
    20. {...metadatas}
    21. autoComplete="new-password"
    22. autoFocus={autoFocus}
    23. didCheckErrors={didCheckErrors}
    24. disabled={disabled}
    25. error={
    26. ? null
    27. : error
    28. }
    29. inputDescription={description}
    30. description={description}
    31. contentTypeUID={layout.uid}
    32. customInputs={{
    33. json: InputJSONWithErrors,
    34. wysiwyg: WysiwygWithErrors,
    35. uid: InputUID,
    36. // Retrieve all the fields that other plugins have registered
    37. ...fieldApi.getFields(),
    38. }}
    39. multiple={get(attribute, 'multiple', false)}
    40. attribute={attribute}
    41. name={keys}
    42. onBlur={onBlur}
    43. onChange={onChange}
    44. options={enumOptions}
    45. step={step}
    46. type={getInputType(type)}
    47. validations={validations}
    48. value={inputValue}
    49. withDefaultValue={false}
    50. />
    51. );
    52. }}
    53. </FormattedMessage>
    54. );
    55. }

    Field API definition

    Plugin’s front-end settings API

    As plugins developer you may need to add some settings into the main application Settings view (it corresponds to the Settings link located in the menu). To do so an API is available in order for a plugin to add links into the main view.

    These settings can be declared directly into the main plugin object so they will dynamically be injected into the view.

    The front-end part of a plugin exports a function which registers the plugin in the administration panel. The argument is composed of two main parameters:

    • registerPlugin: Function
    • settingsBaseURL: String

    Creating the links into the view’s menu

    Each plugin that comes with a setting object will create a new section into the view’s menu.

    The menu section can be declared as follows:

    Path — plugins/my-plugin/admin/src/index.js.

    1. import pluginPkg from '../../package.json';
    2. import pluginId from './pluginId';
    3. export default strapi => {
    4. const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
    5. // Declare the links that will be injected into the settings menu
    6. const menuSection = {
    7. // Unique id of the section
    8. id: pluginId,
    9. // Title of Menu section using i18n
    10. title: {
    11. id: `${pluginId}.foo`,
    12. defaultMessage: 'Super cool setting',
    13. },
    14. // Array of links to be displayed
    15. links: [
    16. {
    17. // Using string
    18. title: 'Setting page 1',
    19. to: `${strapi.settingsBaseURL}/${pluginId}/setting1`,
    20. name: 'setting1',
    21. permissions: [{ action: 'plugins::my-plugin.action-name', subject: null }], // This key is not mandatory it can be null, undefined or an empty array
    22. },
    23. {
    24. // Using i18n with a corresponding translation key
    25. title: {
    26. id: `${pluginId}.bar`,
    27. defaultMessage: 'Setting page 2',
    28. },
    29. to: `${strapi.settingsBaseURL}/${pluginId}/setting2`,
    30. name: 'setting2',
    31. // Define a specific component if needed:
    32. Component: () => <div />,
    33. },
    34. ],
    35. };
    36. const plugin = {
    37. blockerComponent: null,
    38. blockerComponentProps: {},
    39. description: pluginDescription,
    40. icon: pluginPkg.strapi.icon,
    41. id: pluginId,
    42. initializer: () => null,
    43. injectedComponents: [],
    44. isReady: true,
    45. mainComponent: null,
    46. name: pluginPkg.strapi.name,
    47. preventComponentRendering: false,
    48. settings: {
    49. menuSection,
    50. },
    51. trads: {},
    52. };
    53. return strapi.registerPlugin(plugin);
    54. };

    At this point, the plugin creates a new section (Super cool setting) which will contains two links Setting page 1 and Setting page 2 these links don’t point to any component as the corresponding one as not been declared yet.

    Declaring the setting Component

    The exported Setting component which receives settingsBaseURL as props in order to generate a dynamic routing which should be used to associate the two endpoints created with their corresponding components.

    With the configuration from above we could easily create our plugin Settings view.

    Path — plugins/my-plugin/admin/src/containers/Settings/index.js.

    1. import React from 'react';
    2. import PropTypes from 'prop-types';
    3. import { Switch, Route } from 'react-router-dom';
    4. import pluginId from '../../pluginId';
    5. const SettingPage1 = () => (
    6. <div>
    7. <h1>Setting Page 1</h1>
    8. </div>
    9. );
    10. const SettingPage2 = () => (
    11. <div>
    12. <h1>Setting Page 2</h1>
    13. </div>
    14. );
    15. const Settings = ({ settingsBaseURL }) => {
    16. return (
    17. <Switch>
    18. <Route component={SettingPage1} path={`${settingsBaseURL}/${pluginId}/setting1`} />
    19. <Route component={SettingPage2} path={`${settingsBaseURL}/${pluginId}/setting2`} />
    20. </Switch>
    21. );
    22. };
    23. Settings.propTypes = {
    24. settingsBaseURL: PropTypes.string.isRequired,
    25. };
    26. export default Settings;

    Now that the Settings component is declared in your plugin the only thing left is to add it to your settings configuration:

    Path — plugins/my-plugin/admin/src/index.js.

    Adding a setting into the global section

    In order to add a link into the global section of the settings view you need to create a global array containing the links you want to add:

    Path — plugins/my-plugin/admin/src/index.js.

    1. import pluginPkg from '../../package.json';
    2. // Import the component
    3. import Settings from './containers/Settings';
    4. import SettingLink from './components/SettingLink';
    5. import pluginId from './pluginId';
    6. export default strapi => {
    7. const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
    8. // Declare the links that will be injected into the settings menu
    9. const menuSection = {
    10. id: pluginId,
    11. title: {
    12. id: `${pluginId}.foo`,
    13. defaultMessage: 'Super cool setting',
    14. },
    15. links: [
    16. {
    17. title: 'Setting page 1',
    18. to: `${strapi.settingsBaseURL}/${pluginId}/setting1`,
    19. name: 'setting1',
    20. },
    21. {
    22. title: {
    23. id: `${pluginId}.bar`,
    24. defaultMessage: 'Setting page 2',
    25. },
    26. to: `${strapi.settingsBaseURL}/${pluginId}/setting2`,
    27. name: 'setting2',
    28. },
    29. ],
    30. };
    31. const plugin = {
    32. blockerComponent: null,
    33. blockerComponentProps: {},
    34. description: pluginDescription,
    35. icon: pluginPkg.strapi.icon,
    36. id: pluginId,
    37. initializer: () => null,
    38. injectedComponents: [],
    39. isReady: true,
    40. mainComponent: null,
    41. name: pluginPkg.strapi.name,
    42. preventComponentRendering: false,
    43. settings: {
    44. // Add a link into the global section of the settings view
    45. global: {
    46. links: [
    47. {
    48. title: 'Setting link 1',
    49. to: `${strapi.settingsBaseURL}/setting-link-1`,
    50. name: 'settingLink1',
    51. Component: SettingLink,
    52. // Bool : https://reacttraining.com/react-router/web/api/Route/exact-bool
    53. exact: false,
    54. permissions: [{ action: 'plugins::my-plugin.action-name', subject: null }],
    55. },
    56. ],
    57. },
    58. mainComponent: Settings,
    59. menuSection,
    60. },
    61. trads: {},
    62. };
    63. return strapi.registerPlugin(plugin);

    WARNING