构建高效插件
插件开发指南
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.
This is defined exactly the same as it is in browsers providing the URL of the
current module file.
This enables useful patterns such as relative file loading:
import { readFileSync } from 'node:fs';const buffer = readFileSync(new URL('./data.proto', import.meta.url));
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
The identifier of the plugin, usually the package name.
identifier: 'my-plugin', // 唯一标识符 StudioCMSPlugin.name: string
The name of the plugin, displayed in the StudioCMS Dashboard.
name: 'My Plugin', // 插件名称 StudioCMSPlugin.studiocmsMinimumVersion: string
The minimum version of StudioCMS required for this plugin to function correctly.
This is used to ensure compatibility between the plugin and the StudioCMS core.
It should be a semantic version string (e.g., "1.0.0").
If the plugin is not compatible with the current version of StudioCMS, it should not be loaded.
This is a required field.
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 module "studiocms:sdk"
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: any
pages = await module "studiocms:sdk"
sdk.any
GET.any
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: any
page = const pages: any
pages.any
find((page: any
page) => page: any
page.any
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 的内置导航辅助系统。