From 437b6663fd240e8edde12c90cf394ba9531fabe9 Mon Sep 17 00:00:00 2001
From: Pierre Wizla
Date: Tue, 19 Dec 2023 12:06:32 +0100
Subject: [PATCH] Rework and expand Plugins Development section (#1922)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Rework plugin creation and setup page
* Clean up intro
* Add WIP plugin structure docs
* Add marketplace guide
* Fix TOC and next nav item
* Improve the create a plugin page further
* Fix the plugin structure files
* Improve the plugin structure section
* Refine wordings and improve the journey
* Fix comment wording in plugin structure
* Fine-tune the Admin Panel and Server API docs
* Fine-tune the plugin creation and structure docs
* Add guide to store and access data from a plugin
* WIP guides
* Fix breadcrumbs
* Improve intro
* Revert "Fix breadcrumbs"
This reverts commit 94124cc57fd8043d9e6e3d067584a4ef6ddfe7e9.
* Add "pass data" guide
* Add external resources section
* Fix broken link
* Convert "external resources" section to a callout
* Improve plugin middlewares example
* Improve lifecycle functions documentation
* Tweak links to Marketplace
* Remove unused guides links and add additional resources
* Add missing description SEO tags
* Fix blog link
* Clean up TOC to remove duplicates
* Remove Custom Fields from Dev as it's in Plugins
* Display h4s in secondary nav.
* Explain top-level vs. global getters
* Replace "boilerplate" with more accurate descriptions
* Explain hot reloading
* Remove a completed to-do
* Slightly format table
* Break down a sentence that was too long
* Fix bad component naming Tab β TabItem
---
.../dev-docs/api/plugins/admin-panel-api.md | 24 +-
.../docs/dev-docs/api/plugins/server-api.md | 91 +++--
.../docs/dev-docs/configurations/functions.md | 20 ++
.../dev-docs/plugins/developing-plugins.md | 80 ++---
.../plugins/development/create-a-plugin.md | 314 ++++++++++++++++++
.../plugins/development/plugin-structure.md | 39 +++
.../dev-docs/plugins/guides/marketplace.md | 203 +++++++++++
.../guides/pass-data-from-server-to-admin.md | 101 ++++++
.../plugins/guides/store-and-access-data.md | 176 ++++++++++
docusaurus/sidebars.js | 30 +-
docusaurus/src/components/PluginStructure.js | 131 ++++++++
.../ReusableAnnotationComponents.jsx | 36 ++
docusaurus/src/theme/MDXComponents.js | 6 +
.../generate-plugin-content-type.png | Bin 0 -> 74136 bytes
14 files changed, 1132 insertions(+), 119 deletions(-)
create mode 100644 docusaurus/docs/dev-docs/plugins/development/create-a-plugin.md
create mode 100644 docusaurus/docs/dev-docs/plugins/development/plugin-structure.md
create mode 100644 docusaurus/docs/dev-docs/plugins/guides/marketplace.md
create mode 100644 docusaurus/docs/dev-docs/plugins/guides/pass-data-from-server-to-admin.md
create mode 100644 docusaurus/docs/dev-docs/plugins/guides/store-and-access-data.md
create mode 100644 docusaurus/src/components/PluginStructure.js
create mode 100644 docusaurus/src/components/ReusableAnnotationComponents/ReusableAnnotationComponents.jsx
create mode 100644 docusaurus/static/img/assets/development/generate-plugin-content-type.png
diff --git a/docusaurus/docs/dev-docs/api/plugins/admin-panel-api.md b/docusaurus/docs/dev-docs/api/plugins/admin-panel-api.md
index 8525705888..d594e0e529 100644
--- a/docusaurus/docs/dev-docs/api/plugins/admin-panel-api.md
+++ b/docusaurus/docs/dev-docs/api/plugins/admin-panel-api.md
@@ -1,26 +1,28 @@
---
sidebar_label: Admin Panel API
-pagination_prev: dev-docs/plugins/developing-plugins
+pagination_prev: dev-docs/plugins/development/plugin-structure
+toc_max_heading_level: 4
---
# Admin Panel API for plugins
-A Strapi [plugin](/dev-docs/plugins) can interact with both the [back end](/dev-docs/api/plugins/server-api) or the front end of the Strapi app. The Admin Panel API is about the front end part, i.e. it allows a plugin to customize Strapi's [admin panel](/user-docs/intro).
+A Strapi [plugin](/dev-docs/plugins) can interact with both the [back end](/dev-docs/api/plugins/server-api) and the front end of a Strapi application. The Admin Panel API is about the front end part, i.e. it allows a plugin to customize Strapi's [admin panel](/user-docs/intro).
The admin panel is a [React](https://reactjs.org/) application that can embed other React applications. These other React applications are the admin parts of each Strapi plugin.
-To create a plugin that interacts with the Admin Panel API:
-
-1. Create an [entry file](#entry-file).
-2. Within this file, declare and export a plugin interface that uses the [available actions](#available-actions).
-3. Require this plugin interface in a `strapi-admin.js` file at the root of the plugin package folder:
+:::prerequisites
+You have [created a Strapi plugin](/dev-docs/plugins/development/create-a-plugin).
+:::
- ```js title="[plugin-name]/strapi-admin.js"
+The Admin Panel API includes:
- 'use strict';
+- an [entry file](#entry-file) which exports the required interface,
+- [lifecycle functions](#lifecycle-functions) and the `registerTrad()` [async function](#async-function),
+- and several [specific APIs](#available-actions) for your plugin to interact with the admin panel.
- module.exports = require('./admin/src').default;
- ```
+:::note
+The whole code for the admin panel part of your plugin could live in the `/strapi-admin.js|ts` or `/admin/src/index.js|ts` file. However, it's recommended to split the code into different folders, just like the [structure](/dev-docs/plugins/development/plugin-structure) created by the `strapi generate plugin` CLI generator command.
+:::
## Entry file
diff --git a/docusaurus/docs/dev-docs/api/plugins/server-api.md b/docusaurus/docs/dev-docs/api/plugins/server-api.md
index 37cbabebb1..0b02aaf45e 100644
--- a/docusaurus/docs/dev-docs/api/plugins/server-api.md
+++ b/docusaurus/docs/dev-docs/api/plugins/server-api.md
@@ -3,18 +3,29 @@ title: Server API for plugins
sidebar_label: Server API
displayed_sidebar: devDocsSidebar
description: Strapi's Server API for plugins allows a Strapi plugin to customize the back end part (i.e. the server) of your application.
-sidebarDepth: 3
-
---
# Server API for plugins
-A Strapi [plugin](/dev-docs/plugins) can interact with the backend or the [frontend](/dev-docs/api/plugins/admin-panel-api) of the Strapi application. The Server API is about the backend part.
+A Strapi [plugin](/dev-docs/plugins) can interact with both the back end and the [front end](/dev-docs/api/plugins/admin-panel-api) of a Strapi application. The Server API is about the back-end part, i.e. how the plugin interacts with the server part of a Strapi application.
+
+:::prerequisites
+You have [created a Strapi plugin](/dev-docs/plugins/development/create-a-plugin).
+:::
+
+The Server API includes:
+
+- an [entry file](#entry-file) which export the required interface,
+- [lifecycle functions](#lifecycle-functions),
+- a [configuration](#configuration) API,
+- the ability to add [cron](#cron) jobs,
+- and the ability to [customize all elements of the back-end server](#backend-customization).
-Creating and using a plugin interacting with the Server API consists of 2 steps:
+Once you have declared and exported the plugin interface, you will be able to [use the plugin interface](#usage).
-1. Declare and export the plugin interface within the [`strapi-server.js` entry file](#entry-file)
-2. [Use the exported interface](#usage)
+:::note
+The whole code for the server part of your plugin could live in the `/strapi-server.js|ts` or `/server/index.js|ts` file. However, it's recommended to split the code into different folders, just like the [structure](/dev-docs/plugins/development/plugin-structure) created by the `strapi generate plugin` CLI generator command.
+:::
## Entry file
@@ -81,7 +92,7 @@ module.exports = () => ({
## Configuration
-`config` stores the default plugin configuration.
+`config` stores the default plugin configuration. It loads and validates the configuration inputted from the user within the [`./config/plugins.js` configuration file](/dev-docs/configurations/plugins).
**Type**: `Object`
@@ -113,6 +124,10 @@ Once defined, the configuration can be accessed:
- with `strapi.plugin('plugin-name').config('some-key')` for a specific configuration property,
- or with `strapi.config.get('plugin.plugin-name')` for the whole configuration object.
+:::tip
+Run `yarn strapi console` or `npm run strapi console` to access the strapi object in a live console.
+:::
+
## Cron
The `cron` object allows you to add cron jobs to the Strapi instance.
@@ -155,6 +170,12 @@ strapi.cron.jobs
## Backend customization
+All elements of the back-end server of Strapi can be customized through a plugin using the Server API.
+
+:::prerequisites
+To better understand this section, ensure you have read through the [back-end customization](/dev-docs/backend-customization) documentation of a Strapi application.
+:::
+
### Content-types
An object with the [content-types](/dev-docs/backend-customization/models) the plugin provides.
@@ -461,50 +482,54 @@ An object with the [middlewares](/dev-docs/configurations/middlewares) the plugi
**Example:**
-```js title="./src/plugins/my-plugin/strapi-server.js"
-
-"use strict";
-
-module.exports = require('./server');
-```
+```js title="./src/plugins/my-plugin/server/middlewares/your-middleware.js"
-```js title="./src/plugins/my-plugin/server/index.js"
-
-const middlewares = require('./middlewares');
-module.exports = () => ({
- middlewares,
-});
+/**
+ * The your-middleware.js file
+ * declares a basic middleware function and exports it.
+ */
+'use strict';
+module.exports = async (ctx, next) => {
+ console.log("your custom logic")
+ await next();
+}
```
```js title="./src/plugins/my-plugin/server/middlewares/index.js"
-const middlewareA = require('./middleware-a');
-const middlewareB = require('./middleware-b');
+/**
+ * The middleware function previously created
+ * is imported from its file and
+ * exported by the middlewares index.
+ */
+'use strict';
+const yourMiddleware = require('./your-middleware');
module.exports = {
- middlewareA,
- middlewareB,
+ yourMiddleware
};
```
-```js title="./src/plugins/my-plugin/server/middlewares/middleware-a.js"
+```js title="./src/plugins/my-plugin/server/register.js"
-module.exports = (options, { strapi }) => {
- return async (ctx, next) => {
- const start = Date.now();
- await next();
- const delta = Math.ceil(Date.now() - start);
+/**
+ * The middleware is called from
+ * the plugin's register lifecycle function.
+ */
+'use strict';
+const middlewares = require('./middlewares');
- strapi.log.http(`${ctx.method} ${ctx.url} (${delta} ms) ${ctx.status}`);
- };
+module.exports = ({ strapi }) => {
+ strapi.server.use(middlewares.yourMiddleware);
};
```
## Usage
-Once a plugin is exported and loaded into Strapi, its features are accessible in the code through getters. The Strapi instance (`strapi`) exposes top-level getters and global getters.
+Once a plugin is exported and loaded into Strapi, its features are accessible in the code through getters. The Strapi instance (`strapi`) exposes both top-level getters and global getters:
-While top-level getters imply chaining functions, global getters are syntactic sugar that allows direct access using a feature's uid:
+- top-level getters imply chaining functions (e.g., `strapi.plugin('the-plugin-name').controller('the-controller-name'`),
+- global getters are syntactic sugar that allows direct access using a feature's uid (e.g., `strapi.controller('plugin::plugin-name.controller-name')`).
```js
// Access an API or a plugin controller using a top-level getter
diff --git a/docusaurus/docs/dev-docs/configurations/functions.md b/docusaurus/docs/dev-docs/configurations/functions.md
index 678b9f0a57..3931fbd6cc 100644
--- a/docusaurus/docs/dev-docs/configurations/functions.md
+++ b/docusaurus/docs/dev-docs/configurations/functions.md
@@ -11,6 +11,18 @@ The `./src/index.js` file (or `./src/index.ts` file in a [TypeScript-based](/dev
The functions can be synchronous, asynchronous, or return a promise.
+``` mermaid
+flowchart TB
+ A([The Strapi application starts.]) --> B{"register()"}
+ B -- The Strapi application is setup. --> C
+ C{"bootstrap()"} -- The Strapi back-end server starts. --> D
+ D(Request)
+ D
+ click B "#register"
+ click C "#bootstrap"
+ click D "/dev-docs/backend-customization/requests-responses"
+```
+
## Synchronous function
@@ -147,6 +159,8 @@ It can be used to:
- load some [environment variables](/dev-docs/configurations/environment)
- register a [custom field](/dev-docs/custom-fields) that would be used only by the current Strapi application.
+`register()` is the very first thing that happens when a Strapi application is starting. This happens _before_ any setup process and you don't have any access to database, routes, policies, or any other backend server elements within the `register()` function.
+
## Bootstrap
The `bootstrap` lifecycle function, found in `./src/index.js` (or in `./src/index.ts`), is called at every server start.
@@ -157,6 +171,12 @@ It can be used to:
- fill the database with some necessary data
- declare custom conditions for the [Role-Based Access Control (RBAC)](/dev-docs/configurations/rbac) feature
+The `bootstrapi()` function is run _before_ the back-end server starts but _after_ the Strapi application has setup, so you have access to anything from the `strapi` object.
+
+:::tip
+You can run `yarn strapi console` (or `npm run strapi console`) in the terminal and interact with the `strapi` object.
+:::
+
## Destroy
The `destroy` function, found in `./src/index.js` (or in `./src/index.ts`), is an asynchronous function that runs before the application gets shut down.
diff --git a/docusaurus/docs/dev-docs/plugins/developing-plugins.md b/docusaurus/docs/dev-docs/plugins/developing-plugins.md
index 1bae1c4d41..822166d003 100644
--- a/docusaurus/docs/dev-docs/plugins/developing-plugins.md
+++ b/docusaurus/docs/dev-docs/plugins/developing-plugins.md
@@ -1,9 +1,9 @@
---
title: Developing plugins
-# description: todo
+description: Generation introduction about Strapi plugins development
displayed_sidebar: devDocsSidebar
pagination_prev: dev-docs/plugins
-pagination_next: dev-docs/api/plugins/admin-panel-api
+pagination_next: dev-docs/plugins/development/create-a-plugin
---
# Developing Strapi plugins
@@ -12,76 +12,38 @@ pagination_next: dev-docs/api/plugins/admin-panel-api
This section is about developing Strapi plugins to use them as local plugins or to submit them to the Marketplace. Not what you're looking for? Read the [plugins introduction](/dev-docs/plugins) and find your use case and recommended section to read from there.
:::
-Strapi allows the development of plugins that work exactly like the built-in plugins or 3rd-party plugins available from the Marketplace. Once created, your plugin can be:
+Strapi allows the development of plugins that work exactly like the built-in plugins or 3rd-party plugins available from the [Marketplace](https://market.strapi.io). Once created, your plugin can be:
- used as a local plugin, working only with a specific Strapi project,
-- or submitted to the [Marketplace](https://market.strapi.io) to be shared with the community.
+- or [submitted to the Marketplace](https://market.strapi.io/submit-plugin) to be shared with the community.
-The first step to developing a Strapi plugin is to create it using the CLI-based generator. Then you'll be able to leverage the [plugin APIs](#plugin-apis) to add features to your plugin.
+π To start developing a Strapi plugin:
-## Plugin creation
-
-Strapi provides a [command line interface (CLI)](/dev-docs/cli) for creating plugins. To create a plugin:
-
-1. Navigate to the root of a Strapi project.
-2. Run `yarn strapi generate` or `npm run strapi generate` in a terminal window to start the interactive CLI.
-3. Choose "plugin" from the list, press Enter, and give the plugin a name in kebab-case (e.g. `my-plugin`)
-4. Choose either `JavaScript` or `TypeScript` for the plugin language.
-5. Create a plugins configuration file if one does not already exist: `./config/plugins.js` or `./config/plugins.ts` for TypeScript projects.
-6. Enable the plugin by adding it to the [plugins configurations](/dev-docs/configurations/plugins) file:
-
-
-
-
-```js title="./config/plugins.js"
-module.exports = {
- // ...
- "my-plugin": {
- enabled: true,
- resolve: "./src/plugins/my-plugin", // path to plugin folder
- },
- // ...
-};
-```
-
-
-
-
-
-```js title=./config/plugins.ts
-export default {
- // ...
- "my-plugin": {
- enabled: true,
- resolve: "./src/plugins/my-plugin", // path to plugin folder
- },
- // ...
-};
-```
-
-
-
-
-7. Run `npm install` or `yarn` in the newly-created plugin directory.
-8. (_TypeScript-specific_) Run `yarn build` or `npm run build` in the plugin directory. This step transpiles the TypeScript files and outputs the JavaScript files to a `dist` directory that is unique to the plugin.
-9. Run `yarn build` or `npm run build` at the project root.
-10. Run `yarn develop` or `npm run develop` at the project root.
-
-Plugins created using the preceding directions are located in the `plugins` directory of the application (see [project structure](/dev-docs/project-structure)).
-
-:::note
-During plugin development it is helpful to use the `--watch-admin` flag to toggle hot reloading of the admin panel. See the [Admin panel customization](/dev-docs/admin-panel-customization) documentation for more details. (TypeScript specific) While developing your plugin, you can run `yarn develop --watch-admin` or `npm run develop -- --watch-admin` in the plugin directory to watch the changes to the TypeScript server files. From 4.15.1 this is no longer required.
-:::
+1. [Create a plugin](/dev-docs/plugins/development/create-a-plugin) using the CLI-based generator.
+2. Learn more about the [structure of a plugin](/dev-docs/plugins/development/plugin-structure).
+3. Get an overview of the [plugin APIs](#plugin-apis) to add features to your plugin.
+4. Read some [guides](#guides) based on your use case(s).
## Plugin APIs
Strapi provides the following programmatic APIs for plugins to hook into some of Strapi's features:
-
+
:::strapi Custom fields plugins
Plugins can also be used to add [custom fields](/dev-docs/custom-fields) to Strapi.
:::
+
+## Guides
+
+
+
+
+
+
+:::strapi Additional resources
+The Strapi blog features a [tutorial series](https://strapi.io/blog/how-to-create-a-strapi-v4-plugin-server-customization-4-6) about creating a Strapi v4 'Todo' plugin. The [contributors documentation](https://contributor.strapi.io/) can also include additional information useful while developing a Strapi plugin.
+:::
diff --git a/docusaurus/docs/dev-docs/plugins/development/create-a-plugin.md b/docusaurus/docs/dev-docs/plugins/development/create-a-plugin.md
new file mode 100644
index 0000000000..aab1c8efff
--- /dev/null
+++ b/docusaurus/docs/dev-docs/plugins/development/create-a-plugin.md
@@ -0,0 +1,314 @@
+---
+title: Plugin creation & setup
+description: Learn how to create a Strapi plugin and how to start the development servers
+pagination_next: dev-docs/plugins/development/plugin-structure
+---
+
+# Plugin creation and setup
+
+To start developing a Strapi plugin, you need to:
+
+1. create the plugin,
+2. enable the plugin,
+3. install dependencies, build the admin panel, and start the server(s).
+
+:::prerequisites
+You created a Strapi project.
+
+Use the CLI to create a project:
+
+Run the corresponding command in a terminal window, replacing `my-project` with the name of your choice:
+
+
+
+
+
+```bash
+yarn create strapi-app my-project --quickstart
+```
+
+
+
+
+
+```bash
+npx create-strapi-app@latest my-project --quickstart
+```
+
+
+
+
+
+More details can be found in the [CLI installation guide](/dev-docs/installation/cli).
+
+:::
+
+## Create the plugin using the CLI generator
+
+The fastest way to create a Strapi plugin is to use the CLI generator. To do so:
+
+1. Navigate to the root of an existing Strapi project, or create a new one.
+2. Run the following command in a terminal window to start the interactive CLI:
+
+
+
+
+ ```sh
+ yarn strapi generate plugin
+ ```
+
+
+
+
+
+ ```sh
+ npm run strapi generate plugin
+ ```
+
+
+
+
+4. Choose either `JavaScript` or `TypeScript` for the plugin language.
+
+## Enable the plugin
+
+Once the `strapi generate plugin` CLI script has finished running, the minimum required code for the plugin to work is created for you, but the plugin is not enabled yet.
+
+To enable a plugin:
+
+1. If it does not exist already, create the **plugins configuration file** file at the root of the Strapi project.
+2. Enable the plugin by adding the following code to the plugins configuration file:
+
+
+
+
+ ```js title="./config/plugins.js"
+ module.exports = {
+ // ...
+ "my-plugin": { // name of your plugin, kebab-cased
+ enabled: true,
+ resolve: "./src/plugins/my-plugin", // path to the plugin folder
+ },
+ // ...
+ };
+ ```
+
+
+
+
+
+ ```js title=./config/plugins.ts
+ export default {
+ // ...
+ "my-plugin": {
+ enabled: true,
+ resolve: "./src/plugins/my-plugin", // path to plugin folder
+ },
+ // ...
+ };
+ ```
+
+
+
+
+:::tip
+If you plan to use the plugin outside the Strapi project it was created in, move your plugin file outside the Strapi project and change the `resolve` value to the absolute directory path of your plugin.
+:::
+
+## Install dependencies, build the admin panel, and start servers
+
+Once the plugin code has been generated and the plugin is enabled, the next steps slighly differ depending on whether you created a vanilla JavaScript-based plugin or a TypeScript-based plugin (see [step 3](#create-the-plugin-using-the-cli-generator) of the CLI generator instructions).
+
+
+
+
+
+1. Navigate to the folder of the plugin. If created from a Strapi project using the CLI generator, plugins are located in the `src/plugins` folder (see [project structure](/dev-docs/project-structure)).
+
+2. Run the following command in the newly-created plugin directory to install plugin dependencies:
+
+
+
+
+ ```sh
+ yarn
+ ```
+
+
+
+
+
+ ```sh
+ npm install
+ ```
+
+
+
+
+3. Navigate back to the Strapi project root with `cd ../../..` and run the following commands to build the admin panel and start the server(s):
+
+
+
+
+ ```sh
+ yarn build
+ yarn develop
+ ```
+
+
+
+
+
+ ```sh
+ npm run build
+ npm run develop
+ ```
+
+
+
+
+
+
+
+
+1. Navigate to the folder of the plugin. If created from a Strapi project using the CLI generator, plugins are located in the `src/plugins` folder (see [project structure](/dev-docs/project-structure)).
+
+2. Run the following command in the newly-created plugin directory to install plugin dependencies:
+
+
+
+
+ ```sh
+ yarn
+ ```
+
+
+
+
+
+ ```sh
+ npm install
+ ```
+
+
+
+
+3. Still in the plugin directory (e.g., `src/plugins/my-plugin`), run the following command:
+
+
+
+
+ ```sh
+ yarn build
+ ```
+
+
+
+
+
+ ```sh
+ npm run build
+ ```
+
+
+
+
+ This step transpiles the TypeScript files and outputs the JavaScript files to a `dist` directory that is unique to the plugin.
+
+4. Navigate back to the Strapi project root with `cd ../../..` and run the following commands to build the admin panel and start the server(s):
+
+
+
+
+ ```sh
+ yarn build
+ yarn develop
+ ```
+
+
+
+
+
+ ```sh
+ npm run build
+ npm run develop
+ ```
+
+
+
+
+
+
+
+You should now be ready to start developing your plugin.
+
+:::strapi What to read next?
+You can either jump to the [plugin structure](/dev-docs/plugins/development/plugin-structure) documentation or read the [servers and hot reloading](#servers-and-hot-reloading) section to learn more about different ways to start the server.
+:::
+
+:::info Did you know?
+The admin panel needs to be rebuilt after its code has been modified. Rebuilding the admin panel is done by running the `build` command. The `strapi generate plugin` generates code that injects some plugin components (menu link, plugin homepage) into the admin panel. That's why we run the `build` command after the plugin code has been generated and before starting the server.
+:::
+
+### Servers and hot reloading
+
+Strapi itself is **headless** . The admin panel is completely separate from the server.
+
+```mermaid
+graph LR
+ A{Server} -->|Axios instance| B{Admin Panel}
+ B --> A
+```
+
+The server can be started in 2 different ways: you can run the backend server only or start both the server and admin panel servers.
+
+#### Start only the backend server
+
+To start only the backend server, run the following command:
+
+
+
+
+
+```bash
+yarn develop
+```
+
+
+
+
+
+```bash
+npm run develop
+```
+
+
+
+
+
+This will run the server on `localhost:1337` and enable hot reloading only on the back-end server, i.e. it will only auto-reload when changes are made to the server. If you are only doing development in the `./server` directory of your plugin, this will be faster.
+
+#### Start both the backend and admin panel servers
+
+If you are doing development on both the `/server` and `/admin` directories of your plugin, run the following command:
+
+
+
+
+
+```bash
+yarn develop --watch-admin
+```
+
+
+
+
+
+```bash
+npm run develop -- --watch-admin
+```
+
+
+
+
+This will run the server on `localhost:1337` and enable hot reloading on both the back-end and front-end servers, i.e.it will auto-reload when changes are made to the server or the admin panel of Strapi.
diff --git a/docusaurus/docs/dev-docs/plugins/development/plugin-structure.md b/docusaurus/docs/dev-docs/plugins/development/plugin-structure.md
new file mode 100644
index 0000000000..ea99964e7c
--- /dev/null
+++ b/docusaurus/docs/dev-docs/plugins/development/plugin-structure.md
@@ -0,0 +1,39 @@
+---
+title: Plugin structure
+description: Learn more about the structure of a Strapi plugin
+displayed_sidebar: devDocsSidebar
+---
+
+import InteractivePluginStructure from '@site/src/components/PluginStructure.js'
+
+# Plugin structure
+
+When [creating a plugin with the CLI generator](/dev-docs/plugins/development/create-a-plugin), Strapi generates the following boilerplate structure for you in the `./src/plugins/my-plugin` folder:
+
+
+
+A Strapi plugin is divided into 2 parts, each living in a different folder and offering a different API:
+
+| Plugin part | Description | Folder | API |
+|-------------|-------------|--------------|-----|
+| Admin panel | Includes what will be visible in the [admin panel](/user-docs/intro) (components, navigation, settings, etc.) | `/admin` |[Admin Panel API](/dev-docs/api/plugins/admin-panel-api)|
+| Backend server | Includes what relates to the [backend server](/dev-docs/backend-customization) (content-types, controllers, middlewares, etc.) |`/server` |[Server API](/dev-docs/api/plugins/server-api)|
+
+
+
+:::note Notes about the usefulness of the different parts for your specific use case
+- **Server-only plugin**: You can create a plugin that will just use the server part to enhance the API of your application. For instance, this plugin could have its own visible or invisible content-types, controller actions, and routes that are useful for a specific use case. In such a scenario, you don't need your plugin to have an interface in the admin panel.
+
+- **Admin panel plugin vs. application-specific customization**: You can create a plugin to inject some components into the admin panel. However, you can also achieve this by creating a `./src/admin/app.js` file and invoking the `bootstrap` lifecycle function to inject your components. In this case, deciding whether to create a plugin depends on whether you plan to reuse and distribute the code or if it's only useful for a unique Strapi application.
+:::
+
+
+
+:::strapi What to read next?
+The next steps of your Strapi plugin development journey will require you to use any of the Strapi plugins APIs.
+
+2 different types of resources help you understand how to use the plugin APIs:
+
+- The reference documentation for the [Admin Panel API](/dev-docs/api/plugins/admin-panel-api) and [Server API](/dev-docs/api/plugins/server-api) give an overview of what is possible to do with a Strapi plugin.
+- [Guides](/dev-docs/plugins/developing-plugins#guides) cover some specific, use-case based examples.
+:::
diff --git a/docusaurus/docs/dev-docs/plugins/guides/marketplace.md b/docusaurus/docs/dev-docs/plugins/guides/marketplace.md
new file mode 100644
index 0000000000..3ce1887eb6
--- /dev/null
+++ b/docusaurus/docs/dev-docs/plugins/guides/marketplace.md
@@ -0,0 +1,203 @@
+---
+title: Publishing a Strapi plugin to the Marketplace
+# description: todo
+displayed_sidebar: devDocsSidebar
+---
+
+# Publishing your Strapi plugin
+
+_Coming soonβ¦_
+
+:::tip
+Check [this blog post](https://strapi.io/blog/how-to-create-a-strapi-v4-plugin-publish-on-npm-6-6) to learn how to publish your Strapi plugin on npm.
+:::
+
+
+
+
diff --git a/docusaurus/docs/dev-docs/plugins/guides/pass-data-from-server-to-admin.md b/docusaurus/docs/dev-docs/plugins/guides/pass-data-from-server-to-admin.md
new file mode 100644
index 0000000000..bc82d0e3d5
--- /dev/null
+++ b/docusaurus/docs/dev-docs/plugins/guides/pass-data-from-server-to-admin.md
@@ -0,0 +1,101 @@
+---
+title: How to pass data from server to admin panel with a Strapi plugin
+description: Learn how to pass data from server to admin panel with a Strapi plugin
+sidebar_label: Pass data from server to admin
+displayed_sidebar: devDocsSidebar
+---
+
+# How to pass data from server to admin panel with a Strapi plugin
+
+Strapi is **headless** . The admin panel is completely separate from the server.
+
+When [developing a Strapi plugin](/dev-docs/plugins/developing-plugins) you might want to pass data from the `/server` to the `/admin` folder. Within the `/server` folder you have access to the Strapi object and can do database queries whereas in the `/admin` folder you can't.
+
+Passing data from the `/server` to the `/admin` folder can be done using the admin panel's Axios instance:
+
+```mermaid
+graph LR
+ A{Server} -->|Axios instance| B{Admin Panel}
+ B --> A
+```
+
+To pass data from the `/server` to `/admin` folder you would first [create a custom admin route](#create-a-custom-admin-route) and then [get the data returned in the admin panel](#get-the-data-in-the-admin-panel).
+
+## Create a custom admin route
+
+Admin routes are like the routes that you would have for any controller, except that the `type: 'admin'` declaration hides them from the general API router, and allows you to access them from the admin panel.
+
+The following code will declare a custom admin route for the `my-plugin` plugin:
+
+```js title="/my-plugin/server/routes/index.js"
+module.exports = {
+ 'pass-data': {
+ type: 'admin',
+ routes: [
+ {
+ method: 'GET',
+ path: '/pass-data',
+ handler: 'myPluginContentType.index',
+ config: {
+ policies: [],
+ auth: false,
+ },
+ },
+ ]
+ }
+ // ...
+};
+```
+
+This route will call the `index` method of the `myPluginContentType` controller when you send a GET request to the `/my-plugin/pass-data` URL endpoint.
+
+Let's create a basic custom controller that simply returns a simple text:
+
+```js title="/my-plugin/server/controllers/my-plugin-content-type.js"
+'use strict';
+
+module.exports = {
+ async index(ctx) {
+ ctx.body = 'You are in the my-plugin-content-type controller!';
+ }
+}
+```
+
+This means that when sending a GET request to the `/my-plugin/pass-data` URL endpoint, you should get the `You are in the my-plugin-content-type controller!` text returned with the response.
+
+## Get the data in the admin panel
+
+Any request sent from an admin panel component to the endpoint for which we defined the custom route `/my-plugin/pass-data` should now return the text message returned by the custom controller.
+
+So for instance, if you create an `/admin/src/api/foobar.js` file and copy and paste the following code example:
+
+```js title="/my-plugin/admin/src/api/foobar.js"
+import axios from 'axios';
+
+const foobarRequests = {
+ getFoobar: async () => {
+ const data = await axios.get(`/my-plugin/pass-data`);
+ return data;
+ },
+};
+export default foobarRequests;
+```
+
+You will be able to use `foobarRequests.getFoobar()` in the code of an admin panel component and have it return the `You are in the my-plugin-content-type controller!` text with the data.
+
+For instance, within a React component, you could use `useEffect` to get the data after the component initializes:
+
+```js title="/my-plugin/admin/src/components/MyComponent/index.js"
+import foobarRequests from "../../api/foobar";
+const [foobar, setFoobar] = useState([]);
+
+// β¦
+useEffect(() => {
+ foobarRequests.getFoobar().then(res => {
+ setSchemas(res.data);
+ });
+}, [setFoobar]);
+// β¦
+```
+
+This would set the `You are in the my-plugin-content-type controller!` text within the `foobar` data of the component's state.
diff --git a/docusaurus/docs/dev-docs/plugins/guides/store-and-access-data.md b/docusaurus/docs/dev-docs/plugins/guides/store-and-access-data.md
new file mode 100644
index 0000000000..10a07d2e87
--- /dev/null
+++ b/docusaurus/docs/dev-docs/plugins/guides/store-and-access-data.md
@@ -0,0 +1,176 @@
+---
+title: How to store and access data from a Strapi plugin
+description: Learn how to store and access data from a Strapi plugin
+sidebar_label: Store and access data
+displayed_sidebar: devDocsSidebar
+---
+
+# How to store and access data from a Strapi plugin
+
+To store data with a Strapi [plugin](/dev-docs/plugins/developing-plugins), use a plugin content-type. Plugin content-types work exactly like other [content-types](/dev-docs/backend-customization/models). Once the content-type is [created](#create-a-content-type-for-your-plugin), you can start [interacting with the data](#interact-with-data-from-the-plugin).
+
+## Create a content-type for your plugin
+
+To create a content-type with the CLI generator, run the following command in a terminal:
+
+
+
+
+```bash
+yarn strapi generate content-type
+```
+
+
+
+
+
+```bash
+npm run strapi generate content-type
+```
+
+
+
+
+The generator CLI is interactive and asks a few questions about the content-type and the attributes it will contain. Answer the first questions, then for the `Where do you want to add this model?` question, choose the `Add model to existing plugin` option and type the name of the related plugin when asked.
+
+
+
+
+
+The CLI will generate some code required to use your plugin, which includes the following:
+
+- the [content-type schema](/dev-docs/backend-customization/models#model-schema)
+- and a basic [controller](/dev-docs/backend-customization/controllers), [service](/dev-docs/backend-customization/services), and [route](/dev-docs/backend-customization/routes) for the content-type
+
+:::tip
+You may want to create the whole structure of your content-types either entirely with the CLI generator or by directly creating and editing `schema.json` files. We recommend you first create a simple content-type with the CLI generator and then leverage the [Content-Type Builder](/user-docs/content-type-builder) in the admin panel to edit your content-type.
+
+If your content-type is not visible in the admin panel, you might need to set the `content-manager.visible` and `content-type-builder.visible` parameters to `true` in the `pluginOptions` object of the content-type schema:
+
+
+Making a plugin content-type visible in the admin panel:
+
+The following highlighted lines in an example `schema.json` file show how to make a plugin content-type visible to the Content-Type Builder and Content-Manager:
+
+```json title="/server/content-types/my-plugin-content-type/schema.json" {13-20} showLineNumbers
+{
+ "kind": "collectionType",
+ "collectionName": "my_plugin_content_types",
+ "info": {
+ "singularName": "my-plugin-content-type",
+ "pluralName": "my-plugin-content-types",
+ "displayName": "My Plugin Content-Type"
+ },
+ "options": {
+ "draftAndPublish": false,
+ "comment": ""
+ },
+ "pluginOptions": {
+ "content-manager": {
+ "visible": true
+ },
+ "content-type-builder": {
+ "visible": true
+ }
+ },
+ "attributes": {
+ "name": {
+ "type": "string"
+ }
+ }
+}
+
+```
+
+
+:::
+
+### Ensure plugin content-types are imported
+
+The CLI generator might not have imported all the related content-type files for your plugin, so you might have to make the following adjustments after the `strapi generate content-type` CLI command has finished running:
+
+1. In the `/server/index.js` file, import the content-types:
+
+ ```js {7,22} showLineNumbers title="/server/index.js"
+ 'use strict';
+
+ const register = require('./register');
+ const bootstrap = require('./bootstrap');
+ const destroy = require('./destroy');
+ const config = require('./config');
+ const contentTypes = require('./content-types');
+ const controllers = require('./controllers');
+ const routes = require('./routes');
+ const middlewares = require('./middlewares');
+ const policies = require('./policies');
+ const services = require('./services');
+
+ module.exports = {
+ register,
+ bootstrap,
+ destroy,
+ config,
+ controllers,
+ routes,
+ services,
+ contentTypes,
+ policies,
+ middlewares,
+ };
+
+ ```
+
+2. In the `/server/content-types/index.js` file, import the content-type folder:
+
+ ```js title="/server/content-types/index.js"
+ 'use strict';
+
+ module.exports = {
+ // In the line below, replace my-plugin-content-type
+ // with the actual name and folder path of your content type
+ "my-plugin-content-type": require('./my-plugin-content-type'),
+ };
+ ```
+
+3. Ensure that the `/server/content-types/[your-content-type-name]` folder contains not only the `schema.json` file generated by the CLI, but also an `index.js` file that exports the content-type with the following code:
+
+ ```js title="/server/content-types/my-plugin-content-type/index.js
+ 'use strict';
+
+ const schema = require('./schema');
+
+ module.exports = {
+ schema,
+ };
+ ```
+
+## Interact with data from the plugin
+
+Once you have created a content-type for your plugin, you can create, read, update, and delete data.
+
+:::note
+A plugin can only interact with data from the `/server` folder. If you need to update data from the admin panel, please refer to the [passing data guide](/dev-docs/plugins/guides/pass-data-from-server-to-admin).
+:::
+
+To create, read, update, and delete data, you can use either the [Entity Service API](/dev-docs/api/entity-service) or the [Query Engine API](/dev-docs/api/query-engine). While it's recommended to use the Entity Service API, especially if you need access to components or dynamic zones, the Query Engine API is useful if you need unrestricted access to the underlying database.
+
+Use the `plugin::your-plugin-slug.the-plugin-content-type-name` syntax for content-type identifiers in Entity Service and Query Engine API queries.
+
+**Example:**
+
+Here is how to find all the entries for the `my-plugin-content-type` collection type created for a plugin called `my-plugin`:
+
+```js
+// Using the Entity Service API
+let data = await strapi.entityService.findMany('plugin::my-plugin.my-plugin-content-type');
+
+// Using the Query Engine API
+let data = await strapi.db.query('plugin::my-plugin.my-plugin-content-type').findMany();
+````
+
+:::tip
+You can access the database via the `strapi` object which can be found in `middlewares`, `policies`, `controllers`, `services`, as well as from the `register`, `boostrap`, `destroy` lifecycle functions.
+:::
diff --git a/docusaurus/sidebars.js b/docusaurus/sidebars.js
index f287a0d137..c334483cda 100644
--- a/docusaurus/sidebars.js
+++ b/docusaurus/sidebars.js
@@ -215,15 +215,6 @@ const sidebars = {
"dev-docs/api/query-engine/order-pagination",
],
},
- {
- type: 'category',
- label: 'APIs for plugins',
- collapsed: false,
- items: [
- 'dev-docs/api/plugins/admin-panel-api',
- 'dev-docs/api/plugins/server-api',
- ]
- },
]
},
{
@@ -300,14 +291,7 @@ const sidebars = {
}
]
},
- 'dev-docs/plugins-extension',
- 'dev-docs/plugins-development',
'dev-docs/typescript',
- {
- type: 'doc',
- label: 'Custom fields',
- id: 'dev-docs/custom-fields',
- },
{
type: "doc",
label: "Providers",
@@ -465,6 +449,8 @@ const sidebars = {
label: 'Introduction',
id: 'dev-docs/plugins/developing-plugins'
},
+ 'dev-docs/plugins/development/create-a-plugin',
+ 'dev-docs/plugins/development/plugin-structure',
{
type: 'doc',
id: 'dev-docs/api/plugins/admin-panel-api',
@@ -477,6 +463,18 @@ const sidebars = {
},
'dev-docs/custom-fields',
'dev-docs/plugins-extension',
+ {
+ type: 'category',
+ label: 'Guides',
+ link: {
+ type: 'doc',
+ id: 'dev-docs/plugins/developing-plugins',
+ },
+ items: [
+ 'dev-docs/plugins/guides/store-and-access-data',
+ 'dev-docs/plugins/guides/pass-data-from-server-to-admin',
+ ]
+ }
]
}
]
diff --git a/docusaurus/src/components/PluginStructure.js b/docusaurus/src/components/PluginStructure.js
new file mode 100644
index 0000000000..5e8073d801
--- /dev/null
+++ b/docusaurus/src/components/PluginStructure.js
@@ -0,0 +1,131 @@
+import React from 'react'
+import Tabs from '@theme/Tabs'
+import TabItem from '@theme/TabItem'
+
+export default function InteractivePluginStructure() {
+ return (
+
+
+
+
+ The following diagram is interactive: you can click on any file or folder name highlighted in purple to go to the corresponding documentation section.
+
+
+
+ . # root of the plugin folder (e.g., /src/plugins/my-plugin)
+ βββ admin# Admin panel part of your plugin.
+ β βββ src
+ β βββ components # Contains your front-end components
+ β β βββ Initializer
+ β β β βββ index.js # Plugin initializer
+ β β βββ PluginIcon
+ β β βββ index.js # Contains the icon of your plugin in the main navigation
+ β βββ pages # Contains the pages of your plugin
+ β β βββ App
+ β β β βββ index.js # Skeleton around the actual pages
+ β β βββ HomePage
+ β β βββ index.js # Homepage of your plugin
+ β βββ translations # Translations files to make your plugin i18n-friendly
+ β β βββ en.json
+ β β βββ fr.json
+ β βββ utils
+ β β βββ getTrad.js # getTrad function to return the corresponding plugin translations
+ β βββ index.js # Main setup of your plugin, used to register elements in the admin panel
+ β βββ pluginId.js # pluginId variable computed from package.json name
+ βββ node_modules
+ βββ server# Back-end part of your plugin
+ β βββ config
+ β β βββ index.js # Contains the default server configuration
+ β βββ content-types# Content-types specific to your plugin
+ β β βββ index.js # Loads all the plugin's content-types
+ β βββ controllers# Controllers specific to your plugin
+ β β βββ index.js # Loads all the plugin's controllers
+ β β βββ my-controller.js # Custom controller example. You can rename it or delete it.
+ β βββ middlewares# Middlewares specific to your plugin
+ β β βββ index.js # Loads all the plugin's middlewares
+ β βββ policies# Policies specific to your plugin
+ β β βββ index.js # Loads all the plugin's policies
+ β βββ routes# Routes specific to your plugin
+ β β βββ index.js # Contains an example route for the my-controller custom controller example
+ β βββ services# Services specific to your plugin
+ β β βββ index.js # Loads all the plugin's services
+ β β βββ my-service.js # Custom service example. You can rename it or delete it.
+ β βββ bootstrap.js# Function that is called right after the plugin has registered
+ β βββ destroy.js# Function that is called to clean up the plugin after Strapi instance is destroyed
+ β βββ index.js # Loads the code for all the server elements
+ β βββ register.js# Function that is called to load the plugin, before bootstrap.
+ βββ package.json
+ βββ README.md
+ βββ strapi-admin.js# Entrypoint for the admin panel (front end)
+ βββ strapi-server.js# Entrypoint for the server (back end)
+
+
+
+
+
+
+
+
+ The following diagram is interactive: you can click on any file or folder name highlighted in purple to go to the corresponding documentation section.
+
+
+
+ . # root of the plugin folder (e.g., /src/plugins/my-plugin)
+ βββ admin# Admin panel part of your plugin.
+ β βββ src
+ β βββ components # Contains your front-end components
+ β β βββ Initializer
+ β β β βββ index.tsx # Plugin initializer
+ β β βββ PluginIcon
+ β β βββ index.tsx # Contains the icon of your plugin in the main navigation
+ β βββ pages # Contains the pages of your plugin
+ β β βββ App
+ β β β βββ index.tsx # Skeleton around the actual pages
+ β β βββ HomePage
+ β β βββ index.tsx # Homepage of your plugin
+ β βββ translations # Translations files to make your plugin i18n-friendly
+ β β βββ en.json
+ β β βββ fr.json
+ β βββ utils
+ β β βββ getTrad.ts # getTrad function to return the corresponding plugin translations
+ β βββ index.tsx # Main setup of your plugin, used to register elements in the admin panel
+ β βββ pluginId.tsx # pluginId variable computed from package.tsxon name
+ βββ dist # Build of the backend
+ βββ node_modules
+ βββ server# Back-end part of your plugin
+ β βββ config
+ β β βββ index.ts # Contains the default server configuration
+ β βββ content-types# Content-types specific to your plugin
+ β β βββ index.ts # Loads all the plugin's content-types
+ β βββ controllers# Controllers specific to your plugin
+ β β βββ index.ts # Loads all the plugin's controllers
+ β β βββ my-controller.ts # Custom controller example. You can rename it or delete it.
+ β βββ middlewares# Middlewares specific to your plugin
+ β β βββ index.ts # Loads all the plugin's middlewares
+ β βββ policies# Policies specific to your plugin
+ β β βββ index.ts # Loads all the plugin's policies
+ β βββ routes# Routes specific to your plugin
+ β β βββ index.ts # Contains an example route for the my-controller custom controller example
+ β βββ services# Services specific to your plugin
+ β β βββ index.ts # Loads all the plugin's services
+ β β βββ my-service.ts # Custom service example. You can rename it or delete it.
+ β βββ bootstrap.ts# Function that is called right after the plugin has registered
+ β βββ destroy.ts# Function that is called to clean up the plugin after Strapi instance is destroyed
+ β βββ index.ts # Loads the code for all the server elements
+ β βββ register.ts# Function that is called to load the plugin, before bootstrap.
+ βββ custom.d.ts # Generated types
+ βββ package.json
+ βββ README.md
+ βββ strapi-admin.js# Entrypoint for the admin panel (front end)
+ βββ strapi-server.js# Entrypoint for the server (back end)
+ βββ tsconfig.json # TypeScript compiler options for the admin panel part
+ βββ tsconfig.server.json # TypeScript compiler options for the server part
+
+
+
+
+
+
+ );
+}
+
diff --git a/docusaurus/src/components/ReusableAnnotationComponents/ReusableAnnotationComponents.jsx b/docusaurus/src/components/ReusableAnnotationComponents/ReusableAnnotationComponents.jsx
new file mode 100644
index 0000000000..0e60356ccf
--- /dev/null
+++ b/docusaurus/src/components/ReusableAnnotationComponents/ReusableAnnotationComponents.jsx
@@ -0,0 +1,36 @@
+import React from 'react'
+import { Annotation } from '../Annotation';
+
+export function PluginsConfigurationFile() {
+ return (
+
+
+ The plugins configuration file config/plugins.js|ts:
+
+
declares all plugins that are enabled,
+
and can be used to pass additional configuration options to some plugins.
+ A headless CMS is a Content Management System that separates the presentation layer (i.e., the front end, where content is displayed) from the back end (where content is managed).
+
+
+ Strapi is a headless CMS that provides:
+
+
a back-end server exposing an API for your content,
+
and a graphical user interface, called the admin panel, to manage the content.
+
The presentation layer of your website or application powered by Strapi should be handled by another framework, not by Strapi.
+
+
+ )
+}
diff --git a/docusaurus/src/theme/MDXComponents.js b/docusaurus/src/theme/MDXComponents.js
index 639a88788f..22bd80e4b9 100644
--- a/docusaurus/src/theme/MDXComponents.js
+++ b/docusaurus/src/theme/MDXComponents.js
@@ -28,6 +28,7 @@ import {
} from '../components/MultiLanguageSwitcher';
import { Annotation } from '../components/Annotation';
import SubtleCallout from '../components/SubtleCallout';
+import { PluginsConfigurationFile, HeadlessCms } from '../components/ReusableAnnotationComponents/ReusableAnnotationComponents';
export default {
// Re-use the default mapping
@@ -65,4 +66,9 @@ export default {
MultiLanguageSwitcherRequest,
MultiLanguageSwitcherResponse,
Annotation,
+ /**
+ * Reusable annotation components go belowπ
+ */
+ PluginsConfigurationFile,
+ HeadlessCms
};
diff --git a/docusaurus/static/img/assets/development/generate-plugin-content-type.png b/docusaurus/static/img/assets/development/generate-plugin-content-type.png
new file mode 100644
index 0000000000000000000000000000000000000000..1b9cd617a5a4e51cde7f3132cba0ee6d80441d0a
GIT binary patch
literal 74136
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