ElmsPark Guides
PageMotor guide

Building your first PageMotor plugin

A PageMotor plugin is one folder with one PHP file inside. You declare a class, opt into the capabilities you need, activate it, and PageMotor does the rest. This guide takes you from an empty folder to a working [hello-world] shortcode.

About 20 minutes 📁 Plain PHP, no build step 📦 One folder, one class ✓ Adds a working shortcode
What you’ll need: a PageMotor install you can add plugins to, either your own development site or a staging environment with admin access. Basic PHP will do; you do not need Composer, npm, or any other tooling. Just a text editor and a way to upload or edit files on the server.
How PageMotor plugins work. A plugin is a folder inside user-content/plugins/ containing a plugin.php file. That file declares a PHP class which extends PM_Plugin, the framework’s base class. PageMotor includes the file, reads the header comment at the top, and makes the plugin available in the admin. You add capabilities by implementing valet methods: named methods on your class that PageMotor calls at specific moments to register shortcodes, assets, AJAX handlers, settings, and more. Activate the plugin and those capabilities go live.
▶ Prefer to learn it interactively? Tap through the interactive lesson, one idea at a time, about two minutes, with quick questions as you go.

Use this guide with any AI assistant

Download it as a prompt file, paste it into Claude, ChatGPT, Gemini or any LLM, and it will walk you through building the plugin step by step.

↓  Download as LLM prompt

1Make the folder and the header

All third-party plugins live under user-content/plugins/ in your PageMotor install. Create a folder there for your plugin. The name should be kebab-case and should start with a prefix that is unique to you or your organisation, so it cannot collide with any other plugin in the ecosystem.

For this guide, the folder is my-hello-world:

$ mkdir user-content/plugins/my-hello-world
$ touch user-content/plugins/my-hello-world/plugin.php

Open plugin.php and add the header comment. PageMotor reads this comment to identify the plugin, so it must appear at the very top of the file, before any PHP code:

<?php
/*
Name: My Hello World
Author: Your Name
Description: A minimal first plugin with one shortcode.
Version: 1.0
Requires: 0.9
Class: My_Hello_World
License: OpenAttribution
*/

The fields that matter for a first plugin:

FieldWhat it does
NameThe display name shown in the plugin admin.
DescriptionA short description, also shown in the admin.
VersionUsed for cache-busting JS and CSS assets; bump it when you change things.
RequiresMinimum PageMotor version. Set it to match what your install is running.
ClassThe exact PHP class name PageMotor will instantiate. Must match the class declaration below.
Version badge note. PageMotor reads only the first 500 bytes of plugin.php to find the Version: header. Keep the header short, and make sure Version: appears before any long description block.

2The plugin class

Directly below the header comment, declare your class. It must extend PM_Plugin, and the class name must match the Class: field in the header exactly.

<?php
/*
Name: My Hello World
...
Class: My_Hello_World
*/
class My_Hello_World extends PM_Plugin {

}

That is a complete, valid plugin. It does nothing yet, but PageMotor can load and activate it.

Two lifecycle hooks you can override. PM_Plugin provides two override slots for your own code. construct() runs early, before valets are evaluated: use it for database setup, includes, or anything that must exist first. init() runs after all valets have been registered: use it for work that depends on settings or options being available. Do not override __construct() or _init(): those are framework-owned and must not be touched.

3Add a capability: a shortcode

Capabilities are added by implementing valet methods. To register a shortcode, implement shortcodes(). It returns an array mapping shortcode names to method names on your class.

Here is the complete plugin with one [hello-world] shortcode:

<?php
/*
Name: My Hello World
Author: Your Name
Description: A minimal first plugin with one shortcode.
Version: 1.0
Requires: 0.9
Class: My_Hello_World
License: OpenAttribution
*/
class My_Hello_World extends PM_Plugin {

    // Register shortcodes: 'shortcode-name' => 'method_name'
    public function shortcodes() {
        return [
            'hello-world' => 'render_hello'
        ];
    }

    // The handler receives an $args array of any attributes
    public function render_hello($args = []) {
        $name = !empty($args['name']) ? htmlspecialchars($args['name']) : 'world';
        return '<p>Hello, ' . $name . '!</p>';
    }

}

The shortcode handler receives any attributes as $args and should return its HTML, not echo it. So [hello-world name="Kenn"] would return <p>Hello, Kenn!</p>.

Frontend CSS for shortcodes. If your shortcode needs its own CSS, register it with css_sitewide(), not css(). The css() method only loads when an instance of the plugin is placed in the current page template. Shortcodes can appear on any page, so they need css_sitewide() to load on every page. This is a common early mistake.

4Install and activate it

Log into your PageMotor admin and go to the Plugins section. Your plugin should appear in the list, identified by the Name: from the header comment.

  1. Click Install next to it. PageMotor registers the plugin in the database.
  2. Click Activate. The plugin class is now instantiated and its valets are evaluated on every page load.
A fatal in plugin.php can take the site down. PageMotor includes the plugin file directly. A PHP syntax error or an uncaught fatal exception will stop every page from loading until the file is fixed or the plugin is deactivated. Keep early plugins simple. Test each change, and if something breaks, fix the file on the server before anything else.

5See it on a page

Edit any page in the PageMotor content editor and add the shortcode to the page body:

[hello-world]

Save, visit the page, and you should see Hello, world! rendered in the page output.

Try it with an attribute:

[hello-world name="Kenn"]

That should give you Hello, Kenn! The shortcode is live and working.

Shortcodes only run in page content, not in standalone HTML. PageMotor pages can have a type of page (runs the shortcode pipeline) or html (serves verbatim HTML). Shortcodes only fire in type=page content. If you do not see the output, check the page type in the content editor.

The valet methods you can implement

Verified against lib/plugin.php in PageMotor 0.9.4b. Each method is optional; return an empty array [] from any array-returning valet when you have nothing to declare.

MethodWhat it registers or adds
shortcodes()Returns ['shortcode-name' => 'method_name']. Maps shortcode tags to handler methods on your class. Handlers receive $args and should return HTML.
settings()Declares the plugin’s settings fields (theme-agnostic, stored under the class name in the options table). Shown in the plugin’s settings panel in admin.
ajax()Returns ['action-id' => ['method' => 'handler']]. Registers AJAX actions handled by methods on your class.
api()Returns action definitions for the PageMotor API. Similar to ajax() but for the structured API endpoint. Useful for AI and MCP integrations.
css_sitewide()Returns CSS file paths to load on every page. Use for shortcode UI styles. (See note in step 3.)
css()Returns CSS file paths to load only when this plugin is placed in the current template. For Block/Container UI, not shortcodes.
js_sitewide()Returns JS to load in the </body> on every page, sitewide.
js()Returns JS to load only when this plugin appears in the current template.
js_head()Returns JS to load in <head>, sitewide, just before CSS.
html()For type=’box’ plugins: echoes the HTML output of the Box when it is rendered in a template. Must echo, not return.
container_open()For type=’container’ plugins: echoes HTML before the contained boxes.
container_close()For type=’container’ plugins: echoes HTML after the contained boxes.
fonts()Registers fonts through PageMotor’s font system. Use for Bunny Fonts or other privacy-compliant web fonts.
content_types()Registers new content types beyond the built-in page/post.
skills()Registers MCP Skills (AI prompt files) contributed by this plugin.
docs()Registers this plugin’s own documentation with PageMotor’s doc system, making it discoverable via the API and AI agents.
construct()Your pseudo-constructor (override slot). Runs early in the lifecycle, before valets. Use for DB table creation, includes, and stateful setup.
init()Your pseudo-initialiser (override slot). Runs after all valets. Use for work that depends on settings or options being available.

The full list of valets, with their exact return shapes and examples, is in the PageMotor plugin valet methods documentation at documentation.elmspark.com/plugins/.

Settings, AJAX and assets, briefly

The three things most plugins need after a shortcode.

Settings

Implement settings() to give your plugin a settings panel in the PageMotor admin. It returns an array of field definitions. PageMotor handles the form rendering, saving, and retrieval. Saved values are available inside your methods as $this->settings['field_name'].

AJAX

Implement ajax() to add AJAX actions. Return a map of action IDs to handler method names. Action IDs must not start with an underscore (that prefix is reserved for framework actions). Inside a handler, PageMotor has already verified the CSRF token; your job is to do the work and echo json_encode([...]) the response.

Assets

Register JS and CSS with the appropriate valet for where you need it to load. The key distinction is sitewide (loads on every page) versus template-scoped (loads only when the plugin appears in the current template). Shortcodes need sitewide loading. Blocks and Containers can use the template-scoped variants to keep the page light.

The early pitfalls

The things that catch most people in their first few plugins.

Troubleshooting

The most common problems and what to do about them.

The plugin does not appear in the admin plugin list

Check that the folder is in user-content/plugins/ and that plugin.php is inside it. Make sure the header comment at the top of the file is valid: it must open with /* on its own line and close with */. The Name: and Class: fields are required. Confirm the file has no PHP syntax errors (a parse error can prevent it from appearing at all).

The site went blank after activating the plugin

There is a PHP fatal error in plugin.php. Connect to the server, open the file, and look for the mistake. Common causes: a missing semicolon, an unclosed bracket, or a reference to a class or function that does not exist. If you cannot find it quickly, rename the plugin folder temporarily (so PageMotor cannot include it) to bring the site back, then fix the file at your own pace.

The shortcode appears on the page as plain text, not rendered

Two possibilities. First, the plugin may not be active: check the Plugins section. Second, the page type may be set to html rather than page. Check the page’s type setting in the content editor. Shortcodes only run through the pipeline on type=page pages.

The shortcode renders but its CSS is not loading

You are probably using css() instead of css_sitewide(). The css() method only fires when the plugin is placed in the current template as a Box or Container. Switch to css_sitewide() for any styles that need to load alongside a shortcode.

The version badge is missing in the admin

PageMotor reads only the first 500 bytes of plugin.php to find the Version: header. Move the Version: line higher up in the header comment, before any long description, and make sure the plugin also defines a constant matching the class name in all caps (for example MY_HELLO_WORLD). Both are required for the badge to appear.

See also