The small bug was not the headline copy
A storefront hero title looked fine on the source market and wrong on a second market. The visible text was almost the same: "Matcha. Made with uncommon care." The surprising part was the DOM.
One store rendered plain heading text:
<h1 class="hero-video-title font-bethany!">Matcha. Made with </h1>
<h3 class="hero-video-title font-bethany!">uncommon care.</h3>
The other store rendered responsive spans inside the headings:
<h1 class="hero-video-title font-bethany!">
<span class="hidden lg:inline">Matcha. Made with</span>
<span class="lg:hidden">Matcha. Made with </span>
</h1>
<h3 class="hero-video-title font-bethany!">
<span class="hidden lg:inline"> uncommon care.</span>
<span class="lg:hidden">uncommon care.</span>
</h3>
That is a useful kind of Hydrogen production bug because it sits at the exact boundary between code, Shopify Admin content, and market-specific storefront data. If you only look at the visible words, it feels like a CSS issue. If you inspect the DOM, the real question becomes: why did this component decide desktop and mobile titles are different?
What changed in the component
The old implementation controlled breakpoints at the heading level. Desktop headings and mobile headings were separate elements:
{titleTop && (
<h1 className="hero-video-title hidden lg:block">{titleTop}</h1>
)}
{titleBottom && (
<h3 className="hero-video-title hidden lg:block">{titleBottom}</h3>
)}
{(titleTopMobile || titleTop) && (
<h3 className="hero-video-title block lg:hidden">
{titleTopMobile || titleTop}
</h3>
)}
{(titleBottomMobile || titleBottom) && (
<h3 className="hero-video-title block lg:hidden">
{titleBottomMobile || titleBottom}
</h3>
)}
In that model, the desktop title did not care whether a mobile title existed. Desktop rendered the desktop fields. Mobile rendered the mobile fields, with fallback to desktop fields.
The newer implementation tried to reduce duplicate headings by comparing desktop and mobile title values. If the mobile value is present and different, it renders both values inside the same heading and lets Tailwind classes decide which span is visible:
const hasDistinctMobileTop =
titleTopMobile !== undefined && titleTopMobile !== titleTop;
const hasDistinctMobileBottom =
titleBottomMobile !== undefined && titleBottomMobile !== titleBottom;
That comparison is intentionally simple, but it has a consequence: it is a raw string comparison. If two values look visually similar but are not exactly equal, the component chooses the responsive-span path.
Why this can appear only on one market
In a multi-market Hydrogen storefront, the code can be identical while the content data is not. The same component may receive different values because the Storefront API response is scoped by market, language, metaobject entry, translation, or content ordering.
The practical signs are:
- The source market renders plain heading text.
- The second market renders hidden/lg:inline and lg:hidden spans.
- The words look almost the same to a human.
- The component logic says the strings are not exactly the same.
That mismatch can come from a normal space, a leading space, a trailing space, a non-breaking space, a line break, or a translated field that was edited separately. It can also come from the storefront reading a different metaobject entry than the one the team expected.
The Shopify Admin surface to check
For this pattern, the first place to look is the metaobject type that powers the hero text. In this case the storefront query was reading:
metaobjects(type: "landing_hero_video_texts", first: 1) {
nodes {
fields {
key
value
}
}
}
The fields that matter are the desktop and mobile title fields:
| Field | Purpose |
|---|
| hero_video_title_top | Desktop top title |
| hero_video_title_top_for_mobile | Mobile top title |
| hero_video_title_bottom | Desktop bottom title |
| hero_video_title_bottom_for_mobile | Mobile bottom title |
In Shopify Admin, check Content -> Metaobjects -> landing_hero_video_texts. If there are multiple entries, confirm which entry is returned first by the storefront query. Then check the same four fields in any market or language translation layer used by the storefront.
The important part is not whether the values look close. The important part is whether the values are meant to be distinct. If the merchant does not need separate mobile copy, keep the mobile fields empty or make them exactly match the desktop fields. If the merchant does need separate mobile copy, the span-based DOM is expected and should be styled deliberately.
The code lesson
The refactor was not automatically wrong. A single semantic heading can be a reasonable direction. The problem is that the new behavior made a previously harmless content mismatch visible.
That is the production lesson: component refactors should treat CMS-controlled strings as real input, not as clean constants. If a component branches on string equality, the code should make that rule explicit.
A safer implementation usually does one of three things:
- Normalize values before comparing them when whitespace is not meaningful.
- Keep desktop and mobile render paths separate when the existing CSS and content model depend on that DOM shape.
- Add a clear content contract: mobile fields are either empty, exactly matched, or intentionally different.
I prefer making the contract explicit. If mobile copy is meant to be optional, then the renderer should not accidentally switch DOM structure because of one invisible character in Shopify Admin.
How to debug it quickly
Start in the browser, not in a code editor.
- Inspect the hero title DOM on both markets.
- If one market has plain text and the other has responsive spans, the component has received distinct mobile values.
- Log or inspect the raw values before render, including string length.
- Compare the Shopify Admin metaobject fields for desktop, mobile, and translations.
- Decide whether the fix belongs in content, normalization, or component structure.
The common mistake is to patch CSS first. CSS can hide the symptom, but it will not explain why the component chose a different render branch.
When I would change content versus code
If the mobile title fields were not supposed to differ, fix the metaobject values. That keeps the storefront behavior aligned with the content model.
If mobile copy is a real requirement, keep the fields distinct and adjust the component/CSS so the span-based render path is visually intentional.
If multiple stores or markets have already drifted in this way, I would also change code. Normalize values before comparison, document the content contract, and add a small regression test around the title renderer so the next refactor does not silently change heading semantics again.
The best fix depends on ownership. A merchant-editable field can solve a live issue in minutes. A code fix is better when the same failure mode can come back across markets, languages, or future content edits.
Your Shopify store works, but every new feature takes 3x longer than last year? That's when I come in. If a Hydrogen bug sits between storefront code, Shopify Admin content, and deployment workflow, I can help trace the real boundary before the fix turns into guesswork.