Zum Inhalt springen

Plugins nützlich machen

Die Erstellung eines StudioCMS-Plugins ist eine leistungsstarke Möglichkeit, die Funktionalität von StudioCMS zu erweitern. Sie bieten eine einfache und flexible Möglichkeit, neue Funktionen zu deinem StudioCMS-Projekt hinzuzufügen. Das folgende Beispiel zeigt dir, wie du ein StudioCMS-Plugin erstellst und wie es funktioniert.

Um loszulegen, musst du ein neues StudioCMS-Plugin erstellen. Im Folgenden findest du ein grundlegendes Beispiel für die Dateistruktur eines StudioCMS-Plugins:

  • package.json
  • Directorysrc
    • index.ts
    • Directoryroutes
      • […slug].astro
    • Directorydashboard-grid-items
      • MyPluginGridItem.astro

In der Hauptdatei src/index.ts definierst du das StudioCMS Plugin. Das folgende Beispiel zeigt, wie du ein StudioCMS-Plugin definierst, das eine Astro-Integration enthält, um ein einfaches Blog-Beispiel zu erstellen:

index.ts
import {
function definePlugin(options: StudioCMSPluginOptions): 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';
// Definiere die Optionen für das Plugin und die Integration
interface
interface Options
Options
{
Options.route: string
route
: string;
}
export function
function studioCMSPageInjector(options: Options): StudioCMSPlugin
studioCMSPageInjector
(
options: Options
options
:
interface Options
Options
) {
// Löse den Pfad zur aktuellen Datei auf
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
);
// Definiere die Astro-Integration
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:config:setup'?: (options: {
config: AstroConfig;
command: "dev" | "build" | "preview" | "sync";
isRestart: boolean;
updateConfig: (newConfig: DeepPartial<AstroConfig>) => AstroConfig;
addRenderer: (renderer: AstroRenderer) => void;
addWatchFile: (path: URL | string) => void;
injectScript: (stage: InjectedScriptStage, content: string) => void;
injectRoute: (injectRoute: InjectedRoute) => void;
addClientDirective: (directive: ClientDirectiveConfig) => void;
addDevToolbarApp: (entrypoint: DevToolbarAppEntry) => void;
addMiddleware: (mid: AstroIntegrationMiddleware) => void;
createCodegenDir: () => URL;
logger: AstroIntegrationLogger;
}) => void | Promise<void>;
... 10 more ...;
'astro:routes:resolved'?: (options: {
routes: IntegrationResolvedRoute[];
logger: AstroIntegrationLogger;
}) => void | Promise<void>;
} & 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
;
// Injiziere die Route für das Plugin
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;
`,
}
})
}
}
}
}
// Definiere das StudioCMS Plugin
return
function definePlugin(options: StudioCMSPluginOptions): StudioCMSPlugin

Defines a plugin for StudioCMS.

@paramoptions - The configuration options for the plugin.

@returnsThe plugin configuration.

definePlugin
({
identifier: string
identifier
: 'my-plugin',
name: string
name
: 'My Plugin',
studiocmsMinimumVersion: string
studiocmsMinimumVersion
: '0.1.0-beta.8',
integration?: AstroIntegration | AstroIntegration[] | undefined
integration
:
function (local function) myIntegration(options: Options): AstroIntegration
myIntegration
(
options: Options
options
), // Optional, aber empfohlen
// Definiere die Frontend-Navigationslinks für das Plugin (optional)
// Dies ist nützlich, wenn du die eingebauten StudioCMS-Navigationshilfen in deinem Layout verwendest,
// wie z.B. bei der Verwendung des Plugins `@studiocms/blog`.
frontendNavigationLinks?: {
label: string;
href: string;
}[] | undefined
frontendNavigationLinks
: [{
label: string
label
: 'Title here',
href: string
href
:
options: Options
options
?.
Options.route: string
route
|| 'my-plugin' }],
// Wenn du pageTypes erstellst, kannst du auch eine `pageContentComponent` definieren, wenn dein Plugin einen eigenen Inhaltseditor benötigt.
// pageTypes: [{ identifier: 'my-plugin', label: 'Blog Post (My Plugin)', pageContentComponent: resolve('./components/MyContentEditor.astro') }],
// In diesem Beispiel ist es in Ordnung, den Standard-Editor (Markdown) zu verwenden.
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";
required?: boolean | undefined;
readOnly?: boolean | undefined;
type?: "number" | "text" | "password" | "email" | "tel" | "url" | "search" | 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;
} | {
name: string;
label: string;
options: {
label: string;
value: string;
disabled?: boolean | undefined;
}[];
input: "radio";
required?: boolean | undefined;
readOnly?: boolean | undefined;
color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined;
defaultValue?: string | undefined;
direction?: "horizontal" | "vertical" | undefined;
} | {
name: string;
label: string;
options: {
label: string;
value: string;
disabled ...
pageTypes
: [{
identifier: string
identifier
: 'my-plugin',
label: string
label
: 'Blog Post (My Plugin)' }],
// Definiere die Gitterelemente für das Dashboard
// Dies sind die Elemente, die auf dem StudioCMS Dashboard angezeigt werden
// Du kannst so viele Elemente definieren, wie du willst
// In diesem Beispiel definieren wir ein einzelnes Element, das eine Spannweite von 2 hat und die Berechtigung „Editor“ benötigt. Außerdem injizieren wir eine Astro-Komponente, die das einfache benutzerdefinierte HTML-Element ersetzt.
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
: 'Example',
icon?: "bolt" | "code-bracket-solid" | "code-bracket-square-solid" | "exclamation-circle" | "exclamation-circle-solid" | "exclamation-triangle" | "exclamation-triangle-solid" | ... 1280 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
: {
// Verwende immer einfaches HTML ohne `-` oder Sonderzeichen in den Tags. Sie werden durch die Astro-Komponente ersetzt und dieses HTML wird nie gerendert.
html: string

The HTML content of the body.

html
: '<examplegriditem></examplegriditem>',
components?: Record<string, string>

The components within the body. Optional.

components
: {
// Injiziere die Astro-Komponente, um das einfache benutzerdefinierte HTML-Element zu ersetzen
examplegriditem: string
examplegriditem
:
const resolve: (...path: Array<string>) => string
resolve
('./dashboard-grid-items/MyPluginGridItem.astro')
}
}
}
],
});
}

Das obige Beispiel definiert ein StudioCMS-Plugin, das eine Astro-Integration enthält, um ein einfaches Blog-Beispiel zu erstellen. Das Plugin enthält eine Route, die in das StudioCMS-Projekt injiziert wird, und ein Grid-Element, das auf dem StudioCMS-Dashboard angezeigt wird.

Weitere Informationen darüber, wie du eine Astro-Integration erstellst, findest du im Astro Integration Kit^ und in der Astro Integrations Dokumentation^.

In der Datei src/routes/[...slug].astro definierst du die Route für das Plugin. Das folgende Beispiel zeigt, wie du eine Route für das Plugin definierst. Der erste Teil ist das Frontmatter (zwischen den ----Zeichen) und der zweite Teil ist die HTML-Vorlage, die unter dem zweiten --- eingefügt wird.

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' wird hier als Bezeichner für
// den pageType aus der Plugin-Definition verwendet
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, tree?: FolderNode[]) => Promise<CombinedPageData[]>;
config: () => Promise<{
id: number;
title: string;
description: string;
defaultOgImage: string | null;
... 6 more ...;
enableMailer: boolean;
} | 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>;
bySlug: (slug: string, tree?: FolderNode[]) => Promise<CombinedPageData | undefined>;
};
folder: (id: string) => Promise<{
...;
} | undefined>;
};
databaseTable: {
users: () => Promise<{
name: string;
password: string | null;
email: string | null;
url: string | null;
username: string;
id: string;
avatar: string | null;
updatedAt: Date | null;
createdAt: Date | null;
emailVerified: boolean;
notifications: string | null;
}[]>;
oAuthAccounts: () => Promise<{
userId: string;
provider: string ...
GET
.
packagePages: (packageName: string, tree?: FolderNode[]) => Promise<CombinedPageData[]>
packagePages
('my-plugin');
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
|| '');
Template
{
slug && page ? (
<div>
<h1>{page.title}</h1>
<StudioCMSRenderer content={page.defaultContent?.content || ''} />
</div>
) : (
<div>
<h1>My Plugin</h1>
<ul>
{pages.length > 0 && pages.map((page) => (
<li>
<a href={makeRoute(page.slug)}>{page.title}</a>
</li>
))}
</ul>
</div>
)
}

Das obige Beispiel definiert eine dynamische Route^ für das Plugin, die eine Liste von Blogposts anzeigt, wenn kein Slug angegeben wird, und die den Inhalt eines Blogposts anzeigt, wenn ein Slug angegeben wird.

In der Datei src/dashboard-grid-items/MyPluginGridItem.astro definierst du das Grid Item für das Plugin. Das folgende Beispiel zeigt, wie du ein Grid Item für das Plugin definierst:

MyPluginGridItem.astro
---
import { StudioCMSRoutes } from 'studiocms:lib';
import sdk from 'studiocms:sdk';
// 'my-plugin' wird hier als Bezeichner für
// den pageType aus der Plugin-Definition verwendet
const pages = await sdk.GET.packagePages('my-plugin');
// Erhalte die 5 zuletzt aktualisierten Seiten der letzten 30 Tage
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>Recently Updated Pages</h2>
<ul>
{recentlyUpdatedPages.length > 0 && recentlyUpdatedPages.map((page) => (
<li>
<a href={StudioCMSRoutes.mainLinks.contentManagementEdit + `?edit=${page.id}`}>{page.title}</a>
</li>
))}
</ul>
</div>

Das obige Beispiel definiert ein Grid-Element für das Plugin, das die 5 zuletzt aktualisierten Seiten der letzten 30 Tage anzeigt. Das Grid-Element enthält eine Liste mit Links zu den Bearbeitungsseiten der einzelnen Seiten im Content Management.

Wenn du die eingebauten StudioCMS-Navigationshilfen in deinem Projekt verwenden möchtest, ähnlich wie das Plugin @studiocms/blog, kannst du eine eigene Komponente Navigation.astro erstellen:

Navigation.astro
---
import { StudioCMSRoutes } from 'studiocms:lib';
import studioCMS_SDK from 'studiocms:sdk/cache';
import { frontendNavigation } from 'studiocms:plugin-helpers';
// Definiere die Requisiten für die Komponente Navigation
interface Props {
topLevelLinkCount?: number;
};
// Erhalte die Anzahl der Links auf oberster Ebene aus den Requisiten
const { topLevelLinkCount = 3 } = Astro.props;
// Abrufen der Website-Konfiguration und der Seitenliste
const config = (await studioCMS_SDK.GET.siteConfig()).data;
// Hol dir den Titel der Seite aus der Konfiguration
const { title } = config || { title: 'StudioCMS' };
// Erhalte die URL der Hauptseite
const {
mainLinks: { baseSiteURL },
} = StudioCMSRoutes;
// Definiere die Link-Requisiten für die Navigation
type LinkProps = {
text: string;
href: string;
};
// Definiere die Links für die Navigation
const links: LinkProps[] = await frontendNavigation();
---
{/* Wenn keine Dropdown-Elemente */}
{ ( links.length < topLevelLinkCount || links.length === topLevelLinkCount ) && (
<div class="navigation">
<div class="title"><a href={baseSiteURL}>{title}</a></div>
<div class="mini-nav">
<button>Menu</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>
) }
{/* Wenn Dropdown-Elemente */}
{ links.length > topLevelLinkCount && (
<div class="navigation">
<div class="title"><a href={baseSiteURL}>{title}</a></div>
<div class="mini-nav">
<button>Menu</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>More ▼</button>
<div class="dropdown-content">
{ links.slice(topLevelLinkCount).map(({ text, href }) => (
<a {href}>{text}</a>
)) }
</div>
</div>
</div>
) }

Das obige Beispiel definiert eine benutzerdefinierte Komponente Navigation.astro, die die eingebauten StudioCMS-Navigationshilfen verwendet, um ein Navigationsmenü für das Projekt zu erstellen. Die Komponente enthält Links zur Haupt-URL der Website, zur Indexseite und zu allen anderen Seiten, die in der Navigation angezeigt werden sollen.

Du musst nur noch ein paar Stile hinzufügen und schon hast du ein voll funktionsfähiges Navigationsmenü, das mit den eingebauten StudioCMS-Navigationshelfern funktioniert.