WP Manifestindependent plugin directory
manifest / editor / bunnyblock

BunnyBlock releases

An experimental web site builder for WordPress. Lightweight and fast.

by BunnyCloud.IT · github.com/dimitrisp2/bunnyblock · website

0stars
5release downloads
0forks

Install

The author publishes release zips, so WP-CLI can install straight from GitHub:

wp plugin install https://github.com/dimitrisp2/bunnyblock/releases/download/v0.1.0/bunnyblock-0.1.0.zip

Readme

BunnyBlock

A small, fast, extensible page builder for WordPress. JSON documents in, semantic HTML and a static CSS file out. No frontend JavaScript unless an element genuinely needs it — and most do not.

PLEASE NOTE!

This is meant to be an experiment, not a usable product. If you decide to use it, you are on your own! The code is provided for research purposes only, and any security issues should be reported as soon as possible.

Why

Most page builders ship a runtime to the visitor. BunnyBlock does not. A typical page built with it downloads two stylesheets and nothing else — no icon library, no slider bundle sitting on a page that has no slider. The builder is a build tool, not a dependency.

A few elements are genuinely stateful and cannot be built from HTML and CSS alone; those enqueue their own small script, and only on pages that actually use them. The accordion is not one of them — it is native <details>/<summary>, and pages using it stay script-free.

Three decisions follow from that:

  • One renderer. The same PHP renders the published page, the editor canvas, and the live preview. There is no second implementation in JavaScript to drift out of sync, so the preview cannot lie to you.
  • One stylesheet per page. Element styles compile to one rule per element per breakpoint in a cacheable file. No inline style attributes, so the markup stays readable and the CSS stays cacheable by any CDN in front of it.
  • PHP-first extensibility. Registering an ordinary element is a PHP class and one add_action; the editor builds its inspector from the control schema. A genuinely stateful element can additionally override enqueue_frontend_assets(), and that asset is loaded only on documents containing the element.

Requirements

  • WordPress 7.0+ tested, 6.2+ should work as is without guarantees.
  • PHP 8.0+
  • Node 20+ (only to build the editor; not needed at runtime)

Getting started

git clone https://github.com/dimitrisp2/bunnyblock.git wp-content/plugins/bunnyblock
cd wp-content/plugins/bunnyblock
npm install
npm run build

Then activate the plugin. Edit any page or post and click Edit with BunnyBlock, from either the post list row actions or the button above the WordPress editor.

dist/ is not committed. Without it the editor screen shows a build notice instead of loading — that is the reminder to run the command above.

Command What it does
npm run dev Rebuild on change. Not a dev server: WordPress serves the editor, so there is nothing for one to host.
npm run build Typecheck, then build dist/editor.js and dist/editor.css.
npm run typecheck tsc --noEmit on its own.

There is no test suite yet.

How it works

A page is a tree of nodes stored as JSON in post meta, carrying the schema version it was written with:

{
  "version": 1,
  "root": {
    "id": "root",
    "type": "container",
    "props": { "gap": { "$t": "space.lg" } },
    "children": [
      { "id": "a1b2c3", "type": "heading", "props": { "text": "Hello", "tag": "h1" } }
    ]
  }
}

On the frontend, the_content is replaced with the rendered tree for any post that has been saved in BunnyBlock. Styles compile to uploads/bunnyblock/post-<id>.css, alongside a shared uploads/bunnyblock/global.css holding the design tokens and a sub-1KB reset. Both are regenerated on save and served as static files; if the uploads directory is not writable, the CSS is inlined instead so the page still renders correctly.

The editor canvas is an iframe pointed at the post's real URL with a nonced flag. The preview therefore renders inside the actual theme — real header, real footer, real container widths — and the editor's own chrome lives outside the frame where it physically cannot leak in. Every edit posts the working document to /render, which returns HTML and CSS produced by the same renderer the frontend uses.

Values: responsive and tokenised

Every prop value is one of four shapes:

Shape Example
Scalar "24px"
Token reference {"$t": "color.brand"}
Responsive map {"$r": {"base": "16px", "lg": {"$t": "size.xl"}}}
Composite {"top": "10px", "bottom": {"$t": "space.md"}} or [{"_id":"ri-abc","label":"English"}]

Responsiveness is a property of the value, not of the control. Any control becomes responsive with 'responsive' => true, and element authors never write a line of breakpoint code — styles() receives only the props that the breakpoint being compiled actually declares. An element with no responsive overrides emits exactly one rule and zero media queries.

Breakpoints are mobile-first min-width queries (base, sm 480, md 768, lg 1024, xl 1440), so a value set at base cascades upward and you only declare the overrides you need.

Token references compile to var(--bb-color-brand, #7c3aed) — the literal is kept as a fallback, so a page still renders correctly if the global stylesheet fails to load. A rebrand is one edit in the token panel rather than a find-and-replace across every page ever built.

Adding an element

add_action( 'bunnyblock/elements/register', function ( $registry ) {
    $registry->register( new My_Testimonial() );
} );

class My_Testimonial extends \BunnyBlock\Elements\Element_Base {

    public function type(): string  { return 'testimonial'; }   // never change once shipped
    public function title(): string { return 'Testimonial'; }
    public function icon(): string  { return 'square'; }

    public function controls(): array {
        return array(
            \BunnyBlock\Elements\Controls::group( 'content', 'Content', array(
                \BunnyBlock\Elements\Controls::richtext( 'quote', 'Quote' ),
                \BunnyBlock\Elements\Controls::color( 'color', 'Colour', array( 'responsive' => true ) ),
            ) ),
        );
    }

    public function styles( array $props ): array {
        return array(
            'color'      => $props['color'] ?? null,   // `?? null` means "only if set"
            ' cite'      => array( 'font-style' => 'normal' ),
        );
    }

    protected function render( array $props, string $children, array $attributes ): string {
        return \BunnyBlock\Render\Html::tag( 'blockquote', $attributes, $props['quote'] ?? '' );
    }
}

That is the whole story — the editor discovers the element, builds its panel, and sanitizes its props against the same schema. The shared Advanced group (padding, margin, responsive visibility, CSS classes, HTML id) is appended automatically.

One caveat: an element class's icon() method names the small Lucide icon shown for that element in the editor chrome. That map lives in src/components/Icon.tsx, and unknown names fall back to a neutral square. This is separate from Controls::icon(), the searchable content-icon picker.

Stateful elements override enqueue_frontend_assets():

public function enqueue_frontend_assets(): void {
    wp_enqueue_script(
        'my-element',
        plugins_url( 'element.js', __FILE__ ),
        array(),
        '1.0.0',
        true
    );
}

BunnyBlock calls it once per distinct element type in the document. The editor canvas preloads registered element assets because an unsaved element can be inserted after the iframe has loaded; visitor-facing pages remain conditional.

Structured repeating data uses the same schema-driven approach:

Controls::repeater(
    'people',
    __( 'People', 'my-plugin' ),
    array(
        Controls::text( 'name', __( 'Name', 'my-plugin' ) ),
        Controls::text( 'role', __( 'Role', 'my-plugin' ) ),
        Controls::link( 'profile', __( 'Profile', 'my-plugin' ) ),
    ),
    array(
        'itemLabel' => __( 'Person', 'my-plugin' ),
        'addLabel'  => __( 'Add person', 'my-plugin' ),
        'maxItems'  => 20,
    )
);

Each stored item is an ordered prop bag sanitized against those nested controls. The editor provides add, remove, collapse, keyboard-accessible move buttons, drag reorder and undo/redo. Nested fields are intentionally non-responsive. _id is reserved for stable editor identity and should be ignored by the element renderer.

Dynamic content and caching

The Post list element runs a WP_Query at render time, which makes a page's HTML depend on data outside that page. BunnyBlock itself handles that correctly — the stylesheet is compiled from props, not from query results, so it never goes stale, and the HTML is rendered per request. A third-party full-page cache is a different matter: it will keep serving an old list until the page is invalidated. Cache plugins can hook bunnyblock/post_list/rendered to register a dependency.

There is no pagination, deliberately: without JavaScript it would mean URL query parameters that collide with the main query on archive pages, multiply cache keys, and create canonical-URL problems. Use How many plus Skip instead.

Video

The Video element supports media-library files through the browser's native <video> controls and provider URLs through WordPress oEmbed. Provider markup is emitted through WordPress's own trusted oEmbed path; BunnyBlock does not admit arbitrary iframe elements or ship a video runtime of its own. Autoplayed native video is muted automatically to match browser policy. Native videos accept repeatable labelled WebVTT tracks for captions, subtitles, descriptions and chapters.

The element's Caption field is a visible figure caption, distinct from the timed text in a WebVTT track.

Tabs

The Tabs and Tab item elements render every panel as a labelled section first, then a small dependency-free script creates the tablist and wires role="tab", role="tabpanel", aria-selected, aria-controls, roving focus, Left/Right arrows and Home/End. If the script fails, all content remains visible under ordinary headings rather than becoming inaccessible. The script is loaded only on visitor pages whose document contains Tabs.

Icons

The Icon element provides a searchable bundled set of 34 Lucide icons with responsive size, colour and alignment controls. Icons are decorative by default; turn off Decorative to expose an accessible label. The selected value is a catalog name, never SVG supplied by the document.

Add-on elements can reuse the same picker with Controls::icon(). Both the React preview and PHP renderer read assets/icons/catalog.json, and PHP rejects names outside that catalog. The reviewed renderer emits the fixed geometry directly while svg and path remain outside the general HTML allowlist. Lucide is distributed under the ISC license in assets/icons/LICENSE.

Security model

Documents arriving from the editor, an import, or the REST API are never trusted. Unknown element types are dropped along with their subtree; every prop is validated against its control definition and anything without a matching control is discarded, so a crafted document cannot smuggle in a prop and hope some element echoes it. Lengths and colours are validated by pattern rather than escaped, because they land in a stylesheet, and the compiler applies a final check before any value is written. Documents nested past 50 levels are rejected.

Accessibility

The base output targets WCAG 2.1 AA — anything the renderer or the compiled CSS does on its own. Author choices are not policed: pick unreadable colours and you get unreadable colours, without a warning or an override. The line is whose decision caused the failure.

REST API

Namespace bunnyblock/v1. All routes are capability-checked (edit_post for a document, edit_theme_options for site-wide tokens).

Route Purpose
GET /config Element schemas, categories, breakpoints, tokens
GET|POST /document/<id> Read or save a document
POST /render/<id> Render an unsaved document to HTML + CSS
GET|POST /tokens Read or save design tokens

Hooks

bunnyblock/loaded · bunnyblock/elements/register · bunnyblock/elements/categories · bunnyblock/element/controls (and bunnyblock/element/{type}/controls) · bunnyblock/element/render · bunnyblock/breakpoints · bunnyblock/tokens · bunnyblock/global_css · bunnyblock/post_types · bunnyblock/document/migrations · bunnyblock/document/saved · bunnyblock/document/disabled · bunnyblock/sanitize_control · bunnyblock/post_list/query_args · bunnyblock/post_list/rendered

Working alongside the WordPress editor

A post built with BunnyBlock is labelled as such in the post list, and both the classic and block editors show a banner explaining that the body below them is not what visitors see. Back to WordPress editor hands the post back: it clears the flag and deletes the generated stylesheet, but leaves the document untouched, so opening BunnyBlock again brings the layout back exactly as it was.

The content element goes the other way — it places the post's own WordPress editor body inside a BunnyBlock layout, so a page can be laid out here and still have its prose written and edited in the block editor by people who never open the builder.

Project layout

bunnyblock.php          Plugin header and bootstrap
includes/
  Document/             Document model and schema migrations
  Elements/             Element base class, control factories, sanitizer, registry
    Types/              Container, Heading, Paragraph, Image, Content
  Render/               Renderer, CSS compiler, stylesheet files, HTML helpers
  Rest/                 REST controller
  Admin/                Editor screen, canvas preview
  Value.php             Value envelopes — the TypeScript counterpart is src/values.ts
  Tokens.php            Design tokens
  Breakpoints.php       Breakpoint definitions
src/                    Editor app (React, zustand, immer). Never shipped to visitors.
assets/                 Canvas bridge and admin stylesheets

Read the full README on GitHub →

Releases

TagPublishedAssetDownloads
v0.1.0 Jul 31, 2026 bunnyblock-0.1.0.zip 2
v0.0.6 Jul 31, 2026 bunnyblock-0.0.6.zip 0
v0.0.5 Jul 30, 2026 bunnyblock-0.0.5.zip 0
v0.0.4 Jul 30, 2026 bunnyblock-0.0.4.zip 1
v0.0.3 Jul 30, 2026 bunnyblock-0.0.3.zip 0
v0.0.2 Jul 30, 2026 bunnyblock-0.0.2.zip 1
v0.0.1 Jul 30, 2026 bunnyblock-0.0.1.zip 1