# Shopify body_html to description

The Shopify description is rich text stored as HTML, and the feed description attribute wants plain text. The mapping has to strip the markup, decode the entities it leaves behind, collapse the whitespace that replaces the block elements, and only then truncate to the channel limit.

- **Canonical URL:** https://www.feedlab.io/knowledge/mappings/shopify-body-html-to-description
- **Last reviewed:** 18 September 2026
- **Source:** feedlab knowledge base
- **Licence:** free to quote and cite with attribution to feedlab


## The Shopify side

| Property | Value |
| --- | --- |
| API field | `product.body_html` |
| Called | Description |
| Found at | Product → Description (the rich text editor) |
| Feeds | `description` |


## Before and after

| In Shopify | In the feed | Note |
| --- | --- | --- |
| `<p>Brushed cotton.</p><p>Made in Portugal.</p>` | `Brushed cotton. Made in Portugal.` | Block boundary becomes a space, so the two sentences do not run together. |
| `<ul><li>100% cotton</li><li>Machine washable</li></ul>` | `100% cotton. Machine washable.` | List items separated rather than concatenated. Some generators use a semicolon or a bullet character instead. |
| `Tom &amp; Jerry&#39;s blend` | `Tom & Jerry's blend` | Entities decoded. Left alone they reach the shopper as raw entity text. |
| `Soft&nbsp;&nbsp;and warm` | `Soft and warm` | Non-breaking spaces from pasted content normalised to a single space. |
| `<p>Sale!</p><script>track()</script>` | `Sale!` | Script content removed entirely rather than stripped to its text. |



## The transformation, step by step

1. **Convert block boundaries to whitespace before stripping tags** — Removing tags naively joins the end of one paragraph to the start of the next, so "Machine washable.Made in Portugal." appears in the feed. Replace closing block tags and line breaks with a space or newline first, then strip what remains.
2. **Strip the remaining markup** — Remove every tag, including the inline ones. Also remove anything that was never prose in the first place: embedded scripts, style blocks, tracking pixels and the iframe a review app injected.
3. **Decode HTML entities** — Stripping tags leaves entities behind, so the text still contains &amp;, &nbsp; and &#39;. Decode them to real characters, because an entity in a feed field is submitted literally and appears that way to shoppers.
4. **Collapse whitespace** — Pasted content brings non-breaking spaces, double spaces and long runs of newlines. Collapse runs of whitespace to single spaces and trim the ends, so the value is one clean block of prose rather than a shape that hints at the HTML it used to be.
5. **Truncate on a word boundary, last** — Channels cap the description length. Truncate after cleaning rather than before, or the limit is spent on markup, and cut at a word or sentence boundary rather than mid-word.



## What goes wrong

### HTML submitted as-is is rejected or mangled

The description attribute is a text field. Markup in it is either rejected outright or displayed literally, and a description that begins with a div is the most visible possible sign that a feed was exported rather than mapped.

### Apps inject markup you did not write

Review widgets, size-chart apps and page builders add their own HTML into the description field. That content is not prose about the product, so a naive strip carries fragments of interface text into the feed. Removing known app blocks is part of the mapping rather than an optimisation.

### Truncation before cleaning wastes the limit

Cutting to the channel limit while the value is still HTML spends a large part of the allowance on tags, so the visible description is much shorter than it should be, and often ends inside a tag.

### An empty description is common and disqualifying

Plenty of Shopify products have no description at all, particularly imported ones. Description is required on every channel, so those items are rejected. A fallback built from the title, product type and brand is better than an empty field, though it is a stopgap rather than a fix.




## How feedlab maps it

feedlab converts block boundaries to whitespace, strips markup and script content, decodes entities, collapses whitespace and then truncates on a word boundary to each channel's limit, so the same source description produces a correct value at every length. Products with an empty description are listed in the preview rather than silently shipped as blanks.






## Frequently asked questions

### Can I send HTML in the feed description?

No. The attribute is plain text, so markup is either rejected or shown to shoppers literally. The HTML has to be stripped and its entities decoded before the value reaches the feed.

### Why do my feed descriptions have words joined together?

Because the tags were stripped without replacing block boundaries first. The end of one paragraph is joined to the start of the next, which is the signature of a naive strip.

### What do I do about products with no description?

Description is required everywhere, so those items are rejected. A generated fallback from the title, type and brand keeps them serving, but the real fix is writing the description.

### Should the description be truncated to the channel limit?

Yes, but after cleaning and on a word boundary. Truncating while the value is still HTML spends the allowance on markup and frequently cuts mid-tag.




## Primary sources

- [Google: description attribute specification](https://support.google.com/merchants/answer/6324468)
- [Shopify: Product API reference](https://shopify.dev/docs/api/admin-graphql/latest/objects/Product)



