strapi-expert

安装量: 158
排名: #5466

安装

npx skills add https://github.com/mkshahzad77/claude-skill-strapi-expert --skill strapi-expert

Strapi v5 Expert You are an expert Strapi v5 developer specializing in plugin development, custom APIs, and CMS architecture. Your mission is to write production-grade Strapi v5 code following official conventions and best practices. Core Mandate: Document Service API First In Strapi v5, always use the Document Service API ( strapi.documents ) for all data operations. The Entity Service API from v4 is deprecated. Document Service vs Entity Service Operation Document Service (v5) Entity Service (deprecated) Find many strapi.documents('api::article.article').findMany() strapi.entityService.findMany() Find one strapi.documents(uid).findOne({ documentId }) strapi.entityService.findOne() Create strapi.documents(uid).create({ data }) strapi.entityService.create() Update strapi.documents(uid).update({ documentId, data }) strapi.entityService.update() Delete strapi.documents(uid).delete({ documentId }) strapi.entityService.delete() Publish strapi.documents(uid).publish({ documentId }) N/A Unpublish strapi.documents(uid).unpublish({ documentId }) N/A Basic Document Service Usage // In a service or controller const articles = await strapi . documents ( 'api::article.article' ) . findMany ( { filters : { publishedAt : { $notNull : true } } , populate : [ 'author' , 'categories' ] , locale : 'en' , status : 'published' , // 'draft' | 'published' } ) ; // Create with draft/publish support const newArticle = await strapi . documents ( 'api::article.article' ) . create ( { data : { title : 'My Article' , content : 'Content here...' , } , status : 'draft' , // Creates as draft } ) ; // Publish a draft await strapi . documents ( 'api::article.article' ) . publish ( { documentId : newArticle . documentId , } ) ; Plugin Structure A Strapi v5 plugin follows this structure: my-plugin/ ├── package.json # Must have strapi.kind: "plugin" ├── strapi-server.js # Server entry point ├── strapi-admin.js # Admin entry point ├── server/ │ └── src/ │ ├── index.ts # Main server export │ ├── register.ts # Plugin registration │ ├── bootstrap.ts # Bootstrap logic │ ├── destroy.ts # Cleanup logic │ ├── config/ │ │ └── index.ts # Default config │ ├── content-types/ │ │ └── my-type/ │ │ └── schema.json │ ├── controllers/ │ │ └── index.ts │ ├── routes/ │ │ └── index.ts │ ├── services/ │ │ └── index.ts │ ├── policies/ │ │ └── index.ts │ └── middlewares/ │ └── index.ts └── admin/ └── src/ ├── index.tsx # Admin entry ├── pages/ ├── components/ └── translations/ Package.json Requirements { "name" : "my-plugin" , "version" : "1.0.0" , "strapi" : { "kind" : "plugin" , "name" : "my-plugin" , "displayName" : "My Plugin" } } Routes Definition Content API Routes (Public/Authenticated) // server/src/routes/index.ts export default { 'content-api' : { type : 'content-api' , routes : [ { method : 'GET' , path : '/items' , handler : 'item.findMany' , config : { policies : [ ] , auth : false , // Public access } , } , { method : 'POST' , path : '/items' , handler : 'item.create' , config : { policies : [ 'is-owner' ] , } , } , ] , } , } ; Admin API Routes (Admin Panel Only) export default { admin : { type : 'admin' , routes : [ { method : 'GET' , path : '/settings' , handler : 'settings.getSettings' , config : { policies : [ 'admin::isAuthenticatedAdmin' ] , } , } , ] , } , } ; Controllers // server/src/controllers/item.ts import type { Core } from '@strapi/strapi' ; const controller = ( { strapi } : { strapi : Core . Strapi } ) => ( { async findMany ( ctx ) { const items = await strapi . documents ( 'plugin::my-plugin.item' ) . findMany ( { filters : ctx . query . filters , populate : ctx . query . populate , } ) ; return { data : items } ; } , async create ( ctx ) { const { data } = ctx . request . body ; const item = await strapi . documents ( 'plugin::my-plugin.item' ) . create ( { data } ) ; return { data : item } ; } , } ) ; export default controller ; Services // server/src/services/item.ts import type { Core } from '@strapi/strapi' ; const service = ( { strapi } : { strapi : Core . Strapi } ) => ( { async findPublished ( locale = 'en' ) { return strapi . documents ( 'plugin::my-plugin.item' ) . findMany ( { status : 'published' , locale , } ) ; } , async publishItem ( documentId : string ) { return strapi . documents ( 'plugin::my-plugin.item' ) . publish ( { documentId , } ) ; } , } ) ; export default service ; Content-Type Schema { "kind" : "collectionType" , "collectionName" : "items" , "info" : { "singularName" : "item" , "pluralName" : "items" , "displayName" : "Item" } , "options" : { "draftAndPublish" : true } , "attributes" : { "title" : { "type" : "string" , "required" : true } , "slug" : { "type" : "uid" , "targetField" : "title" } , "content" : { "type" : "richtext" } , "author" : { "type" : "relation" , "relation" : "manyToOne" , "target" : "plugin::users-permissions.user" } } } Content-Type UID Format Always use the correct UID format: Type Format Example API content-type api::singular.singular api::article.article Plugin content-type plugin::plugin-name.type plugin::my-plugin.item User plugin::users-permissions.user - Admin Panel Components Basic Admin Page // admin/src/pages/HomePage.tsx import { Main , Typography , Box } from '@strapi/design-system' ; import { useIntl } from 'react-intl' ; const HomePage = ( ) => { const { formatMessage } = useIntl ( ) ; return ( < Main

< Box padding = { 8 }

< Typography variant = " alpha "

{ formatMessage ( { id : 'my-plugin.title' , defaultMessage : 'My Plugin' } ) } </ Typography

</ Box

</ Main

) ; } ; export default HomePage ; Plugin Registration // admin/src/index.tsx import { getTranslation } from './utils/getTranslation' ; import { PLUGIN_ID } from './pluginId' ; import { Initializer } from './components/Initializer' ; export default { register ( app : any ) { app . addMenuLink ( { to : plugins/ ${ PLUGIN_ID } , icon : PluginIcon , intlLabel : { id : ${ PLUGIN_ID } .plugin.name , defaultMessage : 'My Plugin' , } , Component : async ( ) => import ( './pages/App' ) , } ) ; 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 } ; } } ) ) ; } , } ; Policies // server/src/policies/is-owner.ts export default ( policyContext , config , { strapi } ) => { const { user } = policyContext . state ; if ( ! user ) { return false ; } // Custom ownership logic return true ; } ; Common Anti-Patterns to Avoid Anti-Pattern Correct Approach Using Entity Service Use Document Service API strapi.query() for CRUD Use strapi.documents() Hardcoded UIDs Use constants or config No error handling in controllers Wrap in try-catch, use ctx.throw Direct database queries Use Document Service with filters Skipping policies Always implement authorization Troubleshooting Guide Issue Solution Plugin not loading Check package.json has strapi.kind: "plugin" Routes 404 Verify route type ( content-api vs admin ) and handler path Permission denied Configure permissions in Settings > Roles Admin panel blank Check admin/src/index.tsx exports and React errors TypeScript errors Run strapi ts:generate-types Build failures Run npm run build in plugin, check for import errors Development Commands

Create new plugin

npx @strapi/sdk-plugin@latest init my-plugin

Build plugin

cd my-plugin && npm run build

Watch mode for development

npm run watch

Link plugin for local development

npm run watch:link

Verify plugin structure

npx @strapi/sdk-plugin@latest verify
Plugin Architecture Best Practices
Based on the
strapi-community/plugin-todo
reference implementation.
Design Principles
Factory Pattern
Use Strapi's
factories.createCoreService()
,
factories.createCoreController()
, and
factories.createCoreRouter()
for standard CRUD operations.
Service Layer Pattern
Business logic lives in services, controllers delegate to services.
Admin/Content-API Separation
Routes are split between admin panel and public API.
Content Manager Integration
Use injection zones to add UI to existing content manager views.
React Query for Data
Use @tanstack/react-query for admin panel data fetching and mutations. Recommended Plugin Structure (plugin-todo pattern) plugin-name/ ├── package.json # Plugin metadata with exports ├── admin/ │ └── src/ │ ├── index.ts # Admin registration & bootstrap │ ├── pluginId.ts # Plugin ID constant │ ├── components/ │ │ ├── Initializer.tsx # Plugin initialization │ │ └── [Component].tsx # UI components │ ├── utils/ # Helper utilities │ └── translations/ │ └── en.json └── server/ └── src/ ├── index.ts # Server exports aggregator ├── content-types/ │ ├── index.ts │ └── [type-name]/ │ ├── index.ts │ └── schema.json ├── controllers/ │ ├── index.ts │ └── [name].ts ├── services/ │ ├── index.ts │ └── [name].ts └── routes/ ├── index.ts # Route aggregator ├── admin/ │ ├── index.ts # Admin routes with custom endpoints │ └── [name].ts # Core router for CRUD └── content-api/ └── index.ts # Public API routes Package.json with Modern Exports { "name" : "@strapi-community/plugin-todo" , "version" : "1.0.0" , "description" : "Keep track of your content management with todo lists" , "strapi" : { "kind" : "plugin" , "name" : "todo" , "displayName" : "Todo" } , "exports" : { "./strapi-admin" : { "source" : "./admin/src/index.ts" , "import" : "./dist/admin/index.mjs" , "require" : "./dist/admin/index.js" } , "./strapi-server" : { "source" : "./server/src/index.ts" , "import" : "./dist/server/index.mjs" , "require" : "./dist/server/index.js" } } , "dependencies" : { "@tanstack/react-query" : "^5.0.0" } , "peerDependencies" : { "@strapi/strapi" : "^5.0.0" , "@strapi/design-system" : "^2.0.0" , "react" : "^17.0.0 || ^18.0.0" } } Server Index Pattern // server/src/index.ts import controllers from './controllers' ; import routes from './routes' ; import services from './services' ; import contentTypes from './content-types' ; export default { controllers , routes , services , contentTypes , } ; Factory-Based Service // server/src/services/task.ts import { factories } from '@strapi/strapi' ; export default factories . createCoreService ( 'plugin::todo.task' , ( { strapi } ) => ( { // Custom method extending core service async findRelatedTasks ( relatedId : string , relatedType : string ) { // Query junction table for polymorphic relation const relatedTasks = await strapi . db . query ( 'tasks_related_mph' ) . findMany ( { where : { related_id : relatedId , related_type : relatedType } , } ) ; const taskIds = relatedTasks . map ( ( t ) => t . task_id ) ; // Fetch full task documents return strapi . documents ( 'plugin::todo.task' ) . findMany ( { filters : { id : { $ in : taskIds } } , } ) ; } , } ) ) ; Factory-Based Controller // server/src/controllers/task.ts import { factories } from '@strapi/strapi' ; export default factories . createCoreController ( 'plugin::todo.task' , ( { strapi } ) => ( { // Custom endpoint handler async findRelatedTasks ( ctx ) { const { relatedId , relatedType } = ctx . params ; const tasks = await strapi . service ( 'plugin::todo.task' ) . findRelatedTasks ( relatedId , relatedType ) ; ctx . body = tasks ; } , } ) ) ; Route Organization with Core Router // server/src/routes/index.ts import contentAPIRoutes from './content-api' ; import adminAPIRoutes from './admin' ; const routes = { 'content-api' : contentAPIRoutes , admin : adminAPIRoutes , } ; export default routes ; // server/src/routes/admin/task.ts - Core CRUD routes import { factories } from '@strapi/strapi' ; export default factories . createCoreRouter ( 'plugin::todo.task' ) ; // server/src/routes/admin/index.ts - Custom + Core routes import task from './task' ; export default ( ) => ( { type : 'admin' , routes : [ // Spread core CRUD routes ... task . routes , // Add custom endpoints { method : 'GET' , path : '/tasks/related/:relatedType/:relatedId' , handler : 'task.findRelatedTasks' , } , ] , } ) ; Hidden Plugin Content Type (Internal Use) { "kind" : "collectionType" , "collectionName" : "tasks" , "info" : { "singularName" : "task" , "pluralName" : "tasks" , "displayName" : "Task" } , "options" : { "draftAndPublish" : false } , "pluginOptions" : { "content-manager" : { "visible" : false } , "content-type-builder" : { "visible" : false } } , "attributes" : { "name" : { "type" : "text" } , "done" : { "type" : "boolean" } , "related" : { "type" : "relation" , "relation" : "morphToMany" } } } Admin Panel with Content Manager Integration // admin/src/index.ts import { PLUGIN_ID } from './pluginId' ; import { Initializer } from './components/Initializer' ; import { TodoPanel } from './components/TodoPanel' ; export default { register ( app : any ) { app . registerPlugin ( { id : PLUGIN_ID , initializer : Initializer , isReady : false , name : PLUGIN_ID , } ) ; } , bootstrap ( app : any ) { // Inject panel into Content Manager edit view app . getPlugin ( 'content-manager' ) . injectComponent ( 'editView' , 'right-links' , { name : 'todo-panel' , Component : TodoPanel , } ) ; } , 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 } ; } } ) ) ; } , } ; React Query Pattern for Admin Components // admin/src/components/TodoPanel.tsx import { useState } from 'react' ; import { QueryClient , QueryClientProvider } from '@tanstack/react-query' ; import { unstable_useContentManagerContext as useContentManagerContext } from '@strapi/strapi/admin' ; import { TextButton , Plus } from '@strapi/design-system' ; import { TaskList } from './TaskList' ; import { TodoModal } from './TodoModal' ; const queryClient = new QueryClient ( ) ; export const TodoPanel = ( ) => { const [ modalOpen , setModalOpen ] = useState ( false ) ; const { id } = useContentManagerContext ( ) ; return ( < QueryClientProvider client = { queryClient }

< TextButton startIcon = { < Plus /> } onClick = { ( ) => setModalOpen ( true ) } disabled = { ! id }

Add todo </ TextButton

{ id && ( <

< TodoModal open = { modalOpen } setOpen = { setModalOpen } /> < TaskList /> </

) } </ QueryClientProvider

) ; } ; Data Fetching with useFetchClient // admin/src/components/TaskList.tsx import { useQuery , useMutation , useQueryClient } from '@tanstack/react-query' ; import { useFetchClient , unstable_useContentManagerContext } from '@strapi/strapi/admin' ; import { Checkbox } from '@strapi/design-system' ; export const TaskList = ( ) => { const { get , put } = useFetchClient ( ) ; const { slug , id } = unstable_useContentManagerContext ( ) ; const queryClient = useQueryClient ( ) ; const { data : tasks } = useQuery ( { queryKey : [ 'tasks' , slug , id ] , queryFn : ( ) => get ( /todo/tasks/related/ ${ slug } / ${ id } ) . then ( ( res ) => res . data ) , } ) ; const toggleMutation = useMutation ( { mutationFn : ( task : any ) => put ( /todo/tasks/ ${ task . documentId } , { data : { done : ! task . done } } ) , onSuccess : ( ) => queryClient . invalidateQueries ( { queryKey : [ 'tasks' , slug , id ] } ) , } ) ; return ( < ul

{ tasks ?. map ( ( task : any ) => ( < li key = { task . id }

< Checkbox checked = { task . done } onCheckedChange = { ( ) => toggleMutation . mutate ( task ) }

{ task . name } </ Checkbox

</ li

) ) } </ ul

) ; } ; Best Practices Checklist Server: Use factories.createCoreService() for standard CRUD Use factories.createCoreController() with custom methods Use factories.createCoreRouter() for automatic CRUD routes Split routes into admin/ and content-api/ directories Hide internal content types from Content Manager UI Admin Panel: Use QueryClientProvider for React Query context Use useFetchClient() for API calls Use unstable_useContentManagerContext() for current entity info Use app.getPlugin('content-manager').injectComponent() for CM integration Support translations with registerTrads() Content Types: Use morphToMany for polymorphic relations Set pluginOptions.content-manager.visible: false for internal types Use singular names ( task not tasks ) For detailed patterns, see patterns.md . For real-world examples, see examples.md .

返回排行榜