Close-up of a ridged seashell lit in orange and blue, suggesting the theme shell that prints Shopify product description HTML

Shopify Product Description HTML: Choose the Theme Render Path

Shopify Product Description HTML: Choose the Theme Render Path

Oct 29, 2026

Octavian Contis 9 minutes

Share

Open Dawn's main-product section and look at two neighbouring blocks: the title prints {{ product.title | escape }}, and the description prints {{ product.description }} with no filter on it at all.

That pair is the decision.

Shopify's Liquid reference lists product.description as the product's description HTML, the same value as product.content. The strip_html docs even demonstrate a heading and paragraphs coming out of that field unchanged. Shopify's storefront security guidance is equally plain: Liquid leaves output unescaped, and the filter reference has no sanitize filter.

When merchandising pastes supplier HTML, a PIM export, or an embed into the admin description editor, Dawn ships that string as part of the page. Horizon's product-description block does the same when its rich-text setting is empty: product.description is the fallback text, printed without escape.

That output line is the description pipe.

Open the live product template

Do this on an unpublished theme copy, then on the published theme if they differ.

  1. Open the product JSON template the live PDP uses.
  2. Find the section or block that claims to show the description.
  3. Open that Liquid file and write down the exact output line.
  4. Note whether escape, strip_html, metafield_tag, or metafield_text sits on the description pipe.
  5. Note the fallback: Dawn's empty check is product.description != blank. Horizon falls back to product.description when the block's rich-text setting has no visible text.

Write the filename next to the line.

A premium theme often hides the same object behind a snippet name, so the map is wrong until you open the snippet. Then walk one product as a shopper and view source on the description region. If the published theme and the unpublished copy differ, write both output lines, because a preview that still uses Dawn's empty check can hide a Horizon fallback you already shipped.

If you see the headings and lists you typed in admin, the description pipe is raw HTML. Encoded <p> in the page means someone already escaped it, and a div.metafield-rich_text_field means you are already on metafield_tag.

You only need the pipe and the owner.

Three description pipes

Most requests arrive as a layout brief about a joke, a regimen, or a trust line when sixty products share one template. Where does the story sit so the buy box stays clean?

Those are layout questions sitting on a quieter choice: which string is allowed to become HTML.

Three description pipes cover almost every live catalogue.

Raw Liquid keeps {{ product.description }} (or Horizon's fallback) and accepts whatever the admin product editor, a bulk import, or an app with product write access saved. Headings and lists stay as markup on the shopper page, and script-shaped tags or broken paste survive with them. That is the description pipe Dawn ships, and Horizon uses the same field as fallback text when its rich-text setting is empty.

Name those writers first.

Rich-text metafield plus metafield_tag moves formatted story off the native field. Shopify documents metafield_tag as the type-specific HTML wrapper, and a rich_text_field renders as a div with class metafield-rich_text_field, so merchandisers edit a field you named while the native description stays short, empty, or plain. Apps that can write products still cannot write that metafield unless you also grant them the definition.

strip_html plus escape keeps the native field as words: strip_html removes tags and escape turns leftover < and & into text, so the shopper sees copy rather than markup. Formatted story has to live somewhere else, or it becomes a paragraph. Put strip_html first so leftover angle brackets are characters, not a second chance for a tag to survive.

A fourth idea shows up in developer threads: dump the string through json, sanitise it in the browser, then inject it. That path takes the description out of the first HTML response and adds a script to every product page. Shopify's security guidance still prefers an allowlist when the value is saved, plus the Liquid filters that already exist.

Treat JavaScript as a written exception.

Score writers and what shoppers see

Score each product type, not the theme as a whole. A gift card, a bundle, and a hero SKU often deserve different description pipes. Write the score on the same sheet as the filename, because a later theme edit will otherwise reuse the hero SKU decision on a supplier-fed variant.

Who may write the field. Staff in the product editor are one writer, a PIM sync is another, and any installed app with product write access is a third. Raw Liquid is only honest when you can name those writers and live with their HTML.

What the shopper must see. Short formatted copy (two headings, a list, bold claims) wants markup. A legal line or a supplier spec sheet often wants words only.

Whether the template is shared. One JSON product template for a whole catalogue is normal. Shared structure plus per-SKU sentences belongs in metafields. Shared structure plus one HTML dump in product.description is how sixty pages start to read identical until the dump, then unreadable.

Whether the buy box stays first. Trust and story that climb above price and add-to-cart fight the form, and the description pipe does not move that form. A metafield printed in a block below the form can carry the story instead.

Whether you will still be able to edit it next quarter. Canvas and Sidekick can add sections. A new description block that prints the native field raw undoes a metafield decision unless the owner catches it in preview. Preview that block on an unpublished theme, and write the second filename on the same sheet if the new block also prints product.description.

The description pipe

Who writes the HTML?Need formatted markup?Raw Liquid onproduct.descriptionrich_text_field plusmetafield_tagstrip_html plus escapeThe description pipe
The description pipe

Choose a description pipe

Use this matrix on the live template, then fill the last column with a name.

PipeLiquid on the themeWhat the shopper getsWho should edit itChoose this when
Raw native description{{ product.description }}Admin HTML, including tags you did not typeA named merchandiser; no unreviewed app writesTrusted staff, short formatted copy, writers listed
Rich-text metafield{{ product.metafields.namespace.key | metafield_tag }}Formatted story in a typed wrapperMerchandiser on that metafield definitionShared template, per-SKU story, native field kept boring
Plain native description{{ product.description | strip_html | escape }}Words onlyAnyone who can write products; markup cannot leakPIM or supplier HTML, or unknown writers
Words from a metafield{{ product.metafields.namespace.key | metafield_text }}Plain text from a typed fieldThe metafield ownerJSON events, meta tags, or a line that must stay text

metafield_tag on a multi_line_text_field still wraps the string and skips an allowlist. If that metafield stores pasted HTML, you are back on a raw description pipe with a different name. Prefer rich_text_field when the merchandiser needs headings and lists, because the admin control already limits the marks they can add. Printing .value yourself skips the typed wrapper and can dump markup the same way product.description does, so treat that output line as a fourth pipe you did not mean to open.

Shopify's security note for HTML that app users write is to sanitise on save with an allowlist. A custom app you own can do that before it writes a metafield, because a theme cannot invent that allowlist in Liquid.

The save step owns the allowlist.

What to pick for this catalogue

Trusted merchandiser, short formatted copy

Leave Dawn or Horizon on raw product.description if the only writers are people you can name, and the field stays a short story. Add the owner next to the description pipe, then recheck after every app install that asks for product write access.

View source once a quarter on a product you did not edit yourself. If new tags appear, a writer you missed is already on the field, and the description pipe you named in January is no longer the one shoppers receive.

List that writer before you change the Liquid.

Sixty SKUs on one template

Keep the JSON template and the buy box stable, then create one rich_text_field for the sentence that changes (joke, regimen, origin) and print it with metafield_tag under the form. Shared trust lines (returns, lab testing, years trading) belong in a section the merchandiser can hide on products that do not need them.

47 Skin's UK and US shops share one Online Store 2.0 theme. Merchandisers write launch copy, bundles, and regimen notes in sections and metafields they already own, while the Liquid file holds the layout those campaigns sit inside.

Campaign people edit the sentences.

Do not pour sixty personalities into product.description and hope the shared template will sort them. The template will print the dump, and shoppers will scroll past it.

A shared JSON template stays maintainable when the Liquid file owns structure and the metafield owns the sentence that changes, including jokes, regimen steps, and origin lines that would otherwise inflate the native field.

Supplier, PIM, or unknown app HTML

Print the native field as {{ product.description | strip_html | escape }}. Move any formatted story you still want into a metafield the sync cannot write, or sanitise on the way in if you own the app that writes it.

This is the description pipe that looks harsh in a design review and calm in a peak-traffic week. Markup from a feed cannot become a storefront script if it never leaves the field as tags. If you still want a short formatted story on those SKUs, put it in a metafield the sync is not allowed to write, and leave the native field as words.

Canvas or Sidekick just added a description block

Open the new block the way you opened Dawn, and if it prints product.description raw you have a second description pipe. Either match the pipe you already chose, or remove the block before publish.

Preview on an unpublished theme, then name who rolls it back. A second description pipe on a gift-set or bundle template is the usual reason a feed paste returns on a landing page after you cleaned the main PDP.

Name the owner

A description pipe without a name becomes raw Liquid again the next time someone pastes into admin. The four lines below are the whole record: if a line is blank, the next theme edit will guess, and the guess is almost always an unfiltered product.description.

Write four lines and keep them with the theme notes, on the same page as the unpublished-theme checklist:

  • Print line: file, filter chain, fallback
  • Writers: staff role, PIM, apps with product write access
  • Editor: the person who may change HTML or the metafield
  • Rollback: the person who reverts a Canvas or theme-editor change that reprints the native field

Finish the description pipe on one SKU before you restyle the product page. Duplicate landmarks or injected markup can wait until the pipe has a filename, a filter chain, and a named editor, because the same HTML will return after a restyle if the writers are still open.

The title line in Dawn already made a choice, and the description line is waiting for yours.

Frequently Asked Questions

Shopify documents that Liquid leaves output unescaped, and product.description is the product's HTML description, the same string as product.content. Dawn's description block prints product.description with no filter, while Horizon's product-description block uses that field as fallback text when the rich-text setting is empty, again without escape or strip_html. The title block in Dawn uses escape, and that contrast is the description pipe you write down first.

The Liquid filter reference has no sanitize or allowlist filter. You can escape special characters so tags become visible text, or strip_html so only the words remain. Shopify's storefront security guidance says HTML that people write should be sanitised with an allowlist when it is saved, and that rich-text metafields should render through metafield_tag. A theme cannot invent a missing platform filter on the description pipe.

Keep the native field on the description pipe when a named merchandiser owns the admin editor, the HTML is short formatted copy (headings, lists, bold), and no app with product write access is allowed to paste into that field. Record the owner next to the pipe. If a PIM, a supplier feed, or an unknown app can write the description, move formatted story to a rich_text_field and print the native field as plain text, or stop those writers first.

metafield_tag wraps a metafield in a type-specific HTML element. A rich_text_field comes out as a div with class metafield-rich_text_field, which is the path Shopify documents for formatted metafield HTML. Printing .value yourself skips that wrapper and can dump raw markup the same way product.description does. Use metafield_text when you want the words only, for example in a JSON event or a meta tag.

Keep the buy box in the product form. Put shared promises (shipping window, returns line) in a section the merchandiser can hide. Put per-SKU jokes, regimen steps, or lab notes in metafields the template already prints. A catalogue of sixty products that share one JSON template stays maintainable when the Liquid file owns structure and the metafield owns the sentence that changes. Cramming every story into product.description makes the shared template feel like a spec sheet with a long HTML dump underneath.

Any staff user or installed app that can write products can change product.description, and the theme then prints whatever sits in the description pipe. If you keep raw Liquid on that field, treat write access as part of the pipe: list the apps, name who may paste HTML, and move formatted copy to a metafield those apps are not allowed to write. Changing the print line in theme development leaves the write permission in place, so an app can still save into a field you left open.

A client-side sanitiser takes the description out of the first HTML response and adds a script to every product page. Shopify's security guidance prefers an allowlist when the value is saved, plus the Liquid filters that already exist. Use strip_html and escape when the field must stay plain, or metafield_tag when the copy is a rich-text metafield you own. Leave a JavaScript rewrite for cases you have already documented as an exception, with an owner who can remove it.

Related Articles