My experiences with customizing the Strapi CMS
Update: The original post was written based on customizations performed on Strapi 3.6.6. We have, since then, upgraded to Strapi 4.x. I have updated the post to reflect the customizations for Strapi 4.x
A CMS solution that is built in-house from the ground-up offers maximum flexibility. But, it also requires a substantial initial time-to-build. Starting with a customizable open source CMS framework can reduce this initial time-to-build. But, there is always a danger of the ready-to-customize CMS not being customizable to certain future business requirements.
I mean, imagine if the business decides to go multilingual but the ready-to-customize CMS cannot support it without completely re-architecting the solution. To avoid such deadlock situations, it is critical to evaluate the customization capabilities of any ready-to-customize CMS.
I was in a similar situation ~6 months ago when I had to evaluate Node based CMS frameworks for one of my client’s website content management requirements. After the initial comparison of the available frameworks, I undertook a month long proof-of-concept customizing the Strapi CMS to the provided requirements. This was followed up with an implementation of the CMS solution that has now been in production for 3 months.
Based on this experience, I have detailed the kind of CMS customizations that were straight forward to achieve with Strapi. I have also listed the customizations are the non-trivial to implement. The objective of this post is to help someone evaluating Strapi customization capabilities to make an informed decision.
Straightforward strapi customizations
Below is the list of customizations that I found uncomplicated to achieve with the Strapi CMS.
Custom UI validations with lifecycle hooks:
Our content team intended to use the Strapi admin to add content to the system. So, we wanted custom UI validation functions to prevent invalid data from being entered into the system.
We were able to leverage Strapi’s lifecycle hooks to achieve this. Specifically, we added our custom input validation code to execute before create
and update
operations:
module.exports = {
async beforeCreate(event) {
const result = await validateInputs(event)
const err = result?.errors
if(result.error) {
//The admin UI would display the message passed here on the screen.
throw new ApplicationError(result.error.message, 400);
}
}
}
The above solution has worked well for us. But, with this implementation, the input validation executes only when the entire form is submitted. A more preferred implementation would be to validate a field's inputs as the user tabs out of the fields. But, implementing this approach would require overriding the Strapi admin UI source code. And, from my experience with overriding Strapi admin functionalities, this would be non-trivial to implement.
Activity log with middleware:
We wanted to record an audit trail of all the activities performed via the Strapi admin UI.
We created an application-level middleware to achieve this. We defined a function and added it to config/middlewares.js
to execute it on every request to Strapi. Since we were executing it on every request, we carefully defined a list of request patterns for which we wanted our middleware code to execute. This ensured that the middleware code did not affect the speed of the rest of the Admin UI functionality. Within our middleware, we could use a call like getService('activity-log').create(...)
to add an activity log entry.
We also made our activity-log table non-editable for Strapi admin users that were not Super Admins.
Update: Strapi 4 provides API level middlewares. We plan to change our implementation of Activity log to leverage API middlewares. This would minimize the middleware’s speed impact on Admin UI functionalities.
Customize media uploads with custom providers:
We wanted to use the Strapi admin to upload and maintain the images into our CMS system. However, we wanted to disallow uploads with certain names, extensions, mime-types and sizes.
Strapi team provides a very useful provider to upload media assets to S3. We decided to create our own version of this provider by creating ./providers/strapi-provider-upload-S3WithValidations
:
module.exports = {
...
...
const upload = (file, customParams = {}) => {
return new Promise((resolve, reject) => {
if (!['jpg', 'jpeg', 'png', 'webp', 'svg'].includes(file.ext))
return reject('Invalid file format.');
else
...
...
}
....
....
};
},
};
Since we can code our own plugin provider, performing any custom tasks during media upload is straight forward. But, what seems complicated is to add additional fields to hold media related meta-information. This is because it will require overriding the Admin UI code within Strapi's upload plugin to handle these additional fields.
Custom field types in the Strapi admin UI with plugin:
One of our requirements was to have a drop-down field that should have values obtained by querying another table. Strapi has an inbuilt enumeration
type for dropdowns. But, enumeration
can only contain a static list of values.
We had to create our own custom field-type to implement this. To achieve this, we create a Strapi plugin that would contain the implementation for the field behavior. We then added the newly created field to the Strapi admin code (by overriding the admin/admin/src/content-manager/components/inputs/index.js
file).
While the custom field-type implementation was straight forward, the lack of documentation on this made it complicated to implement. But, once implemented, we are certain that we can add more field-types in future (like WYSIGYG editor, color picker, maps location selector, image with annotations, etc) with ease.
Sitemap, email notifications and other customizations with lifecycle hooks:
We wanted to perform a bunch of activities whenever a certain content was published (via the Admin UI):
- Update the sitemap.
- Check for url-redirection handling.
- Clear CDN cache for the specific URLs.
- Send email notification to the concerned persons.
We found lifecycle hooks to be the right place to setup these actions. These have been very straight-forward to implement and maintain.
CMS customizations that can get complicated with Strapi
Below are some of the changes that I experienced to be non-trivial to implement with the Strapi CMS.
Custom content publishing workflow:
Our content publishing workflow was simple and mirrored the Strapi admin’s single-step publishing workflow (Save as Draft -> Publish
).
But, we observed that there is no easy way to customize the content publishing workflow (eg : Save as Draft -> Under Review -> Approved -> Publish
).
One possible way to achieve this may be by defining a custom field (say publication-status
) to contain the workflow status (like draft
, under-review
, approved
, published
). I could then write hooks to ensure the folks with right roles could update this field to right values. I could write middlewares to access-control records (do not expose records other than in published
status to the API client).
However, this kind of an implementation can become complicated to orchestrate and cumbersome to maintain.
Non cosmetic Strapi admin UI modifications:
Cosmetic UI modifications (like changing the logo or the welcome message) are easy to perform. But, the changes beyond such cosmetic alterations required a thorough understanding of the Strapi admin UI code, for which no adequate documentation is available. This made implementing such changes cumbersome and error-prone.
For example, modifying how the Strapi admin allows to re-order components within a dynamic zone (as raised here)can be complicated because it requires understanding how the Strapi admin code performs this and then overriding / adding to the admin UI functionality.
Handling schema updates:
When the model for a Content Type
or a Single Type
is altered by adding or removing the fields to it, there is no builtin mechanism or hook to specify how the schema change needs to be handled for the already existing data.
This required us to plan the model changes and write custom scripts to handle the changes for the existing data. We also backed-up our data before and end of the custom-scripts execution. I suspect this could become a potential concern as our setup grows older and contains more legacy data.
Building the CMS solution on top of the existing data:
Unlike many of the CMS solutions, Strapi maintains the data schema in-code. How this in-code schema is represented within the database is handled by the Strapi framework. This means, Strapi cannot directly be setup on top of an existing CMS database.
For our implementation, the amount of data in the legacy CMS database was limited. So, we migrated it manually by creating new records via the Strapi CMS admin UI. In case if the legacy CMS data would be large in quantity, we would have written custom scripts to read the rows of data and invoke Strapi CMS APIs to insert it into the Strapi CMS system.
Strapi (major) version upgrade
Our original implementation was on Strapi 3.6.6. With the release of Strapi 4, we upgraded to Strapi 4.1.2. We found the upgrade very challenging and time consuming:
-
There is very limited documentation on v3 to v4 upgrade. This resulted in a considerable time being spent on debugging the issues encountered during the upgrade.
-
Strapi 4 comes with breaking changes with respect to customizations (how we register a custom field, how we specify a middleware, etc). As a result, most of our customizations stopped working after upgrade. This required us to
- carefully go through each of our customizations
- refer Strapi 4 documentation to understand the changed implementation
- manually making the changes to get things to work
Based on this experience, we plan to systematically document all our customizations for future major version upgrades.
CMS customizations not feasible with Strapi
Version Control
Our content team desired that, whenever they would update a record, the CMS system would automatically save the previous versions of that record. More so, they wanted to be able to restore any of the previous versions back. This would help them update the content via the Strapi Admin UI without worrying about making wrong changes.
However, we could find no way to build this feature end-to-end. We found a few Strapi plugins offering this feature but they were error-prone and incomplete in functionality. We realized that this would have to be baked into the Strapi’s content manager setup and thus, cannot be added via Strapi’s customization capabilitites.
Conclusion
Strapi offers a very lucrative mix of ready-to-use features and customization capabilities via hooks, middlewares and plugins. That stated, any customization that cannot be setup via these Strapi options can be complicated to implement. As a result, it is vital to evaluate one’s must-have CMS requirements against Strapi’s customization capabilities before going ahead with a Strapi based CMS implementation.
Back in mid-2021, one of my clients was having issues with their in-house CMS. Despite me being their frontend architect, they trusted me to build their CMS solution. At this point, evaluation of various frameworks, approaches and products led me to Strapi. And, I ended up implementing Strapi CMS for their data requirements.
Fast-forward to now, I have worked with multiple orgs on implementing, upgrading & customizing Strapi setups based on their requirements.