Developer Quickstart: Implementing LiveCMS in 5 Minutes
Short answer: To make only certain regions of a WordPress theme editable with LiveCMS, wrap each element in an HTML comment pair such as <!-- lcms:text hero_heading --> … <!-- /lcms -->. Anything you leave unmarked stays locked. Logged-in users with edit_posts toggle LiveCMS from the admin bar and edit only the marked regions on the live page.
To integrate LiveCMS into a WordPress theme, developers must include the core CSS and JS assets and then wrap editable template regions with specific HTML comments (e.g., <!-- lcms:text field_name -->). This markup-driven approach eliminates the need for Advanced Custom Fields (ACF) and allows for real-time frontend editing without touching the WordPress dashboard. It is designed to replace the heavy "edit-save-refresh" loop that currently slows down 90% of custom theme development.
For the agency-facing overview — the gap in block-centric frontend editors, what clients should and should not touch, and when to choose LiveCMS versus Gutenberg or ACF — start with Inline Content Editor for Developer-Built WordPress Themes.
Overview
Template (PHP) LiveCMS Plugin (JS)
───────────────── ───────────────────
<!-- lcms:type name --> → Scanner finds comments
<element>content</element> → Injects data-lcms-* attributes
<!-- /lcms -->
- Developer adds HTML comments to mark editable areas
- The JS scanner runs on page load, finds comments, injects
data-lcms-*attributes on the next element - When an editor clicks, the appropriate modal opens
- On save, the REST API stores the value in WordPress (not in theme files), so the change is live immediately
- On page load, PHP renders the saved value, or the developer's default from the theme's
lcms-data/defaults/folder if nothing has been saved yet - On Publish, LiveCMS also keeps a version snapshot and a plain-text copy in
post_contentfor search
Theme Requirements: wp_head() and wp_footer()
LiveCMS loads through two standard WordPress hook points. If a template skips them, the editor never loads and the admin bar (with the ✏️ LiveCMS toggle) never renders — even for logged-in admins.
wp_head()— inside<head>, just before</head>. Loads the admin bar styles and the plugin'slive-cms.js/live-cms.css(enqueued viawp_enqueue_scripts, which runs inside this hook).wp_footer()— just before</body>. WordPress prints the admin bar markup here.
Templates using get_header() / get_footer() already fire both — nothing to do. Self-contained templates (which skip get_header()/get_footer() to avoid doubling site chrome) must fire the hooks themselves:
<!doctype html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- your design CSS/JS -->
<?php wp_head(); ?>
</head>
<body <?php body_class(); ?>>
<!-- template markup with lcms comment markers -->
<?php wp_footer(); ?>
</body>
</html>
The toggle additionally requires: logged in, a role with the edit_posts capability, and Users → Profile → "Show Toolbar when viewing site" enabled. Anonymous visitors never see the admin bar — that is normal WordPress behavior.
Agent Skill (AI coding assistants)
If you build templates with an AI coding agent (Claude Code, Codex CLI, Kiro, Gemini CLI, or any Agent-Skills-compatible tool), install the bundled skill so the agent marks up templates correctly — wrapping all editable areas with the right <!-- lcms:type field --> comments automatically.
Install (one-time):
# Kiro IDE
mkdir -p .kiro/skills
cp -r wp-content/plugins/live-cms/skills/live-cms .kiro/skills/
# Codex CLI / OpenCode / Cursor (cross-tool convention)
mkdir -p ~/.agents/skills
cp -r wp-content/plugins/live-cms/skills/live-cms ~/.agents/skills/
# Claude Code
mkdir -p ~/.claude/skills
cp -r wp-content/plugins/live-cms/skills/live-cms ~/.claude/skills/
Usage:
Once installed, ask in natural language:
- "Make this page editable with LiveCMS"
- "Add LiveCMS markers to this template"
- "Generate a LiveCMS page from this Figma frame"
Or use the slash command where supported: /live-cms
What the skill does:
The agent will automatically:
- Read your template and identify all editable content areas
- Wrap each area with the correct
<!-- lcms:type field_name -->comment markers - Generate a data-prep header block (
$d = lcms_data()+ per-field variables) - Create the matching
lcms-data/defaults/{template}.jsonfile with meaningful default values
It knows the field types (text, textarea, image, link, repeater, post), naming rules (snake_case, section-prefixed), and what NOT to wrap (nav, icons, backgrounds, footer).
See skills/live-cms/install/README.md for project-scope install and per-agent notes.
Editable regions: make only part of a template editable
An editable region is any element wrapped in a LiveCMS comment pair. The scanner injects data-lcms-* attributes only on marked elements, so the wrappers, grids, and CSS you leave unmarked cannot be edited from the front end. Choose a type for each region (text, textarea, image, link, repeater, and the others below) and give each one a unique field name.
<section class="hero"> <!-- not marked: layout stays locked -->
<!-- lcms:text hero_heading -->
<h1>Welcome</h1>
<!-- /lcms -->
<!-- lcms:link hero_cta -->
<a href="/contact">Get in touch</a>
<!-- /lcms -->
</section>
For the client-handoff side of the same idea, see How to Let Clients Update WordPress Content Without Breaking Layout.
Comment Markup Reference
Every editable area is wrapped with an opening and closing comment:
<!-- lcms:{type} {field_name} -->
<element>content here</element>
<!-- /lcms -->
{type}— the field type (see table below){field_name}— unique name in snake_case, used as the JSON key
Field Types
text — Single-line text
<!-- lcms:text hero_heading -->
<h1>Your Heading Here</h1>
<!-- /lcms -->
Editor: Single-line input
JSON: "hero_heading": "Your Heading Here"
textarea — Multi-line text
<!-- lcms:textarea intro_body -->
<p>A longer paragraph of text that editors can change.</p>
<!-- /lcms -->
Editor: Multi-line textarea styled to match the element's font
JSON: "intro_body": "A longer paragraph..."
image — Image from Media Library
<!-- lcms:image hero_image -->
<div class="image-wrapper">
<img src="<?php echo esc_url($hero_image); ?>" alt="" />
</div>
<!-- /lcms -->
Editor: Two tabs — Media Library browser + drag-and-drop upload
JSON: "hero_image": "/wp-content/uploads/2026/04/photo.jpg" (relative path)
link — Button or link (text + URL + target)
<!-- lcms:link cta_link -->
<a href="<?php echo esc_url($link_url); ?>">
<?php echo esc_html($link_text); ?>
</a>
<!-- /lcms -->
Editor: Text input + URL input + "Open in new tab" checkbox
JSON: "cta_link": { "text": "Learn More", "url": "/about", "target": "" }
Supports: Full URLs, relative paths, anchors (#), mailto:, tel:
repeater — List of items (add/remove/reorder)
<!-- lcms:repeater team_members -->
<div class="grid">
<?php foreach ($members as $m) : ?>
<div class="card">...</div>
<?php endforeach; ?>
</div>
<!-- /lcms -->
Editor: Modal with item list. Each item has: title, textarea, url, thumb (text label), image (Media Library) JSON:
"team_members": [
{ "title": "Jane", "thumb": "CEO", "textarea": "Bio text", "image": "/uploads/jane.jpg", "url": "" },
{ "title": "John", "thumb": "CTO", "textarea": "Bio text", "image": "/uploads/john.jpg", "url": "" }
]
post — Article/post picker
<!-- lcms:post kb_cards.0 -->
<div class="card">...</div>
<!-- /lcms -->
Editor: Search box + list of published posts. Click to select.
JSON: "kb_cards": [{ "post_id": 87 }, { "post_id": 0 }] (0 = auto-fill from latest)
wpblock — Gutenberg block (stored in DB, not JSON)
Generated automatically by the template loop — not manually written.
<!-- lcms:wpblock {index} {editor_type} {block_name} -->
<p>Paragraph content from Gutenberg</p>
<!-- /lcms -->
Editor: textarea for paragraphs, text input for headings, image editor for images
Saves to: wp_posts.post_content (WordPress database)
wpfield — WordPress post field (title, excerpt)
<!-- lcms:wpfield post_title text -->
<h1><?php echo get_the_title(); ?></h1>
<!-- /lcms -->
Editor: text input
Saves to: wp_posts table
wpauthor — Author picker
<!-- lcms:wpauthor post_author -->
<span><?php echo get_the_author(); ?></span>
<!-- /lcms -->
Editor: Dropdown of WordPress users
Saves to: wp_posts.post_author
wpdate — Date picker
<!-- lcms:wpdate post_date -->
<span><?php echo get_the_date('j F Y'); ?></span>
<!-- /lcms -->
Editor: Native HTML date picker
Saves to: wp_posts.post_date
PHP Helper Functions
Provided by includes/json-store.php:
// Get all content for current post/page
$data = lcms_get_content();
$data = lcms_get_content($post_id);
// Get a single field with default fallback
$heading = lcms_get('hero_heading', 'Default Value');
$heading = lcms_get('hero_heading', 'Default', $post_id);
// Get a repeater array
$items = lcms_get_repeater('team_members');
// Resolve image path to full URL
$url = lcms_image_url('/wp-content/uploads/photo.jpg');
// Returns: http://yoursite.local/wp-content/uploads/photo.jpg
// Empty string returns transparent SVG placeholder
Data Storage
themes/{theme}/lcms-data/
└── defaults/ # Developer default values per template
├── page-about-us.json # Matches template filename
└── page-lcms-test.json
- The theme's
lcms-data/folder holds only the developer's default values, committed to git with the theme - Edits made in LiveCMS are saved in WordPress (not in theme files) and go live immediately
- On Publish, LiveCMS also keeps a version snapshot and a plain-text copy of the content in
post_contentfor search and SEO - Title, excerpt, and date edits, and Gutenberg block edits, are saved directly to the post
- If a field has no saved value, the default for the page template is used
- Relative image paths keep default JSON portable across environments
REST API Endpoints
All require authentication (X-WP-Nonce header, edit_posts capability).
| Endpoint | Method | Body | Purpose |
|---|---|---|---|
/live-cms/v1/update-field |
POST | { field, post_id, value } |
Update text/textarea/link/repeater |
/live-cms/v1/upload-image |
POST | FormData: file, field, post_id |
Upload image + save path |
/live-cms/v1/update-post |
POST | { post_id, field, value } |
Update WP post field (title) |
/live-cms/v1/update-block |
POST | { post_id, block_index, value, block_type } |
Update Gutenberg block |
/live-cms/v1/update-block-image |
POST | FormData: file, post_id, block_index |
Replace block image |
/live-cms/v1/update-thumbnail |
POST | FormData: file, post_id |
Update featured image |
/live-cms/v1/update-author |
POST | { post_id, author_id } |
Change post author |
/live-cms/v1/update-date |
POST | { post_id, date } |
Change publish date |
/live-cms/v1/content/{post_id} |
GET | — | Read all JSON content |
/live-cms/v1/posts/search?q=term |
GET | — | Search posts (for post picker) |
/live-cms/v1/authors |
GET | — | List authors (for author picker) |
Template Examples
Basic page template
<?php
/**
* Template Name: My Page
*/
get_header();
$page_id = get_the_ID();
$data = lcms_get_content($page_id);
$heading = isset($data['heading']) ? $data['heading'] : 'Default';
$body = isset($data['body']) ? $data['body'] : '';
$hero_img = lcms_image_url(isset($data['hero_image']) ? $data['hero_image'] : '');
?>
<!-- lcms:text heading -->
<h1><?php echo esc_html($heading); ?></h1>
<!-- /lcms -->
<!-- lcms:textarea body -->
<p><?php echo wp_kses_post($body); ?></p>
<!-- /lcms -->
<!-- lcms:image hero_image -->
<div><img src="<?php echo esc_url($hero_img); ?>" alt="" /></div>
<!-- /lcms -->
<!-- lcms:link cta -->
<?php $cta = isset($data['cta']) && is_array($data['cta']) ? $data['cta'] : []; ?>
<a href="<?php echo esc_url($cta['url'] ?? '#'); ?>">
<?php echo esc_html($cta['text'] ?? 'Click Here'); ?>
</a>
<!-- /lcms -->
<!-- lcms:repeater features -->
<div>
<?php foreach (lcms_get('features', []) as $f) : ?>
<div>
<h3><?php echo esc_html($f['title'] ?? ''); ?></h3>
<p><?php echo wp_kses_post($f['textarea'] ?? ''); ?></p>
</div>
<?php endforeach; ?>
</div>
<!-- /lcms -->
<?php get_footer(); ?>
Single post template (with wpblock)
<?php
$blocks = parse_blocks(get_the_content(null, false));
$idx = 0;
foreach ($blocks as $block) {
if (empty($block['blockName'])) continue;
$rendered = render_block($block);
$type = 'textarea';
if ($block['blockName'] === 'core/heading') $type = 'text';
if ($block['blockName'] === 'core/image') $type = 'wpimage';
$editable = ['core/paragraph', 'core/heading', 'core/image'];
if (in_array($block['blockName'], $editable, true)) {
echo '<!-- lcms:wpblock ' . $idx . ' ' . $type . ' ' . esc_attr($block['blockName']) . ' -->';
echo $rendered;
echo '<!-- /lcms -->';
} else {
echo $rendered;
}
$idx++;
}
?>
AI / Figma MCP Integration
When using AI tools or Figma MCP to generate page templates:
- The AI/MCP generates the HTML/PHP with Tailwind styling as usual
- The developer (or AI with steering rules) adds
<!-- lcms:type name -->comments around editable content - A matching defaults JSON file is created at
lcms-data/defaults/{template-name}.json
Steering rule (.kiro/steering/lcms-page-generation.md) instructs the AI to:
- Wrap all content headings with
<!-- lcms:text --> - Wrap all body paragraphs with
<!-- lcms:textarea --> - Wrap all content images with
<!-- lcms:image --> - Wrap all CTA buttons/links with
<!-- lcms:link --> - Use snake_case field names derived from Figma layer names
- Never wrap navigation, icons, backgrounds, or structural elements
What to wrap:
- Page headings, body text, button labels →
textortextarea - Hero images, feature images →
image - CTA buttons, nav links with editable text →
link - Card grids, team lists, feature lists →
repeater - Article carousels →
post
What NOT to wrap:
- Navigation menus (managed by WP menus)
- SVG icons, decorative graphics
- Background patterns/textures
- Footer content (managed globally)
- CSS classes or layout wrappers
Plugin Files
plugins/live-cms/
├── live-cms.php # Plugin bootstrap, REST API, admin bar
├── uninstall.php # Cleanup on plugin deletion
├── readme.txt # WordPress.org listing metadata
├── LICENSE # GPL-2.0-or-later
├── README.md # This file (developer docs)
├── includes/
│ ├── json-store.php # lcms_get(), lcms_set(), lcms_image_url(), lcms_data()
│ ├── licensing.php # Permission helpers (lcms_can_edit, lcms_can_upload)
│ └── analytics.php # Opt-in activation stats (GA4 Measurement Protocol)
├── assets/
│ ├── live-cms.prod.js # Editor (minified) — comment scanner, all modals
│ ├── live-cms.css # Editor overlay styles
│ ├── lcms-logo.jpeg # Plugin branding
│ └── marked.min.js # Markdown renderer for in-admin docs
└── skills/
└── live-cms/ # AI agent skill (copy to your agent's skills dir)
├── SKILL.md
├── install/README.md
└── references/ # Detailed markup, helpers, and generation rules