Customize the structure of menus and menu items, typically to render a navigation menu on a frontend app.
A plugin for Strapi CMS to customize the structure of menus and menu items.
title
, url
, and link target
of menu items.useCMEditViewDataManager
hook).yarn add strapi-plugin-menus@latest
Don't forget to restart or rebuild your Strapi app when installing a new plugin.
property | type (default) | description |
---|---|---|
maxDepth | number (null ) | Limits how deep menu items can be nested. |
layouts | object ({} ) | Provide form layout configuration for custom attributes. |
maxDepth
Limits how deep menus can be nested. By default, there is no limit.
1// ./config/plugins.js`
2'use strict';
3
4module.exports = {
5 menus: {
6 config: {
7 maxDepth: 3,
8 },
9 },
10};
layouts
Provide form layout configuration for custom attributes. The example below is quite simple. See the Extending section for more details on how to use this feature.
1// ./config/plugins.js`
2'use strict';
3
4module.exports = {
5 menus: {
6 config: {
7 layouts: {
8 menuItem: {
9 link: [
10 {
11 input: {
12 label: 'Example Field Label',
13 name: 'example_field',
14 type: 'text',
15 },
16 grid: {
17 col: 6,
18 },
19 },
20 ],
21 },
22 },
23 },
24 },
25};
The schema and API endpoint documentation for menus will only be generated by the Strapi documentation plugin if menus
is included in the documentation plugin config, like the example below.
1// ./config/plugins.js`
2'use strict';
3
4module.exports = {
5 documentation: {
6 config: {
7 'x-strapi-config': {
8 plugins: ['menus', 'upload', 'users-permissions'],
9 },
10 },
11 },
12};
The
upload
andusers-permissions
values are part of the default config and must be re-declared here unless you want to disable their documentation.
This plugin can be extended to add new attributes to the MenuItem
schema and include editable fields for those attributes in the UI. Follow along with each section below to become familiar with this process.
MenuItem
schema attributesFirst, create the file ./src/extensions/menus/strapi-server.js
. Then copy/paste the code below into that file to get started.
1// ./src/extensions/menus/strapi-server.js`
2'use strict';
3
4module.exports = plugin => {
5 // Get current `MenuItem` attributes.
6 const defaultAttrs = plugin.contentTypes['menu-item'].schema.attributes;
7
8 // Define custom attributes for `MenuItem` the same way they would be defined
9 // on any other schema.
10 const customAttrs = {
11 example_field: {
12 type: 'string',
13 },
14 };
15
16 // Extend the `MenuItem` content type with custom attributes.
17 plugin.contentTypes['menu-item'].schema.attributes = {
18 ...defaultAttrs,
19 ...customAttrs,
20 };
21
22 return plugin;
23};
layouts
for custom attributesNext, we need to extend the form layout to include the new attributes we defined on the schema.
In ./config/plugins.js
, we will configure the layouts
prop to allow our custom attributes to render in the UI. By default, the menu item edit panel has one tab labeled "Link". We can add fields to the "Link" tab by defining layouts.menuItem.link
as an array of field config objects.
New tabs in the edit panel are configured with each key in the layouts.menuItem
object. The example below will add our custom attribute into the "Link" tab and it will occupy the remaining 6 columns of spacing in that panel.
1// ./config/plugins.js`
2'use strict';
3
4module.exports = {
5 menus: {
6 config: {
7 layouts: {
8 menuItem: { // This is the menu item edit panel.
9 link: [ // This is the "link" tab in the menu item edit panel.
10 {
11 input: {
12 label: 'Example Field Label',
13 name: 'example_field',
14 type: 'text',
15 description: 'Example field description',
16 placeholder: 'Type something...'
17 },
18 grid: {
19 col: 6,
20 },
21 },
22 ],
23 },
24 },
25 },
26 },
27};
Each field config object may contain an input
and a grid
prop. Neither are required, but at least one should be present.
The input
prop requires label
, name
, and type
and will be used with the GenericInputs
component from the Strapi helper plugin. Other available props can be found in the example below.
The grid
prop values correspond to a 12-column layout and will be passed directly to the GridItem
component from the Strapi design system.
1{
2 input: {
3 label: 'Field Label',
4 name: '<field_name>', // Same name as defined in the schema.
5 type: '<field_type>', // Reference "Supported field types" section.
6 description: 'Helpful small text under the field input.',
7 placeholder: 'Type something...',
8 required: true,
9 step: 60, // Numbers and time inputs only.
10 options: [ // Select menus only.
11 {
12 label: 'Option Label 1',
13 value: 'option1',
14 },
15 {
16 label: 'Option Label 2',
17 value: 'option2',
18 },
19 {
20 label: 'Option Label 3',
21 value: 'option3',
22 },
23 ],
24 },
25 grid: {
26 col: 6, // Default.
27 s: 12, // Tablet.
28 xs: 12, // Mobile.
29 },
30},
For select
input types, the enum
values associated with the attribute will be used by default for the options
. Or you can provide a custom options
array of objects where each object has a label
and value
prop.
1[
2 {
3 label: 'Option Label',
4 value: 'optionValue',
5 },
6],
For customField
input types, you must include the customField
prop with the custom field UID.
NOTE: Custom fields will not work in this plugin if they rely on
useCMEditViewDataManager
hook.
1{
2 input: {
3 label: 'Example Custom Field',
4 name: 'example_field',
5 type: 'customField',
6 customField: 'plugin::custom-fields.example',
7 },
8},
You may also omit the input
prop and just add some white space with the grid
prop.
1{
2 grid: {
3 col: 6,
4 },
5},
You may optionally provide a translation config object instead of a string value for label
, description
, and placeholder
props. This also applies to label
values in select menu options
. However, this does not enable translations by itself.
You must also include the custom field translations in your ./src/admin/app.js
file as you see in the example below.
1// ./src/admin/app.js`
2'use strict';
3
4export default {
5 config: {
6 locales: ['en'],
7 translations: {
8 en: {
9 'menus.customFields.field_name.label': 'Translated Label',
10 'menus.customFields.field_name.placeholder': 'Translated Placeholder',
11 'menus.customFields.field_name.description': 'Translated Description',
12 },
13 },
14 },
15 // etc.
16};
The following field types in the table below are supported. Some fields use a different type value for the schema and input type.
Field | Schema Type | Input Type |
---|---|---|
Boolean | boolean | bool |
Date | date , time , datetime | same |
email | same | |
Enumeration | enumeration | select |
Media | media | same |
Number | integer , biginteger , decimal , float | number |
Password | password | same |
Relation | relation | same |
Rich Text | richtext | wysiwyg |
Text | string , text | string , text , textarea |
The following field types are NOT supported:
NOTE: By default, rich text fields are not supported unless a custom plugin overrides the core WYSIWYG editor, which is covered in the Strapi guide to creating a new WYSIWYG field in the admin panel.
For reference, here is an example of a 100% complete config with all supported field types. This also demonstrates how tabs and fields can be easily configured.
First, create the file ./src/extensions/menus/strapi-server.js
and add the code below.
1// ./src/extensions/menus/strapi-server.js`
2'use strict';
3
4module.exports = plugin => {
5 // Get current `MenuItem` attributes.
6 const defaultAttrs = plugin.contentTypes['menu-item'].schema.attributes;
7
8 // Define custom attributes for `MenuItem` the same way they would be defined
9 // on any other schema.
10 const customAttrs = {
11 example_bool: {
12 type: 'boolean',
13 },
14 example_text: {
15 type: 'string',
16 },
17 example_email: {
18 type: 'email',
19 },
20 example_password: {
21 type: 'password',
22 },
23 example_richtext: {
24 type: 'richtext',
25 },
26 example_date: {
27 type: 'date',
28 },
29 example_time: {
30 type: 'time',
31 },
32 example_datetime: {
33 type: 'datetime',
34 },
35 example_integer: {
36 type: 'integer',
37 },
38 example_biginteger: {
39 type: 'biginteger',
40 },
41 example_decimal: {
42 type: 'decimal',
43 },
44 example_float: {
45 type: 'float',
46 },
47 example_enum: {
48 type: 'enumeration',
49 enum: [
50 'option1',
51 'option2',
52 'option3',
53 ],
54 },
55 example_media: {
56 type: 'media',
57 allowedTypes: ['images'],
58 multiple: false,
59 },
60 example_relation_one: {
61 type: 'relation',
62 relation: 'oneToOne',
63 target: 'api::example-one.example-one',
64 },
65 example_relation_many: {
66 type: 'relation',
67 relation: 'oneToMany',
68 target: 'api::example-many.example-many',
69 },
70 };
71
72 // Extend the `MenuItem` content type with custom attributes.
73 plugin.contentTypes['menu-item'].schema.attributes = {
74 ...defaultAttrs,
75 ...customAttrs,
76 };
77
78 return plugin;
79};
Next, add the plugin config for menus
to ./config/plugins.js
to include custom form field layouts.
TIP: Despite the simplicity, this is certainly a lot of code. It may be best to move it into a separate file and
require()
it into your main config file to keep things organized.
1// ./config/plugins.js`
2'use strict';
3
4module.exports = {
5 menus: {
6 config: {
7 maxDepth: 3,
8 layouts: {
9 menuItem: {
10 link: [
11 {
12 input: {
13 label: 'Boolean',
14 name: 'example_bool',
15 type: 'bool',
16 },
17 grid: {
18 col: 6,
19 },
20 },
21 ],
22 text: [
23 {
24 input: {
25 label: 'Text',
26 name: 'example_text',
27 type: 'text',
28 },
29 },
30 {
31 input: {
32 label: 'Email',
33 name: 'example_email',
34 type: 'email',
35 },
36 },
37 {
38 input: {
39 label: 'Password',
40 name: 'example_password',
41 type: 'password',
42 },
43 },
44 {
45 input: {
46 label: 'Rich Text',
47 name: 'example_richtext',
48 type: 'wysiwyg',
49 description: 'This field type is not supported unless a custom plugin overrides the core WYSIWYG editor.',
50 },
51 },
52 ],
53 date: [
54 {
55 input: {
56 label: 'Date',
57 name: 'example_date',
58 type: 'date',
59 },
60 grid: {
61 col: 6,
62 },
63 },
64 {
65 input: {
66 label: 'Time',
67 name: 'example_time',
68 type: 'time',
69 },
70 grid: {
71 col: 6,
72 },
73 },
74 {
75 input: {
76 label: 'Date and Time',
77 name: 'example_datetime',
78 type: 'datetime',
79 },
80 },
81 ],
82 number: [
83 {
84 input: {
85 label: 'Integer',
86 name: 'example_integer',
87 type: 'number',
88 },
89 grid: {
90 col: 6,
91 },
92 },
93 {
94 input: {
95 label: 'Big Integer',
96 name: 'example_biginteger',
97 type: 'number',
98 },
99 grid: {
100 col: 6,
101 },
102 },
103 {
104 input: {
105 label: 'Decimal',
106 name: 'example_decimal',
107 type: 'number',
108 },
109 grid: {
110 col: 6,
111 },
112 },
113 {
114 input: {
115 label: 'Float',
116 name: 'example_float',
117 type: 'number',
118 },
119 grid: {
120 col: 6,
121 },
122 },
123 ],
124 media: [
125 {
126 input: {
127 label: 'Media',
128 name: 'example_media',
129 type: 'media',
130 },
131 },
132 ],
133 select: [
134 {
135 input: {
136 label: 'Enumeration',
137 name: 'example_enum',
138 type: 'select',
139 options: [
140 {
141 label: 'Option Label 1',
142 value: 'option1',
143 },
144 {
145 label: 'Option Label 2',
146 value: 'option2',
147 },
148 {
149 label: 'Option Label 3',
150 value: 'option3',
151 },
152 ],
153 },
154 },
155 {
156 input: {
157 label: 'Relation (hasOne)',
158 name: 'example_relation_one',
159 type: 'relation',
160 },
161 },
162 {
163 input: {
164 label: 'Relation (hasMany)',
165 name: 'example_relation_many',
166 type: 'relation',
167 },
168 },
169 ],
170 },
171 },
172 },
173 },
174};
With everything configured properly, you should end up with a menu item edit panel that looks like the image below. Refer to the Supported Field Types section if you are still confused on how to enable the rich text editor.
On the menus plugin home page, use the "Create new menu" button to get started. You will need to provide a title
and a unique slug
value for the new menu. Saving the menu before adding menu items is recommended but not required.
Choosing to clone an existing menu will take you to the edit view as usual, but this time it will be pre-populated with another menu's data. Once the cloned menu is saved, a brand new menu and menu items are created.
Deleting a menu will also delete all of it's menu items.
When clicking on a menu item in the left column, it will reveal action buttons to move the item, delete it, or give it a submenu.
The right column will reveal the edit UI for that item, where the title
is the only required field.
Fetching menus data is the same as fetching any other data using Strapi's REST API features.
Don't forget to enable the public methods for
Menu
andMenuItem
in the Users and Permissions settings, likefind
andfindOne
.
request | endpoint | description |
---|---|---|
GET | /api/menus | Fetch all menus. |
GET | /api/menus/:id | Fetch one menu. |
POST | /api/menus/:id | Create a menu. |
PUT | /api/menus/:id | Update a menu. |
DELETE | /api/menus/:id | Delete a menu. |
name | description |
---|---|
nested | Serialize menu items into a nested format, otherwise they are returned as a flat list. |
Fetch a menu with the ID 3. Nothing is populated by default.
1await fetch('/api/menus/3');
1{
2 "data": {
3 "id": 3,
4 "attributes": {
5 "title": "Main Menu",
6 "slug": "main-menu",
7 "createdAt": "2022-07-24T01:51:19.115Z",
8 "updatedAt": "2022-07-24T01:55:16.153Z"
9 }
10 },
11 "meta": {}
12}
Fetch a menu with the ID 3 with populate
params included.
1await fetch('/api/menus/3?populate=*');
1{
2 "data": {
3 "id": 3,
4 "attributes": {
5 "title": "Main Menu",
6 "slug": "main-menu",
7 "createdAt": "2022-07-24T01:51:19.115Z",
8 "updatedAt": "2022-07-24T01:55:16.153Z",
9 "items": {
10 "data": [
11 {
12 "id": 10,
13 "attributes": {
14 "order": 0,
15 "title": "Parent Page",
16 "url": "/parent-page",
17 "target": null,
18 "createdAt": "2022-07-24T03:33:03.416Z",
19 "updatedAt": "2022-07-24T19:49:38.949Z"
20 }
21 },
22 {
23 "id": 11,
24 "attributes": {
25 "order": 0,
26 "title": "Child Page",
27 "url": "/child-page",
28 "target": null,
29 "createdAt": "2022-07-24T03:33:03.416Z",
30 "updatedAt": "2022-07-24T19:49:38.949Z"
31 }
32 },
33 {
34 "id": 12,
35 "attributes": {
36 "order": 0,
37 "title": "Grandchild Page",
38 "url": "/grandchild-page",
39 "target": null,
40 "createdAt": "2022-07-24T03:33:03.416Z",
41 "updatedAt": "2022-07-24T19:49:38.949Z"
42 }
43 },
44 {
45 "id": 13,
46 "attributes": {
47 "order": 0,
48 "title": "Another Page",
49 "url": "/another-page",
50 "target": null,
51 "createdAt": "2022-07-24T03:33:03.416Z",
52 "updatedAt": "2022-07-24T19:49:38.949Z",
53 "children": []
54 }
55 }
56 ]
57 }
58 }
59 },
60 "meta": {}
61}
Fetch a menu with the ID 3 with the nested
param included.
1await fetch('/api/menus/3?nested&populate=*');
1{
2 "data": {
3 "id": 3,
4 "attributes": {
5 "title": "Main Menu",
6 "slug": "main-menu",
7 "createdAt": "2022-07-24T01:51:19.115Z",
8 "updatedAt": "2022-07-24T01:55:16.153Z",
9 "items": {
10 "data": [
11 {
12 "id": 10,
13 "attributes": {
14 "order": 0,
15 "title": "Parent Page",
16 "url": "/parent-page",
17 "target": null,
18 "createdAt": "2022-07-24T03:33:03.416Z",
19 "updatedAt": "2022-07-24T19:49:38.949Z",
20 "children": [
21 {
22 "id": 11,
23 "attributes": {
24 "order": 0,
25 "title": "Child Page",
26 "url": "/child-page",
27 "target": null,
28 "createdAt": "2022-07-24T03:33:03.416Z",
29 "updatedAt": "2022-07-24T19:49:38.949Z",
30 "children": [
31 {
32 "id": 12,
33 "attributes": {
34 "order": 0,
35 "title": "Grandchild Page",
36 "url": "/grandchild-page",
37 "target": null,
38 "createdAt": "2022-07-24T03:33:03.416Z",
39 "updatedAt": "2022-07-24T19:49:38.949Z",
40 "children": []
41 }
42 }
43 ]
44 }
45 }
46 ]
47 }
48 },
49 {
50 "id": 13,
51 "attributes": {
52 "order": 0,
53 "title": "Another Page",
54 "url": "/another-page",
55 "target": null,
56 "createdAt": "2022-07-24T03:33:03.416Z",
57 "updatedAt": "2022-07-24T19:49:38.949Z",
58 "children": []
59 }
60 }
61 ]
62 }
63 }
64 },
65 "meta": {}
66}
Custom relation fields are not populated by default. Here we fetch a menu with the ID 3 with custom relation fields populated.
The qs library is highly recommended for building the request URL for complicated population objects.
1import qs from 'qs';
2
3const params = {
4 nested: true,
5 populate: {
6 items: {
7 populate: {
8 example_relation: true,
9 another_relation: true,
10 },
11 },
12 },
13};
14
15const query = qs.stringify(params, { addQueryPrefix: true });
16
17// The params above will parse into this query string.
18// ?nested&populate[items][populate][0]=example_relation&populate[items][populate][1]=another_relation
19
20await fetch( `/api/menus/3${query}` );
1{
2 "data": {
3 "id": 3,
4 "attributes": {
5 "title": "Main Menu",
6 "slug": "main-menu",
7 "createdAt": "2022-07-24T01:51:19.115Z",
8 "updatedAt": "2022-07-24T01:55:16.153Z",
9 "items": {
10 "data": [
11 {
12 "id": 10,
13 "attributes": {
14 "order": 0,
15 "title": "Parent Page",
16 "url": "/parent-page",
17 "target": null,
18 "example_relation": {
19 "id": 1,
20 "title": "Example Relation",
21 },
22 "another_relation": {
23 "id": 2,
24 "title": "Another Relation",
25 },
26 "createdAt": "2022-07-24T03:33:03.416Z",
27 "updatedAt": "2022-07-24T19:49:38.949Z",
28 "children": [
29 {
30 "id": 11,
31 "attributes": {
32 "order": 0,
33 "title": "Child Page",
34 "url": "/child-page",
35 "target": null,
36 "example_relation": null,
37 "another_relation": null,
38 "createdAt": "2022-07-24T03:33:03.416Z",
39 "updatedAt": "2022-07-24T19:49:38.949Z",
40 "children": [
41 {
42 "id": 12,
43 "attributes": {
44 "order": 0,
45 "title": "Grandchild Page",
46 "url": "/grandchild-page",
47 "target": null,
48 "example_relation": null,
49 "another_relation": null,
50 "createdAt": "2022-07-24T03:33:03.416Z",
51 "updatedAt": "2022-07-24T19:49:38.949Z",
52 "children": []
53 }
54 }
55 ]
56 }
57 }
58 ]
59 }
60 },
61 {
62 "id": 13,
63 "attributes": {
64 "order": 0,
65 "title": "Another Page",
66 "url": "/another-page",
67 "target": null,
68 "example_relation": {
69 "id": 1,
70 "title": "Example Relation",
71 },
72 "another_relation": null,
73 "createdAt": "2022-07-24T03:33:03.416Z",
74 "updatedAt": "2022-07-24T19:49:38.949Z",
75 "children": []
76 }
77 }
78 ]
79 }
80 }
81 },
82 "meta": {}
83}
Remember to rebuild your app after making changes to some config or other code.
yarn build
# OR
yarn develop
MenuItem
attributes don't save or appear in the database table schema.Custom attributes require both the form layout extension as well as the schema extension. Please make sure both of these are configured as described in the Extending section.
Currently, this plugin does not even support RBAC (role-based access controls). If it appears as if a users does not have permissions to view menus, try to update that user's profile in Strapi or even change their password. This should kick something into place that fixes that user's permissions.
Either something in your menus plugin configuration is setup incorrectly or you may have custom middlewares, plugins, or lifecycle hooks that could be interfering. Check your developer console to gain more insight into the error.
Follow the migration guides to keep your menus plugin up-to-date.
If you are enjoying this plugin and feel extra appreciative, you can buy me a beer or 3 🍺🍺🍺.
url
via relation.npm install strapi-plugin-menus
Check out the available plugin resources that will help you to develop your plugin or provider and get it listed on the marketplace.