跳转到内容

构建高效插件

开发 StudioCMS 插件是扩展平台功能的关键方式,它提供了一种简单灵活的方法来增强项目能力。本文将通过基础示例演示如何创建和开发 StudioCMS 插件。

要创建 StudioCMS 插件,需遵循以下基础文件结构示例:

  • package.json
  • 文件夹src
    • index.ts
    • 文件夹routes
      • […slug].astro
    • 文件夹dashboard-grid-items
      • MyPluginGridItem.astro

请在核心的 src/index.ts 文件中定义您的 StudioCMS 插件。以下示例演示如何创建包含 Astro 集成的插件,实现基础博客功能:

index.ts
import {
function definePlugin(options: StudioCMSPlugin): StudioCMSPlugin

Defines a plugin for StudioCMS.

@paramoptions - The configuration options for the plugin.

@returnsThe plugin configuration.

definePlugin
} from 'studiocms/plugins';
import {
(alias) interface AstroIntegration
import 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.

@paramparams

@paramoptions

@paramoptions.name

@paramoptions.imports

@seehttps://astro-integration-kit.netlify.app/utilities/add-virtual-imports/

@example

// my-integration/index.ts
import { addVirtualImports } from "astro-integration-kit";
addVirtualImports(params, {
name: 'my-integration',
imports: {
'virtual:my-integration/config': `export default ${ JSON.stringify({foo: "bar"}) }`,
},
});

This is then readable anywhere else in your integration:

// myIntegration/src/component/layout.astro
import config from "virtual:my-integration/config";
console.log(config.foo) // "bar"

addVirtualImports
,
const createResolver: (_base: string) => {
resolve: (...path: Array<string>) => string;
}

Allows resolving paths relatively to the integration folder easily. Call it like this:

@param_base - The location you want to create relative references from. import.meta.url is usually what you'll want.

@seehttps://astro-integration-kit.netlify.app/core/create-resolver/

@example

const { resolve } = createResolver(import.meta.url);
const pluginPath = resolve("./plugin.ts");

This way, you do not have to add your plugin to your package.json exports.

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:

@param_base - The location you want to create relative references from. import.meta.url is usually what you'll want.

@seehttps://astro-integration-kit.netlify.app/core/create-resolver/

@example

const { resolve } = createResolver(import.meta.url);
const pluginPath = resolve("./plugin.ts");

This way, you do not have to add your plugin to your package.json exports.

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 AstroIntegration
import 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.

@paramparams

@paramoptions

@paramoptions.name

@paramoptions.imports

@seehttps://astro-integration-kit.netlify.app/utilities/add-virtual-imports/

@example

// my-integration/index.ts
import { addVirtualImports } from "astro-integration-kit";
addVirtualImports(params, {
name: 'my-integration',
imports: {
'virtual:my-integration/config': `export default ${ JSON.stringify({foo: "bar"}) }`,
},
});

This is then readable anywhere else in your integration:

// myIntegration/src/component/layout.astro
import config from "virtual:my-integration/config";
console.log(config.foo) // "bar"

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.

@paramvalue A JavaScript value, usually an object or array, to be converted.

@paramreplacer A function that transforms the results.

@paramspace Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.

stringify
({
route: string
route
})};
export default options;
`,
}
})
}
}
}
}
// 定义 StudioCMS 插件
return
function definePlugin(options: StudioCMSPlugin): StudioCMSPlugin

Defines a plugin for StudioCMS.

@paramoptions - The configuration options for the plugin.

@returnsThe plugin configuration.

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:

深入了解 Astro 集成开发,请参阅 Astro 集成套件^Astro 集成文档^

src/routes/[...slug].astro 文件中定义插件路由时,需要使用特殊语法 --- 划分两个功能区:

  • 第一组 --- 标记的顶部区域称为 ​Frontmatter​
  • 第二组 --- 之后的区域是 ​模板渲染区​
​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 slug
const {
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>

Astro reference

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.

@parampredicate find calls predicate once for each element of the array, in ascending order, until it finds one where predicate returns true. If such an element is found, find immediately returns that element value. Otherwise, find returns undefined.

@paramthisArg If provided, it will be used as the this value for each invocation of predicate. If it is not provided, undefined is used instead.

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 时显示具体文章内容。

src/dashboard-grid-items/MyPluginGridItem.astro 中创建仪表板组件:

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个内容页面。该网格项目包含指向每条内容管理编辑页面的链接列表。

Section titled “FrontendNavigationLinks 导航组件集成”

若需在项目中直接使用 StudioCMS 内置导航辅助工具(例如 @studiocms/blog 插件的实现方式),可通过创建自定义的 Navigation.astro 组件实现:

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;
// 获取站点 URL
const { title } = config || { title: 'StudioCMS' };
// 基础URL
const { 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 的内置导航辅助系统。