Why Hydra

Most CMS platforms force a trade-off: visual editing or frontend freedom. Hydra gives you both.

Visual Editing

True WYSIWYG with drag-and-drop blocks. No special frontend framework required — just HTML attributes.

Any Frontend

Use React, Vue, Nuxt, Next.js, or any stack. Switch between frontends mid-edit for omni-channel delivery.

Truly Decoupled

Your frontend is independent code you own. Upgrade the CMS without rewriting your site; switch frameworks and get the same editing experience.

Open Source

Secure, scalable Plone backend. Host anywhere, control your costs and security.

Quick Start

<!-- pages/[...slug].vue -->
<template>
  <!-- data-block-uid: makes block selectable, draggable, and editable -->
  <div v-for="id in page?.blocks_layout?.items" :key="id"
       :data-block-uid="editing ? id : undefined">
    <!-- data-edit-link: click to edit link URL in sidebar -->
    <a :href="page.blocks[id].link"
       :data-edit-link="editing ? 'link' : undefined">
      <!-- data-edit-media: click to pick/upload image in sidebar -->
      <img :src="page.blocks[id].image"
           :data-edit-media="editing ? 'image' : undefined" />
      <!-- data-edit-text: edit text directly in the preview -->
      <h3 :data-edit-text="editing ? 'title' : undefined">
        {{ page.blocks[id].title }}
      </h3>
      <p :data-edit-text="editing ? 'description' : undefined">
        {{ page.blocks[id].description }}
      </p>
    </a>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { initBridge } from 'hydra-js'

const page = ref(null)
const editing = ref(false)

onMounted(async () => {
  // Only init bridge when loaded inside the editor
  if (window.name.startsWith('hydra')) {
    editing.value = true
    initBridge({
      // Register custom block types with their field schemas
      blocks: {
        card: { blockSchema: { properties: {
          image: { widget: 'image' },
          title: { type: 'string' },
          description: { type: 'string' },
          link: { widget: 'url' },
        }}}
      },
      // Receive live updates as editor changes content
      onEditChange: (data) => { page.value = data }
    })
  } else {
    const res = await fetch(`/++api++${useRoute().path}`)
    page.value = await res.json()
  }
})
</script>
// app/[...slug]/page.jsx
'use client'
import { useState, useEffect } from 'react'
import { initBridge } from 'hydra-js'

export default function Page({ params }) {
  const [page, setPage] = useState(null)
  const [editing, setEditing] = useState(false)

  useEffect(() => {
    // Only init bridge when loaded inside the editor
    if (window.name.startsWith('hydra')) {
      setEditing(true)
      initBridge({
        // Register custom block types with their field schemas
        blocks: {
          card: { blockSchema: { properties: {
            image: { widget: 'image' },
            title: { type: 'string' },
            description: { type: 'string' },
            link: { widget: 'url' },
          }}}
        },
        // Receive live updates as editor changes content
        onEditChange: setPage
      })
    } else {
      fetch(`/++api++/${params.slug?.join('/') || ''}`)
        .then(r => r.json()).then(setPage)
    }
  }, [])

  if (!page) return <div>Loading...</div>

  return page.blocks_layout?.items?.map(id => {
    const block = page.blocks[id]
    return (
      // data-block-uid: makes block selectable, draggable, and editable
      <div key={id} data-block-uid={editing ? id : undefined}>
        {/* data-edit-link: click to edit link URL in sidebar */}
        <a href={block.link}
           data-edit-link={editing ? 'link' : undefined}>
          {/* data-edit-media: click to pick/upload image in sidebar */}
          <img src={block.image}
               data-edit-media={editing ? 'image' : undefined} />
          {/* data-edit-text: edit text directly in the preview */}
          <h3 data-edit-text={editing ? 'title' : undefined}>
            {block.title}
          </h3>
          <p data-edit-text={editing ? 'description' : undefined}>
            {block.description}
          </p>
        </a>
      </div>
    )
  })
}
<!-- src/routes/[...slug]/+page.svelte -->
<script>
  import { onMount } from 'svelte'
  import { initBridge } from 'hydra-js'

  let page = $state(null)
  let editing = $state(false)

  onMount(async () => {
    // Only init bridge when loaded inside the editor
    if (window.name.startsWith('hydra')) {
      editing = true
      initBridge({
        // Register custom block types with their field schemas
        blocks: {
          card: { blockSchema: { properties: {
            image: { widget: 'image' },
            title: { type: 'string' },
            description: { type: 'string' },
            link: { widget: 'url' },
          }}}
        },
        // Receive live updates as editor changes content
        onEditChange: (data) => { page = data }
      })
    } else {
      const res = await fetch(`/++api++${window.location.pathname}`)
      page = await res.json()
    }
  })
</script>

{#if page}
  {#each page.blocks_layout?.items ?? [] as id}
    <!-- data-block-uid: makes block selectable, draggable, and editable -->
    <div data-block-uid={editing ? id : undefined}>
      <!-- data-edit-link: click to edit link URL in sidebar -->
      <a href={page.blocks[id].link}
         data-edit-link={editing ? 'link' : undefined}>
        <!-- data-edit-media: click to pick/upload image in sidebar -->
        <img src={page.blocks[id].image}
             data-edit-media={editing ? 'image' : undefined} />
        <!-- data-edit-text: edit text directly in the preview -->
        <h3 data-edit-text={editing ? 'title' : undefined}>
          {page.blocks[id].title}
        </h3>
        <p data-edit-text={editing ? 'description' : undefined}>
          {page.blocks[id].description}
        </p>
      </a>
    </div>
  {/each}
{/if}
<!-- index.html -->
<div id="content"></div>
<script type="module">
  import { initBridge } from 'hydra-js'

  let editing = false

  // Only init bridge when loaded inside the editor
  if (window.name.startsWith('hydra')) {
    editing = true
    initBridge({
      // Register custom block types with their field schemas
      blocks: {
        card: { blockSchema: { properties: {
          image: { widget: 'image' },
          title: { type: 'string' },
          description: { type: 'string' },
          link: { widget: 'url' },
        }}}
      },
      // Receive live updates as editor changes content
      onEditChange: renderPage
    })
  } else {
    const res = await fetch(`/++api++${location.pathname}`)
    renderPage(await res.json())
  }

  function renderPage(page) {
    const el = document.getElementById('content')
    el.innerHTML = page.blocks_layout.items.map(id => {
      const b = page.blocks[id]
      return `
        <!-- data-block-uid: makes block selectable, draggable, and editable -->
        <div ${editing ? `data-block-uid="${id}"` : ''}>
          <!-- data-edit-link: click to edit link URL in sidebar -->
          <a href="${b.link}"
             ${editing ? 'data-edit-link="link"' : ''}>
            <!-- data-edit-media: click to pick/upload image in sidebar -->
            <img src="${b.image}"
                 ${editing ? 'data-edit-media="image"' : ''} />
            <!-- data-edit-text: edit text directly in the preview -->
            <h3 ${editing ? 'data-edit-text="title"' : ''}>
              ${b.title}
            </h3>
            <p ${editing ? 'data-edit-text="description"' : ''}>
              ${b.description}
            </p>
          </a>
        </div>`
    }).join('')
  }
</script>
Welcome to Hydra

You can use this site to test Hydra

You can log in and experience currently working features (Volto like but on any frontend)

See all Content Types
Welcome to Hydra's many frontends

You are enjoying one of many possible frontends

Frontend freedom makes it easy to create beautiful and fast experiences

See all blocks
Welcome to Hydra

Hydra is the key to have any frontend for Plone 6

Hydra sets your developers free

Read More

You can use this site to test Hydra Edit.

Disclaimer: This instance is reset every night, so all changes will be lost afterwards.

You can log in and use it as an admin user using these credentials: username: admin password: admin

This site uses some recommended add-ons:

  • Some blocks that are suitable to be used with volto-light-theme.
  • volto-form-block

View this site in other frameworks

Find out more about Hydra