Removing p Tags from Contentful List Items

Abstract image used to represent Removing <p> Tags from Contentful List Items
Image by Paweł Czerwiński.

The combination of Gatsby and Contentful together makes for a very powerful development toolset: a very fast, static, website built on top of a modern and futureproof(ish) framework, with content management that a client can easily understand and familiarise themselves with.

I've written about some of the more nuanced issues that can arise when integrating with Contentful before. Today, the awkward bit is spacing: a paragraph inside a list item is valid HTML, but the default paragraph margins may not fit the design.

Specifically: the way that Contentful likes to wrap the content of list items, inside of a <p>, resulting in generated markup that looks like this:

<ul>
  <li>
    <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
  </li>
  <li>
    <p>Nulla viverra est vel cursus facilisis.</p>
  </li>
  <li>
    <p>Nam a tempor dolor, commodo sodales felis.</p>
  </li>
</ul>

Yuck.

Contentful represents rich text as a tree of blocks. A list item can contain a paragraph, several paragraphs or a nested list, so those wrappers carry useful structure. If spacing is the only problem, adjust the CSS first. If you do remove a wrapper, limit it to a single plain paragraph directly inside the list item.

Fortunately, they do provide LIST_ITEM as a BLOCK type within @contentful/rich-text-types, which at least means we can target list items specifically when it comes to rendering them.

The discussion on the GitHub issue prompted the following narrower approach. With React imported, add this handler to your existing rendering options. It uses the children already rendered with those options:

[BLOCKS.LIST_ITEM]: (node, children) => {
  const rendered = React.Children.toArray(children);
  const onlyChild = rendered[0];
  const simpleParagraph =
    node.content.length === 1 &&
    node.content[0].nodeType === BLOCKS.PARAGRAPH &&
    rendered.length === 1 &&
    React.isValidElement(onlyChild) &&
    onlyChild.type === 'p' &&
    Object.keys(onlyChild.props).every((key) => key === 'children');

  return <li>{simpleParagraph ? onlyChild.props.children : children}</li>;
}

The handler checks the direct LIST_ITEM content and unwraps only one ordinary <p> with no extra props. It keeps multiple paragraphs, nested lists and customised paragraph elements intact, rather than running the subtree through a second renderer.

Because children have already passed through the existing rendering options, custom marks and link renderers remain in place. A custom paragraph renderer that adds props or returns a different component also keeps its wrapper. Check those cases alongside the simplest oneparagraph list before adopting the change.

Postscript

June 2026: This Contentful rendering note comes from a Gatsbyera implementation, but the underlying issue still appears in headless CMS projects: richtext renderers often produce technically valid markup that does not match the design system's expectations. In current work I would first check the renderer package, its custom node hooks and whether the list style should be solved in CSS before changing the richtext output.

Looking for technical direction?

I support teams that need senior judgement on React, Next.js, headless CMS architecture, performance, migrations, and technical SEO.