Build a frontend

The actual code you write will depend on the framework you choose. You can look at these examples to help you:

What an integrated frontend looks like

Before you dive into the steps, here's what your frontend ends up doing.

To make a site editable with Hydra you break a page into:

  • Blocks layout — areas of the page that contain a list of blocks that make up your page content.
  • Blocks — discrete visual elements with a schema and settings that can be moved and edited.

- Type, title, icon etc. so the user can pick from a menu. - Fields: string, image, link etc. each with their own sidebar widget. - slate is a special field that contains JSON for a paragraph, heading etc. - blocks fields let a block hold other blocks.

When the page loads inside Hydra's edit iframe, you initialise the bridge and declare your blocks; otherwise you render normally from the API:

let bridge;

if (window.name.startsWith('hydra')) {
    bridge = initBridge({
      page: {
        schema: {
          properties: {
            blocks_layout: { allowedBlocks: ['slate', 'grid', 'myimage'] },
            header_blocks: { allowedBlocks: ['slate', 'image'], maxLength: 3 },
            footer_blocks: { allowedBlocks: ['slate', 'link'] },
          },
        },
      },
      blocks: {
        // we can add custom blocks (or alter builtin ones)
        myimage: {
          blockSchema: {
            properties: {
              image: { widget: 'image' },
              url: { widget: 'url' },
              caption: { type: 'string' },
            }
          }
        }
      },
      onEditChange: (formData) => renderPage(formData),
    });
}
else {
    // When not editing, render from the server api
    renderPage(await fetchContent(path));
}

Page data ends up shaped like this:

{
  ...
  blocks: {
    'text-1': { '@type': 'slate', ... },
    'header-1': { '@type': 'image', ... },
    'footer-1': { '@type': 'slate', ... }
  },
  blocks_layout: { items: ['text-1'] },
  header_blocks: { items: ['header-1'] },
  footer_blocks: { items: ['footer-1'] }
}

Then you augment the rendered HTML with data- attributes (or <!-- hydra ... --> comments) so Hydra can find your blocks and editable fields:

<!-- hydra edit-text=title -->
<div>Page Title</div>

<div id=content>
  <!-- hydra block-uid="1234" edit-text=title(p) edit-media=image(img) edit-link=url -->
  <a href="http://go.to">
    <img src="http://my.img"/>
    <p>A caption</p>
  </a>
</div>

The steps

The steps involved in creating a frontend are roughly the same for all these frameworks:

1. Create a Catch-All Route

Create a route for any path which goes to a single page.

For example, in Nuxt.js you create a file pages/[..slug].vue.

2. Build the Page Template

The page has a template with the static parts of your theme like header and footer. You might also check the content type to render each differently.

3. Fetch Content from Plone REST API

On page setup, take the path and make a REST API call to the contents endpoint to get the JSON for this page.

  • You can use @plone/client for this
  • In some frameworks (such as Nuxt.js) it's better to use their built-in fetch
  • You can also use the Plone GraphQL API

- Note: this is just a wrapper on the REST API rather than a server-side implementation, so it's not more efficient than using the REST API directly

4. Render Page Metadata

In your page template, fill title etc. from the content metadata.

5. Navigation

  1. Adjust the contents API call to use `@expand` and return navigation data in the same call
  2. Create a component for your top-level nav that uses this nav JSON to create a menu

6. Blocks

  1. Create a Block component that takes the id and block JSON as arguments
  2. Use if statements to check the block type and determine how to render that block
  3. If the block is a container, call the Block component recursively
  4. In your page, iterate down the blocks_layout list and render a Block component for each
  5. Rendering Slate — split into a separate component as it's used in many blocks and is also recursive

7. Helper Functions

Several helper functions get reused in many blocks:

  1. Generating a URL for links — all REST API URLs are relative to the API URL, so you need to convert these to the right frontend URL
  2. Generating a URL for an image — blocks have image data in many formats so a helper function is useful

- You may also decide to use your framework or hosting solution for image resizing

8. Listing Blocks

9. Redirects

  1. If your contents call results in a redirect, you will need to do an internal redirect in the framework so the path shown is correct
  2. If you are using SSG, you will need special code to query all the redirects at generate time and add redirect routes

10. Error Pages

If your REST API call returns an error, handle this within the framework to display the error and set the status code.

11. Search Blocks

If you choose to allow Volto's built-in Search Block for end-user customisable search:

12. Form Blocks

Form-block is a plugin that allows a visual form builder:

  • Currently not a container with sub-blocks but this could change
  • Render each field type component (or limit which are available)
  • Produce a compatible JSON submission to the form-block endpoint
  • Handle field validation errors
  • Handle the thank-you page

Deployment patterns

Hydra separates your production frontend from the editing experience, which gives you choice in how each is deployed.

SPA / Hybrid — full visual editing

The simplest setup — your frontend handles both production and editing:

  1. Deploy your frontend as SPA or Hybrid (SSR + client-side hydration).
  2. Deploy Hydra and the Plone API server.
  3. Log in to Hydra, go to user preferences, set your frontend URL.

This gives you all visual editing features including inline text editing, drag and drop, and realtime preview.

SSG / SSR — production speed + visual editing

Get the speed of static generation while keeping visual editing. Deploy two versions of the same frontend:

  1. Production — deploy your frontend in SSG or SSR mode (fast, cacheable).
  2. Editing — deploy the same frontend in SPA mode to a separate URL (used only inside Hydra).
  3. Hydra + Plone — only needs to run during editing, so scale-to-zero / serverless works.
  4. SSG rebuild — for SSG, configure collective.webhook to trigger a rebuild on edit. SSR doesn't need this.

Example: the Nuxt.js demo

The default Hydra demo uses exactly the SSG / SSR pattern above:

  • ProductionSSG on Netlify. All pages statically generated, images optimized, fast globally.
  • Editing — same Nuxt codebase deployed as SPA to a different Netlify URL. Only loaded inside Hydra's iframe.
  • Hydra + Plone — deployed to fly.io with scale-to-zero. Cost is free or minimal since it only runs during editing.

For most frameworks, switching between SSG / SSR and SPA is just a config toggle, so you get the best of both worlds with minimal effort.