DEV Community

Ram Kr Shukla
Ram Kr Shukla

Posted on AI-assisted

A Shopify theme was cutting every product title mid-word. One Liquid filter did it

On a D2C store I work with, I pulled the 12 product pages getting the most search impressions and read their title tags.

Every single one stopped at 69 or 70 characters. Mid-word. The H1 on the page was the same chopped string.

Here's why that hurt more than it looks. Their product names were long and front-loaded: brand words, then claims, then the variant at the very end. Shades, in their case. So the one part of the name that's different between two products is exactly the part the cut removed.

One product had 19 shade pages. In search results they all looked the same.

The culprit

Somewhere in the theme there's a line like this:

{{ product.title | truncate: 70, '' }}
Enter fullscreen mode Exit fullscreen mode

Two things to know about truncate:

  • It counts characters, not words. It'll stop in the middle of one.
  • The second argument is what it appends. By default that's ..., so at least you can see something was cut. Pass an empty string, like above, and it cuts with no sign at all.

Here's what it does to two different shades of the same (made-up) product:

Velvet Matte Long-Wear Transfer-Proof Liquid Lipstick For All Skin Tones: Deep Berry Rose
Velvet Matte Long-Wear Transfer-Proof Liquid Lipstick For All Skin Tones: Nude Pink

both become:
Velvet Matte Long-Wear Transfer-Proof Liquid Lipstick For All Skin Ton
Enter fullscreen mode Exit fullscreen mode

Finding it in your theme

Pull the theme with Shopify CLI and grep:

shopify theme pull
grep -rn "truncate" layout/ sections/ snippets/ templates/
Enter fullscreen mode Exit fullscreen mode

Look in layout/theme.liquid for the <title> tag and in the main product section for the H1. Card snippets in collection grids often truncate too, but that's a design choice and it doesn't touch your title tag.

The fix

For the H1, just delete the filter. A heading has no length limit. If the design breaks with a long name, fix it in CSS.

For the title tag, delete it too. Google already shortens long titles in results based on pixel width and adds its own ellipsis. Cut it yourself first and you've just thrown away words Google would've read.

If the names really are too long, write a shorter one in the product's Search engine listing field in the admin. That fills page_title in the theme. For a whole catalogue, export the product CSV and fill the SEO Title column in a spreadsheet, then import it back.

And if nobody's going to write hundreds of SEO titles, here's a fallback that keeps the variant. It only kicks in for product pages with a long title that has a : before the variant:

{%- liquid
  assign t = page_title
  if template.name == 'product' and t.size > 60 and t contains ': '
    assign parts = t | split: ': '
    assign t = parts.first | truncatewords: 6, '' | append: ': ' | append: parts.last
  endif
-%}
<title>{{ t }} | {{ shop.name }}</title>
Enter fullscreen mode Exit fullscreen mode

Same two products after the fix:

Velvet Matte Long-Wear Transfer-Proof Liquid Lipstick: Deep Berry Rose | Example Store
Velvet Matte Long-Wear Transfer-Proof Liquid Lipstick: Nude Pink | Example Store
Enter fullscreen mode Exit fullscreen mode

Short titles, titles without a colon and non-product pages pass through untouched. Hyphenated words count as one word for truncatewords, so "Long-Wear" doesn't get split.

Change the 6 and the : to match how your store names things.

Checking a whole store

I didn't want to read titles by hand again, so here's a script. It reads the product sitemap, gets each product's real name from Shopify's public /products/<handle>.js endpoint, and compares it with the <title> and <h1> on the page.

It only flags a cut that stops mid-word or ends in .... A store that writes its own custom titles, or keeps the colour out of the H1 on purpose, won't get flagged.

"""Find Shopify product pages whose <title> or <h1> drops the end of the product name.

Usage: python3 shopify_title_check.py https://store.example.com 50
"""
import html
import json
import re
import sys
import time
import urllib.request

UA = {"User-Agent": "Mozilla/5.0 (title-check)"}
DELAY = 2  # Shopify answers 429 if you go much faster
MIN_CUT = 40  # truncate filters cut long strings; shorter mismatches are usually singular vs plural


def get(url):
    req = urllib.request.Request(url, headers=UA)
    with urllib.request.urlopen(req, timeout=20) as r:
        return r.read().decode("utf-8", "replace")


def product_urls(store, limit):
    index = get(f"{store}/sitemap.xml")
    maps = [html.unescape(m) for m in re.findall(r"<loc>([^<]*sitemap_products[^<]*)</loc>", index)]
    urls = []
    for m in maps:
        urls += re.findall(r"<loc>([^<]*/products/[^<]+)</loc>", get(m))
        if len(urls) >= limit:
            break
    return urls[:limit]


def text_of(tag, page):
    m = re.search(rf"<{tag}[^>]*>(.*?)</{tag}>", page, re.S | re.I)
    return html.unescape(re.sub(r"<[^>]+>|\s+", " ", m.group(1))).strip() if m else ""


def cut_short(shown, real):
    """True if shown is the product name chopped by a truncate filter.

    A custom SEO title or a deliberately short H1 isn't flagged. Only a prefix
    of the real name that stops mid-word or ends in an ellipsis is.
    """
    cores = [shown] + [shown[: m.start()] for m in re.finditer(r"\s[|\u2013\u2014-]\s", shown)]
    for core in cores:
        ellipsis = core.endswith(("...", "\u2026"))
        core = core.rstrip(".\u2026 ")
        if core and len(core) < len(real) and real.startswith(core):
            mid_word = core[-1].isalnum() and real[len(core)].isalnum()
            if ellipsis or (mid_word and len(core) >= MIN_CUT):
                return True
    return False


def check(url):
    real = json.loads(get(f"{url}.js"))["title"].strip()
    page = get(url)
    title, h1 = text_of("title", page), text_of("h1", page)
    problems = [f"{name} cut short: {shown!r}"
                for name, shown in (("title", title), ("h1", h1)) if cut_short(shown, real)]
    return real, problems


def main():
    store, limit = sys.argv[1].rstrip("/"), int(sys.argv[2]) if len(sys.argv) > 2 else 50
    bad = 0
    for url in product_urls(store, limit):
        real, problems = check(url)
        if problems:
            bad += 1
            print(f"\n{url}\n  product: {real!r} ({len(real)} chars)")
            for p in problems:
                print("  " + p)
        time.sleep(DELAY)
    print(f"\n{bad} of {limit} product pages cut the name short")


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode
python3 shopify_title_check.py https://your-store.com 50
Enter fullscreen mode Exit fullscreen mode

About that 2-second delay: Shopify starts answering 429 if you crawl it at normal crawler speed, and the script dies halfway through. Found that out the hard way.

It's standard library only, so there's nothing to install.


I drafted this with help from an AI assistant. The store story is from my own client work. The Liquid and the script were tested before publishing.

Top comments (0)