Plugin Development
Extend Contento with custom JavaScript plugins that run in a secure Jint sandbox.
Overview
Contento's plugin system lets you extend the CMS without modifying core code. Plugins are written in modern JavaScript (ES2025) and executed server-side in a Jint 4.6 sandbox — a JavaScript interpreter for .NET that provides safe, isolated execution with full support for arrow functions, const/let, template literals, classes, destructuring, async/await, optional chaining, and more.
Plugins can:
- Modify rendered HTML — inject meta tags, progress bars, share buttons, analytics scripts
- Add SEO enhancements — Open Graph tags, JSON-LD structured data, canonical URLs
- Inject UI components — contact forms, comment widgets, CTA blocks
- Transform content — syntax highlighting, image lazy loading, link processing
let/const, arrow functions, template literals, classes, destructuring, spread/rest, async/await, optional chaining (?.), nullish coalescing (??), and more. Write your plugins using modern, idiomatic JavaScript.
Plugin Structure
A plugin is a single JavaScript file with comment-based metadata at the top and an IIFE (Immediately Invoked Function Expression) wrapping all code:
// name: "my-plugin" // version: "1.0.0" // description: "A short description of what this plugin does." // author: "Your Name" // hooks: ["post:render"] (() => { contento.on('post:render', (context) => { const { document: doc } = context; if (!doc) return; // Your plugin logic here }); })();
Metadata Comments
The comment header at the top of the file is parsed by the plugin system to extract metadata:
| Field | Required | Description |
|---|---|---|
name | Yes | Unique plugin identifier (kebab-case) |
version | Yes | Semantic version (e.g. 1.0.0) |
description | Yes | Brief description of what the plugin does |
author | No | Plugin author name |
hooks | Yes | JSON array of hooks the plugin registers for |
Available Hooks
Hooks are events that the Contento system fires at specific points. Plugins register for hooks using contento.on().
| Hook | When It Fires | Context Object |
|---|---|---|
post:render |
After a post is rendered to HTML, before sending to the browser | Full post data + DOM document |
post:save |
When a post is saved in the editor | Post data before persistence |
comment:submit |
When a comment is submitted | Comment data |
The Context Object
Each hook receives a context object with data relevant to the event. For post:render (the most common hook), the context includes:
| Property | Type | Description |
|---|---|---|
context.document | DOM-like object | A DOM document abstraction you can manipulate |
context.title | string | Post title |
context.slug | string | Post URL slug |
context.content | string | Raw HTML content of the post body |
context.metaTitle | string | SEO meta title (falls back to title) |
context.metaDescription | string | SEO meta description |
context.tags | array | Array of tag strings |
context.excerpt | string | Post excerpt text |
context.publishedAt | string | ISO date string of publication |
context.authorName | string | Author display name |
DOM Manipulation API
The context.document object provides a subset of browser DOM APIs. You can create elements, query selectors, and modify the page structure:
Available Methods
const doc = context.document; // Query elements const el = doc.querySelector('#my-element'); const els = doc.getElementsByTagName('h2'); // Create elements const div = doc.createElement('div'); div.id = 'my-widget'; div.setAttribute('class', 'my-class'); div.innerHTML = '<p>Hello!</p>'; // Append to page doc.body.appendChild(div); doc.head.appendChild(style); // Insert before another element parent.insertBefore(newChild, referenceChild); // Access common properties doc.body; // <body> element doc.head; // <head> element el.firstChild; // first child node el.textContent; // text content
Injecting Styles
The most common pattern is creating a <style> element and appending it to <head>:
const style = doc.createElement('style'); style.textContent = ` #my-widget { position: fixed; bottom: 20px; right: 20px; background: #3D5A80; color: white; padding: 12px 16px; border-radius: 8px; z-index: 9999; } `; doc.head.appendChild(style);
Injecting Scripts
You can add client-side JavaScript via <script> elements. This is useful for interactive widgets:
const script = doc.createElement('script'); script.textContent = ` (() => { document.getElementById('my-btn').addEventListener('click', () => { alert('Clicked!'); }); })(); `; doc.body.appendChild(script);
Installing Plugins
There are two ways to install plugins in Contento:
Via the Admin UI
- Navigate to Admin → Plugins
- Click the "Install Plugin" button
- Paste your plugin manifest JSON (name, slug, version, author, description, hooks)
- Paste your JavaScript entry point code
- Click "Parse" to validate, then "Install"
Via the API
curl -X POST https://your-site.com/api/v1/plugins/install \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "name": "My Plugin", "slug": "my-plugin", "version": "1.0.0", "author": "Your Name", "description": "Does something useful", "entryPoint": "(() => { contento.on('post:render', (ctx) => { /* ... */ }); })();", "permissions": ["post:render"], "settings": "{\"color\":\"#3D5A80\"}" }'
Plugin Settings
Plugins can have a settings JSON object that administrators can modify after installation. Access settings in your plugin code through the context or the admin settings panel:
- Admin → Plugins → click the gear icon on your plugin
- Edit the JSON settings and save
- Settings are passed to your plugin at execution time
// Update settings via API curl -X PUT https://your-site.com/api/v1/plugins/{id}/settings \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "settings": "{\"color\":\"#DC2626\",\"height\":\"4px\"}" }'
Built-in Plugins
Contento ships with 5 built-in plugins that you can use as reference implementations:
| Plugin | Hook | Description |
|---|---|---|
| Reading Progress | post:render |
Adds a gradient progress bar at the top of the page that tracks scroll position with requestAnimationFrame throttling |
| SEO Meta | post:render |
Injects Open Graph tags, Twitter Card meta, and JSON-LD Article schema into <head>. Uses a setMeta helper to avoid duplicates |
| Social Share | post:render |
Adds a floating share toolbar with Twitter, Facebook, LinkedIn, and copy-to-clipboard buttons. Responsive with mobile/desktop layouts |
| SEO Analyzer | post:render |
Scores posts for SEO quality (title, meta, slug, headings, keywords) and readability (Flesch-Kincaid approximation). Shows a floating widget with pass/fail checks |
| Contact Form | post:render |
Renders a styled contact form into a <div id="contento-contact-form"> placeholder. Includes client-side validation and honeypot spam protection |
Example: Building a Plugin
Let's build a "Back to Top" button plugin from scratch.
Step 1: Create the file
// name: "back-to-top" // version: "1.0.0" // description: "Adds a floating back-to-top button that appears after scrolling down." // author: "Your Name" // hooks: ["post:render"] (() => { contento.on('post:render', (context) => { const { document: doc } = context; if (!doc) return; const body = doc.body ?? doc.getElementsByTagName('body')[0]; const head = doc.head ?? doc.getElementsByTagName('head')[0]; if (!body || !head) return; // Create the button const btn = doc.createElement('button'); btn.id = 'back-to-top'; btn.setAttribute('aria-label', 'Back to top'); btn.innerHTML = '↑'; body.appendChild(btn); // Add styles const style = doc.createElement('style'); style.textContent = ` #back-to-top { position: fixed; bottom: 24px; right: 24px; width: 44px; height: 44px; border-radius: 50%; background: #3D5A80; color: white; border: none; font-size: 18px; cursor: pointer; opacity: 0; transition: opacity 0.3s; z-index: 9999; pointer-events: none; } #back-to-top.visible { opacity: 1; pointer-events: auto; } `; head.appendChild(style); // Add scroll behavior and click handler const script = doc.createElement('script'); script.textContent = ` (() => { const btn = document.getElementById('back-to-top'); window.addEventListener('scroll', () => { btn.classList.toggle('visible', window.scrollY > 300); }, { passive: true }); btn.addEventListener('click', () => { window.scrollTo({ top: 0, behavior: 'smooth' }); }); })(); `; body.appendChild(script); }); })();
Step 2: Install via Admin UI
- Go to Admin → Plugins → Install Plugin
- Enter the manifest JSON:
{"name":"Back to Top","slug":"back-to-top","version":"1.0.0","hooks":["post:render"]} - Paste the full JavaScript code above into the entry point field
- Click Parse, then Install
Step 3: Test
Navigate to any published post and scroll down. The button should appear after scrolling past 300px.
Security Model
The Jint sandbox provides several layers of protection:
- No file system access — Plugins cannot read or write files on the server
- No network access — No
fetch,XMLHttpRequest, orrequire - No process access — No
process,child_process, orevalof arbitrary code - Timeout limits — Plugins that take too long are terminated
- Memory limits — Excessive memory allocation is prevented
- Scoped globals — Only the
contentoAPI object and standard JavaScript built-ins are available
<script> tags that run in the browser with full browser API access. Be mindful of what client-side JavaScript your plugins inject.
Best Practices
Always wrap in an IIFE
Prevents variable leakage between plugins:
(() => {
// All your code here
})();
Guard against missing data
Always check that DOM elements and context properties exist before using them:
const doc = context.document; if (!doc) return; const body = doc.body ?? doc.getElementsByTagName('body')[0]; if (!body) return;
Avoid duplicate injection
Check if your elements already exist before injecting:
if (doc.querySelector('#my-widget')) return; // Already injected
Use template literals for multi-line strings
Template literals make HTML generation much cleaner:
const title = context.title; const html = ` <div class="widget"> <h3>${title}</h3> <p>Content</p> </div> `;
Namespace your CSS
Prefix all CSS selectors with your plugin name to avoid conflicts:
const css = ` #my-plugin-widget { ... } .my-plugin-btn { ... } .my-plugin-overlay { ... } `;
Debugging Plugins
If a plugin fails to execute, check these common issues:
| Symptom | Cause | Fix |
|---|---|---|
| Plugin silently does nothing | Syntax error in plugin code | Check the server logs for Jint parse errors |
| Plugin fails intermittently | Missing null checks on context.document |
Add guard clauses at the top |
| "timeout" error | Infinite loop or excessive computation | Optimize logic, add loop limits |
| Styles not applying | CSS specificity conflict with existing styles | Use more specific selectors or !important |
| Plugin not found after install | Plugin is disabled by default | Enable it in Admin → Plugins |
console.log() in your injected client-side scripts (inside <script> tags) to debug in the browser's developer console. The Jint sandbox does not have a console, so server-side logging is not available in plugin code.
Plugin API Reference
contento.on(hook, callback)
Register a callback function for a specific hook event.
| Parameter | Type | Description |
|---|---|---|
hook | string | The hook name (e.g. "post:render") |
callback | function | Function that receives the context object |
Management API Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/plugins | List all installed plugins |
POST | /api/v1/plugins/install | Install a new plugin |
GET | /api/v1/plugins/{id} | Get plugin details |
POST | /api/v1/plugins/{id}/enable | Enable a plugin |
POST | /api/v1/plugins/{id}/disable | Disable a plugin |
PUT | /api/v1/plugins/{id}/settings | Update plugin settings |
DELETE | /api/v1/plugins/{id} | Uninstall a plugin |