feat: MPS-24 — chip strip stays on every SERP page

Operator: 'leave the chits on screen for all serp pages.' The
horizontal tag-chip-strip only rendered on the shop home; drilling
into a category (tag-detail SERP) dropped it, so hopping categories
meant going back.

- Extracted the chip strip (duplicated verbatim in home.j2 + shop.j2)
  into a single _facet_nav.j2 chip_strip(...) macro — DRY, one source
  of truth — and added it to shop_tag.j2 under the header.
- shop_tag_detail view already supplied home_chips / active_tag / sort
  / price, so this was a template-only gap. Active category chip
  highlights on the SERP and carries facet_qs (sort/price compose).
- Search SERP renders home.j2 so it gets the macro for free.

Test: +test_chip_strip_stays_on_tag_detail_serp. 1136 passed.
Docs: mps-24.md Phase 2.8i.
This commit is contained in:
russell@unturf.com 2026-05-16 11:46:16 -04:00
parent 3bb05e4b5d
commit 09f5a68428
No known key found for this signature in database
6 changed files with 85 additions and 39 deletions

View file

@ -363,6 +363,19 @@ Tests (`test_functional.py::TestProductTagsSpa`):
Deferred (occasional click, not the hot path): AJAX-ifying the
"Suggest categories" link — still a full navigation by design.
**Phase 2.8i — chip strip on every SERP page** (shipped 2026-05-16):
operator: "leave the chits on screen for all serp pages." The
horizontal `tag-chip-strip` rendered only on the shop home; drilling
into a category (tag-detail SERP `shop_tag.j2`) dropped it, so hopping
categories meant going back. Extracted the (duplicated) chip strip
from `home.j2`/`shop.j2` into a single `_facet_nav.j2` `chip_strip(...)`
macro and added it to `shop_tag.j2` under the header. The
`shop_tag_detail` view already supplied `home_chips` / `active_tag` /
sort / price, so this was a template-only gap; the active category
chip highlights on the SERP and carries `facet_qs`. Search SERP
already renders `home.j2` so it gets the macro for free. Test:
`test_chip_strip_stays_on_tag_detail_serp`.
**Phase 2.8h — facets compose, not clobber** (shipped 2026-05-16):
operator: "switching one breaks it" — picking a category reset the
active Sort + Price. Cause: the facet category links / "All" link /

View file

@ -118,3 +118,22 @@
{{ facet_form(base_url, tag_base, sort_options, sort_key, price_min, price_max, facet_tags, active_tag) }}
</details>
{% endmacro %}
{# Horizontal category chip strip. Single source of truth — used by the
shop home (home.j2 / shop.j2) AND every SERP page (shop_tag.j2) so
the chips stay on screen as the shopper hops categories. Carries the
active sort/price (facet_qs) so switching category composes. #}
{% macro chip_strip(shop_url, tag_base, home_chips, active_tag, sort_key, price_min, price_max) %}
{% if home_chips %}
<nav class="tag-chip-strip" data-tag-strip aria-label="Browse by category">
<a href="{{ shop_url }}{{ facet_qs(sort_key, price_min, price_max) }}"
class="tag-chip{% if not active_tag %} tag-chip-active{% endif %}"
data-tag-slug="" rel="nofollow">All</a>
{% for chip in home_chips %}
<a href="{{ tag_base }}/tag/{{ chip.slug }}{{ facet_qs(sort_key, price_min, price_max) }}"
class="tag-chip{% if active_tag and active_tag.id == chip.id %} tag-chip-active{% endif %}"
data-tag-slug="{{ chip.slug }}" rel="nofollow">{{ chip.name }}</a>
{% endfor %}
</nav>
{% endif %}
{% endmacro %}

View file

@ -88,24 +88,11 @@
Layout 0 (flat) keeps its existing simple grid with no nav. #}
{% set show_facet_nav = request.shop.home_layout and request.shop.home_layout >= 1 %}
{# MPS-24: tag chip strip — shown on layout 1 (chips) and 2 (lanes).
Chips and the facet left-nav point at the SAME targets (shop home
for "All", {tag_base}/tag/{slug} per category) so the two stay in
lock-step. tag_filter.js keeps full navigation for /tag/ hrefs. #}
{% if home_chips %}
<nav class="tag-chip-strip" data-tag-strip aria-label="Browse by category">
<a href="{{ shop_url }}{{ facet.facet_qs(sort_key, price_min, price_max) }}"
class="tag-chip{% if not active_tag %} tag-chip-active{% endif %}"
data-tag-slug=""
rel="nofollow">All</a>
{% for chip in home_chips %}
<a href="{{ tag_base }}/tag/{{ chip.slug }}{{ facet.facet_qs(sort_key, price_min, price_max) }}"
class="tag-chip{% if active_tag and active_tag.id == chip.id %} tag-chip-active{% endif %}"
data-tag-slug="{{ chip.slug }}"
rel="nofollow">{{ chip.name }}</a>
{% endfor %}
</nav>
{% endif %}
{# MPS-24: tag chip strip — shared macro, shown on layout 1 (chips)
and 2 (lanes) AND every SERP page (shop_tag.j2). Chips + facet
left-nav hit the SAME targets; tag_filter.js keeps full navigation
for /tag/ hrefs. #}
{{ facet.chip_strip(shop_url, tag_base, home_chips, active_tag, sort_key, price_min, price_max) }}
{% if show_facet_nav %}
{# Mobile/tablet: collapsed <details> facets above the grid. #}

View file

@ -15,27 +15,12 @@
{% set tag_base = request.shop.absolute_url(request, slug=False) %}
{% set show_facet_nav = request.shop.home_layout and request.shop.home_layout >= 1 %}
{# MPS-24: tag chip strip — shown on layout 1 (chips) and 2 (lanes). #}
{# Chip strip and the facet left-nav point at the SAME targets: the
shop home for "All" and the canonical tag detail page
({tag_base}/tag/{slug}) per category. tag_filter.js detects the
/tag/ href and keeps full navigation, so a chip and its matching
sidebar entry always land on the same page with the same active
state. #}
{% if home_chips %}
<nav class="tag-chip-strip" data-tag-strip aria-label="Browse by category">
<a href="{{ shop_url }}{{ facet.facet_qs(sort_key, price_min, price_max) }}"
class="tag-chip{% if not active_tag %} tag-chip-active{% endif %}"
data-tag-slug=""
rel="nofollow">All</a>
{% for chip in home_chips %}
<a href="{{ tag_base }}/tag/{{ chip.slug }}{{ facet.facet_qs(sort_key, price_min, price_max) }}"
class="tag-chip{% if active_tag and active_tag.id == chip.id %} tag-chip-active{% endif %}"
data-tag-slug="{{ chip.slug }}"
rel="nofollow">{{ chip.name }}</a>
{% endfor %}
</nav>
{% endif %}
{# MPS-24: tag chip strip — shared macro (single source of truth),
shown on layout 1 (chips) and 2 (lanes) AND every SERP page. Chip
strip + facet left-nav point at the SAME targets; tag_filter.js
keeps full navigation for /tag/ hrefs so a chip and its sidebar
entry land on the same SERP with the same active state. #}
{{ facet.chip_strip(shop_url, tag_base, home_chips, active_tag, sort_key, price_min, price_max) }}
{% if show_facet_nav %}
{# Mobile/tablet: collapsed <details> facets above the grid. #}

View file

@ -11,6 +11,11 @@
<p class="type-body-sm"><a href="{{ shop_url }}" class="shop-theme-link-color">&larr; All products</a></p>
</section>
{# Chip strip stays on screen on every SERP page so the shopper can
hop categories without going back. Same shared macro as the shop
home; active_tag highlights the current category. #}
{{ facet.chip_strip(shop_url, tag_base, home_chips, active_tag, sort_key, price_min, price_max) }}
{# Mobile/tablet: collapsed <details> facets above the grid (CSS hides
it ≥800px where the sidebar takes over). #}
{{ facet.details(shop_url, tag_base, sort_options, sort_key, price_min, price_max, facet_tags, active_tag) }}

View file

@ -8947,6 +8947,43 @@ class TestHomeLayoutAndTags(_AuthenticatedBase):
self.assertIn('tag-detail-header', body)
self.assertIn('serp-list', body)
def test_chip_strip_stays_on_tag_detail_serp(self):
"""Operator: 'leave the chits on screen for all serp pages.'
The horizontal chip strip must render on the tag-detail SERP
(not just the shop home) so the shopper can hop categories
without going back. The current category chip is active."""
shop, product = self._make_shop_with_product("chip-serp-shop")
self.testapp.post(
f"/s/{shop.id}/settings",
{
"form_section": "home-layout-settings",
"home_layout": "2",
"home_layout_tag_limit": "5",
"home_layout_per_lane_limit": "10",
"submit": "Save Home Layout",
},
)
self.testapp.post(
f"/p/{product.id}/edit",
{
"title": product.title,
"description": "One. Two. Three. Four. Five. Six.",
"price": str(product.price),
"visibility": "1",
"tags": "Math",
},
)
res = self.testapp.get(f"/s/{shop.id}/tag/math")
body = res.body.decode()
# The shared chip strip is present on the SERP page.
self.assertIn('class="tag-chip-strip"', body)
self.assertIn('data-tag-strip', body)
# The active category chip is highlighted on the SERP.
self.assertIn('tag-chip tag-chip-active', body)
# And it still has the facet sidebar (chip strip is additive,
# not a replacement).
self.assertIn('facet-nav', body)
def test_facet_links_preserve_sort_and_price(self):
"""Operator report: 'switching one breaks it' — changing category
reset sort/price. Every category link / All link / chip / lane