Skip to content

Plugin System ​

Plugins are the primary extension mechanism in Viseed CMS. Each plugin can contribute database tables, API routes, hooks, and admin UI components.


Using Plugins ​

Register plugins via cms.use() before calling cms.launch():

typescript
import { createCMS } from '@viseed/core'
import { authPlugin } from '@viseed/plugin-auth'
import { blogPlugin } from '@viseed/plugin-blog'
import { menuPlugin } from '@viseed/plugin-menu'
import { pagesPlugin } from '@viseed/plugin-pages'

const cms = createCMS({ db: { driver: 'postgres', url: process.env.DATABASE_URL! } })

cms.use(authPlugin())
cms.use(blogPlugin())
cms.use(menuPlugin())
cms.use(pagesPlugin())

const app = await cms.launch()

The order of cms.use() calls determines plugin initialization order.


Built-in Plugins ​

PackageFunctionWhat it provides
@viseed/plugin-authauthPlugin()User authentication, sessions, role management
@viseed/plugin-blogblogPlugin()Blog posts, categories, admin UI
@viseed/plugin-menumenuPlugin()Navigation menus, admin UI, theme menu injection
@viseed/plugin-pagespagesPlugin()Standalone pages, TOC support, admin UI
@viseed/plugin-common-widgetscommonWidgetsPlugin()Built-in widget types (Tabs, Q&A)

Install what you need:

bash
bun add @viseed/plugin-auth @viseed/plugin-blog @viseed/plugin-menu @viseed/plugin-pages

Plugin Lifecycle ​

When cms.launch() is called, plugins go through the following lifecycle in order:

createCMS()
  └── cms.use(plugin)        ← register plugins

cms.launch()
  ├── createDatabase()       ← DB connection established
  ├── plugin.schema          ← all plugin tables merged
  ├── cms:init hook          ← ViseedCMS instance available
  ├── plugin.routes()        ← Hono routes registered
  ├── plugin.admin           ← admin UI bundles registered
  └── cms:ready hook         ← Hono app fully ready

Lifecycle Hooks ​

HookTriggerArguments
cms:initAfter database connection is establishedViseedCMS instance
cms:readyAfter all routes are registeredHono app instance
admin:registerWhen admin UI loadsComponentRegistry
theme:mountWhen a theme is activatedCMSTheme
theme:beforeRenderBefore each page renderlayoutKey, data, requestContext
theme:activateWhen admin switches the active themeCMSTheme, previous CMSTheme
plugin:enabledWhen a plugin is enabled via adminpluginName
plugin:disabledWhen a plugin is disabled via adminpluginName

Writing a Custom Plugin ​

typescript
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { CMSPlugin } from '@viseed/types'
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core'

const __dirname = dirname(fileURLToPath(import.meta.url))

export function myPlugin(): CMSPlugin {
  return {
    name: 'my-plugin',
    version: '1.0.0',

    // Optional: Drizzle table definitions
    schema: {
      notes: pgTable('my_notes', {
        id: text('id').primaryKey(),
        content: text('content').notNull(),
        createdAt: timestamp('created_at').defaultNow().notNull(),
      }),
    },

    // Optional: lifecycle hooks
    hooks: {
      'cms:init': async (cms) => {
        console.log('my-plugin initializing...')
      },
      'cms:ready': async (app) => {
        console.log('CMS ready, routes are live')
      },
    },

    // Optional: Hono routes
    routes: (app, helpers) => {
      app.get('/api/my-plugin/hello', (c) => c.json({ message: 'Hello!' }))
    },
  }
}

Plugin Schema ​

Plugin tables are passed directly as Drizzle table definitions. The CLI will auto-discover them when the plugin is installed as a @viseed/plugin-* package.

Plugin Routes ​

The routes function receives the Hono app and a helpers object with context utilities:

typescript
routes: (app, helpers) => {
  app.get('/api/my-plugin/items', async (c) => {
    const { site, db } = helpers.resolveRequestContext(c)
    // site — current site (multi-site support)
    // db   — Drizzle database instance
    return c.json({ items: [] })
  })
}

Admin UI ​

To add admin UI pages, compile a Vue component bundle and reference it from the plugin.

Each admin sidebar entry is declared as a PluginAdminMenuItem:

typescript
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import type { CMSPlugin } from '@viseed/types'

const __dirname = dirname(fileURLToPath(import.meta.url))

export function myPlugin(): CMSPlugin {
  return {
    name: 'my-plugin',
    version: '1.0.0',
    admin: {
      menuItems: [
        {
          id: 'my-plugin',           // unique sidebar id
          label: 'My Plugin',        // sidebar label
          icon: '🔌',                // sidebar icon (emoji or text)
          path: '/my-plugin',        // admin route path
          order: 50,                 // lower = higher in sidebar (default 50)
          siteScoped: true,          // prefix path with /:siteId when true
          requiredPermissions: ['site.my-plugin.read'],
          componentExport: 'MyView', // named export in the admin bundle
        },
      ],
      bundlePath: resolve(__dirname, '../dist/admin/index.js'),
    },
  }
}

bundlePath must be the absolute filesystem path to the compiled admin ESM bundle. Use resolve(__dirname, ...) — not new URL(...).pathname — to keep it cross-platform.

Admin bundle structure ​

The admin bundle is a standard Vite library build. The entry file (src/admin/index.ts) must named-export every Vue component referenced by componentExport in the menu items:

typescript
// src/admin/index.ts
export { default as MyView } from './MyView.vue'

Building the admin bundle ​

Create build-admin.ts at the plugin root and use the helper from @viseed/core/build:

typescript
// build-admin.ts
import { buildPluginAdmin } from '@viseed/core/build'

await buildPluginAdmin()

buildPluginAdmin() pre-configures Vite with:

  • Entry: src/admin/index.ts
  • Output: dist/admin/index.js (ESM, CSS injected via JS)
  • Externals: vue, vue-router

Add a script to package.json:

json
{
  "scripts": {
    "build": "bunup && bun run build-admin.ts",
    "build:admin": "bun run build-admin.ts"
  }
}

To override any default (e.g. a custom entry path):

typescript
await buildPluginAdmin({ entry: 'src/admin/main.ts' })

Plugin CMSPlugin Interface ​

typescript
interface CMSPlugin {
  name: string
  version: string
  schema?: Record<string, unknown>       // Drizzle tables
  hooks?: Partial<CMSPluginHooks>        // Lifecycle hooks
  routes?: (app: Hono, helpers: CMSRouteContextHelpers) => void
  lifecycle?: PluginLifecycle
  admin?: {
    menuItems: PluginAdminMenuItem[]
    bundlePath?: string                  // absolute path to compiled ESM bundle
  }
  widgets?: WidgetTypeDef[]              // widget types this plugin contributes
  public?: {
    bundlePath?: string                  // absolute path to compiled public ESM bundle
  }
}

interface PluginAdminMenuItem {
  id: string
  label: string
  icon: string
  path: string                           // admin route path, e.g. '/my-plugin'
  order?: number                         // lower = higher in sidebar; default 50
  siteScoped?: boolean
  requiredPermissions?: string[]
  componentExport?: string               // named export in admin bundle; derived from path if omitted
}