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¶
- JavaScript/TypeScript knowledge
- Understanding of client config and server config
- Basic npm knowledge for packaging
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:
Users install with:
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:
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:
- Plugin is loaded in config file
- Plugin exports correct structure with
nameandinit - No syntax errors in plugin code
- 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:
- Change load order
- Check plugin source code for overrides
- 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¶
- Using existing plugins
- Client configuration and plugins — Client config API
- Server configuration — Server config API
- GrapesJS plugin development — Official docs