Live Preview

To make your site editable with Inka you load hydra.js in your frontend and call initBridge(). This sets up a two-way communication channel that handles authentication, page navigation, and live content updates.


Setting Up the Bridge

Call initBridge() with an onEditChange callback to receive live content updates as the user edits. Your frontend re-renders in real time. Outside edit mode, fetch content from the API as normal.

import { initBridge } from './hydra.js';

let bridge;
if (window.name.startsWith('hydra')) {
    bridge = initBridge({
        onEditChange: (formData) => renderPage(formData),
    });
} else {
    renderPage(await fetchContent(window.location.pathname));
}

The formData passed to onEditChange has the same structure as the Plone REST API response, so the same rendering code works for both live editing and normal page display.

Render every formData you are given — don't run your own "has it changed?" check. The bridge already drops echoes, and only it can tell one: text typed into your page is not sent back to you, so your own copy of the page falls behind what is on screen while someone types. The bridge compares against its own copy, which does include the typing. A frontend that compares against its own data mistakes a real change for an echo — "Reset changes" after typing then sends exactly the content you last rendered, and skipping it leaves the typed text on screen.

Either hashbang (/#!/path) or normal (/path) style paths are supported.

A Simple Page Renderer

Iterate blocks_layout.items and render each block by type. Add data-block-uid so Inka knows which block the user clicked.

<!DOCTYPE html>
<html>
<head>
    <script type="module">
    import { initBridge } from './hydra.js';

    if (window.name.startsWith('hydra')) {
        initBridge({
            onEditChange: (formData) => renderPage(formData),
        });
    } else {
        renderPage(await fetchContent(window.location.pathname));
    }

    function renderPage(data) {
        document.getElementById('content').innerHTML =
            data.blocks_layout.items.map(id => {
                const block = data.blocks[id];
                return `<div data-block-uid="${id}">
                    ${renderBlock(block)}
                </div>`;
            }).join('');
    }

    function renderBlock(block) {
        switch (block['@type']) {
            case 'slate':
                return renderSlate(block.value);
            case 'image':
                return `<img src="${block.url}/@@images/image" />`;
            default:
                return `<pre>${JSON.stringify(block, null, 2)}</pre>`;
        }
    }
    </script>
</head>
<body>
    <div id="content"></div>
</body>
</html>

Allowed Blocks and Page Regions

When initialising the bridge, you can configure rules for what blocks can be added to the page and where. Pages can have multiple blocks fields for different regions (e.g., header, content, footer), each with its own allowed block types and limits. These show as separate sections in the sidebar when no block is selected:

bridge = initBridge({
    page: {
        schema: {
            properties: {
                items: {
                    widget: 'blocks_layout',
                    title: 'Content',
                    allowedBlocks: ['slate', 'image', 'hero', 'columns'],
                },
                header: {
                    widget: 'blocks_layout',
                    title: 'Header',
                    allowedBlocks: ['slate', 'image'],
                    maxLength: 3,
                },
                footer: {
                    widget: 'blocks_layout',
                    title: 'Footer',
                    allowedBlocks: ['slate', 'link'],
                },
            },
        },
    },
});