Skip to content

Creating plugins

Write custom plugins to extend Silex with new features.

Overview

A Silex plugin is a JavaScript module that follows the plugin pattern: it exports an object with an init function. Plugins can be client-side (editor extensions), server-side (connectors and routes), or both.

Plugin structure:

// my-plugin.js
export default {
  name: 'MyPlugin',
  async init(editor, options) {
    console.log('Plugin initialized with options:', options)
  },
}

Plugins receive: - Client plugin: (editor, options) — the GrapesJS editor instance - Server plugin: (config, options) — the ServerConfig instance

Licensing: Plugins are not considered derivative works of Silex under the AGPL license. You can use any license for your plugins.

Contributing to Silex core: If you want to fix bugs or add features to Silex itself (not just plugins), see the contribution guide for setup instructions and good first issues.

Prerequisites

Client-side plugins

Extend the GrapesJS editor with custom UI, blocks, or commands.

Basic structure

// plugins/my-editor-plugin.js
export default {
  name: 'MyEditorPlugin',
  async init(editor, options) {
    console.log('Editor plugin initialized')

    // Access the editor
    const canvas = editor.Canvas
    const blocks = editor.Blocks
    const commands = editor.Commands

    // Add a custom block
    blocks.add('my-block', {
      label: 'My Custom Block',
      content: '<div class="my-block">Custom content</div>',
      category: 'Custom',
      media: '<svg>...</svg>',
      attributes: {
        class: 'gjs-fonts gjs-f-h1',
      },
    })

    // Add a custom command
    commands.add('my-command', {
      run(editor) {
        console.log('Command executed')
      },
      stop(editor) {
        console.log('Command stopped')
      },
    })

    // Listen to editor events
    editor.on('component:create', (component) => {
      console.log('Component created:', component)
    })
  },
}

Adding UI panels

Add a custom panel to the editor:

export default {
  name: 'MyPanel',
  async init(editor, options) {
    const pnm = editor.Panels

    // Create a new panel
    pnm.addPanel({
      id: 'my-panel',
      buttons: [
        {
          id: 'my-button',
          label: 'My Button',
          command: 'my-command',
          className: 'btn-icon-blank',
        },
      ],
    })

    // Add button to existing toolbar
    const topPanel = pnm.getPanel('options')
    topPanel.buttons.add([
      {
        id: 'another-button',
        label: 'Another Button',
        active: false,
        togglable: true,
        command: 'another-command',
      },
    ])
  },
}

Interacting with selected elements

export default {
  name: 'ElementModifier',
  async init(editor, options) {
    editor.Commands.add('bold-selected', {
      run(editor) {
        const selected = editor.getSelected()
        if (selected) {
          selected.addClasses('font-weight-bold')
        }
      },
    })
  },
}

Loading in client config

// client-config.js
import myEditorPlugin from './plugins/my-editor-plugin'

export default async function (config) {
  config.grapesJsConfig.plugins = [
    ...config.grapesJsConfig.plugins,
    myEditorPlugin,
  ]

  config.grapesJsConfig.pluginsOpts = {
    ...config.grapesJsConfig.pluginsOpts,
    MyEditorPlugin: {
      customOption: 'value',
    },
  }
}

Server-side plugins

Extend the backend with connectors, routes, or event listeners.

Basic structure

// plugins/my-server-plugin.js
module.exports = {
  name: 'MyServerPlugin',
  async init(config, options) {
    console.log('Server plugin initialized with options:', options)

    // Add routes to the Express app
    // config.app is available after plugins are loaded
  },
}

Adding connectors

Create a custom storage or hosting connector:

// plugins/my-connector.js
const { Connector, StorageConnector, ConnectorType } = require('@silexlabs/silex-plugins')

class MyStorageConnector extends StorageConnector {
  connectorId = 'my-storage'
  displayName = 'My Storage'
  connectorType = ConnectorType.STORAGE
  color = '#ff0000'
  background = '#ffeeee'
  icon = 'data:image/svg+xml,...'

  async isLoggedIn(session) {
    // Check if user is logged in
    return !!session.myStorageToken
  }

  async getOAuthUrl(session) {
    // Return OAuth login URL
    return `https://my-service.com/oauth?client_id=${this.options.clientId}`
  }

  async listWebsites(session) {
    // Return array of websites
    return []
  }

  async createWebsite(session, data) {
    // Create a new website
    return 'website-id'
  }

  // ... implement other StorageConnector methods
}

module.exports = {
  name: 'MyStorageConnectorPlugin',
  async init(config, options) {
    config.addStorageConnector(
      new MyStorageConnector(config, {
        clientId: options.clientId,
        clientSecret: options.clientSecret,
      })
    )
  },
}

Adding Express routes

module.exports = {
  name: 'MyApiPlugin',
  async init(config, options) {
    // Note: config.addRoutes is called after plugin init
    // Store the route setup for later
    const originalAddRoutes = config.addRoutes.bind(config)

    config.addRoutes = async (app) => {
      await originalAddRoutes(app)

      // Add custom routes
      app.get('/api/my-endpoint', (req, res) => {
        res.json({ message: 'Hello from my plugin' })
      })

      app.post('/api/my-endpoint', (req, res) => {
        const data = req.body
        // Process data
        res.json({ success: true })
      })
    }
  },
}

Listening to server events

const { ServerEvent } = require('@silexlabs/silex/dist/server/events')

module.exports = {
  name: 'MyLoggerPlugin',
  async init(config, options) {
    config.on(ServerEvent.STARTUP_END, () => {
      console.log(`Silex is running at ${config.url}`)
    })

    config.on(ServerEvent.PUBLISH_START, async (publicationData) => {
      console.log(`Publishing: ${publicationData.siteSettings.name}`)
    })

    config.on(ServerEvent.PUBLISH_END, async (error) => {
      if (error) {
        console.error(`Publication failed: ${error.message}`)
      } else {
        console.log('Publication succeeded')
      }
    })
  },
}

Loading in server config

// .silex.js
const myServerPlugin = require('./plugins/my-server-plugin')

module.exports = async function (config) {
  await config.addPlugin(myServerPlugin, {
    apiKey: process.env.MY_API_KEY,
    debug: config.debug,
  })
}

Publication transformers

Customize how websites are compiled and published. See Publication transformers for details.

Example publication transformer plugin:

// plugins/minify-plugin.js
export default {
  name: 'MinifyPlugin',
  async init(config, options) {
    config.addPublicationTransformers({
      transformFile(file) {
        // Minify HTML files
        if (file.type === 'html') {
          // Use a minification library
          const minified = minifyHtml(file.content)
          return { ...file, content: minified }
        }
        return file
      },

      transformPath(path, type) {
        // Change .css to .min.css
        if (type === 'css') {
          return path.replace(/\.css$/, '.min.css')
        }
        return path
      },
    })
  },
}

Load in client config:

import minifyPlugin from './plugins/minify-plugin'

export default async function (config) {
  config.grapesJsConfig.plugins.push(minifyPlugin)
}

Full-stack plugins

Plugins can run on both client and server:

// plugins/full-stack-plugin.js

// Export two different modules
module.exports.client = {
  name: 'MyPlugin-Client',
  async init(editor, options) {
    // Client-side code
    editor.Commands.add('my-command', { /* ... */ })
  },
}

module.exports.server = {
  name: 'MyPlugin-Server',
  async init(config, options) {
    // Server-side code
    config.addStorageConnector(/* ... */)
  },
}

Load separately:

// client-config.js
import { client } from './plugins/full-stack-plugin'
config.grapesJsConfig.plugins.push(client)

// .silex.js
const { server } = require('./plugins/full-stack-plugin')
await config.addPlugin(server, {})

Packaging plugins

For npm publication

Create a package structure:

my-silex-plugin/
├── package.json
├── dist/
│   ├── client.js
│   ├── server.js
│   └── index.js
├── src/
│   ├── client.js
│   ├── server.js
│   └── index.js
└── README.md

package.json:

{
  "name": "my-silex-plugin",
  "version": "1.0.0",
  "main": "dist/index.js",
  "files": ["dist"],
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch"
  },
  "peerDependencies": {
    "@silexlabs/silex": "^3.0.0"
  }
}

Publish to npm:

npm publish

Users install with:

npm install my-silex-plugin

For private use

Keep plugins in your Silex instance directory:

my-silex/
├── .silex.js
├── client-config.js
└── plugins/
    ├── my-storage-connector.js
    ├── my-editor-plugin.js
    └── my-transformer.js

Load with relative paths:

// .silex.js
const myConnector = require('./plugins/my-storage-connector')
await config.addPlugin(myConnector, {})

// client-config.js
import myEditorPlugin from './plugins/my-editor-plugin'

TypeScript support

Write plugins in TypeScript for better IDE support:

// plugins/my-plugin.ts
import { Editor, EditorConfig } from 'grapesjs'

interface MyPluginOptions {
  apiKey: string
  debug?: boolean
}

export default {
  name: 'MyPlugin',
  async init(editor: Editor, options: MyPluginOptions) {
    // Fully typed
    editor.Blocks.add('my-block', { /* ... */ })
  },
}

Compile to JavaScript:

tsc --target es2020 --module commonjs plugins/*.ts

Testing plugins

Test with Jest or another test runner:

// my-plugin.test.js
describe('MyPlugin', () => {
  it('should initialize', async () => {
    const mockEditor = {
      Commands: { add: jest.fn() },
      Blocks: { add: jest.fn() },
    }

    const plugin = require('./my-plugin')
    await plugin.init(mockEditor, { })

    expect(mockEditor.Commands.add).toHaveBeenCalled()
  })
})

Troubleshooting

Plugin not initializing

Check:

  1. Plugin is loaded in config file
  2. Plugin exports correct structure with name and init
  3. No syntax errors in plugin code
  4. Options are passed correctly

Enable debug:

export default {
  name: 'MyPlugin',
  async init(editor, options) {
    console.log('Plugin init called with:', options)
  },
}

Plugin conflicts with GrapesJS

Some plugins modify GrapesJS core behavior. If conflicts occur:

  1. Change load order
  2. Check plugin source code for overrides
  3. Use a different plugin or patch conflicts

Server plugin routes not accessible

Ensure routes are added in config.addRoutes:

config.addRoutes = async (app) => {
  await originalAddRoutes(app)
  app.get('/my-route', (req, res) => { /* ... */ })
}

Routes added outside this function may not work.

See also

Edit this page on GitLab