Skip to main content
Debugging

Avoiding Nested Paragraphs with Shopify Rich Text Settings

Learn why wrapping a Shopify richtext setting in a paragraph creates invalid HTML, how to render it safely, and how to test the result in a complete section.

5 min read
ShopifyLiquidRich textHTML

A common HTML-to-Liquid conversion bug is easy to miss in a Liquid file:

<p class="intro">{{ section.settings.intro }}</p>

If intro is a Shopify richtext setting, its value may already be <p>Welcome to our collection.</p>. The browser then receives a paragraph inside another paragraph, which is invalid HTML. Browsers repair the markup differently than the source suggests, CSS selectors stop behaving predictably, and screen-reader reading order can become harder to reason about.

The correct approach is to give rich text a neutral container and let the setting supply its own paragraphs.

Prerequisites and final result

You only need a development theme and permission to add a section. We will build a small editorial intro section with a plain heading, rich body copy, alignment and an optional link. Its schema is valid JSON and the output never nests a p inside another p.

Shopify's input setting documentation lists text, textarea, inline_richtext, and richtext as distinct setting types. Choosing among them is a content-model decision, not just an editor-style preference.

Why the browser changes nested paragraphs

The HTML content model does not allow a paragraph to contain another paragraph. When an HTML parser encounters this:

<p class="intro"><p>Welcome to our collection.</p></p>

it implicitly closes the first paragraph before opening the second. DevTools may show extra empty paragraphs or sibling elements that do not match the Liquid template. A selector such as .intro p will not match the text because the browser repaired the child paragraph into a sibling.

This usually starts with static input like:

<section class="editorial-intro">
  <h2>Made for everyday use</h2>
  <p>Simple materials, clear care instructions, and useful details.</p>
  <a href="/pages/materials">Read about our materials</a>
</section>

If the merchant needs links, bold emphasis, or multiple paragraphs in that body, richtext is suitable. The original p wrapper must become a div in the Liquid output.

Complete valid section

Create sections/editorial-intro.liquid:

{% style %}
  #shopify-section-{{ section.id }} .editorial-intro {
    max-width: 52rem;
    margin-inline: auto;
    padding: 3rem 1.25rem;
  }

  #shopify-section-{{ section.id }} .editorial-intro--center {
    text-align: center;
  }

  #shopify-section-{{ section.id }} .editorial-intro__body > :first-child {
    margin-top: 0;
  }

  #shopify-section-{{ section.id }} .editorial-intro__body > :last-child {
    margin-bottom: 0;
  }
{% endstyle %}

<section
  class="editorial-intro{% if section.settings.alignment == 'center' %} editorial-intro--center{% endif %}"
  {% if section.settings.heading != blank %}aria-labelledby="EditorialIntroHeading-{{ section.id }}"{% endif %}
>
  {% if section.settings.heading != blank %}
    <h2 id="EditorialIntroHeading-{{ section.id }}">
      {{ section.settings.heading | escape }}
    </h2>
  {% endif %}

  {% if section.settings.body != blank %}
    <div class="editorial-intro__body rte">
      {{ section.settings.body }}
    </div>
  {% endif %}

  {% if section.settings.link_label != blank and section.settings.link != blank %}
    <a href="{{ section.settings.link }}">
      {{ section.settings.link_label | escape }}
    </a>
  {% endif %}
</section>

{% schema %}
{
  "name": "Editorial intro",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Made for everyday use"
    },
    {
      "type": "richtext",
      "id": "body",
      "label": "Body",
      "default": "<p>Simple materials, clear care instructions, and useful details.</p>"
    },
    {
      "type": "select",
      "id": "alignment",
      "label": "Text alignment",
      "options": [
        { "value": "left", "label": "Left" },
        { "value": "center", "label": "Center" }
      ],
      "default": "left"
    },
    {
      "type": "text",
      "id": "link_label",
      "label": "Link label"
    },
    {
      "type": "url",
      "id": "link",
      "label": "Link"
    }
  ],
  "presets": [
    {
      "name": "Editorial intro"
    }
  ]
}
{% endschema %}

Choose the setting type deliberately

Use text for a short value that should be plain text, such as a label. Escape it when outputting it into HTML. A textarea also returns plain text; it is useful for longer copy when formatting is intentionally unavailable.

Use inline_richtext when limited inline formatting is appropriate but block-level paragraphs are not. It can be useful for a stylable heading-like line, although the exact surrounding semantic element is still your responsibility.

Use richtext when the content editor needs paragraph-level formatting or links. Shopify produces HTML for this setting, so render it directly inside a suitable flow container such as a div. Do not apply escape to the whole rich-text value: that would display the HTML tags as text and remove the formatting the setting was designed to provide.

Expected output

With the default body, the relevant result is equivalent to:

<div class="editorial-intro__body rte">
  <p>Simple materials, clear care instructions, and useful details.</p>
</div>

If an editor adds two paragraphs and a link, both paragraphs remain children of the same div. The .rte class also lets many themes apply their established rich-text spacing and link styles. Confirm the class name in your theme rather than assuming every theme implements it identically.

Accessibility and responsive considerations

Keep the section heading in the page's logical heading order. A reusable section should not blindly output an h1, because a template may already have the page's primary heading. This example uses h2; change it only with awareness of the full page structure.

Centered body text can become tiring across long lines, so the content width is limited. The layout needs no special mobile breakpoint: a fluid width, inline padding, and readable maximum line length work from narrow screens upward. The link label must explain its destination without relying on surrounding visual placement.

Debugging steps

  1. Open the rendered page, not just the Liquid source, in browser DevTools.
  2. Inspect the rich-text container. Its direct children should be paragraphs, lists, or other allowed rich-text output.
  3. Run the rendered HTML through an HTML validator if unexpected empty paragraphs appear.
  4. Search the section for patterns like <p>{{ section.settings..
  5. Confirm the matching schema setting is actually richtext; a similarly named plain text setting has different output rules.
  6. Remove copied hard-coded <p> tags before changing the schema type.

Common mistakes

  • Wrapping richtext in p, h1, or h2 elements.
  • Escaping the entire rich-text value and displaying literal <p> text.
  • Using richtext for a button label where plain text is safer and easier to style.
  • Adding a default rich-text value without its required HTML wrapper.
  • Styling only .editorial-intro__body and forgetting margins on its child paragraphs.
  • Assuming the theme's rich-text class and CSS without inspecting the actual theme.

Test checklist

  • Save the section with strict JSON inside {% schema %}.
  • Add one paragraph, multiple paragraphs, a list, and a link in the editor.
  • Confirm no p appears inside another p in the rendered DOM.
  • Check keyboard focus on any content link.
  • Test left and centered alignment on a narrow viewport.
  • Verify the page still has one logical primary heading.
  • Confirm empty body and link fields leave no blank wrappers.

The HTML converter now treats paragraph content as a rich-text setting without retaining an outer paragraph. The schema builder can help compare setting types, and Shopify input setting types provides a broader reference.

Conclusion

The fix is small but the reason matters: a richtext setting is already HTML, not a plain string. Render it in a neutral container, keep plain labels escaped, and inspect the browser's final DOM. That produces valid, predictable markup without taking formatting control away from the merchant.

Found this helpful?

Share it with your network!

Ready to Convert HTML to Liquid?

Try our free HTML to Liquid converter and build your Shopify themes faster.

Try HTML2Liquid Now