strapi-expert
60
总安装量
60
周安装量
#3625
全站排名
安装命令
npx skills add https://github.com/ayhid/claude-skill-strapi-expert --skill strapi-expert
Agent 安装分布
opencode
56
gemini-cli
52
codex
52
kimi-cli
44
amp
44
Skill 文档
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(), andfactories.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-queryfor 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/andcontent-api/directories - Hide internal content types from Content Manager UI
Admin Panel:
- Use
QueryClientProviderfor 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
morphToManyfor polymorphic relations - Set
pluginOptions.content-manager.visible: falsefor internal types - Use singular names (
tasknottasks)
For detailed patterns, see patterns.md. For real-world examples, see examples.md.