In this lesson, we'll unlock the ability to edit content directly in place Stackbit's visual editor.

This is a lesson within the Next.js + Contentful tutorial. Following it requires completing all previous steps. If you're looking for more information on inline or visual editing, please refer to the conceptual guide or the annotations reference docs.

How Inline Editing Works

Inline visual editing is made possible through the use of annotations. These are a combination of data attributes (data-sb-object-id and data-sb-field-path) that enable developers to be completely declarative in what is and is not editable on any given page.

See How Stackbit Works for a more detailed look at how inline editing and annotations work.

The rest of this lesson will involve annotating components to see inline editing come to life.

Annotation Basics

Let's begin with the basics. We'll make the heading property of the hero component editable. Then we'll move on to make the site fully editable.

Setting the Object ID

The first step is to note the ID of the content object using a data-sb-object-id attribute. This points Stackbit to the original of the content on the screen.

With Contentful, every entry has an ID through the sys.id property. This property is already transformed to id on the object by the content utility in utils/content.js.

Add id property to page

We can use this property to set the object ID for the hero component.

// components/Hero.jsx

// ...

export const Hero = (props) => {
  return (
    <div className="px-12 py-24 bg-gray-100" data-sb-object-id={props.id}>
      ...
    </div>
  )
}

As we'll see, annotations can take advantage of the HTML tree structure. That's why we place the object ID on an HTML element that wraps the content for that entry.

You can see the attribute resolve itself for any given page on which a hero component is used by inspecting the HTML output. For example, on the home page, I can navigate through the HTML tree to find the hero at the top of the page and see that the data-sb-object-id has been resolved.

Object ID resolved in DOM

Add the Field Path

The other data attribute we work with is data-sb-field-path. This is used to build a daisy-chained string of properties drilling down to the proper value.

In this case, we want to make the heading property editable. It is at the top level of the entry, so the field path is simply heading. (We'll address more complex scenarios below.)

Add this attribute to the component where the heading is rendered.

// components/Hero.jsx

// ...

export const Hero = (props) => {
  return (
    <div className="px-12 py-24 bg-gray-100" data-sb-object-id={props.id}>
      ...
      <h1 className="mb-6 text-5xl font-bold" data-sb-field-path="heading">
        {props.heading}
      </h1>
      ...
    </div>
  )
}

Edit the Heading

That's all you need to be able to edit the heading inline. Try it out!

Edit hero heading inline

Then check the Contentful entry to see that the content has been updated.

Updated contentful entry

Advanced Annotations

Let's now work through making all other content coming from Contentful inline editable with Stackbit. Through this process we'll encounter various annotation strategies.

Button Component Annotations

Let's begin with the button component. Update the component's JSX code, then read on to learn more about the changes you made.

// components/Button.jsx

// ...

export const Button = (props) => {
  return (
    <Link href={props.url}>
      <a
        className={`py-3 px-6 inline-block border-2 font-semibold rounded-md transition-all duration-300 ${
          themeClassMap[props.theme] ?? themeClassMap['default']
        }`}
        data-sb-object-id={props.id}
      >
        <span data-sb-field-path=".label">{props.label}</span>
      </a>
    </Link>
  )
}

This should look similar to the changes you made when making the heading editable in the hero component, with a bit of nuance:

  • We don't apply the object ID to the wrapping link component. This is the next/link component and it does not render unsupported attributes to the DOM. Therefore, if we passed data-sb-object-id to <Link /> nothing would be rendered to the DOM and the button would not be editable (inline).
  • You could technically choose to merge the object ID and field paths together, but the pattern shown above is a bit easier to read. The downside is that we added a new <span> element to the output.

Without making any further changes to the hero component, you should see that you can now edit the button attributes by opening the page editor panel, or change the button label directly.

Edit button component

This works because everything in Contentful is an entry with an ID value. When we specify data-sb-object-id, Stackbit knows how to piece everything together.

Hero Component Annotations

Let's wrap up the hero component by annotating what remains — body and image.

// components/Hero.jsx

// ...

export const Hero = (props) => {
  return (
    <div className="px-12 py-24 bg-gray-100" data-sb-object-id={props.id}>
      <div className={`flex mx-auto max-w-6xl gap-12 ${themeClassMap[props.theme] ?? themeClassMap['imgRight']}`}>
        <div className="max-w-xl py-20 mx-auto lg:shrink-0">
          <h1 className="mb-6 text-5xl font-bold" data-sb-field-path="heading">
            {props.heading}
          </h1>
          {props.body && (
            <Markdown options={{ forceBlock: true }} className="mb-4 text-lg" data-sb-field-path="body">
              {props.body}
            </Markdown>
          )}
          {props.button && <Button {...props.button} />}
        </div>
        <div className="relative hidden w-full overflow-hidden rounded-md lg:block">
          {props.image && (
            <Image
              src={props.image.src}
              alt={props.image.alt}
              layout="fill"
              objectFit="cover"
              data-sb-field-path="image"
            />
          )}
        </div>
      </div>
    </div>
  )
}

Notice the following:

  • We don't have to do anything further with <Markdown />. Fortunately, the component provided by the markdown-to-jsx library automatically renders the data-sb-field-path attribute.
  • We don't have to do anything to the button.

Stats Component Annotations

Last, but not least, let's round out annotations with the stats component.

// components/Stats.jsx

// ...

export const Stats = (props) => {
  return (
    <div
      className={`py-24 px-12 text-center ${themeClassMap[props.theme] ?? themeClassMap['primary']}`}
      data-sb-object-id={props.id}
    >
      <div className="mx-auto">
        <div className="mb-16">
          <h2 className="mb-4 text-4xl font-bold sm:text-5xl" data-sb-field-path="heading">
            {props.heading}
          </h2>
          <Markdown options={{ forceBlock: true }} className="sm:text-lg" data-sb-field-path="body">
            {props.body}
          </Markdown>
        </div>
        <div className="grid max-w-3xl gap-12 mx-auto sm:grid-cols-3">
          {props.stats.length > 0 && props.stats.map((stat, idx) => <StatItem key={idx} {...stat} />)}
        </div>
      </div>
    </div>
  )
}

const StatItem = (props) => {
  return (
    <div data-sb-object-id={props.id}>
      <div className="mb-3 text-4xl font-bold sm:text-5xl" data-sb-field-path="value">
        {props.value}
      </div>
      <div data-sb-field-path="label">{props.label}</div>
    </div>
  )
}

Notice here that we have two components inside a single file — Stat and StatItem. This is a fairly common pattern among React developers when the child component (StatItem) isn't going to be shared among other components.

Aside from that, the patterns introduced here should already be familiar to you, and you can now edit stats sections.

Edit stats values

Attaching Sections to the Page

We've now annotated all the components that a page can use, but we haven't done anything to the page itself. As a result, although you can add sections, there isn't an easy way to remove or reorder them.

Adding the object ID to the page template solves this problem.

// pages/[[...slug]].jsx

// ...

export default function ComposablePage({ page }) {
  return <div data-sb-object-id={page.id}>...</div>
}

Now you can remove sections from the page (which won't delete them in Contentful).

Remove section from page

And you can also navigate from the section back to the page in the page editor panel.

Navigate to page editor

That's it! We've now made out small site completely editable inline. You can already start to see how Stackbit makes content editing efficient with just a few small code changes.