# Email Provider Source: https://docs.strapi.io/cloud/advanced/email ## Configure the Provider Description: In your Strapi project, create a ./config/env/production/plugins.js or ./config/env/production/plugins.ts file with the following content: (Source: https://docs.strapi.io/cloud/advanced/email#configure-the-provider) Language: JavaScript File path: ./config/env/production/plugins.js ```js module.exports = ({ env }) => ({ // … some unrelated plugins configuration options // highlight-start email: { config: { // … provider-specific upload configuration options go here } // highlight-end // … some other unrelated plugins configuration options } }); ``` --- Language: TypeScript File path: ./config/env/production/plugins.ts ```ts export default ({ env }) => ({ // … some unrelated plugins configuration options // highlight-start email: { config: { // … provider-specific upload configuration options go here } // highlight-end // … some other unrelated plugins configuration options } }); ``` Language: JavaScript File path: ./config/env/production/plugins.js ```js module.exports = ({ env }) => ({ // ... email: { config: { provider: 'sendgrid', providerOptions: { apiKey: env('SENDGRID_API_KEY'), }, settings: { defaultFrom: 'myemail@protonmail.com', defaultReplyTo: 'myemail@protonmail.com', }, }, }, // ... }); ``` --- Language: JavaScript File path: ./config/env/production/plugins.js ```js module.exports = ({ env }) => ({ // ... email: { config: { provider: 'amazon-ses', providerOptions: { key: env('AWS_SES_KEY'), secret: env('AWS_SES_SECRET'), amazon: 'https://email.us-east-1.amazonaws.com', }, settings: { defaultFrom: 'myemail@protonmail.com', defaultReplyTo: 'myemail@protonmail.com', }, }, }, // ... }); ``` --- Language: JavaScript File path: ./config/env/production/plugins.js ```js module.exports = ({ env }) => ({ // ... email: { config: { provider: 'mailgun', providerOptions: { key: env('MAILGUN_API_KEY'), // Required domain: env('MAILGUN_DOMAIN'), // Required url: env('MAILGUN_URL', 'https://api.mailgun.net'), //Optional. If domain region is Europe use 'https://api.eu.mailgun.net' }, settings: { defaultFrom: 'myemail@protonmail.com', defaultReplyTo: 'myemail@protonmail.com', }, }, }, // ... }); ``` Language: TypeScript File path: ./config/env/production/plugins.ts ```ts export default ({ env }) => ({ // ... email: { config: { provider: 'sendgrid', providerOptions: { apiKey: env('SENDGRID_API_KEY'), }, settings: { defaultFrom: 'myemail@protonmail.com', defaultReplyTo: 'myemail@protonmail.com', }, }, }, // ... }); ``` --- Language: TypeScript File path: ./config/env/production/plugins.ts ```ts export default ({ env }) => ({ // ... email: { config: { provider: 'amazon-ses', providerOptions: { key: env('AWS_SES_KEY'), secret: env('AWS_SES_SECRET'), amazon: 'https://email.us-east-1.amazonaws.com', }, settings: { defaultFrom: 'myemail@protonmail.com', defaultReplyTo: 'myemail@protonmail.com', }, }, }, // ... }); ``` --- Language: TypeScript File path: ./config/env/production/plugins.ts ```ts export default ({ env }) => ({ // ... email: { config: { provider: 'mailgun', providerOptions: { key: env('MAILGUN_API_KEY'), // Required domain: env('MAILGUN_DOMAIN'), // Required url: env('MAILGUN_URL', 'https://api.mailgun.net'), //Optional. If domain region is Europe use 'https://api.eu.mailgun.net' }, settings: { defaultFrom: 'myemail@protonmail.com', defaultReplyTo: 'myemail@protonmail.com', }, }, }, // ... }); ``` # Upload Provider Configuration for Strapi Cloud Source: https://docs.strapi.io/cloud/advanced/upload ## Configure the Provider Description: To configure a 3rd-party upload provider in your Strapi project, create or edit the plugins configuration file for your production environment ./config/env/production/plugins.js|ts by adding upload configuration options as follows: (Source: https://docs.strapi.io/cloud/advanced/upload#configure-the-provider) Language: JavaScript File path: ./config/env/production/plugins.js ```js module.exports = ({ env }) => ({ // … some unrelated plugins configuration options // highlight-start upload: { config: { // … provider-specific upload configuration options go here } // highlight-end // … some other unrelated plugins configuration options } }); ``` --- Language: TypeScript File path: ./config/env/production/plugins.ts ```ts export default ({ env }) => ({ // … some unrelated plugins configuration options // highlight-start upload: { config: { // … provider-specific upload configuration options go here } // highlight-end // … some other unrelated plugins configuration options } }); ``` Language: JavaScript File path: ./config/env/production/plugins.js ```js module.exports = ({ env }) => ({ // ... upload: { config: { provider: 'cloudinary', providerOptions: { cloud_name: env('CLOUDINARY_NAME'), api_key: env('CLOUDINARY_KEY'), api_secret: env('CLOUDINARY_SECRET'), }, actionOptions: { upload: {}, uploadStream: {}, delete: {}, }, }, }, // ... }); ``` --- Language: JavaScript File path: ./config/env/production/plugins.js ```js module.exports = ({ env }) => ({ // ... upload: { config: { provider: 'aws-s3', providerOptions: { baseUrl: env('CDN_URL'), rootPath: env('CDN_ROOT_PATH'), s3Options: { credentials: { accessKeyId: env('AWS_ACCESS_KEY_ID'), secretAccessKey: env('AWS_ACCESS_SECRET'), }, region: env('AWS_REGION'), params: { ACL: env('AWS_ACL', 'public-read'), signedUrlExpires: env('AWS_SIGNED_URL_EXPIRES', 15 * 60), Bucket: env('AWS_BUCKET'), }, }, }, actionOptions: { upload: {}, uploadStream: {}, delete: {}, }, }, }, // ... }); ``` Language: TypeScript File path: ./config/env/production/plugins.ts ```ts export default ({ env }) => ({ // ... upload: { config: { provider: 'cloudinary', providerOptions: { cloud_name: env('CLOUDINARY_NAME'), api_key: env('CLOUDINARY_KEY'), api_secret: env('CLOUDINARY_SECRET'), }, actionOptions: { upload: {}, uploadStream: {}, delete: {}, }, }, }, // ... }); ``` --- Language: TypeScript File path: ./config/env/production/plugins.ts ```ts export default ({ env }) => ({ // ... upload: { config: { provider: 'aws-s3', providerOptions: { baseUrl: env('CDN_URL'), rootPath: env('CDN_ROOT_PATH'), s3Options: { credentials: { accessKeyId: env('AWS_ACCESS_KEY_ID'), secretAccessKey: env('AWS_ACCESS_SECRET'), }, region: env('AWS_REGION'), params: { ACL: env('AWS_ACL', 'public-read'), signedUrlExpires: env('AWS_SIGNED_URL_EXPIRES', 15 * 60), Bucket: env('AWS_BUCKET'), }, }, }, actionOptions: { upload: {}, uploadStream: {}, delete: {}, }, }, }, // ... }); ``` ## Configure the Security Middleware Description: Example: (Source: https://docs.strapi.io/cloud/advanced/upload#configure-the-security-middleware) Language: JavaScript File path: ./config/middleware.js ```js module.exports = [ // ... { name: 'strapi::security', config: { contentSecurityPolicy: { useDefaults: true, directives: { 'connect-src': ["'self'", 'https:'], 'img-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'res.cloudinary.com' ], 'media-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'res.cloudinary.com', ], upgradeInsecureRequests: null, }, }, }, }, // ... ]; ``` --- Language: JavaScript File path: ./config/middleware.js ```js module.exports = [ // ... { name: 'strapi::security', config: { contentSecurityPolicy: { useDefaults: true, directives: { 'connect-src': ["'self'", 'https:'], 'img-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'yourBucketName.s3.yourRegion.amazonaws.com', ], 'media-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'yourBucketName.s3.yourRegion.amazonaws.com', ], upgradeInsecureRequests: null, }, }, }, }, // ... ]; ``` Language: TypeScript File path: ./config/middleware.ts ```ts export default [ // ... { name: 'strapi::security', config: { contentSecurityPolicy: { useDefaults: true, directives: { 'connect-src': ["'self'", 'https:'], 'img-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'res.cloudinary.com' ], 'media-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'res.cloudinary.com', ], upgradeInsecureRequests: null, }, }, }, }, // ... ]; ``` --- Language: TypeScript File path: ./config/middleware.ts ```ts export default [ // ... { name: 'strapi::security', config: { contentSecurityPolicy: { useDefaults: true, directives: { 'connect-src': ["'self'", 'https:'], 'img-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'yourBucketName.s3.yourRegion.amazonaws.com', ], 'media-src': [ "'self'", 'data:', 'blob:', 'market-assets.strapi.io', 'yourBucketName.s3.yourRegion.amazonaws.com', ], upgradeInsecureRequests: null, }, }, }, }, // ... ]; ``` # Cloud Cli Source: https://docs.strapi.io/cloud/cli/cloud-cli ## strapi login Description: Log in Strapi Cloud. (Source: https://docs.strapi.io/cloud/cli/cloud-cli#strapi-login) Language: Bash File path: N/A ```bash strapi login ``` ## strapi deploy Description: Deploy a new local project (< 100MB) in Strapi Cloud. (Source: https://docs.strapi.io/cloud/cli/cloud-cli#strapi-deploy) Language: Bash File path: N/A ```bash strapi deploy ``` ## strapi link Description: Links project in the current folder to an existing project in Strapi Cloud. (Source: https://docs.strapi.io/cloud/cli/cloud-cli#strapi-link) Language: Bash File path: N/A ```bash strapi link ``` ## strapi projects Description: Lists all Strapi Cloud projects associated with your account. (Source: https://docs.strapi.io/cloud/cli/cloud-cli#strapi-projects) Language: Bash File path: N/A ```bash strapi projects ``` ## strapi logout Description: Log out of Strapi Cloud. (Source: https://docs.strapi.io/cloud/cli/cloud-cli#strapi-logout) Language: Bash File path: N/A ```bash strapi logout ``` # Caching Source: https://docs.strapi.io/cloud/getting-started/caching ## Cache-Control Header in Strapi Cloud Description: Responses from dynamic apps served by Strapi Cloud are not cached by default. (Source: https://docs.strapi.io/cloud/getting-started/caching#cache-control-header-in-strapi-cloud) Language: Fish File path: N/A ```fish function myHandler(req, res) { // Set the Cache-Control header to cache responses for 1 day res.setHeader('Cache-Control', 'max-age=86400'); // Add your logic to generate the response here } ``` --- Language: JavaScript File path: N/A ```js import { Request, Response } from 'express'; function myHandler(req: Request, res: Response) { // Set the Cache-Control header to cache responses for 1 day res.setHeader('Cache-Control', 'max-age=86400'); // Add your logic to generate the response here } ``` # Strapi Cloud - CLI deployment Source: https://docs.strapi.io/cloud/getting-started/deployment-cli ## Logging in to Strapi Cloud Description: Enter the following command to log into Strapi Cloud: (Source: https://docs.strapi.io/cloud/getting-started/deployment-cli#logging-in-to-strapi-cloud) Language: Bash File path: N/A ```bash yarn strapi login ``` --- Language: Bash File path: N/A ```bash npx run strapi login ``` ## Deploying your project Description: From your terminal, still from the folder of your Strapi project, enter the following command to deploy the project: (Source: https://docs.strapi.io/cloud/getting-started/deployment-cli#deploying-your-project) Language: Bash File path: N/A ```bash yarn strapi deploy ``` --- Language: Bash File path: N/A ```bash npx run strapi deploy ``` # Admin panel customization Source: https://docs.strapi.io/cms/admin-panel-customization ## General considerations Description: Before deployment, the admin panel needs to be built, by running the following command from the project's root directory: (Source: https://docs.strapi.io/cms/admin-panel-customization#general-considerations) Language: Bash File path: /src/admin/app.js ```bash yarn build ``` --- Language: Bash File path: /src/admin/app.js ```bash npm run build ``` ## Basic example Description: The following is an example of a basic customization of the admin panel: (Source: https://docs.strapi.io/cms/admin-panel-customization#basic-example) Language: JavaScript File path: /src/admin/app.js ```js import AuthLogo from "./extensions/my-logo.png"; import MenuLogo from "./extensions/logo.png"; import favicon from "./extensions/favicon.png"; export default { config: { // Replace the Strapi logo in auth (login) views auth: { logo: AuthLogo, }, // Replace the favicon head: { favicon: favicon, }, // Add a new locale, other than 'en' locales: ["fr", "de"], // Replace the Strapi logo in the main navigation menu: { logo: MenuLogo, }, // Override or extend the theme theme: { // overwrite light theme properties light: { colors: { primary100: "#f6ecfc", primary200: "#e0c1f4", primary500: "#ac73e6", primary600: "#9736e8", primary700: "#8312d1", danger700: "#b72b1a", }, }, // overwrite dark theme properties dark: { // ... }, }, // Extend the translations translations: { fr: { "Auth.form.email.label": "test", Users: "Utilisateurs", City: "CITY (FRENCH)", // Customize the label of the Content Manager table. Id: "ID french", }, }, // Disable video tutorials tutorials: false, // Disable notifications about new Strapi releases notifications: { releases: false }, }, bootstrap() {}, }; ``` --- Language: TypeScript File path: /src/admin/app.ts ```ts // Note: you may see some ts errors, don't worry about them import AuthLogo from "./extensions/my-logo.png"; import MenuLogo from "./extensions/logo.png"; import favicon from "./extensions/favicon.png"; export default { config: { // Replace the Strapi logo in auth (login) views auth: { logo: AuthLogo, }, // Replace the favicon head: { // Try to change the origin favicon.png file in the // root of strapi project if this config don't work. favicon: favicon, }, // Add a new locale, other than 'en' locales: ["fr", "de"], // Replace the Strapi logo in the main navigation menu: { logo: MenuLogo, }, // Override or extend the theme theme: { dark:{ colors: { alternative100: '#f6ecfc', alternative200: '#e0c1f4', alternative500: '#ac73e6', alternative600: '#9736e8', alternative700: '#8312d1', buttonNeutral0: '#ffffff', buttonPrimary500: '#7b79ff', // you can see other colors in the link below }, }, light:{ // you can see the light color here just like dark colors https://github.com/strapi/design-system/blob/main/packages/design-system/src/themes/lightTheme/light-colors.ts }, }, }, // Extend the translations // you can see the traslations keys here https://github.com/strapi/strapi/blob/develop/packages/core/admin/admin/src/translations translations: { fr: { "Auth.form.email.label": "test", Users: "Utilisateurs", City: "CITY (FRENCH)", // Customize the label of the Content Manager table. Id: "ID french", }, }, // Disable video tutorials tutorials: false, // Disable notifications about new Strapi releases notifications: { releases: false }, }, bootstrap() {}, }; ``` # Admin panel bundlers Source: https://docs.strapi.io/cms/admin-panel-customization/bundlers ## Vite Description: To extend the usage of Vite, define a function that extends its configuration inside /src/admin/vite.config: (Source: https://docs.strapi.io/cms/admin-panel-customization/bundlers#vite) Language: JavaScript File path: /src/admin/vite.config.js ```js const { mergeConfig } = require("vite"); module.exports = (config) => { // Important: always return the modified config return mergeConfig(config, { resolve: { alias: { "@": "/src", }, }, }); }; ``` --- Language: TypeScript File path: /src/admin/vite.config.ts ```ts import { mergeConfig } from "vite"; export default (config) => { // Important: always return the modified config return mergeConfig(config, { resolve: { alias: { "@": "/src", }, }, }); }; ``` ## Webpack Description: In Strapi 5, the default bundler is Vite. (Source: https://docs.strapi.io/cms/admin-panel-customization/bundlers#webpack) Language: JavaScript File path: /src/admin/webpack.config.js ```js module.exports = (config, webpack) => { // Note: we provide webpack above so you should not `require` it // Perform customizations to webpack config config.plugins.push(new webpack.IgnorePlugin(/\/__tests__\//)); // Important: return the modified config return config; }; ``` --- Language: TypeScript File path: /src/admin/webpack.config.ts ```ts export default (config, webpack) => { // Note: we provide webpack above so you should not `require` it // Perform customizations to webpack config config.plugins.push(new webpack.IgnorePlugin(/\/__tests__\//)); // Important: return the modified config return config; }; ``` Language: Bash File path: /webpack.js ```bash strapi develop --bundler=webpack ``` # Favicon Source: https://docs.strapi.io/cms/admin-panel-customization/favicon ## Favicon Description: Replace the favicon.png file at the root of a Strapi project Edit the strapi::favicon middleware configuration with the following code: (Source: https://docs.strapi.io/cms/admin-panel-customization/favicon#favicon) Language: JavaScript File path: /config/middlewares.js ```js // … { name: 'strapi::favicon', config: { path: 'my-custom-favicon.png', }, }, // … ``` # Homepage customization Source: https://docs.strapi.io/cms/admin-panel-customization/homepage ## Registering custom widgets Description: :::info The examples on the present page will cover registering a widget through a plugin. (Source: https://docs.strapi.io/cms/admin-panel-customization/homepage#registering-custom-widgets) Language: JavaScript File path: src/plugins/my-plugin/admin/src/index.js ```js import pluginId from './pluginId'; import MyWidgetIcon from './components/MyWidgetIcon'; export default { register(app) { // Register the plugin itself app.registerPlugin({ id: pluginId, name: 'My Plugin', }); // Register a widget for the Homepage app.widgets.register({ icon: MyWidgetIcon, title: { id: `${pluginId}.widget.title`, defaultMessage: 'My Widget', }, component: async () => { const component = await import('./components/MyWidget'); return component.default; }, /** * Use this instead if you used a named export for your component */ // component: async () => { // const { Component } = await import('./components/MyWidget'); // return Component; // }, id: 'my-custom-widget', pluginId: pluginId, }); }, bootstrap() {}, // ... }; ``` --- Language: TypeScript File path: src/plugins/my-plugin/admin/src/index.ts ```ts import pluginId from './pluginId'; import MyWidgetIcon from './components/MyWidgetIcon'; import type { StrapiApp } from '@strapi/admin/strapi-admin'; export default { register(app: StrapiApp) { // Register the plugin itself app.registerPlugin({ id: pluginId, name: 'My Plugin', }); // Register a widget for the Homepage app.widgets.register({ icon: MyWidgetIcon, title: { id: `${pluginId}.widget.title`, defaultMessage: 'My Widget', }, component: async () => { const component = await import('./components/MyWidget'); return component.default; }, /** * Use this instead if you used a named export for your component */ // component: async () => { // const { Component } = await import('./components/MyWidget'); // return Component; // }, id: 'my-custom-widget', pluginId: pluginId, }); }, bootstrap() {}, // ... }; ``` Language: JavaScript File path: N/A ```js if ('widgets' in app) { // proceed with the registration } ``` ## Creating a widget component Description: Here's how to implement a basic widget component: (Source: https://docs.strapi.io/cms/admin-panel-customization/homepage#creating-a-widget-component) Language: JavaScript File path: src/plugins/my-plugin/admin/src/components/MyWidget/index.js ```js import React, { useState, useEffect } from 'react'; import { Widget } from '@strapi/admin/strapi-admin'; const MyWidget = () => { const [loading, setLoading] = useState(true); const [data, setData] = useState(null); const [error, setError] = useState(null); useEffect(() => { // Fetch your data here const fetchData = async () => { try { // Replace with your actual API call const response = await fetch('/my-plugin/data'); const result = await response.json(); setData(result); setLoading(false); } catch (err) { setError(err); setLoading(false); } }; fetchData(); }, []); if (loading) { return ; } if (error) { return ; } if (!data || data.length === 0) { return ; } return (
{/* Your widget content here */}
    {data.map((item) => (
  • {item.name}
  • ))}
); }; export default MyWidget; ``` --- Language: TypeScript File path: src/plugins/my-plugin/admin/src/components/MyWidget/index.tsx ```ts import React, { useState, useEffect } from 'react'; import { Widget } from '@strapi/admin/strapi-admin'; interface DataItem { id: number; name: string; } const MyWidget: React.FC = () => { const [loading, setLoading] = useState(true); const [data, setData] = useState(null); const [error, setError] = useState(null); useEffect(() => { // Fetch your data here const fetchData = async () => { try { // Replace with your actual API call const response = await fetch('/my-plugin/data'); const result = await response.json(); setData(result); setLoading(false); } catch (err) { setError(err instanceof Error ? err : new Error(String(err))); setLoading(false); } }; fetchData(); }, []); if (loading) { return ; } if (error) { return ; } if (!data || data.length === 0) { return ; } return (
{/* Your widget content here */}
    {data.map((item) => (
  • {item.name}
  • ))}
); }; export default MyWidget; ``` ## Example: Adding a content metrics widget Description: The following file registers the plugin and the widget: (Source: https://docs.strapi.io/cms/admin-panel-customization/homepage#example-adding-a-content-metrics-widget) Language: JavaScript File path: src/plugins/content-metrics/admin/src/index.js ```js import { PLUGIN_ID } from './pluginId'; import { Initializer } from './components/Initializer'; import { PluginIcon } from './components/PluginIcon'; import { Stethoscope } from '@strapi/icons' export default { register(app) { 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; }, }); app.registerPlugin({ id: PLUGIN_ID, initializer: Initializer, isReady: false, name: PLUGIN_ID, }); // Registers the widget 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, }); }, async registerTrads({ locales }) { return Promise.all( locales.map(async (locale) => { try { const { default: data } = await import(`./translations/${locale}.json`); return { data, locale }; } catch { return { data: {}, locale }; } }) ); }, bootstrap() {}, }; ``` --- Language: JavaScript File path: src/plugins/content-metrics/admin/src/components/MetricsWidget/index.js ```js import React, { useState, useEffect } from 'react'; import { Table, Tbody, Tr, Td, Typography, Box } from '@strapi/design-system'; import { Widget } from '@strapi/admin/strapi-admin' const MetricsWidget = () => { const [loading, setLoading] = useState(true); const [metrics, setMetrics] = useState(null); const [error, setError] = useState(null); useEffect(() => { const fetchMetrics = async () => { try { const response = await fetch('/api/content-metrics/count'); const data = await response.json(); console.log("data:", data); const formattedData = {}; if (data && typeof data === 'object') { Object.keys(data).forEach(key => { const value = data[key]; formattedData[key] = typeof value === 'number' ? value : String(value); }); } setMetrics(formattedData); setLoading(false); } catch (err) { console.error(err); setError(err.message || 'An error occurred'); setLoading(false); } }; fetchMetrics(); }, []); if (loading) { return ( ); } if (error) { return ( ); } if (!metrics || Object.keys(metrics).length === 0) { return No content types found; } return ( {Object.entries(metrics).map(([contentType, count], index) => ( ))}
{String(contentType)} {String(count)}
); }; export default MetricsWidget; ``` --- Language: JavaScript File path: src/plugins/content-metrics/server/src/controllers/metrics.js ```js 'use strict'; module.exports = ({ strapi }) => ({ async getContentCounts(ctx) { try { // Get all content types const contentTypes = Object.keys(strapi.contentTypes) .filter(uid => uid.startsWith('api::')) .reduce((acc, uid) => { const contentType = strapi.contentTypes[uid]; acc[contentType.info.displayName || uid] = 0; return acc; }, {}); // Count entities for each content type for (const [name, _] of Object.entries(contentTypes)) { const uid = Object.keys(strapi.contentTypes) .find(key => strapi.contentTypes[key].info.displayName === name || key === name ); if (uid) { // Using the count() method from the Document Service API const count = await strapi.documents(uid).count(); contentTypes[name] = count; } } ctx.body = contentTypes; } catch (err) { ctx.throw(500, err); } } }); ``` --- Language: JavaScript File path: src/plugins/content-metrics/server/src/routes/index.js ```js export default { 'content-api': { type: 'content-api', routes: [ { method: 'GET', path: '/count', handler: 'metrics.getContentCounts', config: { policies: [], }, }, ], }, }; ``` --- Language: TypeScript File path: src/plugins/content-metrics/admin/src/index.ts ```ts import { PLUGIN_ID } from './pluginId'; import { Initializer } from './components/Initializer'; import { PluginIcon } from './components/PluginIcon'; import { Stethoscope } from '@strapi/icons' export default { register(app) { 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; }, }); app.registerPlugin({ id: PLUGIN_ID, initializer: Initializer, isReady: false, name: PLUGIN_ID, }); // Registers the widget 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, }); }, async registerTrads({ locales }) { return Promise.all( locales.map(async (locale) => { try { const { default: data } = await import(`./translations/${locale}.json`); return { data, locale }; } catch { return { data: {}, locale }; } }) ); }, bootstrap() {}, }; ``` --- Language: TypeScript File path: src/plugins/content-metrics/admin/src/components/MetricsWidget/index.ts ```ts import React, { useState, useEffect } from 'react'; import { Table, Tbody, Tr, Td, Typography, Box } from '@strapi/design-system'; import { Widget } from '@strapi/admin/strapi-admin' const MetricsWidget = () => { const [loading, setLoading] = useState(true); const [metrics, setMetrics] = useState(null); const [error, setError] = useState(null); useEffect(() => { const fetchMetrics = async () => { try { const response = await fetch('/api/content-metrics/count'); const data = await response.json(); console.log("data:", data); const formattedData = {}; if (data && typeof data === 'object') { Object.keys(data).forEach(key => { const value = data[key]; formattedData[key] = typeof value === 'number' ? value : String(value); }); } setMetrics(formattedData); setLoading(false); } catch (err) { console.error(err); setError(err.message || 'An error occurred'); setLoading(false); } }; fetchMetrics(); }, []); if (loading) { return ( ); } if (error) { return ( ); } if (!metrics || Object.keys(metrics).length === 0) { return No content types found; } return ( {Object.entries(metrics).map(([contentType, count], index) => ( ))}
{String(contentType)} {String(count)}
); }; export default MetricsWidget; ``` --- Language: JavaScript File path: src/plugins/content-metrics/server/src/controllers/metrics.js ```js 'use strict'; module.exports = ({ strapi }) => ({ async getContentCounts(ctx) { try { // Get all content types const contentTypes = Object.keys(strapi.contentTypes) .filter(uid => uid.startsWith('api::')) .reduce((acc, uid) => { const contentType = strapi.contentTypes[uid]; acc[contentType.info.displayName || uid] = 0; return acc; }, {}); // Count entities for each content type using Document Service for (const [name, _] of Object.entries(contentTypes)) { const uid = Object.keys(strapi.contentTypes) .find(key => strapi.contentTypes[key].info.displayName === name || key === name ); if (uid) { // Using the count() method from Document Service instead of strapi.db.query const count = await strapi.documents(uid).count(); contentTypes[name] = count; } } ctx.body = contentTypes; } catch (err) { ctx.throw(500, err); } } }); ``` --- Language: JavaScript File path: src/plugins/content-metrics/server/src/routes/index.js ```js export default { 'content-api': { type: 'content-api', routes: [ { method: 'GET', path: '/count', handler: 'metrics.getContentCounts', config: { policies: [], }, }, ], }, }; ``` # Admin panel customization - URL, host, and path configuration Source: https://docs.strapi.io/cms/admin-panel-customization/host-port-path ## Update the admin panel's path only Description: To make the admin panel accessible at another path, for instance at http://localhost:1337/dashboard, define or update the url property in the admin panel configuration file as follows: (Source: https://docs.strapi.io/cms/admin-panel-customization/host-port-path#update-the-admin-panel-s-path-only) Language: JavaScript File path: /config/server.js ```js module.exports = ({ env }) => ({ host: env("HOST", "0.0.0.0"), port: env.int("PORT", 1337), }); ``` --- Language: TypeScript File path: /config/server.ts ```ts export default ({ env }) => ({ host: env("HOST", "0.0.0.0"), port: env.int("PORT", 1337), }); ``` Language: JavaScript File path: /config/admin.js ```js module.exports = ({ env }) => ({ // … other configuration properties url: "/dashboard", }); ``` ## Update the admin panel's host and port Description: This is done in the admin panel configuration file, for example to host the admin panel on my-host.com:3000 properties should be updated follows: (Source: https://docs.strapi.io/cms/admin-panel-customization/host-port-path#update-the-admin-panel-s-host-and-port) Language: JavaScript File path: ./config/admin.js ```js module.exports = ({ env }) => ({ host: "my-host.com", port: 3000, // Additionally you can define another path instead of the default /admin one 👇 // url: '/dashboard' }); ``` --- Language: TypeScript File path: ./config/admin.ts ```ts export default ({ env }) => ({ host: "my-host.com", port: 3000, // Additionally you can define another path instead of the default /admin one 👇 // url: '/dashboard' }); ``` # Locales & translations Source: https://docs.strapi.io/cms/admin-panel-customization/locales-translations ## Defining locales Description: To update the list of available locales in the admin panel, set the config.locales array in src/admin/app file: (Source: https://docs.strapi.io/cms/admin-panel-customization/locales-translations#defining-locales) Language: JavaScript File path: /src/admin/app.js ```js export default { config: { locales: ["ru", "zh"], }, bootstrap() {}, }; ``` --- Language: TypeScript File path: /src/admin/app.ts ```ts export default { config: { locales: ["ru", "zh"], }, bootstrap() {}, }; ``` ## Extending translations Description: These keys can be extended through the config.translations key in src/admin/app file: (Source: https://docs.strapi.io/cms/admin-panel-customization/locales-translations#extending-translations) Language: JavaScript File path: /src/admin/app.js ```js export default { config: { locales: ["fr"], translations: { fr: { "Auth.form.email.label": "test", Users: "Utilisateurs", City: "CITY (FRENCH)", // Customize the label of the Content Manager table. Id: "ID french", }, }, }, bootstrap() {}, }; ``` --- Language: TypeScript File path: /src/admin/app.ts ```ts export default { config: { locales: ["fr"], translations: { fr: { "Auth.form.email.label": "test", Users: "Utilisateurs", City: "CITY (FRENCH)", // Customize the label of the Content Manager table. Id: "ID french", }, }, }, bootstrap() {}, }; ``` Language: JavaScript File path: /src/admin/app.js ```js export default { config: { locales: ["fr"], translations: { fr: { "Auth.form.email.label": "test", // Translate a plugin's key/value pair by adding the plugin's name as a prefix // In this case, we translate the "plugin.name" key of plugin "content-type-builder" "content-type-builder.plugin.name": "Constructeur de Type-Contenu", }, }, }, bootstrap() {}, }; ``` --- Language: TypeScript File path: /src/admin/app.ts ```ts export default { config: { locales: ["fr"], translations: { fr: { "Auth.form.email.label": "test", // Translate a plugin's key/value pair by adding the plugin's name as a prefix // In this case, we translate the "plugin.name" key of plugin "content-type-builder" "content-type-builder.plugin.name": "Constructeur de Type-Contenu", }, }, }, bootstrap() {}, }; ``` # Logos Source: https://docs.strapi.io/cms/admin-panel-customization/logos ## Updating logos Description: To update the logos, put image files in the /src/admin/extensions folder, import these files in src/admin/app and update the corresponding keys as in the following example: (Source: https://docs.strapi.io/cms/admin-panel-customization/logos#updating-logos) Language: JavaScript File path: /src/admin/app.js ```js import AuthLogo from "./extensions/my-auth-logo.png"; import MenuLogo from "./extensions/my-menu-logo.png"; export default { config: { // … other configuration properties auth: { // Replace the Strapi logo in auth (login) views logo: AuthLogo, }, menu: { // Replace the Strapi logo in the main navigation logo: MenuLogo, }, // … other configuration properties bootstrap() {}, }; ``` --- Language: TypeScript File path: /src/admin/app.ts ```ts import AuthLogo from "./extensions/my-auth-logo.png"; import MenuLogo from "./extensions/my-menu-logo.png"; export default { config: { // … other configuration properties auth: { // Replace the Strapi logo in auth (login) views logo: AuthLogo, }, menu: { // Replace the Strapi logo in the main navigation logo: MenuLogo, }, // … other configuration properties bootstrap() {}, }; ``` # Theme extension Source: https://docs.strapi.io/cms/admin-panel-customization/theme-extension ## Theme extension Description: The following example shows how to override the primary color by customizing the light and dark theme keys in the admin panel configuration: (Source: https://docs.strapi.io/cms/admin-panel-customization/theme-extension#theme-extension) Language: JavaScript File path: /src/admin/app.js ```js export default { config: { theme: { light: { colors: { primary600: "#4A6EFF", }, }, dark: { colors: { primary600: "#9DB2FF", }, }, }, }, bootstrap() {}, } ``` --- Language: TypeScript File path: /src/admin/app.ts ```ts export default { config: { theme: { light: { colors: { primary600: '#4A6EFF', }, }, dark: { colors: { primary600: '#9DB2FF', }, }, }, }, bootstrap() {}, } ``` # Strapi AI for content managers Source: https://docs.strapi.io/cms/ai/for-content-managers ## Activation and configuration Description: All Strapi AI features can be enabled or disabled globally through the admin panel configuration: (Source: https://docs.strapi.io/cms/ai/for-content-managers#activation) Language: JavaScript File path: /config/admin.js|ts ```js module.exports = { // ... ai: { enabled: true, // set to false to disable all Strapi AI features }, }; ``` # AI for developers Source: https://docs.strapi.io/cms/ai/for-developers ## Connection details Description: Add to your .cursor/mcp.json file: (Source: https://docs.strapi.io/cms/ai/for-developers#connection-details) Language: JSON File path: .cursor/mcp.json ```json { "mcpServers": { "strapi-docs": { "url": "https://strapi-docs.mcp.kapa.ai" } } } ``` --- Language: JSON File path: .vscode/mcp.json ```json { "servers": { "strapi-docs": { "type": "http", "url": "https://strapi-docs.mcp.kapa.ai" } } } ``` # Strapi Client Source: https://docs.strapi.io/cms/api/client ## Installation Description: To use the Strapi Client in your project, install it as a dependency using your preferred package manager: (Source: https://docs.strapi.io/cms/api/client#installation) Language: Bash File path: N/A ```bash yarn add @strapi/client ``` --- Language: Bash File path: N/A ```bash npm install @strapi/client ``` --- Language: Bash File path: N/A ```bash pnpm add @strapi/client ``` ## Basic configuration Description: With Javascript, import the strapi function and create a client instance: (Source: https://docs.strapi.io/cms/api/client#basic-configuration) Language: JavaScript File path: N/A ```js import { strapi } from '@strapi/client'; const client = strapi({ baseURL: 'http://localhost:1337/api' }); ``` --- Language: JavaScript File path: N/A ```js import { strapi } from '@strapi/client'; const client = strapi({ baseURL: 'http://localhost:1337/api' }); ``` --- Language: TypeScript File path: ./src/api/[apiName]/routes/[routerName].ts ```ts ``` ## Authentication Description: If your Strapi instance uses API tokens, configure the Strapi Client as follows: (Source: https://docs.strapi.io/cms/api/client#authentication) Language: JavaScript File path: N/A ```js const client = strapi({ baseURL: 'http://localhost:1337/api', auth: 'your-api-token-here', }); ``` ## General purpose fetch Description: The Strapi Client provides access to the underlying JavaScript fetch function to make direct API requests. (Source: https://docs.strapi.io/cms/api/client#general-purpose-fetch) Language: JavaScript File path: N/A ```js const result = await client.fetch('articles', { method: 'GET' }); ``` ## Working with collection types Description: Usage examples: (Source: https://docs.strapi.io/cms/api/client#working-with-collection-types) Language: JavaScript File path: N/A ```js const articles = client.collection('articles'); // Fetch all english articles sorted by title const allArticles = await articles.find({ locale: 'en', sort: 'title', }); // Fetch a single article const singleArticle = await articles.findOne('article-document-id'); // Create a new article const newArticle = await articles.create({ title: 'New Article', content: '...' }); // Update an existing article const updatedArticle = await articles.update('article-document-id', { title: 'Updated Title' }); // Delete an article await articles.delete('article-id'); ``` ## Working with single types Description: Usage examples: (Source: https://docs.strapi.io/cms/api/client#working-with-single-types) Language: JavaScript File path: N/A ```js const homepage = client.single('homepage'); // Fetch the default homepage content const defaultHomepage = await homepage.find(); // Fetch the Spanish version of the homepage const spanishHomepage = await homepage.find({ locale: 'es' }); // Update the homepage draft content const updatedHomepage = await homepage.update( { title: 'Updated Homepage Title' }, { status: 'draft' } ); // Delete the homepage content await homepage.delete(); ``` ## find Description: The method can be used as follows: (Source: https://docs.strapi.io/cms/api/client#find) Language: JavaScript File path: N/A ```js // Initialize the client const client = strapi({ baseURL: 'http://localhost:1337/api', auth: 'your-api-token', }); // Find all file metadata const allFiles = await client.files.find(); console.log(allFiles); // Find file metadata with filtering and sorting const imageFiles = await client.files.find({ filters: { mime: { $contains: 'image' }, // Only get image files name: { $contains: 'avatar' }, // Only get files with 'avatar' in the name }, sort: ['name:asc'], // Sort by name in ascending order }); ``` ## findOne Description: The method can be used as follows: (Source: https://docs.strapi.io/cms/api/client#findone) Language: JavaScript File path: N/A ```js // Initialize the client const client = strapi({ baseURL: 'http://localhost:1337/api', auth: 'your-api-token', }); // Find file metadata by ID const file = await client.files.findOne(1); console.log(file.name); console.log(file.url); console.log(file.mime); // The file MIME type ``` ## update Description: The methods can be used as follows: (Source: https://docs.strapi.io/cms/api/client#update) Language: JavaScript File path: N/A ```js // Initialize the client const client = strapi({ baseURL: 'http://localhost:1337/api', auth: 'your-api-token', }); // Update file metadata const updatedFile = await client.files.update(1, { name: 'New file name', alternativeText: 'Descriptive alt text for accessibility', caption: 'A caption for the file', }); ``` ## Method Signature Description: The method supports uploading files as Blob (in browsers or Node.js) or as Buffer (in Node.js only). (Source: https://docs.strapi.io/cms/api/client#method-signature) Language: TypeScript File path: /github.com/strapi/client/blob/60a0117e361346073bed1959d354c7facfb963b3/src/files/types.ts ```ts const client = strapi({ baseURL: 'http://localhost:1337/api' }); const fileInput = document.querySelector('input[type="file"]'); const file = fileInput.files[0]; try { const result = await client.files.upload(file, { fileInfo: { alternativeText: 'A user uploaded image', caption: 'Uploaded via browser', }, }); console.log('Upload successful:', result); } catch (error) { console.error('Upload failed:', error); } ``` Language: TypeScript File path: /github.com/strapi/client/blob/60a0117e361346073bed1959d354c7facfb963b3/src/files/types.ts ```ts import { readFile } from 'fs/promises'; const client = strapi({ baseURL: 'http://localhost:1337/api' }); const filePath = './image.png'; const mimeType = 'image/png'; const fileContentBuffer = await readFile(filePath); const fileBlob = new Blob([fileContentBuffer], { type: mimeType }); try { const result = await client.files.upload(fileBlob, { fileInfo: { name: 'Image uploaded as Blob', alternativeText: 'Uploaded from Node.js Blob', caption: 'Example upload', }, }); console.log('Blob upload successful:', result); } catch (error) { console.error('Blob upload failed:', error); } ``` --- Language: TypeScript File path: /github.com/strapi/client/blob/60a0117e361346073bed1959d354c7facfb963b3/src/files/types.ts ```ts import { readFile } from 'fs/promises'; const client = strapi({ baseURL: 'http://localhost:1337/api' }); const filePath = './image.png'; const fileContentBuffer = await readFile(filePath); try { const result = await client.files.upload(fileContentBuffer, { filename: 'image.png', mimetype: 'image/png', fileInfo: { name: 'Image uploaded as Buffer', alternativeText: 'Uploaded from Node.js Buffer', caption: 'Example upload', }, }); console.log('Buffer upload successful:', result); } catch (error) { console.error('Buffer upload failed:', error); } ``` Language: TypeScript File path: N/A ```ts async upload(file: Blob, options?: BlobUploadOptions): Promise async upload(file: Buffer, options: BufferUploadOptions): Promise ``` ## Response Structure Description: The strapi.client.files.upload() method returns an array of file objects, each with fields such as: (Source: https://docs.strapi.io/cms/api/client#response-structure) Language: JSON File path: /github.com/strapi/client/blob/60a0117e361346073bed1959d354c7facfb963b3/src/files/types.ts ```json { "id": 1, "name": "image.png", "alternativeText": "Uploaded from Node.js Buffer", "caption": "Example upload", "mime": "image/png", "url": "/uploads/image.png", "size": 12345, "createdAt": "2025-07-23T12:34:56.789Z", "updatedAt": "2025-07-23T12:34:56.789Z" } ``` ## delete Description: The method can be used as follows: (Source: https://docs.strapi.io/cms/api/client#delete) Language: TypeScript File path: /github.com/strapi/client/blob/main/src/files/types.ts ```ts // Initialize the client const client = strapi({ baseURL: 'http://localhost:1337/api', auth: 'your-api-token', }); // Delete a file by ID const deletedFile = await client.files.delete(1); console.log('File deleted successfully'); console.log('Deleted file ID:', deletedFile.id); console.log('Deleted file name:', deletedFile.name); ``` # Document Service API Source: https://docs.strapi.io/cms/api/document-service ## Example Description: ``` --- Language: JavaScript File path: N/A ```js import { FormData } from 'formdata-node'; import fetch, { blobFrom } from 'node-fetch'; const file = await blobFrom('./1.png', 'image/png'); const form = new FormData(); form.append('files', file, "1.png"); form.append( 'fileInfo', JSON.stringify({ name: 'Homepage hero', alternativeText: 'Person smiling while holding laptop', caption: 'Hero image used on the homepage', }) ); const response = await fetch('http://localhost:1337/api/upload', { method: 'post', body: form, }); ``` ## Upload entry files Description: For example, given the Restaurant model attributes: (Source: https://docs.strapi.io/cms/api/rest/upload#upload-entry-files) Language: JSON File path: /src/api/restaurant/content-types/restaurant/schema.json ```json { // ... "attributes": { "name": { "type": "string" }, "cover": { "type": "media", "multiple": false, } } // ... } ``` Language: HTML File path: N/A ```html
``` ## Update fileInfo Description: fileInfo is the only accepted parameter, and describes the fileInfo to update: (Source: https://docs.strapi.io/cms/api/rest/upload#update-fileinfo) Language: JavaScript File path: N/A ```js import { FormData } from 'formdata-node'; import fetch from 'node-fetch'; const fileId = 50; const newFileData = { alternativeText: 'My new alternative text for this image!', }; const form = new FormData(); form.append('fileInfo', JSON.stringify(newFileData)); const response = await fetch(`http://localhost:1337/api/upload?id=${fileId}`, { method: 'post', body: form, }); ``` ## Models definition Description: The following example lets you upload and attach one file to the avatar attribute: (Source: https://docs.strapi.io/cms/api/rest/upload#models-definition) Language: JSON File path: /src/api/restaurant/content-types/restaurant/schema.json ```json { // ... { "attributes": { "pseudo": { "type": "string", "required": true }, "email": { "type": "email", "required": true, "unique": true }, "avatar": { "type": "media", "multiple": false, } } } // ... } ``` Language: JSON File path: /src/api/restaurant/content-types/restaurant/schema.json ```json { // ... { "attributes": { "name": { "type": "string", "required": true }, "covers": { "type": "media", "multiple": true, } } } // ... } ``` # Controllers Source: https://docs.strapi.io/cms/backend-customization/controllers ## Adding a new controller Description: with the interactive CLI command strapi generate - or manually by creating a JavaScript file: - in ./src/api/[api-name]/controllers/ for API controllers (this location matters as controllers are auto-loaded by Strapi from there) - or in a folder like ./src/plugins/[plugin-name]/server/controllers/ for plugin controllers, though they can be created elsewhere as long as the plugin interface is properly exported in the strapi-server.js file (see Server API for Plugins documentation) (Source: https://docs.strapi.io/cms/backend-customization/controllers#adding-a-new-controller) Language: JavaScript File path: ./src/api/restaurant/controllers/restaurant.js ```js const { createCoreController } = require('@strapi/strapi').factories; module.exports = createCoreController('api::restaurant.restaurant', ({ strapi }) => ({ // Method 1: Creating an entirely custom action async exampleAction(ctx) { try { ctx.body = 'ok'; } catch (err) { ctx.body = err; } }, // Method 2: Wrapping a core action (leaves core logic in place) async find(ctx) { // some custom logic here ctx.query = { ...ctx.query, local: 'en' } // Calling the default core action const { data, meta } = await super.find(ctx); // some more custom logic meta.date = Date.now() return { data, meta }; }, // Method 3: Replacing a core action with proper sanitization async find(ctx) { // validateQuery (optional) // to throw an error on query params that are invalid or the user does not have access to await this.validateQuery(ctx); // sanitizeQuery to remove any query params that are invalid or the user does not have access to // It is strongly recommended to use sanitizeQuery even if validateQuery is used const sanitizedQueryParams = await this.sanitizeQuery(ctx); const { results, pagination } = await strapi.service('api::restaurant.restaurant').find(sanitizedQueryParams); const sanitizedResults = await this.sanitizeOutput(results, ctx); return this.transformResponse(sanitizedResults, { pagination }); } })); ``` --- Language: TypeScript File path: ./src/api/restaurant/controllers/restaurant.ts ```ts import { factories } from '@strapi/strapi'; export default factories.createCoreController('api::restaurant.restaurant', ({ strapi }) => ({ // Method 1: Creating an entirely custom action async exampleAction(ctx) { try { ctx.body = 'ok'; } catch (err) { ctx.body = err; } }, // Method 2: Wrapping a core action (leaves core logic in place) async find(ctx) { // some custom logic here ctx.query = { ...ctx.query, local: 'en' } // Calling the default core action const { data, meta } = await super.find(ctx); // some more custom logic meta.date = Date.now() return { data, meta }; }, // Method 3: Replacing a core action with proper sanitization async find(ctx) { // validateQuery (optional) // to throw an error on query params that are invalid or the user does not have access to await this.validateQuery(ctx); // sanitizeQuery to remove any query params that are invalid or the user does not have access to // It is strongly recommended to use sanitizeQuery even if validateQuery is used const sanitizedQueryParams = await this.sanitizeQuery(ctx); const { results, pagination } = await strapi.service('api::restaurant.restaurant').find(sanitizedQueryParams); // sanitizeOutput to ensure the user does not receive any data they do not have access to const sanitizedResults = await this.sanitizeOutput(results, ctx); return this.transformResponse(sanitizedResults, { pagination }); } })); ``` Language: JavaScript File path: N/A ```js module.exports = { routes: [ { method: 'GET', path: '/hello', handler: 'api::hello.hello.index', } ] } ``` --- Language: JavaScript File path: ./src/api/hello/controllers/hello.js ```js module.exports = { async index(ctx, next) { // called by GET /hello ctx.body = 'Hello World!'; // we could also send a JSON }, }; ``` --- Language: TypeScript File path: ./src/api/hello/routes/hello.ts ```ts export default { routes: [ { method: 'GET', path: '/hello', handler: 'api::hello.hello.index', } ] } ``` --- Language: TypeScript File path: ./src/api/hello/controllers/hello.ts ```ts export default { async index(ctx, next) { // called by GET /hello ctx.body = 'Hello World!'; // we could also send a JSON }, }; ``` ## Controllers & Routes: How routes reach controller actions Description: The example below adds a new controller action and exposes it through a custom route without duplicating the existing CRUD route definitions: (Source: https://docs.strapi.io/cms/backend-customization/controllers#controllers-routes-how-routes-reach-controller-actions) Language: JavaScript File path: ./src/api/restaurant/controllers/restaurant.js ```js const { createCoreController } = require('@strapi/strapi').factories; module.exports = createCoreController('api::restaurant.restaurant', ({ strapi }) => ({ async exampleAction(ctx) { const specials = await strapi.service('api::restaurant.restaurant').find({ filters: { isSpecial: true } }); return this.transformResponse(specials.results); }, })); ``` Language: JavaScript File path: ./src/api/restaurant/routes/01-custom-restaurant.js ```js module.exports = { routes: [ { method: 'GET', path: '/restaurants/specials', handler: 'api::restaurant.restaurant.exampleAction', }, ], }; ``` ## Sanitization when utilizing controller factories Description: :::warning Because these methods use the model associated with the current controller, if you query data that is from another model (i.e., doing a find for "menus" within a "restaurant" controller method), you must instead use the strapi.contentAPI methods, such as strapi.contentAPI.sanitize.query described in Sanitizing Custom Controllers, or else the result of your query will be sanitized against the wrong model. (Source: https://docs.strapi.io/cms/backend-customization/controllers#sanitization-when-utilizing-controller-factories) Language: JavaScript File path: ./src/api/restaurant/controllers/restaurant.js ```js const { createCoreController } = require('@strapi/strapi').factories; module.exports = createCoreController('api::restaurant.restaurant', ({ strapi }) => ({ async find(ctx) { await this.validateQuery(ctx); const sanitizedQueryParams = await this.sanitizeQuery(ctx); const { results, pagination } = await strapi.service('api::restaurant.restaurant').find(sanitizedQueryParams); const sanitizedResults = await this.sanitizeOutput(results, ctx); return this.transformResponse(sanitizedResults, { pagination }); } })); ``` --- Language: TypeScript File path: ./src/api/restaurant/controllers/restaurant.ts ```ts import { factories } from '@strapi/strapi'; export default factories.createCoreController('api::restaurant.restaurant', ({ strapi }) => ({ async find(ctx) { const sanitizedQueryParams = await this.sanitizeQuery(ctx); const { results, pagination } = await strapi.service('api::restaurant.restaurant').find(sanitizedQueryParams); const sanitizedResults = await this.sanitizeOutput(results, ctx); return this.transformResponse(sanitizedResults, { pagination }); } })); ``` ## Sanitization and validation when building custom controllers Description: :::note Depending on the complexity of your custom controllers, you may need additional sanitization that Strapi cannot currently account for, especially when combining the data from multiple sources. (Source: https://docs.strapi.io/cms/backend-customization/controllers#sanitize-validate-custom-controllers) Language: JavaScript File path: ./src/api/restaurant/controllers/restaurant.js ```js module.exports = { async findCustom(ctx) { const contentType = strapi.contentType('api::test.test'); await strapi.contentAPI.validate.query(ctx.query, contentType, { auth: ctx.state.auth }); const sanitizedQueryParams = await strapi.contentAPI.sanitize.query(ctx.query, contentType, { auth: ctx.state.auth }); const documents = await strapi.documents(contentType.uid).findMany(sanitizedQueryParams); return await strapi.contentAPI.sanitize.output(documents, contentType, { auth: ctx.state.auth }); } } ``` --- Language: TypeScript File path: ./src/api/restaurant/controllers/restaurant.ts ```ts export default { async findCustom(ctx) { const contentType = strapi.contentType('api::test.test'); await strapi.contentAPI.validate.query(ctx.query, contentType, { auth: ctx.state.auth }); const sanitizedQueryParams = await strapi.contentAPI.sanitize.query(ctx.query, contentType, { auth: ctx.state.auth }); const documents = await strapi.documents(contentType.uid).findMany(sanitizedQueryParams); return await strapi.contentAPI.sanitize.output(documents, contentType, { auth: ctx.state.auth }); } } ``` ## Extending core controllers Description: :::tip The backend customization examples cookbook shows how you can overwrite a default controller action, for instance for the create action. (Source: https://docs.strapi.io/cms/backend-customization/controllers#extending-core-controllers) Language: JavaScript File path: N/A ```js async find(ctx) { // some logic here const { data, meta } = await super.find(ctx); // some more logic return { data, meta }; } ``` --- Language: JavaScript File path: N/A ```js async findOne(ctx) { // some logic here const response = await super.findOne(ctx); // some more logic return response; } ``` --- Language: JavaScript File path: N/A ```js async create(ctx) { // some logic here const response = await super.create(ctx); // some more logic return response; } ``` --- Language: JavaScript File path: N/A ```js async update(ctx) { // some logic here const response = await super.update(ctx); // some more logic return response; } ``` --- Language: JavaScript File path: N/A ```js async delete(ctx) { // some logic here const response = await super.delete(ctx); // some more logic return response; } ``` Language: JavaScript File path: N/A ```js async find(ctx) { // some logic here const response = await super.find(ctx); // some more logic return response; } ``` --- Language: JavaScript File path: N/A ```js async update(ctx) { // some logic here const response = await super.update(ctx); // some more logic return response; } ``` --- Language: JavaScript File path: N/A ```js async delete(ctx) { // some logic here const response = await super.delete(ctx); // some more logic return response; } ``` ## Usage Description: Controllers are declared and attached to a route. (Source: https://docs.strapi.io/cms/backend-customization/controllers#usage) Language: JavaScript File path: N/A ```js // access an API controller strapi.controller('api::api-name.controller-name'); // access a plugin controller strapi.controller('plugin::plugin-name.controller-name'); ``` # Authentication flow with JWT Source: https://docs.strapi.io/cms/backend-customization/examples/authentication ## Examples cookbook: Authentication flow with JWT Description: This file uses the formik package - install it using yarn add formik and restart the dev server. (Source: https://docs.strapi.io/cms/backend-customization/examples/authentication#examples-cookbook-authentication-flow-with-jwt) Language: JavaScript File path: /client/pages/auth/login.js ```js import React from 'react'; import { useFormik } from 'formik'; import { Button, Input } from '@nextui-org/react'; import Layout from '@/components/layout'; import { getStrapiURL } from '@/utils'; const Login = () => { const { handleSubmit, handleChange } = useFormik({ initialValues: { identifier: '', password: '', }, onSubmit: async (values) => { /** * API URLs in Strapi are by default prefixed with /api, * but because the API prefix can be configured * with the rest.prefix property in the config/api.js file, * we use the getStrapiURL() method to build the proper full auth URL. **/ const res = await fetch(getStrapiURL('/auth/local'), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(values), }); /** * Gets the JWT from the server response */ const { jwt } = await res.json(); /** * Stores the JWT in the localStorage of the browser. * A better implementation would be to do this with an authentication context provider * or something more sophisticated, but it's not the purpose of this tutorial. */ localStorage.setItem('token', jwt); }, }); /** * The following code renders a basic login form * accessible from the localhost:3000/auth/login page. */ return (

Login

); }; export default Login; ``` ## Configuration Description: First, enable session management in your /config/plugins.js: (Source: https://docs.strapi.io/cms/backend-customization/examples/authentication#configuration) Language: JavaScript File path: /config/plugins.js ```js module.exports = ({ env }) => ({ 'users-permissions': { config: { jwtManagement: 'refresh', sessions: { accessTokenLifespan: 604800, // 1 week (default) maxRefreshTokenLifespan: 2592000, // 30 days idleRefreshTokenLifespan: 604800, // 7 days }, }, }, }); ``` ## Enhanced Login Component Description: Here's an updated login component that handles both JWT and refresh tokens: (Source: https://docs.strapi.io/cms/backend-customization/examples/authentication#enhanced-login-component) Language: JavaScript File path: /client/pages/auth/enhanced-login.js ```js import React, { useState } from 'react'; import { useFormik } from 'formik'; import { Button, Input } from '@nextui-org/react'; import Layout from '@/components/layout'; import { getStrapiURL } from '@/utils'; const EnhancedLogin = () => { const [isLoading, setIsLoading] = useState(false); const { handleSubmit, handleChange } = useFormik({ initialValues: { identifier: '', password: '', }, onSubmit: async (values) => { setIsLoading(true); try { const res = await fetch(getStrapiURL('/auth/local'), { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(values), }); const data = await res.json(); if (res.ok) { // Store both tokens (session management mode) if (data.refreshToken) { localStorage.setItem('accessToken', data.jwt); localStorage.setItem('refreshToken', data.refreshToken); } else { // Legacy mode - single JWT localStorage.setItem('token', data.jwt); } // Redirect to protected area window.location.href = '/dashboard'; } else { console.error('Login failed:', data.error); } } catch (error) { console.error('Login error:', error); } finally { setIsLoading(false); } }, }); return (

Enhanced Login

); }; export default EnhancedLogin; ``` # Custom middlewares Source: https://docs.strapi.io/cms/backend-customization/examples/middlewares ## Populating an analytics dashboard in Google Sheets with a custom middleware Description: Additional information can be found in the official . (Source: https://docs.strapi.io/cms/backend-customization/examples/middlewares#populating-an-analytics-dashboard-in-google-sheets-with-a-custom-middleware) Language: JavaScript File path: src/api/restaurant/middlewares/utils.js ```js const { google } = require('googleapis'); const createGoogleSheetClient = async ({ keyFile, sheetId, tabName, range, }) => { async function getGoogleSheetClient() { const auth = new google.auth.GoogleAuth({ keyFile, scopes: ['https://www.googleapis.com/auth/spreadsheets'], }); const authClient = await auth.getClient(); return google.sheets({ version: 'v4', auth: authClient, }); } const googleSheetClient = await getGoogleSheetClient(); const writeGoogleSheet = async (data) => { googleSheetClient.spreadsheets.values.append({ spreadsheetId: sheetId, range: `${tabName}!${range}`, valueInputOption: 'USER_ENTERED', insertDataOption: 'INSERT_ROWS', resource: { majorDimension: 'ROWS', values: data, }, }); }; const updateoogleSheet = async (cell, data) => { googleSheetClient.spreadsheets.values.update({ spreadsheetId: sheetId, range: `${tabName}!${cell}`, valueInputOption: 'USER_ENTERED', resource: { majorDimension: 'ROWS', values: data, }, }); }; const readGoogleSheet = async () => { const res = await googleSheetClient.spreadsheets.values.get({ spreadsheetId: sheetId, range: `${tabName}!${range}`, }); return res.data.values; }; return { writeGoogleSheet, updateoogleSheet, readGoogleSheet, }; }; module.exports = { createGoogleSheetClient, }; ``` Language: JavaScript File path: src/api/restaurant/middlewares/analytics.js ```js 'use strict'; const { createGoogleSheetClient } = require('./utils'); const serviceAccountKeyFile = './gs-keys.json'; // Replace the sheetId value with the corresponding id found in your own URL const sheetId = '1P7Oeh84c18NlHp1Zy-5kXD8zgpoA1WmvYL62T4GWpfk'; const tabName = 'Restaurants'; const range = 'A2:C'; const VIEWS_CELL = 'C'; const transformGSheetToObject = (response) => response.reduce( (acc, restaurant) => ({ ...acc, [restaurant[0]]: { id: restaurant[0], name: restaurant[1], views: restaurant[2], cellNum: Object.keys(acc).length + 2 // + 2 because we need to consider the header and that the initial length is 0, so our first real row would be 2, }, }), {} ); module.exports = (config, { strapi }) => { return async (context, next) => { // Generating google sheet client const { readGoogleSheet, updateoogleSheet, writeGoogleSheet } = await createGoogleSheetClient({ keyFile: serviceAccountKeyFile, range, sheetId, tabName, }); // Get the restaurant ID from the params in the URL const restaurantId = context.params.id; const restaurant = await strapi.entityService.findOne( 'api::restaurant.restaurant', restaurantId ); // Read the spreadsheet to get the current data const restaurantAnalytics = await readGoogleSheet(); /** * The returned data comes in the shape [1, "Mint Lounge", 23], * and we need to transform it into an object: {id: 1, name: "Mint Lounge", views: 23, cellNum: 2} */ const requestedRestaurant = transformGSheetToObject(restaurantAnalytics)[restaurantId]; if (requestedRestaurant) { await updateoogleSheet( `${VIEWS_CELL}${requestedRestaurant.cellNum}:${VIEWS_CELL}${requestedRestaurant.cellNum}`, [[Number(requestedRestaurant.views) + 1]] ); } else { /** If we don't have the restaurant in the spreadsheet already, * we create it with 1 view. */ const newRestaurant = [[restaurant.id, restaurant.name, 1]]; await writeGoogleSheet(newRestaurant); } // Call next to continue with the flow and get to the controller await next(); }; }; ``` Language: JavaScript File path: src/api/restaurant/routes/restaurant.js ```js 'use strict'; const { createCoreRouter } = require('@strapi/strapi').factories; module.exports = createCoreRouter('api::restaurant.restaurant', { config: { findOne: { auth: false, policies: [], middlewares: ['api::restaurant.analytics'], }, }, }); ``` # Custom policies Source: https://docs.strapi.io/cms/backend-customization/examples/policies ## Creating a custom policy Description: In the /api folder of the project, create a new src/api/review/policies/is-owner-review.js file with the following code: (Source: https://docs.strapi.io/cms/backend-customization/examples/policies#creating-a-custom-policy) Language: JavaScript File path: src/api/review/policies/is-owner-review.js ```js module.exports = async (policyContext, config, { strapi }) => { const { body } = policyContext.request; const { user } = policyContext.state; // Return an error if there is no authenticated user with the request if (!user) { return false; } /** * Queries the Restaurants collection type * using the Entity Service API * to retrieve information about the restaurant's owner. */ const [restaurant] = await strapi.entityService.findMany( 'api::restaurant.restaurant', { filters: { slug: body.restaurant, }, populate: ['owner'], } ); if (!restaurant) { return false; } /** * If the user submitting the request is the restaurant's owner, * we don't allow the review creation. */ if (user.id === restaurant.owner.id) { return false; } return true; }; ``` ## Sending custom errors through policies Description: In the /api folder of the project, update the previously created is-owner-review custom policy as follows (highlighted lines are the only modified lines): (Source: https://docs.strapi.io/cms/backend-customization/examples/policies#sending-custom-errors-through-policies) Language: JSON File path: /api/review/policies/is-owner-review.js ```json { "data": null, "error": { "status": 403, "name": "PolicyError", "message": "Policy Failed", "details": {} } } ``` --- Language: JSON File path: N/A ```json { "data": null, "error": { "status": 403, "name": "PolicyError", "message": "The owner of the restaurant cannot submit reviews", "details": { "policy": "is-owner-review", "errCode": "RESTAURANT_OWNER_REVIEW" } } } ``` Language: JavaScript File path: src/api/review/policies/is-owner-review.js ```js const { errors } = require('@strapi/utils'); const { PolicyError } = errors; module.exports = async (policyContext, config, { strapi }) => { const { body } = policyContext.request; const { user } = policyContext.state; // Return an error if there is no authenticated user with the request if (!user) { return false; } /** * Queries the Restaurants collection type * using the Entity Service API * to retrieve information about the restaurant's owner. */ const filteredRestaurants = await strapi.entityService.findMany( 'api::restaurant.restaurant', { filters: { slug: body.restaurant, }, populate: ['owner'], } ); const restaurant = filteredRestaurants[0]; if (!restaurant) { return false; } /** * If the user submitting the request is the restaurant's owner, * we don't allow the review creation. */ if (user.id === restaurant.owner.id) { // highlight-start /** * Throws a custom policy error * instead of just returning false * (which would result into a generic Policy Error). */ throw new PolicyError('The owner of the restaurant cannot submit reviews', { errCode: 'RESTAURANT_OWNER_REVIEW', // can be useful for identifying different errors on the front end }); // highlight-end } return true; }; ``` ## Using custom errors on the front end Description: Example front-end code to display toast notifications for custom errors or successful review creation: (Source: https://docs.strapi.io/cms/backend-customization/examples/policies#using-custom-errors-on-the-front-end) Language: JavaScript File path: /client/components/pages/restaurant/RestaurantContent/Reviews/new-review.js ```js import { Button, Input, Textarea } from '@nextui-org/react'; import { useFormik } from 'formik'; import { useRouter } from 'next/router'; import React from 'react'; import { getStrapiURL } from '../../../../../utils'; // highlight-start /** * A notification will be displayed on the front-end using React Hot Toast * (See https://github.com/timolins/react-hot-toast). * React Hot Toast should be added to your project's dependencies; * Use yarn or npm to install it and it will be added to your package.json file. */ import toast from 'react-hot-toast'; class UnauthorizedError extends Error { constructor(message) { super(message); } } // highlight-end const NewReview = () => { const router = useRouter(); const { handleSubmit, handleChange, values } = useFormik({ initialValues: { note: '', content: '', }, onSubmit: async (values) => { // highlight-start /** * The previously added code is wrapped in a try/catch block. */ try { // highlight-end const res = await fetch(getStrapiURL('/reviews'), { method: 'POST', body: JSON.stringify({ restaurant: router.query.slug, ...values, }), headers: { Authorization: `Bearer ${localStorage.getItem('token')}`, 'Content-Type': 'application/json', }, }); // highlight-start const { data, error } = await res.json(); /** * If the Strapi backend server returns an error, * we use the custom error message to throw a custom error. * If the request is a success, we display a success message. * In both cases, a toast notification is displayed on the front-end. */ if (error) { throw new UnauthorizedError(error.message); } toast.success('Review created!'); return data; } catch (err) { toast.error(err.message); console.error(err); } }, // highlight-end }); return (

Write your review