Renderer Plugins
Introduction
Section titled “Introduction”StudioCMS renderer plugins provide a way to extend and customize the rendering process of your StudioCMS application. They allow you to modify how content is rendered on the frontend by adding custom components, wrappers, or other modifications to the rendering pipeline.
Creating a Renderer Plugin
Section titled “Creating a Renderer Plugin”The main plugin file
Section titled “The main plugin file”To get started with creating a renderer plugin, you need to define a StudioCMS plugin that registers your custom page type and its associated renderer component. Below is an example of how to create a simple renderer plugin that adds a custom page type with a renderer and an editor component.
import { function definePlugin(options: StudioCMSPluginDef): StudioCMSPluginDef
Defines a plugin for StudioCMS.
definePlugin } from 'studiocms/plugins';import { 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';import { (alias) interface AstroIntegrationimport AstroIntegration
AstroIntegration } from 'astro';
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);
// Define the StudioCMS Pluginexport const const myPlugin: () => { readonly identifier: string; readonly name: string; readonly studiocmsMinimumVersion?: string | undefined | undefined; readonly requires?: readonly string[] | undefined; readonly hooks: { "studiocms:astro-config"?: ((args: { readonly logger: AstroIntegrationLogger; readonly addIntegrations: (args: AstroIntegration | AstroIntegration[]) => Promise<void>; }) => Promise<void>) | ((args: { readonly logger: AstroIntegrationLogger; readonly addIntegrations: (args: AstroIntegration | AstroIntegration[]) => Promise<void>; }) => void) | undefined; "studiocms:auth"?: ((args: { readonly logger: AstroIntegrationLogger; readonly setAuthService: (args: { oAuthProvider: { readonly name: string; readonly formattedName: string; readonly svg: string; readonly endpointPath: string; readonly requiredEnvVariables?: readonly string[] | undefined; }; }) => Promise<void>; }) => Promise<void>) | ((args: { readonly logger: AstroIntegrationLogger; readonly setAuthService: (args: { oAuthProvider: { readonly name: string; readonly formattedName: string; readonly svg: string; readonly endpointPath: string; readonly requiredEnvVariables?: readonly string[] | undefined; }; }) => Promise<void>; }) => void) | undefined; "studiocms:dashboard"?: ((args: { readonly logger: AstroIntegrationLogger; readonly setDashboard: (args: { translations: { [x: string]: { [x: string]: { [x: string]: string; }; }; }; dashboardGridItems?: readonly { readonly name: string; readonly span: 1 | 2 | 3; readonly variant: "default" | "filled"; readonly requiresPermission?: "owner" | "admin" | "editor" | "visitor" | undefined; readonly header?: { readonly title: string; readonly icon?: "heroicons:academic-cap" | "heroicons:academic-cap-16-solid" | "heroicons:academic-cap-20-solid" | "heroicons:academic-cap-solid" | "heroicons:adjustments-horizontal" | "heroicons:adjustments-horizontal-16-solid" | "heroicons:adjustments-horizontal-20-solid" | "heroicons:adjustments-horizontal-solid" | "heroicons:adjustments-vertical" | "heroicons:adjustments-vertical-16-solid" | "heroicons:adjustments-vertical-20-solid" | "heroicons:adjustments-vertical-solid" | "heroicons:archive-box" | "heroicons:archive-box-16-solid" | "heroicons:archive-box-20-solid" | "heroicons:archive-box-arrow-down" | "heroicons:archive-box-arrow-down-16-solid" | "heroicons:archive-box-arrow-down-20-solid" | "heroicons:archive-box-arrow-down-solid" | "heroicons:archive-box-solid" | "heroicons:archive-box-x-mark" | "heroicons:archive-box-x-mark-16-solid" | "heroicons:archive-box-x-mark-20-solid" | "heroicons:archive-box-x-mark-solid" | "heroicons:arrow-down" | "heroicons:arrow-down-16-solid" | "heroicons:arrow-down-20-solid" | "heroicons:arrow-down-circle" | "heroicons:arrow-down-circle-16-solid" | "heroicons:arrow-down-circle-20-solid" | "heroicons:arrow-down-circle-solid" | "heroicons:arrow-down-left" | "heroicons:arrow-down-left-16-solid" | "heroicons:arrow-down-left-20-solid" | "heroicons:arrow-down-left-solid" | "heroicons:arrow-down-on-square" | "heroicons:arrow-down-on-square-16-solid" | "heroicons:arrow-down-on-square-20-solid" | "heroicons:arrow-down-on-square-solid" | "heroicons:arrow-down-on-square-stack" | "heroicons:arrow-down-on-square-stack-16-solid" | "heroicons:arrow-down-on-square-stack-20-solid" | "heroicons:arrow-down-on-square-stack-solid" | "heroicons:arrow-down-right" | "heroicons:arrow-down-right-16-solid" | "heroicons:arrow-down-right-20-solid" | "heroicons:arrow-down-right-solid" | "heroicons:arrow-down-solid" | "heroicons:arrow-down-tray" | "heroicons:arrow-down-tray-16-solid" | "heroicons:arrow-down-tray-20-solid" | "heroicons:arrow-down-tray-solid" | "heroicons:arrow-left" | "heroicons:arrow-left-16-solid" | "heroicons:arrow-left-20-solid" | "heroicons:arrow-left-circle" | "heroicons:arrow-left-circle-16-solid" | "heroicons:arrow-left-circle-20-solid" | "heroicons:arrow-left-circle-solid" | "heroicons:arrow-left-end-on-rectangle" | "heroicons:arrow-left-end-on-rectangle-16-solid" | "heroicons:arrow-left-end-on-rectangle-20-solid" | "heroicons:arrow-left-end-on-rectangle-solid" | "heroicons:arrow-left-on-rectangle" | "heroicons:arrow-left-on-rectangle-20-solid" | "heroicons:arrow-left-on-rectangle-solid" ...
myPlugin = () => function definePlugin(options: StudioCMSPluginDef): StudioCMSPluginDef
Defines a plugin for StudioCMS.
definePlugin({ identifier: string
identifier: 'my-plugin', name: string
name: 'My Plugin', hooks: { "studiocms:astro-config"?: ((args: { readonly logger: AstroIntegrationLogger; readonly addIntegrations: (args: AstroIntegration | AstroIntegration[]) => Promise<void>; }) => Promise<void>) | ((args: { readonly logger: AstroIntegrationLogger; readonly addIntegrations: (args: AstroIntegration | AstroIntegration[]) => Promise<void>; }) => void) | undefined; ... 5 more ...; "studiocms:sitemap"?: ((args: { readonly logger: AstroIntegrationLogger; readonly setSitemap: (args: { triggerSitemap?: boolean | undefined; sitemaps?: readonly { readonly pluginName: string; readonly sitemapXMLEndpointPath: string; }[] | undefined; }) => Promise<void>; }) => Promise<void>) | ((args: { readonly logger: AstroIntegrationLogger; readonly setSitemap: (args: { triggerSitemap?: boolean | undefined; sitemaps?: readonly { readonly pluginName: string; readonly sitemapXMLEndpointPath: string; }[] | undefined; }) => Promise<void>; }) => void) | undefined;}
hooks: { 'studiocms:rendering': ({ setRendering: (args: { pageTypes?: readonly { readonly identifier: string; readonly label: string; readonly description?: string | undefined; readonly fields?: readonly ({ readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; readonly input: "checkbox"; readonly defaultChecked?: boolean | undefined; readonly size?: "sm" | "md" | "lg" | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly placeholder?: string | undefined; readonly input: "input"; readonly type?: "number" | "text" | "password" | "email" | "tel" | "url" | "search" | undefined; readonly defaultValue?: string | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly placeholder?: string | undefined; readonly input: "textarea"; readonly defaultValue?: string | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; readonly input: "radio"; readonly options: { readonly value: string; readonly label: string; readonly disabled?: boolean | undefined; }[]; readonly defaultValue?: string | undefined; readonly direction?: "horizontal" | "vertical" | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; readonly input: "select"; readonly options: { readonly value: string; readonly label: string; readonly disabled?: boolean | undefined; }[]; readonly type?: "search" | "basic" | undefined; readonly defaultValue?: string | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly fields: readonly ({ readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; readonly input: "checkbox"; readonly defaultChecked?: boolean | undefined; readonly size?: "sm" | "md" | "lg" | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly placeholder?: string | undefined; readonly input: "input"; readonly type?: "number" | "text" | "password" | "email" | "tel" | "url" | "search" | undefined; readonly defaultValue?: string | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly placeholder?: string | undefined; readonly input: "textarea"; readonly defaultValue?: string | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; readonly input: "radio"; readonly options: { readonly value: string; readonly label: string; readonly disabled?: boolean | undefined; }[]; readonly defaultValue?: string | undefined; readonly direction?: "horizontal" | "vertical" | undefined; } | { readonly ...
setRendering }) => { setRendering: (args: { pageTypes?: readonly { readonly identifier: string; readonly label: string; readonly description?: string | undefined; readonly fields?: readonly ({ readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; readonly input: "checkbox"; readonly defaultChecked?: boolean | undefined; readonly size?: "sm" | "md" | "lg" | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly placeholder?: string | undefined; readonly input: "input"; readonly type?: "number" | "text" | "password" | "email" | "tel" | "url" | "search" | undefined; readonly defaultValue?: string | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly placeholder?: string | undefined; readonly input: "textarea"; readonly defaultValue?: string | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; readonly input: "radio"; readonly options: { readonly value: string; readonly label: string; readonly disabled?: boolean | undefined; }[]; readonly defaultValue?: string | undefined; readonly direction?: "horizontal" | "vertical" | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; readonly input: "select"; readonly options: { readonly value: string; readonly label: string; readonly disabled?: boolean | undefined; }[]; readonly type?: "search" | "basic" | undefined; readonly defaultValue?: string | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly fields: readonly ({ readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; readonly input: "checkbox"; readonly defaultChecked?: boolean | undefined; readonly size?: "sm" | "md" | "lg" | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly placeholder?: string | undefined; readonly input: "input"; readonly type?: "number" | "text" | "password" | "email" | "tel" | "url" | "search" | undefined; readonly defaultValue?: string | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly placeholder?: string | undefined; readonly input: "textarea"; readonly defaultValue?: string | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; readonly input: "radio"; readonly options: { readonly value: string; readonly label: string; readonly disabled?: boolean | undefined; }[]; readonly defaultValue?: string | undefined; readonly direction?: "horizontal" | "vertical" | undefined; } | { readonly ...
setRendering({ pageTypes?: readonly { readonly identifier: string; readonly label: string; readonly description?: string | undefined; readonly fields?: readonly ({ readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; readonly input: "checkbox"; readonly defaultChecked?: boolean | undefined; readonly size?: "sm" | "md" | "lg" | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly placeholder?: string | undefined; readonly input: "input"; readonly type?: "number" | "text" | "password" | "email" | "tel" | "url" | "search" | undefined; readonly defaultValue?: string | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly placeholder?: string | undefined; readonly input: "textarea"; readonly defaultValue?: string | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; readonly input: "radio"; readonly options: { readonly value: string; readonly label: string; readonly disabled?: boolean | undefined; }[]; readonly defaultValue?: string | undefined; readonly direction?: "horizontal" | "vertical" | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; readonly input: "select"; readonly options: { readonly value: string; readonly label: string; readonly disabled?: boolean | undefined; }[]; readonly type?: "search" | "basic" | undefined; readonly defaultValue?: string | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly fields: readonly ({ readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; readonly input: "checkbox"; readonly defaultChecked?: boolean | undefined; readonly size?: "sm" | "md" | "lg" | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly placeholder?: string | undefined; readonly input: "input"; readonly type?: "number" | "text" | "password" | "email" | "tel" | "url" | "search" | undefined; readonly defaultValue?: string | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly placeholder?: string | undefined; readonly input: "textarea"; readonly defaultValue?: string | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; readonly input: "radio"; readonly options: { readonly value: string; readonly label: string; readonly disabled?: boolean | undefined; }[]; readonly defaultValue?: string | undefined; readonly direction?: "horizontal" | "vertical" | undefined; } | { readonly name: string; readonly label: string; readonly required?: boolean | undefined; readonly readOnly?: boolean | undefined; readonly color?: "primary" | "success" | "warning" | "danger" | "info" | "mono" | undefined; readonly input: "select"; readonly options: { readonly value: string; readonly label: string; readonly ...
pageTypes: [ { identifier: string
identifier: 'my-custom-page-type', label: string
label: 'My Custom Page Type', rendererComponent?: string | undefined
rendererComponent: const resolve: (...path: Array<string>) => string
resolve('./components/render.js'), pageContentComponent?: string | undefined
pageContentComponent: const resolve: (...path: Array<string>) => string
resolve('./components/Editor.astro') } ] }) } }});The renderer component
Section titled “The renderer component”For the renderer component, you need to create a JavaScript or TypeScript file (if you have a build step) that exports an object conforming to the PluginRenderer type. This object should include the rendering logic for your custom page type.
import type { (alias) interface PluginRendererimport PluginRenderer
Represents a plugin renderer with optional sanitization and rendering logic.
PluginRenderer } from 'studiocms/types';
const const render: { name: string; renderer: (content: string) => Promise<string>; sanitizeOpts: {};}
render = { PluginRenderer.name: string
name: 'my-custom-renderer', PluginRenderer.renderer?: GenericAsyncFn<string, string>
renderer: async (content: string
content: string) => { // Custom rendering logic goes here return content: string
content; }, PluginRenderer.sanitizeOpts?: SanitizeOptions
sanitizeOpts: {},} satisfies (alias) interface PluginRendererimport PluginRenderer
Represents a plugin renderer with optional sanitization and rendering logic.
PluginRenderer;
export default const render: { name: string; renderer: (content: string) => Promise<string>; sanitizeOpts: {};}
render;The Editor component
Section titled “The Editor component”For the editor component, you need to create an Astro component that provides a user interface for editing the content of your custom page type. This component will receive the current content as a prop and should emit updates to the content as the user makes changes. The <textarea> below is used in the final page edit form, allowing users to edit the content directly. So any changes made in any custom editor component should update the value of this <textarea> to ensure the content is saved correctly.
---import type { PluginPageTypeEditorProps } from 'studiocms/types';
interface Props extends PluginPageTypeEditorProps {}
const { content } = Astro.props;---<div class="editor-container"> <textarea id="page-content" name="page-content">{content}</textarea></div>Further Reading
Section titled “Further Reading”If you want to build a custom frontend for your StudioCMS project, you can use the StudioCMS Renderer component and SDK to render the content from StudioCMS using your custom renderer plugins.