构建高效插件
插件开发指南
Section titled “插件开发指南”开发 StudioCMS 插件是扩展平台功能的关键方式,它提供了一种简单灵活的方法来增强项目能力。本文将通过基础示例演示如何创建和开发 StudioCMS 插件。
要创建 StudioCMS 插件,需遵循以下基础文件结构示例:
- package.json
文件夹src
- index.ts
文件夹routes
- […slug].astro
文件夹dashboard-grid-items
- MyPluginGridItem.astro
请在核心的 src/index.ts
文件中定义您的 StudioCMS 插件。以下示例演示如何创建包含 Astro 集成的插件,实现基础博客功能:
import { function definePlugin(options: StudioCMSPlugin): StudioCMSPlugin
Defines a plugin for StudioCMS.
definePlugin } from 'studiocms/plugins';import { (alias) interface AstroIntegrationimport AstroIntegration
AstroIntegration } from 'astro';import { const addVirtualImports: HookUtility<"astro:config:setup", [{ name: string; imports: Imports; __enableCorePowerDoNotUseOrYouWillBeFired?: boolean;}], void>
Creates a Vite virtual module and updates the Astro config.
Virtual imports are useful for passing things like config options, or data computed within the integration.
addVirtualImports, const createResolver: (_base: string) => { resolve: (...path: Array<string>) => string;}
Allows resolving paths relatively to the integration folder easily. Call it like this:
createResolver } from 'astro-integration-kit';
// 定义插件选项接口interface interface Options
Options { Options.route: string
route: string;}
export function function studioCMSPageInjector(options: Options): StudioCMSPlugin
studioCMSPageInjector(options: Options
options: interface Options
Options) { // 解析当前文件路径 const { const resolve: (...path: Array<string>) => string
resolve } = function createResolver(_base: string): { resolve: (...path: Array<string>) => string;}
Allows resolving paths relatively to the integration folder easily. Call it like this:
createResolver(import.
The type of import.meta
.
If you need to declare that a given property exists on import.meta
,
this type may be augmented via interface merging.
meta.ImportMeta.url: string
The absolute file:
URL of the module.
url);
// 定义 Astro 集成 function function (local function) myIntegration(options: Options): AstroIntegration
myIntegration(options: Options
options: interface Options
Options): (alias) interface AstroIntegrationimport AstroIntegration
AstroIntegration { const const route: string
route = `/${options: Options
options?.Options.route: string
route || 'my-plugin'}`;
return { AstroIntegration.name: string
The name of the integration.
name: 'my-astro-integration', AstroIntegration.hooks: { 'astro:db:setup'?: (options: { extendDb: (options: { configEntrypoint?: URL | string; seedEntrypoint?: URL | string; }) => void; }) => void | Promise<void>; ... 12 more ...; 'studiocms:plugins'?: PluginHook<...>;} & Partial<...>
The different hooks available to extend.
hooks: { "astro:config:setup": (params: { config: AstroConfig; command: "dev" | "build" | "preview" | "sync"; isRestart: boolean; updateConfig: (newConfig: DeepPartial<AstroConfig>) => AstroConfig; ... 8 more ...; logger: AstroIntegrationLogger;}
params) => { const { const injectRoute: (injectRoute: InjectedRoute) => void
injectRoute } = params: { config: AstroConfig; command: "dev" | "build" | "preview" | "sync"; isRestart: boolean; updateConfig: (newConfig: DeepPartial<AstroConfig>) => AstroConfig; ... 8 more ...; logger: AstroIntegrationLogger;}
params;
// 注入插件路由 const injectRoute: (injectRoute: InjectedRoute) => void
injectRoute({ entrypoint: string | URL
entrypoint: const resolve: (...path: Array<string>) => string
resolve('./routes/[...slug].astro'), pattern: string
pattern: `/${const route: string
route}/[...slug]`, prerender?: boolean
prerender: false, })
// 添加虚拟导入 function addVirtualImports(params: { config: AstroConfig; command: "dev" | "build" | "preview" | "sync"; isRestart: boolean; updateConfig: (newConfig: DeepPartial<AstroConfig>) => AstroConfig; ... 8 more ...; logger: AstroIntegrationLogger;}, args_0: { ...;}): void
Creates a Vite virtual module and updates the Astro config.
Virtual imports are useful for passing things like config options, or data computed within the integration.
addVirtualImports(params: { config: AstroConfig; command: "dev" | "build" | "preview" | "sync"; isRestart: boolean; updateConfig: (newConfig: DeepPartial<AstroConfig>) => AstroConfig; ... 8 more ...; logger: AstroIntegrationLogger;}
params, { name: string
name: 'my-astro-integration', imports: Imports
imports: { 'myplugin:config': ` export const options = ${var JSON: JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
JSON.JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)
Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
stringify({ route: string
route })}; export default options; `, } }) } } } }
// 定义 StudioCMS 插件 return function definePlugin(options: StudioCMSPlugin): StudioCMSPlugin
Defines a plugin for StudioCMS.
definePlugin({ StudioCMSPlugin.identifier: string
identifier: 'my-plugin', // 唯一标识符 StudioCMSPlugin.name: string
name: 'My Plugin', // 插件名称 StudioCMSPlugin.studiocmsMinimumVersion: string
studiocmsMinimumVersion: '0.1.0-beta.18', // 兼容最低版本 StudioCMSPlugin.hooks: { 'studiocms:astro:config'?: PluginHook<...>; 'studiocms:config:setup'?: PluginHook<...>;} & Partial<...>
hooks: { // 添加 Astro 集成 'studiocms:astro:config': ({ addIntegrations: (args_0: AstroIntegration | AstroIntegration[]) => void
addIntegrations }) => { addIntegrations: (args_0: AstroIntegration | AstroIntegration[]) => void
addIntegrations(function (local function) myIntegration(options: Options): AstroIntegration
myIntegration(options: Options
options)); }, // 插件配置设置 'studiocms:config:setup': ({ setDashboard: (args_0: { settingsPage?: { fields: ({ name: string; label: string; input: "checkbox"; required?: boolean | undefined; readOnly?: boolean | undefined; color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; defaultChecked?: boolean | undefined; size?: "sm" | "md" | "lg" | undefined; } | { name: string; label: string; input: "input"; type?: "number" | "text" | "password" | "email" | "tel" | "url" | "search" | undefined; required?: boolean | undefined; readOnly?: boolean | undefined; placeholder?: string | undefined; defaultValue?: string | undefined; } | { name: string; label: string; input: "textarea"; required?: boolean | undefined; readOnly?: boolean | undefined; placeholder?: string | undefined; defaultValue?: string | undefined; } | { options: { value: string; label: string; disabled?: boolean | undefined; }[]; name: string; label: string; input: "radio"; required?: boolean | undefined; readOnly?: boolean | undefined; color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; defaultValue?: string | undefined; direction?: "horizontal" | "vertical" ...
setDashboard, setFrontend: (args_0: { frontendNavigationLinks?: { label: string; href: string; }[] | undefined;}) => void
setFrontend, setRendering: (args_0: { pageTypes?: { label: string; identifier: string; fields?: ({ name: string; label: string; input: "checkbox"; required?: boolean | undefined; readOnly?: boolean | undefined; color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; defaultChecked?: boolean | undefined; size?: "sm" | "md" | "lg" | undefined; } | { name: string; label: string; input: "input"; type?: "number" | "text" | "password" | "email" | "tel" | "url" | "search" | undefined; required?: boolean | undefined; readOnly?: boolean | undefined; placeholder?: string | undefined; defaultValue?: string | undefined; } | { name: string; label: string; input: "textarea"; required?: boolean | undefined; readOnly?: boolean | undefined; placeholder?: string | undefined; defaultValue?: string | undefined; } | { options: { value: string; label: string; disabled?: boolean | undefined; }[]; name: string; label: string; input: "radio"; required?: boolean | undefined; readOnly?: boolean | undefined; color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; defaultValue?: string | undefined; direction ...
setRendering }) => { // 仪表板网格项配置 setDashboard: (args_0: { settingsPage?: { fields: ({ name: string; label: string; input: "checkbox"; required?: boolean | undefined; readOnly?: boolean | undefined; color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; defaultChecked?: boolean | undefined; size?: "sm" | "md" | "lg" | undefined; } | { name: string; label: string; input: "input"; type?: "number" | "text" | "password" | "email" | "tel" | "url" | "search" | undefined; required?: boolean | undefined; readOnly?: boolean | undefined; placeholder?: string | undefined; defaultValue?: string | undefined; } | { name: string; label: string; input: "textarea"; required?: boolean | undefined; readOnly?: boolean | undefined; placeholder?: string | undefined; defaultValue?: string | undefined; } | { options: { value: string; label: string; disabled?: boolean | undefined; }[]; name: string; label: string; input: "radio"; required?: boolean | undefined; readOnly?: boolean | undefined; color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; defaultValue?: string | undefined; direction?: "horizontal" | "vertical" ...
setDashboard({ dashboardGridItems?: GridItemInput[] | undefined
dashboardGridItems: [ { GridItemInput.name: string
The name of the grid item.
name: 'example', // 组件名称 GridItemInput.span: 1 | 2 | 3
The span of the grid item, which can be 1, 2, or 3.
span: 2, // 网格跨度 GridItemInput.variant: "default" | "filled"
The variant of the grid item, which can be 'default' or 'filled'.
variant: 'default', // 显示变体 GridItemInput.requiresPermission?: "editor" | "owner" | "admin" | "visitor"
The required permission level to view the grid item.
Optional. Can be 'owner', 'admin', 'editor', or 'visitor'.
requiresPermission: 'editor', // 所需权限 GridItemInput.header?: { title: string; icon?: HeroIconName;}
The header of the grid item.
Optional.
header: { title: string
The title of the header.
title: '示例', icon?: "map" | "bolt" | "code-bracket-solid" | "code-bracket-square-solid" | "exclamation-circle" | "exclamation-circle-solid" | "exclamation-triangle" | "exclamation-triangle-solid" | ... 1279 more ... | "x-mark-solid"
The icon of the header.
Optional.
icon: 'bolt' }, // 标题和图标 GridItemInput.body?: { html: string; components?: Record<string, string>; sanitizeOpts?: SanitizeOptions;}
The body of the grid item.
Optional.
body: { html: string
The HTML content of the body.
html: '<examplegriditem></examplegriditem>', // 占位HTML components?: Record<string, string>
The components within the body.
Optional.
components: { // 实际渲染组件 examplegriditem: string
examplegriditem: const resolve: (...path: Array<string>) => string
resolve('./dashboard-grid-items/MyPluginGridItem.astro') } } } ], });
// 前端导航配置 setFrontend: (args_0: { frontendNavigationLinks?: { label: string; href: string; }[] | undefined;}) => void
setFrontend({ frontendNavigationLinks?: { label: string; href: string;}[] | undefined
frontendNavigationLinks: [{ label: string
label: '我的插件', href: string
href: options: Options
options?.Options.route: string
route || 'my-plugin' }], });
// 页面类型配置 setRendering: (args_0: { pageTypes?: { label: string; identifier: string; fields?: ({ name: string; label: string; input: "checkbox"; required?: boolean | undefined; readOnly?: boolean | undefined; color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; defaultChecked?: boolean | undefined; size?: "sm" | "md" | "lg" | undefined; } | { name: string; label: string; input: "input"; type?: "number" | "text" | "password" | "email" | "tel" | "url" | "search" | undefined; required?: boolean | undefined; readOnly?: boolean | undefined; placeholder?: string | undefined; defaultValue?: string | undefined; } | { name: string; label: string; input: "textarea"; required?: boolean | undefined; readOnly?: boolean | undefined; placeholder?: string | undefined; defaultValue?: string | undefined; } | { options: { value: string; label: string; disabled?: boolean | undefined; }[]; name: string; label: string; input: "radio"; required?: boolean | undefined; readOnly?: boolean | undefined; color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; defaultValue?: string | undefined; direction ...
setRendering({ pageTypes?: { label: string; identifier: string; fields?: ({ name: string; label: string; input: "checkbox"; required?: boolean | undefined; readOnly?: boolean | undefined; color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; defaultChecked?: boolean | undefined; size?: "sm" | "md" | "lg" | undefined; } | { name: string; label: string; input: "input"; type?: "number" | "text" | "password" | "email" | "tel" | "url" | "search" | undefined; required?: boolean | undefined; readOnly?: boolean | undefined; placeholder?: string | undefined; defaultValue?: string | undefined; } | { name: string; label: string; input: "textarea"; required?: boolean | undefined; readOnly?: boolean | undefined; placeholder?: string | undefined; defaultValue?: string | undefined; } | { options: { value: string; label: string; disabled?: boolean | undefined; }[]; name: string; label: string; input: "radio"; required?: boolean | undefined; readOnly?: boolean | undefined; color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; defaultValue?: string | undefined; direction?: "horizontal" | "vertical" | undefined; } | { options: { value: string; label: string; disabled?: boolean | undefined; } ...
pageTypes: [{ identifier: string
identifier: 'my-plugin', label: string
label: '博客文章(我的插件)' }], }) } } });}
上述示例定义了一个包含 Astro 集成的 StudioCMS 插件,用于构建基础博客系统。该插件通过以下核心功能扩展 StudioCMS:
路由实现示例
Section titled “路由实现示例”在 src/routes/[...slug].astro
文件中定义插件路由时,需要使用特殊语法 ---
划分两个功能区:
- 第一组
---
标记的顶部区域称为 Frontmatter - 第二组
---
之后的区域是 模板渲染区
import { const StudioCMSRenderer: any
StudioCMSRenderer } from 'studiocms:renderer';import const sdk: { addPageToFolderTree: (tree: FolderNode[], folderId: string, newPage: FolderNode) => FolderNode[]; ... 29 more ...; notificationSettings: { site: { get: () => Promise<{ id: string; emailVerification: boolean; requireAdminVerification: boolean; requireEditorVerification: boolean; oAuthBypassVerification: boolean; }>; update: (settings: { ...; }) => Promise<{ id: string; emailVerification: boolean; requireAdminVerification: boolean; requireEditorVerification: boolean; oAuthBypassVerification: boolean; }>; }; };}
sdk from 'studiocms:sdk';import const config: { route: string;}
config from 'myplugin:config';
// 生成完整路由路径const const makeRoute: (slug: string) => string
makeRoute = (slug: string
slug: string) => { return `/${const config: { route: string;}
config.route: string
route}/${slug: string
slug}`;}
// 获取 'my-plugin' 类型的所有页面const const pages: CombinedPageData[]
pages = await const sdk: { addPageToFolderTree: (tree: FolderNode[], folderId: string, newPage: FolderNode) => FolderNode[]; ... 29 more ...; notificationSettings: { site: { get: () => Promise<{ id: string; emailVerification: boolean; requireAdminVerification: boolean; requireEditorVerification: boolean; oAuthBypassVerification: boolean; }>; update: (settings: { ...; }) => Promise<{ id: string; emailVerification: boolean; requireAdminVerification: boolean; requireEditorVerification: boolean; oAuthBypassVerification: boolean; }>; }; };}
sdk.type GET: { database: { users: () => Promise<CombinedUserData[]>; pages: { (includeDrafts?: boolean, hideDefaultIndex?: boolean, tree?: FolderNode[], metaOnly?: false, paginate?: PaginateInput): Promise<CombinedPageData[]>; (includeDrafts?: boolean, hideDefaultIndex?: boolean, tree?: FolderNode[], metaOnly?: true, paginate?: PaginateInput): Promise<MetaOnlyPageData[]>; }; folderPages: { (id: string, includeDrafts?: boolean, hideDefaultIndex?: boolean, tree?: FolderNode[], metaOnly?: false, paginate?: PaginateInput): Promise<CombinedPageData[]>; (id: string, includeDrafts?: boolean, hideDefaultIndex?: boolean, tree?: FolderNode[], metaOnly?: true, paginate?: PaginateInput): Promise<MetaOnlyPageData[]>; }; config: () => Promise<{ ...; } | undefined>; folders: () => Promise<{ ...; }[]>; }; databaseEntry: { users: { byId: (id: string) => Promise<CombinedUserData | undefined>; byUsername: (username: string) => Promise<CombinedUserData | undefined>; byEmail: (email: string) => Promise<CombinedUserData | undefined>; }; pages: { byId: { (id: string, tree?: FolderNode[]): Promise<CombinedPageData | undefined>; (id: string, tree?: FolderNode[], metaOnly?: boolean): Promise<MetaOnlyPageData | undefined>; }; bySlug: { (slug: string, tree?: FolderNode[]): Promise<CombinedPageData | undefined>; ( ...
GET.packagePages: (packageName: string, tree?: FolderNode[]) => Promise<CombinedPageData[]> (+1 overload)
packagePages('my-plugin');
// 获取当前 URL slugconst { const slug: string | undefined
slug } = const Astro: AstroGlobal<Record<string, any>, AstroComponentFactory, Record<string, string | undefined>>
Astro.AstroGlobal<Record<string, any>, AstroComponentFactory, Record<string, string | undefined>>.params: Record<string, string | undefined>
Parameters passed to a dynamic page generated using getStaticPaths
Example usage:
---export async function getStaticPaths() { return [ { params: { id: '1' } }, ];}
const { id } = Astro.params;---<h1>{id}</h1>
params;
// 查找匹配页面const const page: CombinedPageData | undefined
page = const pages: CombinedPageData[]
pages.Array<CombinedPageData>.find(predicate: (value: CombinedPageData, index: number, obj: CombinedPageData[]) => unknown, thisArg?: any): CombinedPageData | undefined (+1 overload)
Returns the value of the first element in the array where predicate is true, and undefined
otherwise.
find((page: CombinedPageData
page) => page: CombinedPageData
page.slug: string
slug === const slug: string | undefined
slug || '');
{ slug && page ? ( <div> <h1>{page.title}</h1> <StudioCMSRenderer content={page.defaultContent?.content || ''} /> </div> ) : ( <div> <h1>我的插件</h1> <ul> {pages.length > 0 && pages.map((page) => ( <li> <a href={makeRoute(page.slug)}>{page.title}</a> </li> ))} </ul> </div> )}
此动态路由^在没有 slug 参数时显示文章列表,有 slug 时显示具体文章内容。
仪表板组件示例
Section titled “仪表板组件示例”在 src/dashboard-grid-items/MyPluginGridItem.astro
中创建仪表板组件:
---import { StudioCMSRoutes } from 'studiocms:lib';import sdk from 'studiocms:sdk';
// 获取 'my-plugin' 类型的所有页面const pages = await sdk.GET.packagePages('my-plugin');
// 筛选最近30天更新的前5篇文章const recentlyUpdatedPages = pages .filter((page) => { const now = new Date(); const thirtyDaysAgo = new Date(now.setDate(now.getDate() - 30)); return new Date(page.updatedAt) > thirtyDaysAgo; }) .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) .slice(0, 5);---
<div> <h2>最近更新</h2> <ul> {recentlyUpdatedPages.length > 0 && recentlyUpdatedPages.map((page) => ( <li> <!-- 跳转到编辑页面 --> <a href={StudioCMSRoutes.mainLinks.contentManagementEdit + `?edit=${page.id}`}> {page.title} </a> </li> ))} </ul></div>
该示例为插件定义了一个网格项目,用于展示最近30天内更新的5个内容页面。该网格项目包含指向每条内容管理编辑页面的链接列表。
FrontendNavigationLinks 导航组件集成
Section titled “FrontendNavigationLinks 导航组件集成”若需在项目中直接使用 StudioCMS 内置导航辅助工具(例如 @studiocms/blog
插件的实现方式),可通过创建自定义的 Navigation.astro
组件实现:
---import { StudioCMSRoutes } from 'studiocms:lib';import studioCMS_SDK from 'studiocms:sdk/cache';import { frontendNavigation } from 'studiocms:plugin-helpers';
// 组件属性定义interface Props { topLevelLinkCount?: number;};
// 默认显示3个顶级导航项const { topLevelLinkCount = 3 } = Astro.props;
// 获取站点标题const config = (await studioCMS_SDK.GET.siteConfig()).data;
// 获取站点 URLconst { title } = config || { title: 'StudioCMS' };
// 基础URLconst { mainLinks: { baseSiteURL } } = StudioCMSRoutes;
// 导航项类型type LinkProps = { text: string; href: string;};
// 获取导航链接const links: LinkProps[] = await frontendNavigation();---{/* 无下拉菜单的导航模式 */}{ ( links.length < topLevelLinkCount || links.length === topLevelLinkCount ) && ( <div class="navigation"> <div class="title"><a href={baseSiteURL}>{title}</a></div> <div class="mini-nav"> <button>菜单</button> <div class="mini-nav-content"> { links.map(({ text, href }) => ( <a {href}>{text}</a> )) } </div> </div> { links.map(({ text, href }) => ( <a class="links" {href}>{text}</a> )) } </div>) }
{/* 带下拉菜单的导航模式 */}{ links.length > topLevelLinkCount && ( <div class="navigation"> <div class="title"><a href={baseSiteURL}>{title}</a></div>
<div class="mini-nav"> <button>菜单</button> <div class="mini-nav-content"> { links.map(({ text, href }) => ( <a {href}>{text}</a> )) } </div> </div> { links.slice(0, topLevelLinkCount).map(({ text, href }) => ( <a class="links" {href}>{text}</a> )) } <div class="dropdown"> <button>更多 ▼</button> <div class="dropdown-content"> { links.slice(topLevelLinkCount).map(({ text, href }) => ( <a {href}>{text}</a> )) } </div> </div> </div>) }
上述示例定义了一个自定义的 Navigation.astro
组件,该组件利用 StudioCMS 内置的导航辅助工具为项目创建导航菜单。此组件包含:
- 主站点URL链接
- 首页导航项
- 所有配置为在导航中显示的其他页面链接
您只需添加自定义样式,即可获得一个功能完备的导航菜单,完美集成 StudioCMS 的内置导航辅助系统。