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
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
- Go to Plugins inside Shiply CMS.
- Activate the plugin.
- Visit a page on your website.
- 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:
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:
$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:
-
Save the row
-
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:
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:
-
Admin Builder
-
Public Form
Admin Builder Interface
The admin builder is where forms are created and managed.
The builder should allow users to:
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:
-
Validate the form
-
Generate a form ID
-
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:
-
Loads the form
-
Decodes JSON
-
Generates HTML
-
Returns the form
Save Submissions
When visitors submit forms:
-
Load the saved form
-
Validate fields
-
Collect submission data
-
Save the submission
-
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
-
Read delivery settings
-
Validate recipient emails
-
Build the email
-
Add reply-to address
-
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.