Shiply CMS Plugin Guide

How To Create A Simple Shiply Plugin

This guide will walk you through the process from start to finish building a simple plugin

At its core, a plugin is simply a folder inside:

shiply_cms_content/plugins

When Shiply CMS loads, it looks for a plugin.json file, reads the plugin information, loads the main PHP class from Plugin.php, and then handles any frontend assets you've registered.

Depending on what you're building, Shiply CMS can:

  • Load JavaScript and CSS files
  • Render shortcodes
  • Create admin pages
  • Handle AJAX requests
  • Store and retrieve plugin settings
  • Create database tables when needed

Thinking About Plugins the Simple Way

One thing I've noticed after building plugins for years is that developers often overcomplicate the architecture before they've written a single line of code.

In reality, most plugins fit into three areas.

Frontend

Everything visitors see.

  • JavaScript
  • CSS
  • HTML output
  • Shortcodes

AJAX

Requests sent from the browser.

  • Saving data
  • Loading data
  • Forms
  • Live updates

Backend

Everything happening behind the scenes.

  • Database operations
  • Validation
  • Plugin settings
  • API responses

A Simple Plugin Structure

Most developers start by creating a huge folder structure because they're thinking about future features.

I usually recommend starting small.

shiply_cms_content/
└── plugins/
    └── my-plugin/
        ├── plugin.json
        ├── Plugin.php
        └── assets/
            ├── frontend.html
            ├── my-plugin.css
            └── my-plugin.js
That's all you need to build your first Shiply CMS plugin.

Naming Conventions That Save Headaches Later

Item Example
Folder my-plugin
Slug my-plugin
Main File Plugin.php
Class MyPlugin

Folder Names

Use lowercase words separated by hyphens.

Good
  • my-plugin
  • video-resizer
  • geo-location
Avoid
  • MyPlugin
  • my_plugin
  • My Plugin

Class Names

class MyPlugin
{
}

The plugin.json File

This file tells Shiply CMS what your plugin is and how it should be loaded.

{
  "slug": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "description": "Adds a custom feature to Shiply CMS.",
  "author": "Your Name",
  "author_url": "https://example.com",

  "frontend_scripts": [
    "/assets/my-plugin.js"
  ],

  "frontend_styles": [
    "/assets/my-plugin.css"
  ],

  "admin_menu": {
    "parent": "plugins",
    "label": "My Plugin",
    "icon": "fa-plug",
    "route": "/my-plugin",
    "position": 40
  },

  "admin_url": "/my-plugin/admin",

  "main": "Plugin.php",
  "class": "MyPlugin",

  "shortcode": {
    "code": "",
    "label": "My Plugin",
    "description": "Render My Plugin output."
  }
}
You don't need every field.
Only add what your plugin actually uses.

The Fields You'll Use Most Often

frontend_scripts

Loads JavaScript on public pages.

"frontend_scripts": ["/assets/my-plugin.js"]
frontend_styles

Loads CSS on public pages.

"frontend_styles": ["/assets/my-plugin.css"]
admin_menu

Adds a menu item inside the admin panel.

admin_url

Points Shiply CMS to your admin page.

main / class

Tells Shiply CMS which PHP file and class should be loaded.

Your First Plugin Should Be Boring

Whenever someone asks me where to start, I usually recommend building a frontend-only plugin first.

No database tables.

No AJAX requests.

No settings pages.

Just load some JavaScript and make something happen.

One simple example is automatically adding UTM tracking parameters to outbound links.

Example Plugin Structure

external-utm-links/
├── plugin.json
├── Plugin.php
└── assets/
    └── external-utm-links.js

plugin.json

{
  "slug": "external-utm-links",
  "name": "External UTM Links",
  "version": "1.0.0",
  "description": "Adds a utm_source parameter to external links when a page loads.",
  "author": "Your Name",
  "author_url": "https://example.com",
  "frontend_scripts": [
    "/assets/external-utm-links.js"
  ],
  "main": "Plugin.php",
  "class": "ExternalUtmLinksPlugin"
}

JavaScript Example

This script scans all links on the page and automatically adds a utm_source parameter to external links.

(function () {
    'use strict';

    function isExternalUrl(url) {
        return (url.protocol === 'http:' || url.protocol === 'https:')
            && url.origin !== window.location.origin;
    }

    function addUtmSource(anchor) {
        var rawHref = anchor.getAttribute('href') || '';

        if (!rawHref) {
            return;
        }

        try {
            var url = new URL(rawHref, window.location.href);

            if (isExternalUrl(url) && !url.searchParams.has('utm_source')) {
                url.searchParams.set('utm_source', window.location.hostname);
                anchor.setAttribute('href', url.toString());
            }
        } catch (error) {
            // Ignore invalid links.
        }
    }

    function scanLinks() {
        document.querySelectorAll('a[href]').forEach(addUtmSource);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', scanLinks);
    } else {
        scanLinks();
    }
})();

Plugin.php

<?php

class ExternalUtmLinksPlugin
{
    public function register(PluginContext $plugin): void
    {
        // Nothing special needed.
        // Shiply CMS automatically loads
        // frontend assets declared in plugin.json.
    }
}

Activating the Plugin

  1. Go to Plugins inside Shiply CMS.
  2. Activate the plugin.
  3. Visit a page on your website.
  4. Open DevTools and inspect an external link.

You should see the utm_source parameter automatically added to outbound links.

Create A Plugin with AJAX and Backend Storage

In this guide, we'll build a complete Simple Contact Form plugin.

The plugin will:

  • Display a frontend contact form with a shortcode

  • Submit data using AJAX

  • Save submissions to a database table

  • Send email notifications

  • Provide an admin settings page

  • Store plugin settings

By the end, you'll have a complete plugin structure you can reuse for many other Shiply CMS projects.


How the Plugin Works

The contact form consists of three parts.

Frontend

The visitor sees and fills out the form.

Example:

Name
Email
Message

The frontend collects data and sends it to the backend.


AJAX

The browser submits form data without refreshing the page.

Request:

/api/plugins/simple-contact-form/submit

AJAX connects the frontend form to the backend route.


Backend

The backend:

  • Validates input

  • Saves submissions

  • Sends email notifications

  • Returns a JSON response


Plugin Structure

Create the following files:

shiply_cms_content/
  plugins/
    simple-contact-form/
      plugin.json
      Plugin.php
      assets/
        frontend.html
        simple-contact-form.css
        simple-contact-form.js

Create plugin.json

The manifest tells Shiply CMS how the plugin should load.

{
  "slug": "simple-contact-form",
  "name": "Simple Contact Form",
  "version": "1.0.0",
  "description": "Displays a shortcode contact form, saves submissions, and sends notification emails.",
  "author": "Your Name",
  "author_url": "https://example.com",

  "frontend_scripts": [
    "/assets/simple-contact-form.js"
  ],

  "frontend_styles": [
    "/assets/simple-contact-form.css"
  ],

  "admin_menu": {
    "parent": "plugins",
    "label": "Contact Form",
    "icon": "fa-envelope",
    "route": "/plugins/simple-contact-form",
    "position": 40
  },

  "admin_url": "/simple-contact-form/admin",

  "main": "Plugin.php",
  "class": "SimpleContactFormPlugin",

  "shortcode": {
    "code": "[ shiply_plugin slug=\"simple-contact-form\" ]",
    "label": "Simple Contact Form",
    "description": "Render the Simple Contact Form."
  }
}

Shortcode Format

Shiply CMS renders plugin output using shortcodes.

Basic shortcode:

[ shiply_plugin slug="simple-contact-form" ]

Additional attributes can be added later:

[ shiply_plugin slug="simple-contact-form" form="contact" ]

Frontend

Build the visitor-facing form first.

The frontend should only:

  • Display the form

  • Collect data

  • Send data to the backend

It should never write directly to the database.


Create the Form Template

Create:

assets/frontend.html
<form class="shiply-simple-contact-form" novalidate>

  <div class="mb-3">
    <label class="form-label">Name</label>
    <input class="form-control"
           type="text"
           name="name"
           required>
  </div>

  <div class="mb-3">
    <label class="form-label">Email</label>
    <input class="form-control"
           type="email"
           name="email"
           required>
  </div>

  <div class="mb-3">
    <label class="form-label">Message</label>
    <textarea class="form-control"
              name="message"
              rows="5"
              required></textarea>
  </div>

  <button class="btn btn-primary"
          type="submit">
      Send Message
  </button>

  <div class="shiply-simple-contact-message mt-3"
       aria-live="polite"></div>

</form>

Collect Form Values

Inside:

assets/simple-contact-form.js
function formData(form) {
    return {
        name: form.querySelector('[name="name"]').value,
        email: form.querySelector('[name="email"]').value,
        message: form.querySelector('[name="message"]').value,
        page_url: window.location.href
    };
}

function setMessage(form, text, type) {
    var message = form.querySelector('.shiply-simple-contact-message');

    message.className =
        'shiply-simple-contact-message mt-3 alert alert-' + type;

    message.textContent = text;
}

AJAX

AJAX submits data to the backend without reloading the page.

Endpoint:

/api/plugins/simple-contact-form/submit

Submit the Form

(function () {
    'use strict';

    var apiBase =
        (window.shiply_ajax_url || '').replace(/\/$/, '');

    var endpoint =
        apiBase + '/simple-contact-form/submit';

    document.addEventListener('submit', function (event) {

        var form =
            event.target.closest('.shiply-simple-contact-form');

        if (!form) {
            return;
        }

        event.preventDefault();

        var payload = formData(form);

        payload.shiply_ajax_token =
            window.shiply_ajax_token || '';

        fetch(endpoint, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            credentials: 'same-origin',
            body: JSON.stringify(payload)
        })
        .then(function (response) {
            return response.json();
        })
        .then(function (response) {

            if (response &&
                response.status === 'success') {

                form.reset();

                setMessage(
                    form,
                    response.msg || 'Message sent.',
                    'success'
                );

                return;
            }

            setMessage(
                form,
                (response && response.msg) ||
                'Unable to send message.',
                'danger'
            );
        })
        .catch(function () {

            setMessage(
                form,
                'Unable to send message.',
                'danger'
            );

        });
    });

}());

Expected Response

Keep responses simple.

{
  "status": "success",
  "msg": "Thank you. Your message was sent."
}

Backend

The backend handles:

  • Validation

  • Database storage

  • Email notifications

  • Admin settings

  • Shortcode rendering


Create the Database Table

Inside register():

private const TABLE_NAME =
    'shiply_simple_contact_submissions';

Create the table:

private function ensureTable(PDO $db): void
{
    $db->exec(
        'CREATE TABLE IF NOT EXISTS '
        . self::TABLE_NAME .
        ' (...)'
    );
}

Always use:

CREATE TABLE IF NOT EXISTS

so updates remain safe.


Register Plugin Routes

Inside:

public function register(
    PluginContext $plugin
): void

Register:

  • Frontend submission route

  • Admin page route

  • Settings route

$plugin->post('submit', ...);

$plugin->route(
    'GET',
    $plugin->ajaxPath('admin'),
    ...
);

$plugin->route(
    'POST',
    $plugin->ajaxPath('settings'),
    ...
);

Render the Shortcode

Shiply CMS calls:

renderShortcode()

whenever this shortcode appears:

[ shiply_plugin slug="simple-contact-form" ]

Example:

public function renderShortcode(
    array $attrs
): string
{
    $template =
        __DIR__ . '/assets/frontend.html';

    if (!is_file($template)) {
        return '';
    }

    return (string)
        file_get_contents($template);
}

Validate and Save Submissions

Always validate before writing to the database.

if ($name === '') {
    return array(
        'status' => 'error',
        'msg' => 'Name is required.'
    );
}

Validate email:

if (
    !filter_var(
        $email,
        FILTER_VALIDATE_EMAIL
    )
) {
    return array(
        'status' => 'error',
        'msg' => 'Enter a valid email address.'
    );
}

Save using prepared statements:

$statement = $db->prepare(
    'INSERT INTO ...'
);

$statement->execute(
    array(...)
);

Never place visitor data directly inside SQL.


Admin Settings

The plugin appears in the Plugins section as:

Contact Form

Shiply CMS automatically provides:

  • Activate

  • Deactivate

  • Delete

Your plugin provides:

  • Shortcode Settings

  • Notification Settings


Shortcode Settings

Display the shortcode for users to copy.

[ shiply_plugin slug="simple-contact-form" ]

This can be shown inside the admin interface.


Settings Screen

Allow users to configure:

  • From Email

  • From Name

  • To Emails

  • Email Template

Save settings using:

$plugin->setOption(
    'settings',
    $settings,
    'no'
);

Read settings using:

$plugin->getOption(
    'settings',
    array()
);

Email Notifications

When a submission is received:

  1. Save the row

  2. Send notification emails

Example:

private function sendNotification(
    PluginContext $plugin,
    string $name,
    string $email,
    string $message
): void
{
    // Email logic
}

Recipients should always be validated before sending.

filter_var(
    $email,
    FILTER_VALIDATE_EMAIL
);

Complete Plugin

Once everything is working, combine:

  • Table creation

  • Routes

  • Shortcode rendering

  • Validation

  • Database writes

  • Settings

  • Email notifications

into a single Plugin.php file.

The full example provided above demonstrates the complete structure for a production-ready contact form plugin.


Development Checklist

Before releasing your plugin, verify the following:

✅ plugin.json exists

✅ Plugin.php exists

✅ assets folder exists

✅ Shortcode is registered

[ shiply_plugin slug="simple-contact-form" ]

✅ Frontend assets load correctly

✅ AJAX includes:

shiply_ajax_token

✅ Submission route uses:

$plugin->post('submit', ...);

✅ Admin routes use:

$plugin->requireAdminToken();

✅ Database writes use prepared statements

✅ Email addresses are validated

✅ Settings use:

$plugin->setOption();

and

$plugin->getOption();

Following these practices will help you build secure, reliable, and maintainable Shiply CMS plugins.

Advanced Plugin Development

This guide walks through building a complete Form Builder plugin for Shiply CMS.

This is considered an advanced plugin because it includes:

  • Admin screens

  • AJAX routes

  • Database tables

  • Frontend shortcode rendering

  • Form submissions

  • Settings management

  • Email notifications

By the end, you'll have a reusable foundation for building more complex plugins inside Shiply CMS.


What You'll Build

The Form Builder plugin allows administrators to:

  • Create and save forms

  • Add and configure fields

  • Place forms on pages with shortcodes

  • Collect visitor submissions

  • View submissions

  • Configure email delivery settings

  • Receive email notifications


Plugin Structure

Start with a simple folder structure.

shiply_cms_content/
  plugins/
    form-builder/
      plugin.json
      Plugin.php
      assets/
        admin.css
        admin.js
        frontend.css
        frontend.js

Recommended Structure

Keep responsibilities separated:

File Purpose
Plugin.php Backend logic
admin.js Admin builder functionality
admin.css Admin styling
frontend.js Public form functionality
frontend.css Public form styling

As the plugin grows, this structure makes maintenance much easier.


Create plugin.json

The manifest tells Shiply CMS how to load and display the plugin.

{
  "slug": "form-builder",
  "name": "Form Builder",
  "version": "1.0.0",
  "description": "Build reusable forms with a three-column admin builder and manage notification recipient emails.",

  "admin_menu": {
    "parent": "plugins",
    "label": "Form Builder",
    "icon": "fa-wpforms",
    "route": "/plugins/form-builder",
    "position": 30
  },

  "admin_url": "/form-builder/admin",

  "main": "Plugin.php",
  "class": "FormBuilderPlugin",

  "shortcode": {
    "code": "[ shiply_plugin slug=\"form-builder\" form=\"form-id\" ]",
    "label": "Form Builder",
    "description": "Render a saved form builder form."
  }
}

Important Manifest Fields

Field Purpose
slug Unique plugin ID
admin_menu Creates the admin menu item
admin_url Route used to load admin HTML
main Main PHP file
class PHP class Shiply CMS loads
shortcode Shortcode shown to administrators

Frontend Planning

Before writing backend code, focus on the user experience.

The Form Builder has two interfaces:

  1. Admin Builder

  2. Public Form


Admin Builder Interface

The admin builder is where forms are created and managed.

The builder should allow users to:

  • View saved forms

  • Create new forms

  • Add fields

  • Edit field settings

  • Save forms

  • Copy shortcodes

  • Configure delivery settings

  • Review submissions


Example Layout

<div class="shiply-form-builder-admin">

  <div class="shiply-form-builder-layout">

    <aside class="shiply-form-builder-panel">
      <h3>Saved Forms</h3>
      <div class="shiply-fb-form-list"></div>
    </aside>

    <main class="shiply-form-builder-workspace">

      <input
        class="shiply-fb-name"
        type="text"
        placeholder="Form name">

      <div class="shiply-fb-workspace"></div>

      <button
        class="btn btn-primary shiply-fb-save"
        type="button">
        Save Form
      </button>

    </main>

    <aside class="shiply-form-builder-panel">

      <h3>Fields</h3>

      <button
        class="btn btn-light shiply-fb-add"
        data-type="text"
        type="button">
        Text
      </button>

      <button
        class="btn btn-light shiply-fb-add"
        data-type="email"
        type="button">
        Email
      </button>

      <button
        class="btn btn-light shiply-fb-add"
        data-type="textarea"
        type="button">
        Textarea
      </button>

    </aside>

  </div>

</div>

Frontend State

The browser needs temporary state while the admin edits a form.

Nothing is saved until the Save button is clicked.

var state = {

    forms: [],

    active: {
        form_id: '',
        name: '',
        fields: [],
        submitText: 'Submit',
        successMessage: 'Thank you. Your form has been submitted.'
    },

    selectedIndex: -1

};

Default Field Structure

function defaultField(type) {

    return {
        type: type || 'text',
        label: 'New Field',
        name: 'new-field',
        placeholder: '',
        required: false
    };

}

Public Form Interface

This is what visitors see.

Example shortcode:

[ shiply_plugin slug="form-builder" form="form-abc123" ]

Example output:

<form
    class="shiply-form-builder-form"
    data-form-id="form-abc123"
    novalidate>

  <input
      type="hidden"
      name="form_id"
      value="form-abc123">

  <div class="shiply-form-builder-field-wrap">
    <label>Name</label>
    <input type="text" name="name" required>
  </div>

  <div class="shiply-form-builder-field-wrap">
    <label>Email</label>
    <input type="email" name="email" required>
  </div>

  <button type="submit">
      Submit
  </button>

  <div
      class="shiply-form-builder-message"
      aria-live="polite"></div>

</form>

Frontend Validation

Validate fields before sending data to the backend.

function validateField(field) {

    var value =
        (field.value || '').trim();

    if (
        field.hasAttribute('required')
        && value === ''
    ) {
        return 'This field is required.';
    }

    if (
        field.type === 'email'
        && value !== ''
        && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
    ) {
        return 'Enter a valid email address.';
    }

    return '';

}

Rule

Frontend code should:

✅ Collect data

✅ Validate data

✅ Submit data

Frontend code should never:

❌ Save directly to the database

❌ Trust user input


AJAX Layer

AJAX connects the browser to PHP.

Both admin actions and public submissions use AJAX.


AJAX Endpoints

Endpoint Method Used By Purpose
/api/plugins/form-builder/admin GET Admin Load builder screen
/api/plugins/form-builder/forms-list GET Admin Load forms
/api/plugins/form-builder/forms-save POST Admin Save form
/api/plugins/form-builder/settings-save POST Admin Save settings
/api/plugins/form-builder/submit POST Visitor Submit form

Save Form AJAX Request

function saveForm() {

    return fetch(
        '/api/plugins/form-builder/forms-save',
        {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },

            credentials: 'same-origin',

            body: JSON.stringify({
                form_id: state.active.form_id,
                name: state.active.name,
                submit_text: state.active.submitText,
                success_message: state.active.successMessage,
                fields_json: JSON.stringify(state.active.fields)
            })
        }
    )
    .then(function (response) {
        return response.json();
    });

}

Submit Public Form

Always include:

  • shiply_ajax_token

  • page_url

  • referrer_url

var data = new FormData(form);

data.append(
    'shiply_ajax_token',
    window.shiply_ajax_token || ''
);

data.append(
    'page_url',
    window.location.href || ''
);

data.append(
    'referrer_url',
    document.referrer || ''
);

Submit to:

/api/plugins/form-builder/submit

AJAX Response Format

Keep responses predictable.

{
  "status": "success",
  "msg": "Form saved.",
  "forms": [],
  "submissions": []
}

Backend Responsibilities

The backend is responsible for:

  • Permissions

  • Validation

  • Database operations

  • Settings

  • Rendering

  • Email delivery

The backend makes the final decisions.


Register Routes

Inside:

public function register(
    PluginContext $plugin
): void

Register:

  • Admin routes

  • Settings routes

  • Save routes

  • Submission routes

Example:

$plugin->route('GET', ...);

$plugin->route('POST', ...);

$plugin->post('submit', ...);

Create Database Tables

Create separate tables for:

Table Purpose
shiply_form_builder_forms Form definitions
shiply_form_builder_emails Notification recipients
shiply_form_builder_submissions Visitor submissions

Use:

CREATE TABLE IF NOT EXISTS

for all plugin-owned tables.


Save Form Definitions

When the admin clicks Save:

  1. Validate the form

  2. Generate a form ID

  3. Save the JSON structure

Example:

$formId = $plugin->codeId(
    'form',
    'shiply_form_builder_forms',
    'form_id'
);

Always validate:

if ($name === '') {
    return array(
        'status' => 'error',
        'msg' => 'Form name is required.'
    );
}

Render Shortcodes

When this shortcode appears:

[ shiply_plugin slug="form-builder" form="form-abc123" ]

Shiply CMS calls:

renderShortcode()

The plugin:

  1. Loads the form

  2. Decodes JSON

  3. Generates HTML

  4. Returns the form


Save Submissions

When visitors submit forms:

  1. Load the saved form

  2. Validate fields

  3. Collect submission data

  4. Save the submission

  5. Send notifications

Example:

$this->saveSubmission(
    $db,
    $formId,
    $formName,
    $submissionFields,
    $pageUrl,
    $referrerUrl,
    $geo
);

Email Notifications

The plugin can notify one or more recipients.

Settings are stored with:

$plugin->setOption(
    'delivery_settings',
    $settings,
    'no'
);

Read with:

$plugin->getOption(
    'delivery_settings',
    array()
);

Email Flow

  1. Read delivery settings

  2. Validate recipient emails

  3. Build the email

  4. Add reply-to address

  5. Send notifications

Always validate:

filter_var(
    $email,
    FILTER_VALIDATE_EMAIL
);

before sending.


Security and Data Safety

Advanced plugins handle sensitive data.

Follow these rules every time.


Protect Admin Routes

$plugin->requireAdminToken();

Use before:

  • Loading admin screens

  • Saving forms

  • Deleting records

  • Updating settings


Use Prepared Statements

Never place user input directly into SQL.

$statement = $db->prepare(...);

Escape HTML Output

htmlspecialchars(
    $value,
    ENT_QUOTES,
    'UTF-8'
);

Validate Email Addresses

filter_var(
    $email,
    FILTER_VALIDATE_EMAIL
);

Useful Helper Methods

cleanId()

Creates safe IDs.

$this->cleanId($value);

limitText()

Prevents oversized content.

$this->limitText($value, 190);

esc()

Escapes HTML output.

$this->esc($value);

cleanRichHtml()

Allows approved HTML tags.

$this->cleanRichHtml($html);

ensureColumn()

Repairs missing columns during upgrades.

$this->ensureColumn(...);

Advanced Plugin Checklist

Before releasing your plugin, verify the following:

✅ Create a valid plugin.json

✅ Define slug, class, main, and shortcode

✅ Add register(PluginContext $plugin)

✅ Create admin routes

✅ Create public submit routes

✅ Use CREATE TABLE IF NOT EXISTS

✅ Use $plugin->codeId() for public IDs

✅ Store flexible data as JSON

✅ Implement renderShortcode()

✅ Include shiply_ajax_token in frontend requests

✅ Protect admin routes

$plugin->requireAdminToken();

✅ Use prepared statements

✅ Escape output

✅ Validate emails

✅ Display success messages

✅ Test:

  • Create Form

  • Save Form

  • Render Shortcode

  • Submit Form

  • Send Email

  • Delete Form

  • View Submissions

Following these practices will help you build scalable, secure, and maintainable Shiply CMS plugins.

Shiply CMS Plugin Functions and Methods

This guide serves as a quick reference for the most commonly used Shiply CMS plugin functionality.

You'll use these methods when working with:

  • Plugin registration

  • AJAX routes

  • Settings

  • Database access

  • Shortcodes

  • Frontend assets

  • Admin pages

Most plugin functionality starts inside:

public function register(PluginContext $plugin): void
{
    // Plugin setup
}

The $plugin object gives your plugin access to routes, request input, JSON responses, saved settings, database access, file paths, editor loading, and helper methods.


Quick Start Plugin Structure

Start with the smallest working plugin.

shiply_cms_content/
  plugins/
    my-plugin/
      plugin.json
      Plugin.php
      assets/
        my-plugin.js

plugin.json

Every plugin starts with a manifest file.

{
  "slug": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "description": "A small plugin that sends data to the backend.",

  "frontend_scripts": [
    "/assets/my-plugin.js"
  ],

  "main": "Plugin.php",
  "class": "MyPlugin",

  "shortcode": {
    "code": "[ shiply_plugin slug=\"my-plugin\" ]",
    "label": "My Plugin",
    "description": "Render My Plugin output."
  }
}

Plugin.php

This file contains the backend logic for your plugin.

<?php

class MyPlugin
{
    public function register(PluginContext $plugin): void
    {
        $plugin->post('submit', function (array $input) {

            $name = trim((string) ($input['name'] ?? ''));

            if ($name === '') {
                return array(
                    'status' => 'error',
                    'msg' => 'Name is required.'
                );
            }

            return array(
                'status' => 'success',
                'msg' => 'Hello, ' . $name . '.'
            );
        });
    }

    public function renderShortcode(array $attrs): string
    {
        return '<form class="my-plugin-form">'
            . '<input name="name" placeholder="Name" required>'
            . '<button type="submit">Send</button>'
            . '<div class="my-plugin-message"></div>'
            . '</form>';
    }
}

Frontend JavaScript

Create:

assets/my-plugin.js
(function () {

    document.addEventListener(
        'submit',
        function (event) {

            var form =
                event.target.closest('.my-plugin-form');

            if (!form) {
                return;
            }

            event.preventDefault();

            var data = new FormData(form);

            data.append(
                'shiply_ajax_token',
                window.shiply_ajax_token || ''
            );

            fetch(
                (window.shiply_ajax_url || '')
                + '/my-plugin/submit',
                {
                    method: 'POST',
                    body: data,
                    credentials: 'same-origin'
                }
            )
            .then(function (response) {
                return response.json();
            })
            .then(function (response) {

                form.querySelector(
                    '.my-plugin-message'
                ).textContent =
                    response.msg || '';

            });

        }
    );

}());

Naming Rules

Most plugin loading issues happen when names don't match.

Item Example Rule
Plugin Folder my-plugin Lowercase words separated by hyphens
Slug my-plugin Usually matches folder
Main File Plugin.php Must point to the PHP file
Class MyPlugin Must match Plugin.php
Route /api/plugins/my-plugin/submit Uses slug and action
Shortcode [ shiply_plugin slug="my-plugin" ] Must match plugin.json

plugin.json Fields

Shiply CMS reads plugin.json to determine how your plugin should load.

Field Required Purpose
slug Yes Unique plugin ID
name Yes Plugin name
version Recommended Version number
description Recommended Plugin description
frontend_scripts No Public JavaScript
frontend_styles No Public CSS
frontend_html No Static HTML output
admin_menu No Admin menu item
admin_url No Admin route
settings No Plugin settings
main Yes (PHP Plugins) Main PHP file
class Yes (PHP Plugins) PHP class
shortcode No Shortcode metadata

Full Manifest Example

{
  "slug": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "description": "Adds a custom feature to Shiply CMS.",

  "frontend_scripts": [
    "/assets/my-plugin.js"
  ],

  "frontend_styles": [
    "/assets/my-plugin.css"
  ],

  "admin_menu": {
    "parent": "plugins",
    "label": "My Plugin",
    "icon": "fa-plug",
    "route": "/my-plugin",
    "position": 40
  },

  "admin_url": "/my-plugin/admin",

  "main": "Plugin.php",
  "class": "MyPlugin",

  "shortcode": {
    "code": "[ shiply_plugin slug=\"my-plugin\" ]",
    "label": "My Plugin",
    "description": "Render My Plugin output."
  }
}

Manifest Examples by Plugin Type

Different plugins need different configuration.


Frontend-Only Plugin

{
  "slug": "responsive-helper",
  "name": "Responsive Helper",

  "frontend_scripts": [
    "/assets/responsive-helper.js"
  ],

  "frontend_styles": [
    "/assets/responsive-helper.css"
  ],

  "main": "Plugin.php",
  "class": "ResponsiveHelperPlugin"
}

Shortcode Plugin

{
  "slug": "profile-card",
  "name": "Profile Card",

  "main": "Plugin.php",
  "class": "ProfileCardPlugin",

  "shortcode": {
    "code": "[ shiply_plugin slug=\"profile-card\" user=\"user-id\" ]",
    "label": "Profile Card",
    "description": "Render a saved profile card."
  }
}

Admin Page Plugin

{
  "slug": "report-dashboard",
  "name": "Report Dashboard",

  "admin_url": "/report-dashboard/admin",

  "main": "Plugin.php",
  "class": "ReportDashboardPlugin"
}

AJAX and Data Plugin

{
  "slug": "lead-capture",
  "name": "Lead Capture",

  "frontend_scripts": [
    "/assets/lead-capture.js"
  ],

  "frontend_styles": [
    "/assets/lead-capture.css"
  ],

  "main": "Plugin.php",
  "class": "LeadCapturePlugin",

  "shortcode": {
    "code": "[ shiply_plugin slug=\"lead-capture\" ]",
    "label": "Lead Capture Form",
    "description": "Render the lead capture form."
  }
}

Plugin Class Methods

These are the primary methods your plugin can implement.


register()

Main setup method.

Use it to:

  • Create tables

  • Register routes

  • Load settings

  • Configure behavior

public function register(
    PluginContext $plugin
): void
{
    // Setup
}

renderShortcode()

Returns frontend HTML when a shortcode is rendered.

Example shortcode:

[ shiply_plugin slug="your-plugin" ]
public function renderShortcode(
    array $attrs
): string
{
    return '<div>Hello World</div>';
}

availableShortcodes()

Provides shortcode entries for the shortcode picker.

Useful when creating:

  • Forms

  • Products

  • Knowledge bases

  • Dynamic content

public function availableShortcodes(): array
{
    return [];
}

renderGlobalTags()

Outputs page-wide tags.

Examples:

  • Analytics scripts

  • Tracking pixels

  • Meta tags

  • Global JavaScript

public function renderGlobalTags(): string
{
    return '<script src="/analytics.js"></script>';
}

PluginContext Methods

Inside register(), Shiply CMS provides a PluginContext object.

Method Purpose
$plugin->slug() Get plugin slug
$plugin->manifest() Read plugin.json
$plugin->path() File path
$plugin->dir() Alias of path()
$plugin->route() Register routes
$plugin->get() Register GET routes
$plugin->post() Register POST routes
$plugin->ajaxPath() Build AJAX paths
$plugin->input() Read request data
$plugin->json() Send JSON response
$plugin->requireClientToken() Require client token
$plugin->requireAdminToken() Require admin token
$plugin->getOption() Read options
$plugin->setOption() Save options
$plugin->db() Get PDO connection
$plugin->codeId() Generate unique IDs
$plugin->editor() Load editor
$plugin->http() Make HTTP requests

Routes

Routes allow JavaScript and PHP to communicate.


Route Cheat Sheet

Need PHP Method URL Token
Public Config $plugin->get() /api/plugins/my-plugin/config Not Required
Frontend Submit $plugin->post() /api/plugins/my-plugin/submit shiply_ajax_token
Admin Page $plugin->route() /api/plugins/my-plugin/admin requireAdminToken()
Save Settings $plugin->route() /api/plugins/my-plugin/settings requireAdminToken()

Shortcodes

Shiply CMS renders frontend output using shortcodes.

Basic shortcode:

[ shiply_plugin slug="my-plugin" ]

Shortcode with attributes:

[ shiply_plugin slug="my-plugin" item="entry-123" ]

Multiple Shortcodes

'code' => '[ shiply_plugin slug="my-plugin" item="featured" ]'

Excluding Global Tags

[ shiply_plugin slug="my-plugin" event="exclude" ]

or

[ shiply_plugin slug="my-plugin" event="exclude_this_page" ]

Settings and Options

Save settings:

$plugin->setOption(
    'button_color',
    '#2563eb',
    'no'
);

Read settings:

$buttonColor =
    $plugin->getOption(
        'button_color',
        '#2563eb'
    );

Arrays are automatically stored as JSON.


Database Access

Use:

$plugin->db();

to get the PDO connection.

Always use:

CREATE TABLE IF NOT EXISTS

for plugin-owned tables.

Always use prepared statements:

$statement = $db->prepare(...);

$statement->execute(...);

Frontend AJAX Variables

Available automatically:

Variable Purpose
window.shiply_ajax_url Plugin API URL
window.shiply_ajax_token AJAX security token

Example:

var endpoint =
    (window.shiply_ajax_url || '')
    + '/my-plugin/submit';

Rich Text Editor

Load the Shiply CMS editor:

$plugin->editor(
    '#my-plugin-content'
);

Outbound HTTP Requests

Use:

$response = $plugin->http(
    'POST',
    'https://api.example.com/entries',
    array(...)
);

Response includes:

  • ok

  • status_code

  • error

  • body


Security Rules

Follow these rules for every plugin.

Rule Example
Protect admin routes $plugin->requireAdminToken();
Include AJAX token shiply_ajax_token
Use prepared statements $db->prepare(...)->execute(...);
Escape HTML htmlspecialchars(...)
Validate emails filter_var(..., FILTER_VALIDATE_EMAIL)
Validate URLs filter_var(..., FILTER_VALIDATE_URL)
Clean IDs preg_replace(...)

Troubleshooting

Problem Check
Plugin missing Verify plugin.json and slug
Class won't load Verify class and file names
Shortcode blank Verify plugin active
JavaScript missing Verify asset path
Invalid token Verify shiply_ajax_token
Admin page missing Verify admin_url
Database errors Verify tables and columns

Plugin Development Checklist

Before releasing your plugin:

✅ Use register() for setup

✅ Use $plugin->post() for frontend submissions

✅ Include shiply_ajax_token

✅ Protect admin routes

$plugin->requireAdminToken();

✅ Use prepared statements

✅ Use $plugin->db()

✅ Use plugin options for settings

$plugin->getOption();

$plugin->setOption();

✅ Use renderShortcode() for dynamic output

✅ Use availableShortcodes() for multiple shortcodes

✅ Use renderGlobalTags() only when necessary

✅ Escape all user-generated content

Following these practices will help you build secure, reliable, and maintainable Shiply CMS plugins.