npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cloudcannon/editable-regions

v0.0.22

Published

Visual Editing for the CloudCannon CMS.

Readme

Editable Regions

Visual Editing for the CloudCannon CMS.

Editable Regions let you mark up any HTML output with attributes (or web components) that tell CloudCannon's Visual Editor exactly what is editable: text, images, arrays, components, and hard-coded source markup. Edits re-render the preview live and write your changes back to your structured data or source files.

Table of contents

For integration-specific setup and options checkout the dedicated docs pages:

Installation

Install the package from npm:

npm install @cloudcannon/editable-regions

For Hugo sites, install the module at a pinned version:

hugo mod get github.com/CloudCannon/editable-regions@<version>

Quickstart

Astro

Add the integration to astro.config.mjs:

import { defineConfig } from "astro/config";
import editableRegions from "@cloudcannon/editable-regions/astro-integration";

export default defineConfig({
  integrations: [editableRegions()],
});

Create a registration script that imports and registers each component that you want to live edit:

// src/scripts/register-components.js
import { registerAstroComponent } from "@cloudcannon/editable-regions/astro";
import CTA from "../components/CTA.astro";

registerAstroComponent("cta", CTA);

Load that script from your layout:

<script>
  // Check that the page is loaded in CloudCannon before loading your Editable Components
  if (window.inEditorMode) {
    import("../scripts/register-components.js").catch((error) => {
      console.warn("Failed to load CloudCannon component registration:", error);
    });
  }
</script>

If your Astro components contain React, Svelte, or Vue components, add the matching side-effect renderer import to the same registration file:

import "@cloudcannon/editable-regions/astro-react-renderer";
import "@cloudcannon/editable-regions/astro-svelte-renderer";
import "@cloudcannon/editable-regions/astro-vue-renderer";

Then mark up your templates:

<h1 data-editable="text" data-prop="title">{title}</h1>
<editable-component data-component="cta" data-prop="cta">
  <CTA {...cta} />
</editable-component>

Eleventy

Add the plugin to eleventy.config.mjs:

import editableRegions from "@cloudcannon/editable-regions/eleventy";

export default function (eleventyConfig) {
  eleventyConfig.addPlugin(editableRegions);

  return {
    dir: { input: "src", includes: "_includes", output: "_site" },
  };
}

On a CommonJS config, use require instead:

const editableRegions = require("@cloudcannon/editable-regions/eleventy");

module.exports = function (eleventyConfig) {
  eleventyConfig.addPlugin(editableRegions);

  return {
    dir: { input: "src", includes: "_includes", output: "_site" },
  };
};

After every build, the plugin emits register-components.js into your output directory, a bundle containing a client-side Liquid engine, your filters/shortcodes/tags (auto-mirrored from your real Eleventy config), and your include templates. Load it in your layout:

<script>
  // Check that the page is loaded in CloudCannon before loading your Editable Components
  if (window.inEditorMode) {
    import("/register-components.js").catch((error) => {
      console.warn("Failed to load CloudCannon component registration:", error);
    });
  }
</script>

The plugin automatically registers your Liquid includes as components.

<div data-editable="component" data-component="author-card" data-prop-author-name="authorName">
  {% include "author-card", author-name: authorName %}
</div>

The plugin also registers an includeWith tag for spreading a props object into an include:

<div data-editable="component" data-component="post-meta" data-prop="postMeta">
  {% includeWith "post-meta", postMeta %}
</div>

Hugo

Install the package as a Hugo module:

# hugo.toml
[module]
  [[module.imports]]
    path = "github.com/CloudCannon/editable-regions"

Include the partial in your site <head>:

{{ partial "editable-regions" . }}

The module automatically registers your Hugo partials as components.

<div data-editable="component" data-component="card" data-prop="card">
  {{- partial "card" .Params.card -}}
</div>

React

For a standalone React SPA, register your components from your client entry point:

// src/index.js
import { registerReactComponent } from "@cloudcannon/editable-regions/react";
import CTA from "./components/CTA.jsx";

registerReactComponent("cta", CTA);

Then mark up your templates:

<editable-component data-component="cta" data-prop="cta">
  <CTA {...props.cta} />
</editable-component>

Svelte

For a standalone Svelte SPA, register your components from your client entry point:

// src/index.js
import { registerSvelteComponent } from "@cloudcannon/editable-regions/svelte";
import CTA from "./components/CTA.svelte";

registerSvelteComponent("cta", CTA);

Then mark up your templates:

<editable-component data-component="cta" data-prop="cta">
  <CTA {...cta} />
</editable-component>

Vue

For a standalone Vue SPA, register your components from your client entry point:

// src/index.js
import { registerVueComponent } from "@cloudcannon/editable-regions/vue";
import CTA from "./components/CTA.vue";

registerVueComponent("cta", CTA);

Then mark up your templates:

<editable-component data-component="cta" data-prop="cta">
  <CTA v-bind="cta" />
</editable-component>

Next.js

In a Next.js app, wrap your page content in the EditableRegions root component. Editable regions inside it connect only after the app has finished hydrating:

// app/layout.js
import { EditableRegions } from "@cloudcannon/editable-regions/react";

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <EditableRegions tag="main">{children}</EditableRegions>
      </body>
    </html>
  );
}

Then mark up your templates — pages stay server components, the region markup is plain HTML:

// app/page.js
export default function Page() {
  return (
    <h1>
      <editable-text data-prop="title">Hello from Next.js</editable-text>
    </h1>
  );
}

Register your components from a client component, gated on window.inEditorMode so the code stays out of production bundles:

// app/components/CloudCannon.js
"use client";

import { useEffect } from "react";
import { registerReactComponent } from "@cloudcannon/editable-regions/react";
import CTA from "./CTA.jsx";

export default function CloudCannon() {
  useEffect(() => {
    if (window.inEditorMode) {
      registerReactComponent("cta", CTA);
    }
  }, []);

  return null;
}

For a fully static site, export the pages as plain HTML — the build writes the static site to out/ with no server runtime needed:

// next.config.js
const nextConfig = {
  output: "export",
};

export default nextConfig;

SvelteKit

In a SvelteKit app, wrap the routed page in the EditableRegions root component in the root layout. Editable regions inside it connect only after the app has finished hydrating:

<!-- src/routes/+layout.svelte -->
<script>
  import { onMount } from "svelte";
  import {
    EditableRegions,
    registerSvelteComponent,
  } from "@cloudcannon/editable-regions/svelte";
  import CTA from "$lib/components/CTA.svelte";

  let { children } = $props();

  onMount(() => {
    if (window.inEditorMode) {
      registerSvelteComponent("cta", CTA);
    }
  });
</script>

<EditableRegions tag="main">
  {@render children()}
</EditableRegions>

Then mark up your templates:

<!-- src/routes/+page.svelte -->
<h1>
  <editable-text data-prop="title">Hello from SvelteKit</editable-text>
</h1>

For a fully static site, use the static adapter and prerender every route — the build writes the static site to build/:

// svelte.config.js
import adapter from "@sveltejs/adapter-static";

const config = {
  kit: {
    adapter: adapter(),
  },
};

export default config;
// src/routes/+layout.js
export const prerender = true;

Nuxt

In a Nuxt app, wrap the routed page in the EditableRegions root component in the layout. Editable regions inside it connect only after the app has finished hydrating:

<!-- app/layouts/default.vue -->
<script setup>
import { EditableRegions } from "@cloudcannon/editable-regions/vue";
</script>

<template>
  <EditableRegions tag="main">
    <slot />
  </EditableRegions>
</template>

Tell Vue to treat the editable-* elements as native elements rather than components:

// nuxt.config.ts
export default defineNuxtConfig({
  vue: {
    compilerOptions: {
      isCustomElement: (tag) => tag.startsWith("editable-"),
    },
  },
});

Then mark up your templates:

<!-- app/pages/index.vue -->
<template>
  <h1>
    <editable-text data-prop="title">Hello from Nuxt</editable-text>
  </h1>
</template>

Register your components from a client plugin, gated on window.inEditorMode so the code stays out of production bundles:

// app/plugins/cloudcannon.client.ts
import { registerVueComponent } from "@cloudcannon/editable-regions/vue";
import CTA from "../components/CTA.vue";

export default defineNuxtPlugin(() => {
  if (window.inEditorMode) {
    registerVueComponent("cta", CTA);
  }
});

For a fully static site, configure Nitro to prerender the site by crawling from the index page, then build with nuxt generate — the build writes the static site to .output/public:

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    prerender: {
      crawlLinks: true,
      routes: ["/"],
    },
  },
});

How editable regions work

An editable region is a piece of your page that the Visual Editor can edit in place. When your page is loaded in the Visual Editor, CloudCannon hydrates your elements into editable regions. Outside of CloudCannon your elements are plain HTML; your users see exactly the markup you authored.

The Editable Regions can edit any structured data configured in your CloudCannon site, including your page front matter and content, your datasets, and your collections.

Defining editable regions

You define one by adding an HTML attribute to an existing element, or by wrapping content in the equivalent web component:

| Attribute | Web component | | --- | --- | | data-editable="text" | <editable-text> | | data-editable="image" | <editable-image> | | data-editable="source" | <editable-source> | | data-editable="array" | <editable-array> | | data-editable="array-item" | <editable-array-item> | | data-editable="component" | <editable-component> |

The two forms are equivalent. Data attributes are useful when you are attaching editable regions to existing markup, whereas the web components are useful when there isn't existing markup to attach an attribute to or you don't have direct access to the markup.

<h1 data-editable="text" data-prop="title">{title}</h1>

<editable-image data-prop-src="src">
    <MyFigureComponent src={src} />
</editable-image>

Passing values to editable regions

Every editable region receives the value it edits through a data-prop selector. Selectors point into your data, relative to the root of the current file (typically its front matter) or the nearest editable ancestor. The region resolves the selector and re-renders as that value changes; edits write back through the same selector.

<h4 data-editable="text" data-prop="name">{testimonial.name}</h4>

Regions can also receive multiple named values with data-prop-* attributes. The attribute suffix is the name the value is passed under, and the attribute value is a second selector:

<img
  data-editable="image"
  data-prop-src="heroImage"
  data-prop-alt="heroImageAlt"
/>

Here the image region receives two values: src selected from heroImage, and alt selected from heroImageAlt. The names and types of values each region expects are listed in its Options section below.

Value regions

Value regions edit a single value. They are the leaves of your page and don't allow any other editable regions to nest within them.

Text

Add data-editable="text" and a data-prop naming the structured data key to edit:

<h4 data-editable="text" data-prop="name">{testimonial.name}</h4>

Or use the editable-text web component:

<h4><editable-text data-prop="name">{testimonial.name}</editable-text></h4>

Editing file content

If the element you want to edit is the file's body content (Markdown/HTML content rather than a front matter key) you can use the special @content syntax (See Referencing external data for more information).

<div data-editable="text" data-prop="@content">
    <Content />
</div>

By default @content refers to the content of the current page. If you want to reference another page's content you can combine it with another external data selector like @file.

<div data-editable="text" data-prop="@file[README.md].@content">
    <Content />
</div>

Options

| Option | Type | Required | Description | | --- | --- | --- | --- | | data-prop | Selector | Yes | The data to edit, selected value must be a string. | | data-type | "span" | "text" | "block" | No | Controls the editing surface: span edits plain text, text edits paragraph-level rich text, and block edits multi-paragraph rich text. | | data-defer-mount | Boolean | No | When set, the editor only initializes the region the first time it is clicked, instead of on page load. Defaults to false. |

The data-type default is calculated from the value being edited: regions editing rich text — source regions, @content, or values with a Rich Text input configured — default to block or text, and plain values default to span. If the value has no configured input, CloudCannon falls back to the family of the host element (e.g. an h2 behaves as text, a div as block).

Image

Add data-editable="image" to (or around) an <img> element and bind its attributes with data-prop-*:

<img
  data-editable="image"
  data-prop-src="heroImage"
  data-prop-alt="heroImageAlt"
  data-prop-title="heroImageTitle"
  src={heroImage}
  alt={heroImageAlt}
  title={heroImageTitle}
/>

Or use the editable-image web component:

<editable-image data-prop-src="heroImage" data-prop-alt="heroImageAlt">
  <img src={heroImage} alt={heroImageAlt} />
</editable-image>

The region must contain an <img> element, either as its child or as the element itself.

Clicking an image region opens the Edit Image panel in the Visual Editor: upload from your machine, pick an existing file from the repository or a DAM, and edit alt/title details.

Editing picture elements

Image regions can also wrap a <picture> element. Put the region attributes on the <picture>; when the image changes, CloudCannon updates the preview URL on the <source> elements as well as the <img>:

<picture data-editable="image" data-prop-src="heroImage" data-prop-alt="heroImageAlt">
  <source srcset={heroImageAvif} type="image/avif" />
  <img src={heroImage} alt={heroImageAlt} />
</picture>

Options

| Option | Type | Required | Description | | --- | --- | --- | --- | | data-prop | Selector<{ src: string, alt?: string, title?: string}> | No | Selects an object holding the image details. Each image attribute binds to <selector>.src, <selector>.alt, and <selector>.title. | | data-prop-src | Selector | No | Selects the data for the image source. | | data-prop-alt | Selector | No | Selects the data for the alt text. | | data-prop-title | Selector | No | Selects the data for the title text. |

At least one selector is required. When you bind an object with data-prop, its value should only contain the src, alt, and title keys; anything else raises an error card suggesting you remove the extra binding.

Structural regions

Structural regions define the shape of your page rather than a single value. Unlike value editable regions, structural editable regions can have other editable regions nested within them and they can compose with each other.

Components

A component region binds a piece of markup to a registered component that the editor can re-render as its data changes.

<editable-component data-component="cta" data-prop="cta">
  <CTA {...cta} />
</editable-component>

Options

| Option | Type | Required | Description | | --- | --- | --- | --- | | data-component | String | Yes | The key the component was registered under (in Hugo, the partial path with extension, e.g. card.html). | | data-prop | Selector | No | The data passed to the component as props. |

Inside the component's own file, add more editable regions for its text, images, and nested arrays. When a user edits the component's data in the Visual Editor, the registered renderer re-renders the component and the result is updated in place.

Arrays and array items

An array region defines a region of markup that is built from a list of repeated elements. An array item region defines the boundary of a single element in that array. Array and array item regions always work as a pair, and as a rule of thumb they will be positioned on either side of the loop in your template.

<ul data-editable="array" data-prop="testimonials">
  {testimonials.map((testimonial) => (
    <li data-editable="array-item">
      <h4 data-editable="text" data-prop="name">{testimonial.name}</h4>
      <p data-editable="text" data-prop="quote">{testimonial.quote}</p>
    </li>
  ))}
</ul>

Adding items to an array

When a user adds an item in the Visual Editor, the editor has to invent markup for the new item. How it does that depends on how the array is set up:

Homogeneous arrays with a component

If the array names a component with data-component, every item is rendered by that component. A new item is created as a bare array item, and the registered component renders it from the item's data alone; no template markup is needed.

<ul data-editable="array" data-prop="testimonials" data-component="testimonial">
  {testimonials.map(() => (
    <li data-editable="array-item" data-component="testimonial">
      <Testimonial />
    </li>
  ))}
</ul>
Homogeneous arrays with a template

A <template> element inside the array region is a blueprint for new items. A template without a data-id is the blueprint for every new item:

<ul data-editable="array" data-prop="features">
  <template>
    <li>
      <h4 data-editable="text" data-prop="title"></h4>
      <p data-editable="text" data-prop="description"></p>
    </li>
  </template>
</ul>

Multi-root templates are automatically wrapped in an editable-array-item. With neither a template nor a component, the editor clones the markup of an existing item instead; if the array is empty and has neither, you get an error card naming what to add.

Heterogeneous arrays with an id key and component key

When items have different shapes, each item names its own component. The array's data-id-key and data-component-key tell the editor which item keys hold the id and the component name; see page building below for a full setup. A new item is created as a bare array item, and its component renders it:

<main data-editable="array" data-prop="contentBlocks" data-component-key="_name">
  <section data-editable="array-item" data-id="HeroBlock" data-component="HeroBlock">
    <h2 data-editable="text" data-prop="title"></h2>
  </section>
</main>

Each item's _name value is used as both its id and its component, so a new item named HeroBlock is rendered by the registered HeroBlock component.

Heterogeneous arrays with an id key and template

A keyed <template data-id="…"> is the blueprint for new items with that id; each new item picks its shape from the right template. The item's id comes from the array's data-id-key; a plain <template> (no data-id) is the fallback for ids without a keyed template.

<ul data-editable="array" data-prop="blocks" data-id-key="type">
  <template data-id="hero">
    <section class="hero">
      <h2 data-editable="text" data-prop="title"></h2>
    </section>
  </template>
  <template data-id="stats">
    <section class="stats"><!-- ... --></section>
  </template>
</ul>

Special props

Two selectors are provided by the array structure itself rather than your data:

  • @length — provided by array regions; the item count.
  • @index — provided by each array item; the item's position.

Reference them like any other selector, with data-prop="@length" or data-prop="@index".

Options

Array region:

| Option | Type | Required | Description | | --- | --- | --- | --- | | data-prop | Selector | Yes | The array to edit. | | data-id-key | String | No | The item key that holds each item's unique identifier. See page building. | | data-component | String | No | The registered component to render items that don't resolve a component from the component key. | | data-component-key | String | No | The item key that holds the name of the registered component to render each item. | | data-direction | "row" | "column" | "row-reverse" | "column-reverse" | No | The direction the items are laid out in, so item controls and drag-and-drop work correctly. Defaults to the element's flex direction when it is a flex container, otherwise column. |

Array item region:

| Option | Type | Required | Description | | --- | --- | --- | --- | | data-id | String | No | The item's unique identifier, used to match items to <template> blueprints and reconcile the DOM as items move. Usually populated from the array's data-id-key. | | data-component | String | No | The registered component used to render this item. Usually populated from the array's data-component-key. |

The array region assigns each item its data-prop (the item's index) at hydration; you don't set it yourself. Array item regions must be the immediate child of the array element — keep the pair as close together as possible.

With the regions in place, editors can add, duplicate, delete, reorder, and drag items — with the preview updating live.

How prop selectors compose

Every data-prop selector is relative to the value of the nearest editable ancestor. The chain composes from the top of the page down:

<main data-editable="array" data-prop="blocks">
  {blocks.map((block, i) => (
    <section data-editable="array-item">
      <h2 data-editable="text" data-prop="title">{block.title}</h2>
      <img data-editable="image" data-prop-src="image" src={block.image} />
    </section>
  ))}
</main>

Reading the regions inside-out:

  • The array region holds blocks — the whole array.
  • Each array item is handed one entry: the array gives its items selectors blocks.0, blocks.1, …
  • The text region inside the item resolves title against that value: blocks.0.title.

Writes travel the same chain in reverse. Editing the <h2> dispatches a change event for title; each ancestor prepends its own segment until the root region executes blocks.0.title = "New value" against the underlying file. That's also why the best practice is to add regions parent-first: a region nested inside an array or component can't resolve its selector until the ancestors exist.

Page building

Combining the heterogeneous array setup with registered components gives you page building: each item in the array is a content block, and editors can add, reorder, and edit blocks to compose the page.

<main
  data-editable="array"
  data-prop="contentBlocks"
  data-id-key="_name"
  data-component-key="_name"
>
  {contentBlocks.map((block) => {
    const Component = components[block._name];
    return (
      <section data-editable="array-item" data-id={block._name} data-component={block._name}>
        <Component {...block} />
      </section>
    );
  })}
</main>

With front matter like:

contentBlocks:
  - _name: HeroBlock
    title: We're on a mission
    description: Lorem ipsum dolor sit amet…
  - _name: StatsBlock
    stats:
      - figure: $200m
        text: Venture capital raised
      - figure: 40+
        text: Amazing team members
  - _name: ContactBlock
    text: Want to get in contact with us?
    button: Click here

Register each block component, and editors can add any block type, reorder them, and edit each one's contents. Keeping _name as both the id and the component key is common and convenient, but they can be separate keys. If both keys are the same then you only need to specify data-component-key and data-id-key will default to the same value.

Referencing external data

data-prop selectors are relative to the current file (or the nearest editable ancestor). To point at data that lives somewhere else, start the selector with an @ reference:

| Reference | Resolves to | | --- | --- | | @collections[blog] | The blog collection — usable directly as an array region's value | | @data[nav] | The nav dataset (data files managed outside the page) | | @file[about.md] | Another file's structured data | | @file[about.md].@content | Another file's body content | | @content | The current file's body content |

<ul data-editable="array" data-prop="@collections[blog]">
  <li data-editable="array-item">
    <h4 data-editable="text" data-prop="title" />
  </li>
</ul>

Notes:

  • @content on a text region gives you a rich text editor over the file's Markdown/HTML body — see Editing file content for details and examples.
  • Unqualified selectors after an @ reference (e.g. @file[about.md].seo.title) continue into the resolved object.

Literal props

For values that don't live in any file at all, data-literal-prop-* attributes pass literal values to a region. The attribute suffix is the name the value is passed under (see passing values to editable regions):

<editable-component
  data-component="author-card"
  data-literal-prop-name="C. Kent"
  data-literal-prop-role="Support engineer"
/>

Here the author-card component receives { name: "C. Kent", role: "Support engineer" }.

Values that parse as JSON are passed as structured values:

<editable-component
  data-component="author-card"
  data-literal-prop-social='{"twitter": "@cloudcannon"}'
/>

Here the component receives { social: { twitter: "@cloudcannon" } }. Values that don't parse as JSON are passed as plain strings.

Source regions

Value and structural regions all edit structured data, and your templates re-render it. Source regions are the exception: they edit the hard-coded markup of the template file itself.

How they work

<p
  data-editable="source"
  data-path="src/pages/about.astro"
  data-key="about-intro"
>
  We've been building websites since 2014.
</p>

When the file changes in the editor, the runtime fetches the raw source of the file, finds the element by locating its data-key attribute, and then edits its content directly in the source file.

Limitations

  • Static markup only. The region edits literal HTML in the file. If the element contains any templating you must have a matching Snippet configured, otherwise it cannot be safely parsed by the editor.
  • data-key values must be unique within a file, and the attribute must survive into the built output.
  • Because source editable regions edit the source directly, edits from one region will update all other places where that file is referenced. Any dynamic content that isn't configured as a Snippet will be overwritten with static content.

When to use them

Use a source region when there is no structured data key behind the text. For example, landing pages and standalone templates with hard-coded copy, or a call-to-action baked into a layout. It's the escape hatch that makes hard-coded HTML editable without restructuring your templates around a data file.

If you can move the value into front matter or a data file, prefer a text region: data-driven regions re-render live and keep your content where content belongs.

Options

| Option | Type | Required | Description | | --- | --- | --- | --- | | data-path | String | Yes | The template file the text lives in, relative to the site root. A leading / is optional. This is a file path, not a selector. | | data-key | String | Yes | The name identifying this region within the file. Must be unique among all data-key values in the same file. | | data-type | "span" | "text" | "block" | No | Controls the rich text editing surface, as with text regions. Source content is edited as HTML. | | data-input-config | JSON object | No | Inline input configuration for the editor, as a JSON object. Takes precedence over data-input. | | data-input | String | No | The slug of the CloudCannon input configuration to resolve for this region, used when data-input-config isn't set. |