Basic · 5 of 5

Multi-page

Drop multiple Builder instances on one host page — header, body, footer side-by-side. Each carries its own JSON state; events stay scoped to the instance that emitted them.

Walkthrough

  1. Unique mount IDs per instance

    Every new Builder({ mainContainer }) needs a CSS-selector-unique mount point. The convention shown below: #header-builder · #body-builder · #footer-builder with matching widget + settings containers per builder.

    HTML
    <section class="editor">
      <h2>Header</h2>
      <div id="header-builder"            class="frame"></div>
      <div id="header-widgets"            style="display:none"></div>
      <div id="header-settings"           style="display:none"></div>
    </section>
    
    <section class="editor">
      <h2>Body</h2>
      <div id="body-builder"              class="frame"></div>
      <div id="body-widgets"              style="display:none"></div>
      <div id="body-settings"             style="display:none"></div>
    </section>
    
    <section class="editor">
      <h2>Footer</h2>
      <div id="footer-builder"            class="frame"></div>
      <div id="footer-widgets"            style="display:none"></div>
      <div id="footer-settings"           style="display:none"></div>
    </section>
  2. Three Builder instances, one shared set of ingredients

    Resolve the theme bundle ONCE server-side (same ThemeRegistry::resolveBundle() as Quick Start) and emit the four named globals (THEME_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL). Then mount N Builders, each with its own canvas / widgets / settings IDs but sharing the same templates + config. The instances are isolated — selecting in the header doesn't deselect in the body.

    JavaScript
    // Server already emitted THEME_JSON / THEME_TEMPLATES / THEME_CONFIG_DATA / MEDIA_URL
    // for the SimpleText sample.
    const builders = {};
    
    for (const slug of ['header', 'body', 'footer']) {
      const builder = new Builder({
        mainContainer:     '#' + slug + '-builder',
        widgetsContainer:  '#' + slug + '-widgets',
        settingsContainer: '#' + slug + '-settings',
      });
      builder.load(THEME_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
      builders[slug] = builder;
    }
    
    // Different sample per instance? Resolve N bundles server-side
    // (one per sample) and pass distinct globals like
    // `window.HEADER_THEME_JSON` / `window.BODY_THEME_JSON` / etc.
  3. Save all three together

    Compose a single payload by reading getHtml() + getData() from every instance. getData() returns the JSON shape (everything needed for round-trip load()); getHtml() returns the rendered HTML string. Send as one POST so your backend persists the whole page atomically.

    JavaScript
    document.querySelector('#save-all').addEventListener('click', async () => {
      const sections = Object.fromEntries(
        Object.entries(builders).map(([slug, b]) => [
          slug,
          { html: b.getHtml(), data: b.getData() },
        ])
      );
      await fetch('/backend/save.php', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ slug: 'my-page', sections }),
      });
    });

Live demo

demo-mini-builder--minimal
Section
Section
Section

The whole snippet

Click Copy on any file to grab it byte-identical, or Expand to read inline (capped at ~520 px so the page stays scannable). Multi-file snippets surface a side-by-side grid — copy only the files you need.

<?php
/**
 * snippet.php — three Builder instances on one host page (header / body
 * / footer) sharing one server-resolved bundle, plus a #save-all button
 * that POSTs all three sections together.
 *
 * Drop this file alongside `dist/`, `themes/default/`, and the W2-shipped
 * `backend/` dir, run `php -S localhost:8000`, open
 * http://localhost:8000/snippet.php — three independent canvases stack
 * vertically; click Save All Sections to round-trip the composed payload.
 *
 * Triple-surface parity (D13.B.parity):
 *   The live demo on /examples/basic/5-multi-page/ proves multi-instance
 *   isolation; this snippet adds the #save-all button described in Step 3
 *   so the same multi-instance pattern is end-to-end runnable.
 *
 * Why one bundle, three mounts:
 *   `ThemeRegistry::resolveBundle()` reads the sample + every referenced
 *   template ONCE server-side. Three Builders share the same bundle —
 *   THEME_TEMPLATES / THEME_CONFIG_DATA / MEDIA_URL are identical across
 *   sections; only the canvas mount selector differs.
 */
declare(strict_types=1);

require_once __DIR__ . '/../../../backend/_lib/ThemeRegistry.php';

$bundle = (new \DemoBuilder\ThemeRegistry(__DIR__ . '/../../../themes'))
    ->resolveBundle('default', 'master/sample/email/SimpleText');
?>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>My Page Builder — Multi-page</title>
    <link rel="stylesheet" href="/dist/builder.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=block">
    <style>
        body { margin: 0; padding: 24px; font-family: system-ui, sans-serif; max-width: 1200px; margin: 0 auto; }
        .my-toolbar    { display: flex; align-items: center; gap: 8px; padding: 12px 0; border-bottom: 1px solid #e5e7eb; margin-bottom: 24px; }
        .my-toolbar__btn { padding: 8px 16px; background: #2563eb; color: #fff; border: 0; border-radius: 4px; font: inherit; cursor: pointer; }
        .my-toolbar__btn:hover { background: #1d4ed8; }
        .my-toolbar__status { margin-left: 8px; font-size: 12px; color: #6b7280; }
        .editor        { margin-bottom: 32px; }
        .editor h2     { margin: 0 0 8px; font: 600 16px system-ui; color: #374151; }
        .editor .frame { height: 320px; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }
    </style>
</head>
<body>

<div class="my-toolbar">
    <h1 style="margin: 0; font-size: 18px;">My multi-section page</h1>
    <button id="save-all" type="button" class="my-toolbar__btn" style="margin-left: auto;">Save all sections</button>
    <span id="save-all-status" class="my-toolbar__status"></span>
</div>

<section class="editor">
    <h2>Header</h2>
    <div id="header-builder" class="frame"></div>
</section>
<section class="editor">
    <h2>Body</h2>
    <div id="body-builder" class="frame"></div>
</section>
<section class="editor">
    <h2>Footer</h2>
    <div id="footer-builder" class="frame"></div>
</section>

<script>
    // PHP → JS handoff: four named ingredients for builder.load(), in order.
    // Same convention as 1-quickstart / 2-hello-world / 4-dark-mode (see
    // BUILDER.md RULE I). One bundle, three Builder instances below.
    window.THEME_JSON        = <?= json_encode($bundle->themeJson,      JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
    window.THEME_TEMPLATES   = <?= json_encode($bundle->themeTemplates, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
    window.THEME_CONFIG_DATA = <?= json_encode($bundle->configData,     JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
    window.MEDIA_URL         = <?= json_encode($bundle->mediaUrl,       JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
</script>

<script src="/dist/builder.js"></script>

<script>
    // Three Builder instances, one shared set of ingredients. Each is
    // fully isolated — events / pinSet / recent-colors / active-controls
    // are owned per-Builder (see BUILDER.md RULE G). `widgetsContainer:
    // null` + `settingsContainer: null` opt-out cleanly via W6.A.A.1
    // null-tolerance.
    const builders = {};
    for (const slug of ['header', 'body', 'footer']) {
        const b = new Builder({
            mainContainer:     '#' + slug + '-builder',
            widgetsContainer:  null,
            settingsContainer: null,
        });
        b.load(THEME_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
        builders[slug] = b;
    }

    // Save-all: walk the 3 instances, compose one payload, POST atomically.
    document.getElementById('save-all').addEventListener('click', async () => {
        const status = document.getElementById('save-all-status');
        status.textContent = 'Saving...';
        const sections = Object.fromEntries(
            Object.entries(builders).map(([slug, b]) => [
                slug,
                { html: b.getHtml(), data: b.getData() },
            ])
        );
        try {
            // The default /backend/save.php takes a single (dir, sample,
            // html, data) tuple — for the multi-section shape, your buyer
            // wires their own handler that walks `sections` and writes
            // each. Below shows the simplest version: stringify the whole
            // payload, write it as one JSON file. Adapt to your storage.
            const fd = new FormData();
            fd.append('dir',    '_b5_demo');
            fd.append('sample', 'sample/MultiPage');
            fd.append('html',   Object.values(sections).map(s => s.html).join('\n'));
            fd.append('data',   JSON.stringify({ sections }));
            const res = await fetch('/backend/save.php', { method: 'POST', body: fd });
            const j   = await res.json();
            status.textContent = j.status === 'success' ? 'Saved ✓ all 3 sections' : ('Error: ' + (j.message || 'unknown'));
        } catch (err) {
            status.textContent = 'Error: ' + err.message;
        }
    });
</script>
</body>
</html>

Notes

Memory cost: each Builder instance carries its own JSON state, history stack, and DOM tree. Three builders is fine; thirty on one page would be wasteful — at that scale, switch to a single builder + a sidebar of "section" tabs.

Cross-instance interactions. The instances don't share state by default. If you need them to (e.g. a colour applied in the header should propagate to the body), wire it through the public events bus: builders.header.events.on('format:change', (data) => builders.body.applyFormat(data)).

Different themes per builder. Each new Builder({...}) can load a different theme JSON. The widgets palette is per-instance; the canvas paints whichever templates the loaded theme exposes.

Save shape. The snippet's POST shape is illustrative — your buyer might prefer a flat array, or one POST per builder. Save to MySQL shows how to denormalise into a relational schema.

The live demo above proves multi-instance isolation — three Builders, three independent canvases, three independent histories. The snippet.php below adds the #save-all button that walks the three instances together; drop it into your own host page to see the full pattern (header / body / footer) in production shape.