Strapi is a great content management system, and my go-to these days. Often, the underdog Scrappy-Doo comes to mind. Like a show with failing ratings, I've grown the need add some character to Strapi. Custom configuration and utilities have accumulated over time, and patch-package
has served me well.
# Problem
Recently, with the release of v4.15.0
, the customization I once injected via patch-package
no longer has a path into the build. As a Strapi power-user, I'd like to redirect to a custom dashboard after logging in to the Strapi dashboard. Let's explore a solution I found on the Strapi Forums!
tl;dr? Have a peek at the GitHub repo, or, even faster, the Gist.
# Demo
# Details
We're going to save what we can from ./patches
directly into a Strapi plugin, and try to handle redirection from the ho-hum /admin
route to our handy custom dashboard.
# Minor Change?
Recently the Strapi team started to refactor the entire codebase with TypeScript, and are doing so with standard releases.
Why this was done is another conversation, but needless to say, the plugin and developer community were blindsided to find components and logic that were once customizable at build time — GASP — have been hidden away in obfuscation.
Some time around Strapi v4.15.0
, most of the application started shipping without its source. The project now includes only compiled code, leaving no way for a downstream developer to customize Strapi in a way previously possible.
Oddly, Strapi's own documentation remains unchanged in this regard, indicating patch-package
as a means of applying advanced customization. But I digress, let's take a look at what can be done!
# Solution, Part 1
I took some time to throw away the patch-package
patches I'd put together, and recreate them with a Strapi plugin. This was easy, and gets us 95% of the way to a fully customized landing page after sign in.
We're going to:
- Install Strapi
- Create a plugin
- Add custom middleware
- Use Strapi's design system
- Install, configure, and use the plugin
Let's get started!
# Install Strapi
We could use an existing installation, but starting fresh might be a good idea if poking a production application makes you queazy.
npx create-strapi-app@latest --quickstart strapi-dashbored
That's not a typo. Our plugin shall be called "strapi-dashbored". The above command installs Strapi with a basic sqlite
database, so we don't need to do any additional configuration.
If creating a new installation, set up a super admin account now.
We will be brought to the Strapi Dashboard, which in all its exciting and mystical glory, looks like this:
Let's dig in.
# Add the "Dashbored" Plugin
Strapi includes a bunch of useful command line utilities. A list is available via npm run strapi -- --help
. Let's run the strapi generate
command. Remember to cd path/to/install
before running the below!
> strapi-dashbored@0.1.0 strapi
> strapi generate
Strapi Generators
api - Generate a basic API
controller - Generate a controller for an API
content-type - Generate a content type for an API
❯ plugin - Generate a basic plugin
policy - Generate a policy for an API
middleware - Generate a middleware for an API
migration - Generate a migration
(Move up and down to reveal more choices)
The command line dialog will ask what we're trying to generate. Arrow-key down to plugin
and press enter. Then, pick a name for the plugin:
? Plugin name dashbored
Select the flavor of JavaScript:
? Choose your preferred language (Use arrow keys)
❯ JavaScript
TypeScript
The plugin will be generated, and Strapi provides us with the next step. If following along with a new installation, or plugins are uncharted territory, the plugins.js
file may need to be created.
You can now enable your plugin by adding the following in ./config/plugins.js
──────────────────────────────────────
module.exports = {
// ...
'dashbored': {
enabled: true,
resolve: './src/plugins/dashbored'
},
// ...
}
──────────────────────────────────────
My applied changes reflecting this addition (File: config/plugins.js
):
module.exports = {
dashbored: {
enabled: true,
resolve: "./src/plugins/dashbored",
},
};
After adding the above, we should rebuild the project for good measure. This isn't always needed, but is good muscle-memory to build.
> strapi-dashbored@0.1.0 build
> strapi build
⠋ Building build context
[INFO] Including the following ENV variables as part of the JS bundle:
- ADMIN_PATH
- STRAPI_ADMIN_BACKEND_URL
- STRAPI_TELEMETRY_DISABLED
✔ Building build context (48ms)
⠋ Building admin panel
✔ Building admin panel (4853ms)
Great! Let's take a peek at the admin panel now. Start Strapi again via npm run develop
.
Thar she blows! Our plugin is now installed, showing in the side nav, and ready to for a hot branding.
# Customize the Plugin
The only function our plugin has at the moment is looking bad in dark mode. It seems someone forgot to use the Strapi Design System while creating the plugin generator scripts. Hmmm?
¯\_(ツ)_/¯
# Name & Description
The main display of a plugin's name and description in the admin panel is controlled by a strapi
property in the package.json
file in its directory. Update the name
, description
, and displayName
fields:
File: src/plugins/dashbored/package.json
|
|
|
|
|
|
The default plugin icon feels off. Do we want a puzzle piece puzzling our users? No! Onward.
# Plugin Icon
The plugin's Side Panel icon can be customized. We'll change the icon using built-in Strapi Design System icons.
Options (some of which are no longer work!), can be found here thanks to GitHub genius @andresmv94.
As this is a dashboard plugin, let's use the <Dashboard />
icon. Pop open src/plugins/dashbored/PluginIcon/index.js
and adjust like so:
/**
*
* PluginIcon
*
*/
import React from 'react';
import { Dashboard } from '@strapi/icons';
const PluginIcon = () => <Dashboard />;
export default PluginIcon;
Remember to npm run build
, npm run develop
, and hard-refresh your browser to see the icon and capitalization change.!
# Plugin Sanity-Check
Where one was once able to peruse layouts in node_modules/@strapi/admin/admin/src
locally, you will find nothing but chunked exports and imports. Take a peek at the GitHub repo and Strapi Design System for layout and content ideas.
Let's add some custom UI to the plugin Home Page by editing — you guessed it — the HomePage
component:
/*
*
* HomePage
*
*/
import React from "react";
// import PropTypes from 'prop-types';
import pluginId from "../../pluginId";
const HomePage = () => {
return (
<div>
<h1>{pluginId}'s Bacon Bed, Eggs, and Breakfast</h1>
<p>Happy coding</p>
</div>
);
};
export default HomePage;
Remember to npm run build
again before banging your head against any walls. After a build and npm run develop
, we can see the change:
# Full Plugin View
Let's skip the Strapi Design system trudgery and take a peek at the final code and view. I stole most of this directly from the Strapi dashboard code.
Note
We may be attacked by the IDE's Red Squiggle Monster, TypeScript is likely angry about JSX flags. Pop open jsconfig.json
in the project's base directory, and add "jsx": "react",
to compilerOptions
as such:
|
|
|
|
|
|
|
|
|
Open up the HomePage
component and add the below (File: src/plugins/dashboard/admin/src/pages/HomePage/index.js
).
Important
You'll also need the assets
directory (on line 11
). Grab the files here.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Next, we'll add some blocks of content. Create File: src/plugins/dashboard/admin/src/components/ContentBlocks.js
:
import React from "react";
import { useHistory } from "react-router-dom";
import { Box, Flex, Grid, GridItem } from "@strapi/design-system";
import { ContentBox, useTracking } from "@strapi/helper-plugin";
import {
FeatherSquare,
InformationSquare,
ChartBubble,
Crown,
} from "@strapi/icons";
import { useIntl } from "react-intl";
import styled from "styled-components";
const BlockLink = styled.a`
text-decoration: none;
`;
const StyledChartBubble = styled(ChartBubble)`
path {
fill: #7289da !important;
}
`;
const StyledInformationSquare = styled(InformationSquare)`
path {
stroke: #7289da !important;
}
`;
const StyledCrown = styled(Crown)`
path {
fill: #7289da !important;
stroke: #7289da !important;
}
`;
const StyledFeatherSquare = styled(FeatherSquare)`
path {
stroke: #7289da !important;
}
`;
const ContentBlocks = () => {
const { formatMessage } = useIntl();
const { trackUsage } = useTracking();
const { push } = useHistory();
const navigate = (e, url) => {
e.preventDefault();
push(url);
};
return (
<Flex direction="column" alignItems="stretch" gap={5}>
<Grid gap={5}>
<GridItem col={3}>
<BlockLink
href="#"
onClick={(e) => {
navigate(
e,
"/content-manager/collectionType/api::post.post?page=1&pageSize=100"
);
}}
>
<ContentBox
title={formatMessage({
id: "dashbored.posts.title",
defaultMessage: "Posts",
})}
subtitle={formatMessage({
id: "dashbored.posts.label",
defaultMessage: "Edit Posts",
})}
icon={<StyledFeatherSquare />}
iconBackground="primary100"
/>
</BlockLink>
</GridItem>
<GridItem col={3}>
<BlockLink
href="#"
onClick={(e) => {
navigate(
e,
"/content-manager/collectionType/api::project.project?page=1&pageSize=100"
);
}}
>
<ContentBox
title={formatMessage({
id: "dashbored.projects.title",
defaultMessage: "Edit Projects",
})}
subtitle={formatMessage({
id: "dashbored.projects.subtitle",
defaultMessage: "Add or Manage Projects",
})}
icon={<StyledCrown />}
iconBackground="primary100"
/>
</BlockLink>
</GridItem>
<GridItem col={3}>
<BlockLink
href="#"
onClick={(e) => {
navigate(e, "/content-manager/singleType/api::home.home");
}}
>
<ContentBox
title={formatMessage({
id: "app.components.HomePage.HomeContent.title",
defaultMessage: "Edit Projects",
})}
subtitle={formatMessage({
id: "app.components.HomePage.HomeContent.subtitle",
defaultMessage: "Edit Project Content",
})}
icon={<StyledInformationSquare />}
iconBackground="primary100"
/>
</BlockLink>
</GridItem>
<GridItem col={3}>
<BlockLink
href="https://analytics.com"
target="_blank"
rel="noopener noreferrer nofollow"
>
<ContentBox
title={formatMessage({
id: "app.components.HomePage.analytics.title",
defaultMessage: "Analytics",
})}
subtitle={formatMessage({
id: "app.components.HomePage.analytics.subtitle",
defaultMessage: "See the traffic",
})}
icon={<StyledChartBubble />}
iconBackground="primary100"
/>
</BlockLink>
</GridItem>
</Grid>
</Flex>
);
};
export default ContentBlocks;
A very important part of ContentBlocks.jsx
is the import
of { useHistory }
from "react-router-dom";
(line 2
above). This must be used for internal Strapi admin links, otherwise you'll have full browser-reloads between pages.
There's a small navigate
helper function to handle the links we've added on lines 48-51
below (File: src/plugins/dashbored/admin/src/components/ContentBlocks.js
):
|
|
|
|
|
It's used on the <BlockLink />
components (File: src/plugins/dashbored/admin/src/components/ContentBlocks.js
):
|
|
|
|
|
|
|
|
|
|
|
After the above changes have been applied, we may behold our custom dashboard, in all its glory:
Now, to semi-replace the admin dashboard with our own, let's look into applying redirects after authentication with Strapi.
# Solution, Part 2
After a user logs in to the CMS, they're redirected one of several places. For our usecase, this is the /admin
route. This used to be as simple as patch-package
ing the default HomePage
component, but we can no longer have nice things.
Don't be fooled — this is great! Because we can learn about middlewares
instead.
# Harry, You're a Middleware
As its name implies, middleware
sits in the middle, between processes and actions in our Strapi application. Like chipping a dog with dromosagnosia, we can inject our own middleware into Strapi's configuration.
A great example is posted on StackOverflow, and we'll do just this, but have Strapi do some of the work. Let's use strapi generate
again, but this time to conjure some middleware. It's as easy as
npm run strapi generate
- arrow down to
middleware
- name your middlware
- arrow down to
Add middleware to root of project
- Abracadabra!
> strapi-dashbored@0.1.0 strapi
> strapi generate
? Strapi Generators middleware - Generate a middleware for an API
? Middleware name rebored
? Where do you want to add this middleware? Add middleware to root of project
✔ ++ /middlewares/rebored.js
This can just as easily be named "redirect", but I'm staying on brand. Because that's what marketing gurus do.
Let's add the redirects. Pop open src/middlewares/rebored.js
, and scribe the below:
'use strict';
/**
* `rebored` middleware
*/
module.exports = (config, { strapi }) => {
const redirects = ["/", "/index.html", "/admin", "/admin/"].map((path) => ({
method: "GET",
path,
handler: (ctx) => ctx.redirect("/admin/plugins/dashbored"),
config: { auth: false },
}));
strapi.server.routes(redirects);
};
Here, we add some redirection routes based on the paths logged-in users hit by default. The idea is to catch the dog before it runs too far.
Like our plugin, we must add this new middleware to Strapi's configuration. Unfurl config/middlewares.js
. Let us wet our pens:
|
|
|
|
|
|
|
|
|
|
|
|
|
# Conclusion
That's it! After npm run build
ing and restarting Strapi, we'll see the new behavior when signing in. Sometimes. It rarely works in develop
mode, but has been working well for me in production and not-production environments.
If the old carnival-banner strewn "Welcome on board 👋" page strays from our new route, refresh the browser. Our now chipped wandering Strapi should find its way home to Dashbored land.
I hope this post brings together some ideas for the developer community. If anyone knows how to permanently override an internal Strapi route, to avoid race conditions between client and server, please share in the Gist!
# Update
I'm not sure why I hadn't thought to share a possible medium for your blank canvas until now. But it makes a great segue into another forthcoming post about Plausible Analytics, soon to come in 2024!
Here's the complete custom dashboard for dgrebb.com. Plausible's brand palette fits in quite nicely, don't you think?
One can do this by making a public link and shareable embed. Check out Plausible Analytics' guide.
Then, embed it into ContentBlocks.js
, ~Line 146, like so:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
There's more Plausible Analytics next time, but for now feel free to read about the Privacy Policy, or check out analytics for dgrebb.com.