Midground1.1.0

Midground developer documentation

1. Overview #

theme/midground/                 Theme: presentation, fallback layout, WooCommerce styles
plugin/midground-builder/
  midground-builder.php          Bootstrap, constants, activation/deactivation
  uninstall.php                  Clean-up on delete (never deletes pages)
  wpml-config.xml                What WPML copies, translates or ignores
  includes/                      PHP (autoload: Midground\X\Y -> includes/X/Y.php)
    Plugin.php                   Service registry and upgrades
    Core/                        Css (value validation), Ids, Migrate_Legacy (Kulisse 1.0 -> Midground)
    Model/                       Document (validation), Props (field sanitising), Migrations, Storage
    Elements/                    Registry + definitions/*.php (67 elements)
    Render/                      Renderer, Context, Dynamic (dynamic data sources)
    Style/                       Style_Compiler, style-keys.json, Globals (design values), Font_Library
    Effects/Recipes.php          Motion recipes and their resolution to keyframes
    Templates/                   Templates (CPT mg_template), Conditions, header/footer site parts
    Components/                  Components (CPT mg_component) and REST
    Import/                      Importer (block/classic import and restore), Transfer (export/import files)
    Media/                       Models (GLB/glTF validation), Background_Video, Geocode, Icons
    Frontend/                    Frontend (assets, CSS), Consent, Seo
    Integrations/                Plugins (forms, booking), Reviews, Multilingual, Assistant (AI)
    License/                     License (updates), Provider, Provider_Edd, Provider_Lemon
    Admin/, Rest/, Woo/, Library/, Cli/
  library/                       Sections and page layouts (PHP data)
  src/                           Source for the editor, runtime and shared logic (built with esbuild)
    editor/                      React via wp.element: store, canvas (iframe), drag and drop, panels, tour
    runtime/                     Front end: fx.js (motion), media.js (consent, background video, maps),
                                 carousel.js, countdown.js, popups.js, reveal.js, counters.js,
                                 models.js + viewer3d.js (3D)
    shared/                      Used by the editor and the tests: style-compiler.js, doc-ops.js, recipes.js
  assets/                        Built files (committed, so the plugin works without a build)
  docs/                          The user guide as HTML (built from docs/*.md)
demo/midground-demo/             Optional: starter sites, the Skumring showcase, placeholder pictures (CC0)
tests/                           js/ (node:test), php/ (integration in WordPress), e2e/ (Playwright)
tools/                           build.mjs, package.py, i18n.sh, docs-site.mjs, perf.mjs, demo tools
dev/                             Docker Compose environment (WordPress, MariaDB, WP-CLI)

Principles: the theme owns presentation only; all content and logic live in the plugin, so content survives a theme switch. The server is authoritative for HTML (one PHP renderer), while the style compiler exists in both PHP and JS with shared test data, so the editor can show styles without a round trip and the published page is identical.

2. The document format #

Every page, template and component stores one document as JSON in the post meta _mg_doc:

{
  "version": 2,
  "settings": {
    "layout": "full", "headerOverlay": false, "background": "", "previewPost": 0,
    "seo": { "title": "", "description": "", "image": { "id": 12, "url": "…" }, "noindex": false }
  },
  "content": [
    {
      "id": "a1b2c3d4",
      "type": "section",
      "name": "Intro",
      "props": { "width": "contained" },
      "style": { "base": { "paddingTop": "var(--mg-s-8)" }, "mobile": { "paddingTop": "48px" } },
      "fx": [ { "id": "f1e2d3c4", "recipe": "rise", "intensity": 80, "trigger": "view", "start": 0, "end": 60 } ],
      "attrs": { "anchor": "intro", "classes": "my-class", "ariaLabel": "" },
      "bgVideo": { "source": "file", "file": { "id": 40, "url": "…" }, "poster": { "id": 41, "url": "…" }, "mobile": "poster" },
      "hideOn": [ "mobile" ],
      "hidden": false,
      "locked": false,
      "children": [ { "id": "b2c3d4e5", "type": "heading", "props": { "text": "Hello <em>world</em>", "level": "h1" } } ]
    }
  ]
}

Validation (Model\Document::normalize()) runs on every save, autosave and import: unknown fields are dropped, props are sanitised by the element's schema (Model\Props), styles are validated per key, links allow only http(s), mailto, tel, relative paths and #anchors, HTML goes through wp_kses (scripts in Custom HTML only with unfiltered_html), maximum depth 24 and 4,000 nodes, and elements of an unknown type (for example from a disabled extension) are kept with generically sanitised data (warning unknown_type:<type>).

Migrations: version is compared with MIDGROUND_SCHEMA_VERSION. Older documents are upgraded when read (Model\Migrations), step by step; extensions can add steps with the filter midground/migrations. Documents from a newer version are left alone.

Kulisse 1.0: sites built with the product's earlier name are moved over once, after the old plugin is deactivated (Core\Migrate_Legacy): options, post types, meta keys, CSS variable references, the block comment and the theme's settings. Static copies are rendered again in the background.

3. Storage and life cycle #

Model\Storage::save():

  1. Normalises the document and stores _mg_doc and _mg_enabled before the post is updated, so WordPress copies the meta into the revision (wp_post_revision_meta_keys).
  2. The first time: stores the original content in _mg_original (can be restored exactly).
  3. Writes a static HTML copy to post_content, wrapped in the block <!-- wp:midground/document {"id":…,"hash":…,"layout":…} --> … <!-- /wp:midground/document -->. With the plugin active, the block is rendered live from the document; without the plugin, WordPress shows the copy. Search, excerpts and SEO tools read the copy. Shortcodes stay as shortcodes in the copy.
  4. Stores a hash of the copy in _mg_hash (to notice edits made in other editors) and the generated CSS in _mg_css (a cache, tagged with midground_css_generation).

Autosaves go to _mg_autosave without publishing. Restoring a revision in WordPress brings the document along. Deactivation deletes nothing. uninstall.php deletes the CSS cache and scheduled jobs; templates, components, settings, keys and the licence only when "delete on uninstall" is chosen. After a plugin update the static copies are refreshed in the background (WP-Cron, 20 posts per run), or with wp midground refresh.

4. Rendering #

Render\Renderer::render_doc( $doc, Context $ctx ) renders recursively. Context::$mode is frontend, editor (adds data-mg-id, data-mg-slot, data-mg-inline) or static (for the copy in post_content: no editor or effect attributes, shortcodes kept raw). Element CSS is compiled by Style\Style_Compiler with class selectors .mg-<id> and collected per page in one <style> block. Design values become CSS variables (--mg-c-*, --mg-s-*, --mg-t-*, --mg-r-*, --mg-shadow-*, --mg-font-*) on :root, shared with the theme. Fonts copied into the WordPress font library are served with @font-face rules from the site itself.

Templates: Templates decides which template applies per location (header, footer, content) with Conditions::resolve(). Themes ask through the filter midground_render_location (the Midground theme) or the function midground_location( 'header' ). Content templates take over through template_include. On first run a simple header and footer are created (filter midground/create_default_parts); an untouched default part is retired when another header or footer is imported.

5. Extension API #

A complete, tested example is in docs/examples/midground-example-extension (its own element, recipe, dynamic source and editor control). The PHP test extensions: the documented example plugin registers and renders runs it.

Elements #

add_action( 'midground/register_elements', function () {
	midground_register_element( array(
		'type'     => 'my-notice',              // ^[a-z][a-z0-9-]{1,40}$
		'label'    => __( 'Notice', 'my-textdomain' ),
		'category' => 'blocks',                 // layout|basic|blocks|navigate|dynamic|commerce|motion|utility
		'icon'     => 'info',                   // a Lucide icon name
		'inline'   => array( 'text' ),          // props edited directly on the canvas
		'style'    => array( 'typography', 'spacing', 'border', 'appearance' ),
		'props'    => array(
			'text' => array( 'type' => 'inline', 'label' => __( 'Text', 'my-textdomain' ), 'default' => '' ),
			'tone' => array( 'type' => 'segmented', 'options' => array( 'info' => 'Info', 'warn' => 'Warning' ), 'default' => 'info' ),
		),
		'render'   => function ( array $node, array $props, $ctx, $r ): string {
			$p = '<p' . \Midground\Render\Renderer::attrs( $r->inline_attr( $ctx, 'text' ) ) . '>' . $props['text'] . '</p>';
			return $r->wrap( $node, $ctx, $p, array( 'class' => 'my-notice--' . $props['tone'] ) );
		},
	) );
} );

Field types (props.*.type): text, textarea, inline (inline HTML), richtext, html, url, link, number, range (min/max/step), toggle, select, segmented (options), color, length, icon, media, gallery, model, post, term, menu, component, posts, terms, query, repeater (fields), datetime (YYYY-MM-DDTHH:MM), place ({ address, lat, lng }), formref and bookingref (a form or booking from a supported plugin), key, object. Shared keys: label, help, default, show (show the field only when other props have given values), dynamic (which dynamic data types the field accepts).

Other definition keys: children (true or a list of allowed child types), parent (allowed parents), preset (children/style created on insert), style_targets (key or @group → selector suffix, for elements whose styles belong on an inner tag), style_vars (own responsive keys that become CSS variables), requires (callable; when it returns false, a stub is registered that keeps the content and shows the unavailable message in the editor), dynamic (depends on context), script (needs the runtime), tag (default wrapper tag), keywords, description.

$r->wrap() must be used for the outermost tag: it adds classes, id/anchor, aria-label, effects, background video and editor markers. $r->children( $node, $ctx ) renders children; $r->slot_attr() marks where children are when they are not directly inside the wrapper.

Motion recipes #

add_action( 'midground/register_recipes', function () {
	midground_register_recipe( 'my-sway', array(
		'label'   => __( 'Sway', 'my-textdomain' ),
		'group'   => 'through',            // enter|through|exit|appear|pointer|hover|reveal
		'trigger' => 'view',               // view|page|pin|appear|pointer|hover|reveal
		'keys'    => array( array( 0, array( 'rz' => -4 ) ), array( 1, array( 'rz' => 4 ) ) ),
	) );
} );

Channels: tx, ty, tz (px), px, py (drift as a share of the screen width / scroll distance), rx, ry, rz (degrees), s (scale, rest 1), o (0–1, rest 1), cx, cy (clipping from the right/bottom in %). Recipes are resolved on the server to keyframes scaled by strength and screen-size factors (data-mg-fx). The runtime combines effects on one element: movement and rotation add up, scale and opacity multiply, clipping takes the maximum. pointer and hover recipes have axes; pointer recipes can have a touch recipe used on touch screens. reveal recipes split the text into words or letters.

Dynamic data sources #

add_action( 'midground/register_dynamic_sources', function () {
	midground_register_dynamic_source( 'site.year', array(
		'label'   => __( 'Current year', 'my-textdomain' ),
		'group'   => 'site',
		'type'    => 'text',                // text|html|url|media|number
		'resolve' => fn( $args, $ctx ) => wp_date( 'Y' ),
	) );
} );

A binding is stored as { "dyn": "site.year", "args": {}, "fallback": "…" } in a field that allows it.

The supported plugins can be extended with filters:

The editor #

After start-up the editor sends the event midground:editor-ready with window.MidgroundEditor. Register your own content controls as React components (through wp.element):

document.addEventListener( 'midground:editor-ready', ( e ) => {
	e.detail.contentExtensions[ 'my-notice' ] = ( { node } ) =>
		wp.element.createElement( 'p', null, 'Characters: ' + ( node.props.text || '' ).length );
} );

window.MidgroundEditor.store (get/set/subscribe/change) and window.MidgroundEditor.actions (setProp, addElement, select …) are available to extensions; startTour() and showShortcuts() open the tour and the shortcut list. The runtime on the site sends midground:ready with window.Midground (engine, models, refresh( scope )).

Hooks and filters #

HookTypeUse
midground/bootedactionThe plugin has started
midground/register_elements / _recipes / _dynamic_sourcesactionRegister extensions
midground/document_savedaction( $post_id, $doc ) after a save
midground/render_contextactionAdjust the render context
midground/rest_routesactionAdd REST routes under midground/v1
midground/upgradedaction( $previous_version ) after an update
midground/site_part_replacedaction( 'header' | 'footer' ) retire the untouched default part
midground/post_typesfilterPost types that can be built with Midground
midground/migrationsfilterSchema migrations
midground/model_hostsfilterHosts allowed for external 3D models
midground/template_for_locationfilterOverride the chosen template per location
midground/query_argsfilterAdjust queries in post and product lists
midground/editor_configfilterThe configuration the editor receives
midground/always_enqueue_stylesfilterLoad Midground CSS on every page
midground/inject_header_in_other_themesfilterHeader/footer templates in other themes
midground/create_default_partsfilterCreate a header and footer on first run (default true)
midground/font_collectionfilterThe font collection behind Add fonts (default google-fonts)
midground/woo_scripts_neededfilterWhether a request needs WooCommerce's scripts
midground/license_providerfilterThe licence provider object (see section 9)
midground/guide_urlfilterWhere the editor's Help menu opens the user guide
midground/form_plugins, /booking_plugins, /consent_plugins, /seo_pluginfilterSee above
midground_render_locationfilterFor themes: ( false, 'header' ) → true when Midground rendered it
midground_page_layoutfilter (theme)Page layout in the Midground theme

PHP functions: midground_register_element(), midground_register_recipe(), midground_register_dynamic_source(), midground_is_builder_post(), midground_render_post(), midground_location().

6. REST API (/wp-json/midground/v1) #

All routes need a logged-in user and a nonce (X-WP-Nonce), and check capabilities per post.

RouteMethodCapability
/documents/{id}GET, PUTedit_post (publishing needs publish_posts, otherwise "pending")
/documents/{id}/autosavePOST, DELETEedit_post
/documents/{id}/lockPOSTedit_post
/documents/{id}/revisions, /revisions/{rev}GETedit_post
/renderPOSTedit_post for the owner
/globalsGET / PUTedit_posts / edit_theme_options
/templates/{id}GET, PUTedit_theme_options
/site-parts/{type}POSTedit_theme_options (create a header or footer)
/components, /components/{id}GET, POSTedit_posts / edit_pages
/libraryGETedit_posts
/import/{id}/preview, /import/{id}/restorePOSTedit_post
/fonts/catalog, /fonts, /fonts/{id}GET, POST, DELETEedit_theme_options
/geocodeGETedit_posts (address search through OpenStreetMap Nominatim, cached)
/integrationsGETedit_posts (forms and booking plugins found)
/aiPOSTedit_posts, the assistant turned on and a key set
/tourPOSTedit_posts (remember that the tour was seen)

Requests over 4 MB are refused (413). Saving while another user holds the lock gives 423.

7. WP-CLI #

wp midground status                    # what Midground manages; warns about edits made elsewhere
wp midground refresh [<id>...]         # render static copies and CSS again (no revisions)
wp midground-demo list                 # starter sites and whether they are imported
wp midground-demo import [--site=<slug>] [--parts=site,templates,design,fonts,shop]
wp midground-demo remove [--site=<slug>]

import without --site imports the Skumring showcase; remove without --site removes every starter site. The slugs are skumring, agency, restaurant, shop, local, experience and saas.

8. Build, tests and translations #

npm ci && npm run build            # esbuild: editor, block editor, runtime, viewer3d (+ CSS)
npm test                           # node:test – style compiler, CSS values, motion
npm run test:php                   # tests/php inside WordPress through WP-CLI (Docker)
npm run test:e2e                   # Playwright: desktop Chromium, Pixel 7, Firefox and WebKit
node tools/perf.mjs                # performance measurements
bash tools/i18n.sh                 # .pot, nb_NO .po/.mo and JSON for the editor
node tools/docs-site.mjs           # the user guide as HTML (plugin docs/ and dist/docs)
python tools/package.py            # ZIP files in dist/

The style compiler has shared test data in tests/fixtures/*.json that runs both in JS (tests/js) and PHP (tests/php/test-render.php) to make sure the results are identical.

Translations: strings are extracted from PHP and from the built JS files (so the JSON files get the right names for wp_set_script_translations). Norwegian translations are kept in tools/i18n/nb_NO.json (key domain|context|text); tools/i18n.sh merges them in.

The starter sites live in demo/midground-demo/starters/<slug>.php (content built with the helpers in starters/helpers.php and the builder's library/helpers.php). Their placeholder pictures are generated by tools/demo/placeholders.py, and the previews on the Starter sites screen by tools/demo/previews.sh.

9. Licences and updates #

Midground has a neutral licence layer so it can be sold through different shops. Choose the shop in wp-config.php of the site that sells, or in the build:

define( 'MIDGROUND_LICENSE_PROVIDER', 'edd' );                 // or 'lemonsqueezy'
define( 'MIDGROUND_LICENSE_STORE', 'https://shop.example.com' ); // EDD only
define( 'MIDGROUND_LICENSE_ITEM', 'Midground' );                // EDD product name

Other shops can be connected with the filter midground/license_provider, returning an object that implements Midground\License\Provider (activate, deactivate, check, latest). With a valid licence, updates of the plugin and the theme are offered through WordPress's normal update screens. Without a provider, the licence box says that licences are not set up.

10. Security and privacy #