Simply copy and paste the following command line in your terminal to create your first Strapi project.
npx create-strapi-app
my-project
In this tutorial we will see how you can create a new field for the admin panel.
For this example, we will replace the default WYSIWYG with CKEditor in the Content Manager by creating a new plugin that will add a new field in your application.
yarn create strapi-app my-app --quickstart --no-run
# or
npx create-strapi-app@latest my-app --quickstart --no-run
The --no-run
flag was added as we will run additional commands to create a plugin right after the project generation.
cd my-app
yarn strapi generate
Choose "plugin" from the list, press Enter and name the plugin wysiwyg
.
1
2
3
4
5
6
7
8
9
10
// path: ./config/plugins.js
module.exports = {
// ...
wysiwyg: {
enabled: true,
resolve: "./src/plugins/wysiwyg", // path to plugin folder
},
// ...
};
cd src/plugins/wysiwyg
yarn add @ckeditor/ckeditor5-react @ckeditor/ckeditor5-build-classic
# Go back to the application root folder
cd ../../..
yarn develop --watch-admin
Note: Launching the Strapi server in watch mode without creating a user account first will open localhost:1337
with a JSON format error. Creating a user on localhost:8081
prevents this alert.
We now need to create our new WYSIWYG, which will replace the default one in the Content Manager.
In this part we will create 3 components:
MediaLib
component used to insert media in the editorEditor
component that uses CKEditor as the WYSIWYG editorWysiwyg
component to wrap the CKEditorThe following code examples can be used to implement the logic for the 3 components:
Example of a MediaLib component used to insert media in the editor:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// path: ./src/plugins/wysiwyg/admin/src/components/MediaLib/index.js
import React from "react";
import { prefixFileUrlWithBackendUrl, useLibrary } from "@strapi/helper-plugin";
import PropTypes from "prop-types";
const MediaLib = ({ isOpen, onChange, onToggle }) => {
const { components } = useLibrary();
const MediaLibraryDialog = components["media-library"];
const handleSelectAssets = (files) => {
const formattedFiles = files.map((f) => ({
alt: f.alternativeText || f.name,
url: prefixFileUrlWithBackendUrl(f.url),
mime: f.mime,
}));
onChange(formattedFiles);
};
if (!isOpen) {
return null;
}
return (
<MediaLibraryDialog
onClose={onToggle}
onSelectAssets={handleSelectAssets}
/>
);
};
MediaLib.defaultProps = {
isOpen: false,
onChange: () => {},
onToggle: () => {},
};
MediaLib.propTypes = {
isOpen: PropTypes.bool,
onChange: PropTypes.func,
onToggle: PropTypes.func,
};
export default MediaLib;
Example of an Editor component using CKEditor as the WYSIWYG editor:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// path: ./src/plugins/wysiwyg/admin/src/components/Editor/index.js
import React from "react";
import PropTypes from "prop-types";
import styled from "styled-components";
import { CKEditor } from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
import { Box } from "@strapi/design-system/Box";
const Wrapper = styled(Box)`
.ck-editor__main {
min-height: ${200 / 16}em;
> div {
min-height: ${200 / 16}em;
}
// Since Strapi resets css styles, it can be configured here (h2, h3, strong, i, ...)
}
`;
const configuration = {
toolbar: [
"heading",
"|",
"bold",
"italic",
"link",
"bulletedList",
"numberedList",
"|",
"indent",
"outdent",
"|",
"blockQuote",
"insertTable",
"mediaEmbed",
"undo",
"redo",
],
};
const Editor = ({ onChange, name, value, disabled }) => {
return (
<Wrapper>
<CKEditor
editor={ClassicEditor}
disabled={disabled}
config={configuration}
data={value || ""}
onReady={(editor) => editor.setData(value || "")}
onChange={(event, editor) => {
const data = editor.getData();
onChange({ target: { name, value: data } });
}}
/>
</Wrapper>
);
};
Editor.defaultProps = {
value: "",
disabled: false,
};
Editor.propTypes = {
onChange: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string,
disabled: PropTypes.bool,
};
export default Editor;
Example of a Wysiwyg component wrapping CKEditor:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
// path: ./src/plugins/wysiwyg/admin/src/components/Wysiwyg/index.js
import React, { useState } from "react";
import PropTypes from "prop-types";
import { Stack } from "@strapi/design-system/Stack";
import { Box } from "@strapi/design-system/Box";
import { Button } from "@strapi/design-system/Button";
import { Typography } from "@strapi/design-system/Typography";
import Landscape from "@strapi/icons/Landscape";
import MediaLib from "../MediaLib";
import Editor from "../Editor";
import { useIntl } from "react-intl";
const Wysiwyg = ({
name,
onChange,
value,
intlLabel,
disabled,
error,
description,
required,
}) => {
const { formatMessage } = useIntl();
const [mediaLibVisible, setMediaLibVisible] = useState(false);
const handleToggleMediaLib = () => setMediaLibVisible((prev) => !prev);
const handleChangeAssets = (assets) => {
let newValue = value ? value : "";
assets.map((asset) => {
if (asset.mime.includes("image")) {
const imgTag = `<p><img src="${asset.url}" alt="${asset.alt}"></img></p>`;
newValue = `${newValue}${imgTag}`;
}
// Handle videos and other type of files by adding some code
});
onChange({ target: { name, value: newValue } });
handleToggleMediaLib();
};
return (
<>
<Stack size={1}>
<Box>
<Typography variant="pi" fontWeight="bold">
{formatMessage(intlLabel)}
</Typography>
{required && (
<Typography variant="pi" fontWeight="bold" textColor="danger600">
*
</Typography>
)}
</Box>
<Button
startIcon={<Landscape />}
variant="secondary"
fullWidth
onClick={handleToggleMediaLib}
>
Media library
</Button>
<Editor
disabled={disabled}
name={name}
onChange={onChange}
value={value}
/>
{error && (
<Typography variant="pi" textColor="danger600">
{formatMessage({ id: error, defaultMessage: error })}
</Typography>
)}
{description && (
<Typography variant="pi">{formatMessage(description)}</Typography>
)}
</Stack>
<MediaLib
isOpen={mediaLibVisible}
onChange={handleChangeAssets}
onToggle={handleToggleMediaLib}
/>
</>
);
};
Wysiwyg.defaultProps = {
description: "",
disabled: false,
error: undefined,
intlLabel: "",
required: false,
value: "",
};
Wysiwyg.propTypes = {
description: PropTypes.shape({
id: PropTypes.string,
defaultMessage: PropTypes.string,
}),
disabled: PropTypes.bool,
error: PropTypes.string,
intlLabel: PropTypes.shape({
id: PropTypes.string,
defaultMessage: PropTypes.string,
}),
name: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
required: PropTypes.bool,
value: PropTypes.string,
};
export default Wysiwyg;
The last step is to register the wysiwyg
field with the Wysiwyg
component using addFields()
. Replace the content of the admin/src/index.js
field of the plugin with the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// path: ./src/plugins/wysiwyg/admin/src/index.js
import pluginPkg from "../../package.json";
import Wysiwyg from "./components/Wysiwyg";
import pluginId from "./pluginId";
const name = pluginPkg.strapi.name;
export default {
register(app) {
app.addFields({ type: "wysiwyg", Component: Wysiwyg });
app.registerPlugin({
id: pluginId,
isReady: true,
name,
});
},
bootstrap() {},
};
And voilà, if you create a new collection type or single type with a rich text field you will see the implementation of CKEditor instead of the default WYSIWYG:
Get all the latest Strapi updates, news and events.