Make any WordPress theme client-editable — without giving up clean code.
Bindery lets a developer decide exactly what a non-technical client may change on a site, then gives that client a locked, multilingual, edit-in-place experience over only those things. The theme keeps its hand-written, performant markup; the client edits text on the live page like a document; nobody can break the layout.
- Requires: WordPress 6.5+ · PHP 8.1+
- License: GPL-2.0-or-later
- Status: 0.1.0
You can build a WordPress site two ways:
- Hand-coded / AI-generated themes — fast, clean, fast-loading markup. But the content is hardcoded, so the client can't change a headline or a phone number without a developer.
- Page builders (Elementor, Bricks, …) — the client can edit everything, but the output is bloated, unpredictable markup and the editing surface is a mess.
These have always been mutually exclusive: clean code or client-editable. Bindery is the missing middle. The developer keeps 100% control of the code and CSS; the client gets a tidy, locked editing surface over exactly what was declared editable — and nothing else.
It's built entirely on native WordPress primitives (the Block Bindings API,
block.json, the REST API), so it adds no markup ownership and no builder lock-in.
- Edit on the live page. A floating "✎ Edit page" button turns declared text regions into inline-editable fields. Click, type, click away — saved.
- Five ways to make content editable (mix and match):
- Settings page, zero code — tick which HTML tags become editable site-wide.
- Attribute helper — mark one element in your template editable.
- Blocks — drop a Bindery block into the block editor.
- Template tags — render a declared field anywhere in PHP.
- Auto mode — make all existing page text editable automatically.
- Locked by design. Only declared regions are editable; structure, layout and everything unmarked stay untouchable.
- Multilingual out of the box — one field, a value per locale.
- Revision history + one-click restore for every edit.
- Export / import all values (site migrations, staging → production).
- Theme-portable — verified across 10 themes (Astra, OceanWP, Neve, the default Twenty-* themes, and block/FSE themes).
A Bindery field is one editable thing — identified by a key (hero_title),
with a type (text, richtext, url, image, repeater, …) and a scope
(page = per-page, or global = site-wide).
declare a field → Bindery resolves its value → renders clean markup
(stored override per locale ?? your code default)
- Values live in their own table (
wp_bindery_values), one row per (object, field, locale) — never mixed into your markup. - The default never gets stored. If the client hasn't edited a field, the value is whatever your code says. Once they edit it, the override wins (for that locale).
- Editing is whitelisted. The REST endpoint only accepts writes to fields that actually appear on the page being edited, each sanitized by its type. A client can never write to something you didn't expose.
Architecture is a small DI container + service providers, with four pluggable seams
(field types, value sources, storage adapters, locale providers), all driven by
filters. See DEVELOPERS.md for the internals.
- Copy the
binderyfolder intowp-content/plugins/. - Activate Bindery in Plugins.
On activation it creates its tables, grants the editing capability
(bindery_edit_content) to Administrators and Editors, and adds a Bindery item
to the admin menu. (The pre-built JS in build/ is shipped — you don't need to run
a build to use the plugin. Rebuild only if you change the source: npm i && npm run build.)
Go to wp-admin → Bindery. This is all a site owner needs:
- Editable Content — turn on "Let clients edit existing page text in place" and tick which elements are editable: Headings, Paragraphs, List items, Quotes, etc. Choose which post types it applies to.
- Editing Experience — show/hide the floating button, auto-enter edit mode for editors, strict mode, and the accent colour.
- Permissions — which roles may edit (Administrators always can).
- History & Data — keep a revision history (and how many versions per field), Export / Import all values as JSON, and the delete-data-on-uninstall switch.
With "auto content editing" on, visit any page, click ✎ Edit page, and start editing the existing text. Done — no code at all.
For any user with the editing capability, Bindery adds a floating ✎ Edit page button on the front end of singular pages. Clicking it:
- outlines every editable region,
- makes them
contenteditable, - saves each on blur (per current locale),
- offers a language switcher when more than one locale exists.
Everything not declared editable is left completely alone. Press ✓ Done to exit.
The cleanest path for a developer who owns the theme. You write the markup; Bindery just prints the hooks and resolves the value.
<h1 <?php bindery_attrs( 'hero_title', array( 'type' => 'h1' ) ); ?>><?php
echo esc_html( (string) bindery_value( 'hero_title' ) );
?></h1>bindery_attrs( $key, $args )printsdata-bindery-*attributes only for capable users (visitors get clean markup) and only if the field isn't locked.bindery_value( $key )returns the resolved value (override ?? default).
The field is now editable through the front-end overlay and persists per page — and nothing else in that template is touchable.
A global (site-wide) field is identical with 'scope' => 'global' — edit it once on
any page and it updates everywhere (great for an announcement bar or footer line).
// Echo a field rendered + escaped by its type:
bindery_field( 'phone', array( 'type' => 'text', 'default' => '+1 555 0100' ) );
// Get the raw resolved value:
$tagline = bindery_value( 'tagline', array( 'default' => 'Boutique stays.' ) );
// Loop a repeater (e.g. a list of features):
foreach ( bindery_rows( 'features' ) as $row ) {
echo '<h3>' . esc_html( $row['title'] ) . '</h3>';
echo '<p>' . esc_html( $row['body'] ) . '</p>';
}| Function | Purpose |
|---|---|
bindery_value( $key, $args, $object_id ) |
Resolved value (override ?? default). |
bindery_field( $key, $args, $object_id ) |
Echo the value rendered + escaped by its type. |
bindery_get_field( … ) |
Same, returned as a string. |
bindery_rows( $key, $args, $object_id ) |
Repeater rows for looping. |
bindery_attrs( $key, $args, $object_id ) |
Print the overlay hooks onto your own tag. |
bindery_register_field( $key, $args ) |
Declare a field explicitly (e.g. in functions.php). |
Common $args: type (h1–h6, p, span, text, richtext, link,
image, repeater, …), default, scope (page | global), locked (visible
but not editable), capability, label.
Bindery ships eight self-contained blocks that store their content in the Bindery store (per locale) instead of in the post markup:
editable-text · cards (repeater grid) · slider (carousel) · image ·
button · icon · form (with submissions + spam protection) · section
(background image + inner blocks).
Their colours adapt to the active theme via --bindery-* CSS variables (with
neutral fallbacks), so they look native anywhere; a theme can set those variables
to apply its own palette. There are also ready-made block patterns (Hero,
Features, Testimonials, Contact, full Landing page) under the "Bindery" category.
Every field is locale-aware by default. The overlay's language switcher reloads the
page in a locale and edits store a value per locale; unedited locales fall back to
your code default. Plug in WPML/Polylang via the bindery/locale_provider filter,
or use the built-in provider with a ?lang= parameter.
Every edit is recorded with who changed it and when (capped per field, configurable).
From the Settings page: History tab → Export values (JSON) to download a full snapshot, or pick a file to Import.
From WP-CLI:
wp bindery export --file=values.json # export all values to JSON
wp bindery import --file=values.json # import (history-suppressed)
wp bindery history hero_title --object=9 --locale=en_US # list versions
wp bindery restore 42 # restore a field to version #42Imports are sanitised (wp_kses_post) and bounded, so a hand-edited file can't
inject scripts or exhaust storage.
- Capability-gated. Editing requires
bindery_edit_content; the settings page and its REST routes requiremanage_options. Administrators always retain the editing capability. - Whitelisted writes. The editor REST endpoint accepts writes only for fields
present on the page (discovered from its own blocks/markup), rejects
lockedfields, and re-checks the per-field capability. - Sanitized in, escaped out. Each field type sanitizes on save and escapes on
render (
esc_html,wp_kses_post,esc_url…). Auto-marked text is stored as plain text and rendered viatextContent, so injected HTML is neutralised. - Visitors get clean markup. The
data-bindery-*hooks and the overlay assets are emitted only for capable, logged-in users.
| Filter | What it controls |
|---|---|
bindery/settings |
Override any resolved setting in code (wins over the UI). |
bindery/auto_editable |
Enable/disable auto content editing per post. |
bindery/strict_overlay |
Overlay edits only hand-coded regions, not blocks. |
bindery/storage_adapter |
Swap the storage backend (table, meta, custom). |
bindery/cache_storage |
Toggle the per-request value cache. |
bindery/locale_provider |
Provide locales (WPML/Polylang adapter). |
bindery/record_history / bindery/history_cap |
History on/off and version cap. |
bindery/lock_editor / bindery/lock_mode |
Lock the block editor's structure. |
bindery/register (register custom field types/sources), bindery/booted,
bindery/activated, bindery/form_submitted.
The four registries (field types, value sources, storage adapters, locale
providers) are all filter-driven — add your own without forking. See
DEVELOPERS.md for interfaces and examples.
stored override for the current locale (any value the client saved — even "")
└─ else: the default you passed in code
The default is never persisted, so improving it in code reaches every site that
hasn't overridden the field. A field is only as editable as you declare it: remove
the declaration and the value quietly falls back to your code — nothing is ever
orphaned in the page markup. (You can reshape this rule with the
bindery/resolve_value filter or a custom value source.)
- WordPress 6.5+, PHP 8.1+.
- Runtime needs no Composer (a tiny PSR-4 autoloader ships in the plugin) and no
build (the compiled JS is in
build/). - For development:
composer install(PHPUnit, PHPStan, PHPCS) andnpm install(the@wordpress/scriptsbuild). Quality gates:composer test,composer phpstan,composer lint,npm run build.
GPL-2.0-or-later. See the plugin header for details.
