Docs Plugin Development

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
Modern JavaScript: The Jint 4.6 engine supports nearly all ES2015–ES2025 features including 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:

FieldRequiredDescription
nameYesUnique plugin identifier (kebab-case)
versionYesSemantic version (e.g. 1.0.0)
descriptionYesBrief description of what the plugin does
authorNoPlugin author name
hooksYesJSON 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().

HookWhen It FiresContext 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:

PropertyTypeDescription
context.documentDOM-like objectA DOM document abstraction you can manipulate
context.titlestringPost title
context.slugstringPost URL slug
context.contentstringRaw HTML content of the post body
context.metaTitlestringSEO meta title (falls back to title)
context.metaDescriptionstringSEO meta description
context.tagsarrayArray of tag strings
context.excerptstringPost excerpt text
context.publishedAtstringISO date string of publication
context.authorNamestringAuthor 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

  1. Navigate to Admin → Plugins
  2. Click the "Install Plugin" button
  3. Paste your plugin manifest JSON (name, slug, version, author, description, hooks)
  4. Paste your JavaScript entry point code
  5. 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:

  1. Admin → Plugins → click the gear icon on your plugin
  2. Edit the JSON settings and save
  3. 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:

PluginHookDescription
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 = '&uarr;';
        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

  1. Go to Admin → Plugins → Install Plugin
  2. Enter the manifest JSON: {"name":"Back to Top","slug":"back-to-top","version":"1.0.0","hooks":["post:render"]}
  3. Paste the full JavaScript code above into the entry point field
  4. 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, or require
  • No process access — No process, child_process, or eval of arbitrary code
  • Timeout limits — Plugins that take too long are terminated
  • Memory limits — Excessive memory allocation is prevented
  • Scoped globals — Only the contento API object and standard JavaScript built-ins are available
Client-side scripts: While the plugin sandbox restricts server-side execution, plugins can inject <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:

SymptomCauseFix
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
Tip: During development, use 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.

ParameterTypeDescription
hookstringThe hook name (e.g. "post:render")
callbackfunctionFunction that receives the context object

Management API Endpoints

MethodEndpointDescription
GET/api/v1/pluginsList all installed plugins
POST/api/v1/plugins/installInstall a new plugin
GET/api/v1/plugins/{id}Get plugin details
POST/api/v1/plugins/{id}/enableEnable a plugin
POST/api/v1/plugins/{id}/disableDisable a plugin
PUT/api/v1/plugins/{id}/settingsUpdate plugin settings
DELETE/api/v1/plugins/{id}Uninstall a plugin