Company
Date Published
Author
Paul Bratslavsky
Word count
3806
Language
English
Hacker News points
None

Summary

In this post, we will take a look at how customize Strapi dashboard by building a widget plugin for Strapi. Strapi Widgets are a way to add custom widgets to the Strapi admin panel. They are a great way to add customize Strapi dashboard for you clients. We will build our own dashboard The Strapi admin homepage is now fully customizable. With the new Widget API, developers can create dashboard components that display project stats, content overviews, links to workflows, custom metrics or visualizations, and more. It’s a new way to surface what matters most for each team. We will be building a widget that displays the number of content types in the Strapi application. This guide is based on Strapi v5 docs. You can find the original docs here. If you prefer to watch a video, you can check out the following video. I wanted to make a guide that is more hands on and practical. So I will walk you through the steps of building the widget. To simplify the process of setting up a Strapi plugin, we will use the Strapi Plugin CLI tool to help us accomplish this. We will start with the following command. npx @strapi/sdk-plugin@latest init my-first-widget. Make sure to run this command in the root of your Strapi application. This will create a new plugin in the src/plugins directory. We will create a frontend component and register it as a widget. First, let's quickly take a look at the plugin structure. We will just focus on the most important items where we will be working in. In the admin folder more closely we will see the following. index.ts - This is the entry point for our plugin. It is responsible for registering and configuring the plugin with the admin panel. components - This is the responsible for all of our frontend components. pages - This is the responsible for all frontend pages and routes. We will take a look at the server folder more closely later. But for now, let's register a new widget. We will start by creating a new component in the src/components folder named MetricsWidget.tsx. And and the following code. const MetricsWidget = () => { return ( <div> <h1> Hello World from my first widget</h1> </div> ); }; export default MetricsWidget; Now that we have a component, we need to register it as a widget. To do this, let's navigate to the admin/src/index.ts file and start by removing the following code. app.addMenuLink({ to: `plugins/${PLUGIN_ID}`, icon: PluginIcon, intlLabel: { id: `${PLUGIN_ID}.plugin.name`, defaultMessage: PLUGIN_ID }, Component: async () => { const { App } = await import("./pages/App"); return App; }, }); The above code is responsible for registering our plugin in the admin panel menu, not something we need for this use case. Next, let's register our widget component that we created earlier by adding the following code. app.widgets.register({ icon: Stethoscope, title: { id: `${PLUGIN_ID}.widget.metrics.title`, defaultMessage: "Content Metrics", }, component: async () => { const component = await import("./components/MetricsWidget"); return component.default; }, id: "content-metrics", pluginId: PLUGIN_ID, }); Make sure to import the Stethoscope icon from @strapi/icons-react. The completed file should look like this. import { PLUGIN_ID } from "./pluginId"; import { Initializer } from "./components/Initializer"; import { Stethoscope } from "@strapi/icons"; export default { register(app: any) { app.widgets.register({ icon: Stethoscope, title: { id: `${PLUGIN_ID}.widget.metrics.title`, defaultMessage: "Content Metrics", }, component: async () => { const component = await import("./components/MetricsWidget"); return component.default; }, id: "content-metrics", pluginId: PLUGIN_ID, }); app.registerPlugin({ id: PLUGIN_ID, initializer: Initializer, isReady: false, name: PLUGIN_ID, }); } async registerTrads({ locales }: { locales: string[] }) { return Promise.all(locales.map(async (locale) => { try { const { default: data } = await import(`./translations/${locale}.json`); return { data, locale }; } catch { return { data: {}, locale }; } }); } }; Now to test that everything is working, first in your terminal navigate to the src/plugins/my-first-widget directory and run the following command. npm run build npm run watch This will build the plugin and start the plugin in watch mode with hot reloading. Now in another terminal navigate to the root of your Strapi application and run the following command. npm run dev This will start the Strapi application. You should be able to find your plugin in the admin panel in the left sidebar. Nice, now we have a plugin that is working. Let's create a new widget. First, let's quickly take a look at the plugin structure. We will just focus on the most important items where we will be working in. In the server folder more closely we will see the following. src - This is the responsible for all of our backend code. config - This is the responsible for all of our configuration files. content-types - This is the responsible for all of our content types. controllers - This is the responsible for all of our custom controller logic. middlewares - This is the responsible for all of our middleware functions. policies - This is the responsible for all of our policy definitions. routes - This is the responsible for all of our API routes. services - This is the responsible for all of our service classes. bootstrap.ts - This is the entry point for our plugin's bootstrap code. destroy.ts - This is the entry point for our plugin's destruction code. index.js - This is the entry point for our plugin's main code. register.ts - This is the entry point for our plugin's registration code. We will be working in the src/controllers and src/routes folders. You can learn more about Strapi backend customizations here. Let's start by creating a new controller. You can learn more about Strapi controllers here. Let's navigate to the src/controllers and make the following changes in the controller.ts file. import type { Core } from "@strapi/strapi"; const controller = ({ strapi }: { strapi: Core.Strapi }) => ({ async getContentCounts(ctx) { try { // TODO: Add custom logic here ctx.body = "Hello from the server"; } catch (err) { ctx.throw(500, err); } }); export default controller; Now that we have our basic controller, let's update the routes to be able to fetch data from our controller in the "Content API" and "Admin API". Content API - This is the API that is used to fetch data from the public website. Admin API - This is the API that is used to fetch data from the admin panel. This is the API that is used to fetch data from the admin panel. Let's navigate to the src/routes folder and make the following changes. In the content-api.ts file, let's make the following changes. const routes = [ { method: "GET", path: "/count", handler: "controller.getContentCounts", config: { policies: [] } }, ]; This will create a new route that will be able to fetch data from our controller that we can use to fetch data from an external frontend application. Let's try it out. First, in the Admin Panel navigate to the Settings - Users & Permissions plugin - Roles - Public role. You should now see our newly created custom route ( getCustomCounts ) from our plugin that powers our widget. We can test it out in Postman by making a GET request to the following URL: http://localhost:1337/api/my-first-widget/count We should see the following response: { "message": "Hello from the server" } Now that we have a working Content API route, let's see how we can do the same by creating a Admin API route that will be internal route used by our Strapi Admin Panel. Let's navigate to the src/routes folder and make the following changes. First let’s create a new file named admin-api.ts and add the following code. const routes = [ { method: "GET", path: "/count", handler: "controller.getContentCounts", config: { policies: [] } }, ]; Now, let's update the index.js file to include our new route. import adminAPIRoutes from "./admin-api"; import contentAPIRoutes from "./content-api"; const routes = { admin: { type: "admin", routes: adminAPIRoutes }, "content-api": { type: "content-api", routes: contentAPIRoutes }, }; export default routes; Step 5: Update Frontend Component to fetch our test data. And finally, let's update our component in the src/components/MetricsWidget.tsx file to fetch data from our new route. To accomplish this, we will use the useFetchClient provided by Strapi. Let’s update the MetricsWidget.tsx file with the following code. import { useState, useEffect } from 'react'; import { useFetchClient } from '@strapi/strapi/admin'; import { Widget } from '@strapi/admin/strapi-admin'; import { Table, Tbody, Tr, Td, Typography } from "@strapi/design-system"; import { PLUGIN_ID } from '../pluginId'; const PATH = '/count'; const MetricsWidget = () => { const { get } = useFetchClient(); const [loading, setLoading] = useState(true); const [metrics, setMetrics] = useState({}); const [error, setError] = useState(null); useEffect(() => { const fetchMetrics = async () => { try { const { data } = await get(PLUGIN_ID + PATH); console.log('data:', data); const formattedData: Record<string, string | number> = {}; if (data &amp; typeof data === 'object') { await Promise.all(Object.entries(data).map(async ([key, value] =&gt; { if (typeof value === 'function') { const result = await value(); formattedData[key] = typeof result === 'number' ? result : String(result); } else { formattedData[key] = typeof value === 'number' ? value : String(value); } }))); setMetrics(formattedData); setLoading(false); } catch (err) { console.error(err); setError(err instanceof Error ? err.message : 'An error occurred'); setLoading(false); } }; fetchMetrics(); if (loading) { return &lt;Widget.Loading /&gt;; } if (error) { return &lt;Widget.Error /&gt;; } if (!metrics || Object.keys(metrics).length === 0) { return &lt;Widget.NoData&gt;No content types found&lt;/Widget.NoData&gt;; } return (&lt;Table&gt;&lt;Tbody&gt;{Object.entries(metrics).map(([contentType, count], index =&gt; (&lt;Tr key={index}&gt;&lt;Td&gt;&lt;Typography variant="omega"&gt;{String(contentType)}&lt;/Typography&gt;&lt;/Td&gt;&lt;/Tr&gt;)&lt;/Tbody&gt;&lt;/Table&gt;);}); }; export default MetricsWidget; Now, if you navigate to the Admin Panel you should see the final result. Yay, we have a cool new widget that displays the number of content types in our project. Now, you can start building your own widgets and share them with the community. Building a custom widget for Strapi may seem complex at first, but once you go through it step by step, it’s actually pretty straightforward. You now have a working widget that shows content counts right inside the Strapi admin panel—and it looks great thanks to the built-in Design System. Widgets like this can be a powerful way to add helpful tools for your team or clients.