feat: MPS-24 — smaller SERP thumbnails + featured/random right rail
Operator: smaller SERP thumbnails + a right column (featured products, fallback random; all SERP + shop home). - Thumbnails: serp-list thumb column 140/200/260/320 -> 88/110/130/150 across container tiers (frees room for body + the new rail). - _featured_rail_products(request, shop) in shop.py: shop's curated featured_product_ids (public/ready/deduped/capped); random public-ready fallback so it's never empty; bounded func.random() query (CWE-407-safe). Injected into _build_home_layout_context (home/shop/search) + shop_tag_detail ctx. - Shared _facet_nav.j2:featured_rail macro, rendered as the 3rd child of the existing .tag-detail-layout on shop_tag.j2 / home.j2 / shop.j2 (within show_facet_nav). - CSS: <1100px rail is full-width beneath results; >=1100px 3-col facet | results | sticky rail; <800px stacks. Grid-only, tokens. /styleguide#serprail added. - Tests: +test_serp_featured_rail_renders_with_random_fallback, +test_serp_featured_rail_prefers_curated_featured; test_tag_detail_price_filter_narrows_grid rescoped to the results grid (rail is unfiltered discovery by design). 1139 passed. Docs: mps-24.md Phase 2.8l, design-system.md.
This commit is contained in:
parent
2301ab33cc
commit
479dc0ed23
10 changed files with 314 additions and 15 deletions
|
|
@ -289,6 +289,7 @@ All components are documented with live examples at `/styleguide`. The styleguid
|
|||
| `.tag-chip-add` | `product_edit.j2` | Add-a-tag row, grid `1fr auto`, stacks to one column under 600px |
|
||||
| `.tag-chip-flash` | `product_edit.j2` | Toast region reusing `.tag-flash-toast` / `.tag-flash-{success,error,info}` |
|
||||
| `[data-product-tags]` / `[data-product-tags-url]` | `product_edit.j2` | JS hooks for `product_tags.js`: container + the `/p/{id}/tags` endpoint URL |
|
||||
| `.serp-rail` / `.serp-rail-list` / `.serp-rail-card` | `_facet_nav.j2:featured_rail` macro on `shop_tag.j2` / `home.j2` / `shop.j2` (Phase 2.8l) | Right rail on SERP pages — curated featured products or random fallback. 3rd grid child of `.tag-detail-layout`; full-width below results 800–1099px, sticky right column ≥1100px. `/styleguide#serprail` |
|
||||
| `[data-focus-section]` / `[data-focus-heading]` / `[data-focus-list]` | `shop_tags.j2` (Phase 2.8) | Stable bulk-tagger focus container — always in the DOM, `hidden` until a tag is focused; `tag_bulk.js` swaps the product list in place instead of a full reload |
|
||||
| `[data-tag-focus-link]` | `shop_tags.j2` (Phase 2.8) | Tag chip in the All-tags list; `tag_bulk.js` intercepts the click and fetches `?focus=<slug>` as JSON |
|
||||
| `.tag-list-dragging` / `.tag-list-drop-target` | `shop_tags.j2` (Phase 2.8) | Dragged row (dimmed) + active drop position during HTML5 drag-to-reorder; `tag_bulk.js` POSTs `set_order` on drop |
|
||||
|
|
|
|||
|
|
@ -363,6 +363,24 @@ 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.8l — SERP right rail (featured / random) + smaller thumbs**
|
||||
(shipped 2026-05-16): operator wanted smaller SERP thumbnails and a
|
||||
right column. Thumb column shrunk (140/200/260/320 → 88/110/130/150
|
||||
across container tiers). New right rail: `_featured_rail_products`
|
||||
helper (shop.featured_product_ids — public/ready/deduped/capped;
|
||||
random public-ready fallback so it's never empty; bounded
|
||||
`func.random()` query — CWE-407-safe), injected into home/shop/search
|
||||
(`_build_home_layout_context`) + `shop_tag_detail` ctx. Shared
|
||||
`_facet_nav.j2:featured_rail` macro rendered as the 3rd child of the
|
||||
existing `.tag-detail-layout` on `shop_tag.j2` / `home.j2` /
|
||||
`shop.j2`. CSS: 2-col (facet|results) <1100px with the rail
|
||||
full-width beneath; 3-col (facet|results|sticky rail) ≥1100px; rail
|
||||
stacks under results <800px. `/styleguide#serprail`. Tests:
|
||||
`test_serp_featured_rail_renders_with_random_fallback`,
|
||||
`test_serp_featured_rail_prefers_curated_featured`;
|
||||
`test_tag_detail_price_filter_narrows_grid` rescoped to the results
|
||||
grid (the rail is unfiltered discovery by design).
|
||||
|
||||
**Phase 2.8k — auto-suggest: way more, stop missing `holiday`**
|
||||
(shipped 2026-05-16): operator: "100 suggested tags is not enough, we
|
||||
need way more — missing holiday holidays". Two `100` caps in
|
||||
|
|
|
|||
|
|
@ -1583,7 +1583,9 @@ section.serp-list {
|
|||
}
|
||||
article.serp-list-row {
|
||||
display: grid;
|
||||
grid-template-columns: 140px minmax(0, 1fr);
|
||||
/* Smaller thumbnails (was 140/200/260/320 across the tiers below)
|
||||
— frees horizontal room for the body text + a right rail. */
|
||||
grid-template-columns: 88px minmax(0, 1fr);
|
||||
gap: var(--space-4, 16px);
|
||||
align-items: start;
|
||||
padding: var(--space-3, 12px);
|
||||
|
|
@ -1645,7 +1647,7 @@ p.serp-list-excerpt {
|
|||
grows. The parent .serp-list is the query container. */
|
||||
@container (min-width: 600px) {
|
||||
article.serp-list-row {
|
||||
grid-template-columns: 200px minmax(0, 1fr);
|
||||
grid-template-columns: 110px minmax(0, 1fr);
|
||||
gap: var(--space-5, 20px);
|
||||
}
|
||||
h3.serp-list-title {
|
||||
|
|
@ -1654,12 +1656,12 @@ p.serp-list-excerpt {
|
|||
}
|
||||
@container (min-width: 900px) {
|
||||
article.serp-list-row {
|
||||
grid-template-columns: 260px minmax(0, 1fr);
|
||||
grid-template-columns: 130px minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
@container (min-width: 1200px) {
|
||||
article.serp-list-row {
|
||||
grid-template-columns: 320px minmax(0, 1fr);
|
||||
grid-template-columns: 150px minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1938,14 +1940,100 @@ div.tag-detail-layout {
|
|||
}
|
||||
@media (min-width: 800px) {
|
||||
div.tag-detail-layout {
|
||||
/* facet | results. The featured rail (3rd child) flows to a
|
||||
full-width row beneath until there's room for a 3rd column. */
|
||||
grid-template-columns: 220px minmax(0, 1fr);
|
||||
gap: var(--space-5, 20px);
|
||||
}
|
||||
div.tag-detail-layout > aside.serp-rail {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
/* The sidebar replaces the top chip strip on wide viewports. */
|
||||
nav.tag-chip-strip-mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1100px) {
|
||||
div.tag-detail-layout {
|
||||
/* facet | results | featured rail */
|
||||
grid-template-columns: 220px minmax(0, 1fr) 260px;
|
||||
}
|
||||
div.tag-detail-layout > aside.serp-rail {
|
||||
grid-column: auto;
|
||||
align-self: start;
|
||||
position: sticky;
|
||||
top: var(--space-4, 16px);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
* SERP right rail — featured products (or a random rotation). Compact
|
||||
* cards so it complements, not competes with, the result list. MPS-24.
|
||||
* ===================================================================*/
|
||||
aside.serp-rail {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: var(--space-3, 12px);
|
||||
align-content: start;
|
||||
}
|
||||
h2.serp-rail-title {
|
||||
margin: 0;
|
||||
font-size: var(--type-body-size, 1rem);
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #333);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
ul.serp-rail-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
gap: var(--space-3, 12px);
|
||||
}
|
||||
li.serp-rail-card {
|
||||
display: grid;
|
||||
grid-template-columns: 56px minmax(0, 1fr);
|
||||
gap: var(--space-2, 8px);
|
||||
align-items: start;
|
||||
padding: var(--space-2, 8px);
|
||||
border: 1px solid var(--border-color, #e5e7eb);
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
background: var(--surface, #fff);
|
||||
}
|
||||
a.serp-rail-thumb-link {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
}
|
||||
img.serp-rail-thumb {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
}
|
||||
.serp-rail-body {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: var(--space-1, 4px);
|
||||
}
|
||||
a.serp-rail-card-title {
|
||||
font-size: var(--type-body-sm-size, 0.875rem);
|
||||
line-height: 1.3;
|
||||
text-decoration: none;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a.serp-rail-card-title:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
span.serp-rail-card-price {
|
||||
font-size: var(--type-body-sm-size, 0.8125rem);
|
||||
font-weight: 600;
|
||||
color: var(--color-green, #4d7a1f);
|
||||
}
|
||||
[data-theme="dark"] li.serp-rail-card {
|
||||
background: var(--dark-button-bg, #2d3748);
|
||||
border-color: var(--dark-border, #475569);
|
||||
}
|
||||
|
||||
/* ===================================================================
|
||||
* Mobile/tablet <details> facet accordion — hidden on desktop where
|
||||
|
|
|
|||
|
|
@ -137,3 +137,34 @@
|
|||
</nav>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{# Right rail for SERP pages — the shop's featured products, or a
|
||||
random rotation when none are curated (server decides; see
|
||||
views/shop.py:_featured_rail_products). Compact cards so it sits
|
||||
beside the results without competing. Sibling of .tag-detail-content
|
||||
inside .tag-detail-layout. #}
|
||||
{% macro featured_rail(products, request) %}
|
||||
{% if products %}
|
||||
<aside class="serp-rail" aria-label="Featured products">
|
||||
<h2 class="serp-rail-title type-title">Featured</h2>
|
||||
<ul class="serp-rail-list">
|
||||
{% for p in products %}
|
||||
<li class="serp-rail-card">
|
||||
{% if "thumbnail1" in p.extensions %}
|
||||
<a href="{{ p.absolute_url(request) }}" class="serp-rail-thumb-link" rel="nofollow">
|
||||
<img src="{{ request.shop_cdn_endpoint }}/{{ p.s3_path }}/thumbnail1?ts={{ p.updated_timestamp }}"
|
||||
class="serp-rail-thumb" loading="lazy" alt="" />
|
||||
</a>
|
||||
{% endif %}
|
||||
<div class="serp-rail-body">
|
||||
<a href="{{ p.absolute_url(request) }}" class="serp-rail-card-title shop-theme-link-color">{{ p.title }}</a>
|
||||
{% if p.is_sellable %}
|
||||
<span class="serp-rail-card-price">${{ '{:,.2f}'.format(p.price) }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</aside>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
|
|
|||
|
|
@ -222,6 +222,8 @@
|
|||
|
||||
{% if show_facet_nav %}
|
||||
</div> {# /.tag-detail-content #}
|
||||
{# Right rail — featured (or random) products, 3rd grid child. #}
|
||||
{{ facet.featured_rail(featured_rail, request) }}
|
||||
</div> {# /.tag-detail-layout #}
|
||||
{% endif %}
|
||||
|
||||
|
|
|
|||
|
|
@ -129,6 +129,8 @@
|
|||
|
||||
{% if show_facet_nav %}
|
||||
</div> {# /.tag-detail-content #}
|
||||
{# Right rail — featured (or random) products, 3rd grid child. #}
|
||||
{{ facet.featured_rail(featured_rail, request) }}
|
||||
</div> {# /.tag-detail-layout #}
|
||||
{% endif %}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@
|
|||
|
||||
</div>
|
||||
|
||||
{# Right rail — featured (or random) products. 3rd grid child of
|
||||
.tag-detail-layout: facet sidebar | results | rail. #}
|
||||
{{ facet.featured_rail(featured_rail, request) }}
|
||||
|
||||
</div>
|
||||
|
||||
{%- endblock -%}
|
||||
|
|
|
|||
|
|
@ -193,6 +193,7 @@
|
|||
<a href="#cart">Cart</a>
|
||||
<a href="#comments">Comments</a>
|
||||
<a href="#tagchips">Tag Chips</a>
|
||||
<a href="#serprail">SERP Rail</a>
|
||||
<a href="#toggle">Toggle</a>
|
||||
<a href="#ribbon">Ribbon</a>
|
||||
<a href="#taskbar">Task Bar</a>
|
||||
|
|
@ -1180,6 +1181,41 @@ The snippet sets inline margin-left for depth-N replies (depth * 20px).</div>
|
|||
</div>
|
||||
|
||||
|
||||
<!-- ======== SERP RAIL ======== -->
|
||||
<div class="sg-section" id="serprail">
|
||||
<div class="sg-section-title">SERP Rail</div>
|
||||
<p style="max-width:600px;line-height:1.6;">
|
||||
Right-hand rail on SERP pages (tag detail, search, filtered shop
|
||||
home) — the shop's curated featured products, or a random
|
||||
rotation when none are set (<code>views/shop.py:_featured_rail_products</code>).
|
||||
3rd grid child of <code>.tag-detail-layout</code>: facet sidebar |
|
||||
results | rail. Full-width below the results at 800–1099px,
|
||||
a sticky right column ≥1100px. Grid only.
|
||||
</p>
|
||||
<div class="sg-demo">
|
||||
<aside class="serp-rail" aria-label="Featured products" style="max-width:280px;">
|
||||
<h2 class="serp-rail-title type-title">Featured</h2>
|
||||
<ul class="serp-rail-list">
|
||||
<li class="serp-rail-card">
|
||||
<span class="serp-rail-thumb-link"><span class="serp-rail-thumb" style="display:block;aspect-ratio:1;background:var(--surface-dim,#f3f4f6);border-radius:var(--radius-sm,4px);"></span></span>
|
||||
<div class="serp-rail-body">
|
||||
<a href="#serprail" class="serp-rail-card-title shop-theme-link-color">Valentine's Day Color by Number</a>
|
||||
<span class="serp-rail-card-price">$3.00</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="serp-rail-card">
|
||||
<span class="serp-rail-thumb-link"><span class="serp-rail-thumb" style="display:block;aspect-ratio:1;background:var(--surface-dim,#f3f4f6);border-radius:var(--radius-sm,4px);"></span></span>
|
||||
<div class="serp-rail-body">
|
||||
<a href="#serprail" class="serp-rail-card-title shop-theme-link-color">Stone Fox Novel Study</a>
|
||||
<span class="serp-rail-card-price">$5.00</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ======== TOGGLE ======== -->
|
||||
<div class="sg-section" id="toggle">
|
||||
<div class="sg-section-title">Toggle / Details</div>
|
||||
|
|
|
|||
|
|
@ -8987,6 +8987,70 @@ class TestHomeLayoutAndTags(_AuthenticatedBase):
|
|||
# not a replacement).
|
||||
self.assertIn('facet-nav', body)
|
||||
|
||||
def _make_ready_tagged_product(self, shop_name, title, tag="Math"):
|
||||
"""A content product (always is_ready) + public + tagged, so it
|
||||
shows in both the SERP grid and the featured rail."""
|
||||
shop = self._create_shop_helper(
|
||||
user_creds=self.user1_creds,
|
||||
shop_params={**self.shop1_params, "name": shop_name},
|
||||
)
|
||||
params = dict(self.product1_params)
|
||||
params["title"] = title
|
||||
params.pop("is_sellable", None) # content product → is_ready
|
||||
self.testapp.post(f"/p/new?shop_id={shop.id}", params)
|
||||
from ..models.product import get_all_products
|
||||
product = get_all_products(self.dbsession).all()[-1]
|
||||
self.testapp.post(
|
||||
f"/p/{product.id}/edit",
|
||||
{
|
||||
"title": product.title,
|
||||
"description": "One. Two. Three. Four. Five. Six.",
|
||||
"visibility": "1",
|
||||
"tags": tag,
|
||||
},
|
||||
)
|
||||
return shop, product
|
||||
|
||||
def test_serp_featured_rail_renders_with_random_fallback(self):
|
||||
"""SERP pages get a right rail. With no featured set configured
|
||||
it falls back to a random rotation of the shop's public ready
|
||||
products, so the rail is never empty."""
|
||||
shop, product = self._make_ready_tagged_product(
|
||||
"rail-shop", "RandomRailItem"
|
||||
)
|
||||
res = self.testapp.get(f"/s/{shop.id}/tag/math")
|
||||
body = res.body.decode()
|
||||
self.assertIn('class="serp-rail"', body)
|
||||
self.assertIn('serp-rail-list', body)
|
||||
self.assertIn('>Featured<', body)
|
||||
rail = body.split('class="serp-rail"', 1)[1]
|
||||
self.assertIn(product.title, rail)
|
||||
|
||||
def test_serp_featured_rail_prefers_curated_featured(self):
|
||||
"""When the operator curates featured_product_ids, the rail
|
||||
shows those."""
|
||||
from ..models.shop import get_shop_by_id
|
||||
shop, product = self._make_ready_tagged_product(
|
||||
"rail-feat-shop", "CuratedRailItem"
|
||||
)
|
||||
# Snapshot scalars BEFORE the commit detaches the ORM objects.
|
||||
shop_id = str(shop.id)
|
||||
pid = str(product.id)
|
||||
title = product.title
|
||||
import json as _json
|
||||
import transaction
|
||||
self.dbsession.expire_all()
|
||||
s = get_shop_by_id(self.dbsession, shop_id)
|
||||
s.featured_product_ids_json = _json.dumps([pid])
|
||||
self.dbsession.add(s)
|
||||
self.dbsession.flush()
|
||||
transaction.manager.commit()
|
||||
res = self.testapp.get(f"/s/{shop_id}/tag/math")
|
||||
body = res.body.decode()
|
||||
self.assertIn('class="serp-rail"', body)
|
||||
rail = body.split('class="serp-rail"', 1)[1]
|
||||
self.assertIn(title, rail)
|
||||
|
||||
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
|
||||
|
|
@ -9074,23 +9138,31 @@ class TestHomeLayoutAndTags(_AuthenticatedBase):
|
|||
import transaction
|
||||
transaction.manager.commit()
|
||||
|
||||
# Upper bound — only cheap_widget passes.
|
||||
# Scope price assertions to the RESULTS grid only — the new
|
||||
# featured/random rail (<aside class="serp-rail">) intentionally
|
||||
# shows shop products regardless of the price filter (it's a
|
||||
# discovery rail, not filtered), so assert against the portion
|
||||
# of the page before the rail.
|
||||
def _grid(b):
|
||||
return b.split('class="serp-rail"')[0]
|
||||
|
||||
# Upper bound — only cheap_widget passes (in the results grid).
|
||||
res = self.testapp.get(f"/s/{shop_id}/tag/tools?price_max=10")
|
||||
body = res.body.decode()
|
||||
self.assertIn("cheap_widget", body)
|
||||
self.assertNotIn("pricey_widget", body)
|
||||
grid = _grid(res.body.decode())
|
||||
self.assertIn("cheap_widget", grid)
|
||||
self.assertNotIn("pricey_widget", grid)
|
||||
|
||||
# Lower bound — only pricey_widget passes.
|
||||
res = self.testapp.get(f"/s/{shop_id}/tag/tools?price_min=10")
|
||||
body = res.body.decode()
|
||||
self.assertIn("pricey_widget", body)
|
||||
self.assertNotIn("cheap_widget", body)
|
||||
grid = _grid(res.body.decode())
|
||||
self.assertIn("pricey_widget", grid)
|
||||
self.assertNotIn("cheap_widget", grid)
|
||||
|
||||
# No bound — both visible.
|
||||
# No bound — both visible in the grid.
|
||||
res = self.testapp.get(f"/s/{shop_id}/tag/tools")
|
||||
body = res.body.decode()
|
||||
self.assertIn("cheap_widget", body)
|
||||
self.assertIn("pricey_widget", body)
|
||||
grid = _grid(res.body.decode())
|
||||
self.assertIn("cheap_widget", grid)
|
||||
self.assertIn("pricey_widget", grid)
|
||||
|
||||
def test_shop_home_lanes_renders_facet_sidebar_and_mobile_rows(self):
|
||||
"""Layout 2 (lanes) shop home gets facet sidebar + SERP rows for
|
||||
|
|
|
|||
|
|
@ -336,6 +336,48 @@ def _sort_products(products, sort_key, dbsession=None, shop_id=None):
|
|||
return sorted(products, key=lambda p: -(p.created_timestamp or 0))
|
||||
|
||||
|
||||
def _featured_rail_products(request, shop, limit=6):
|
||||
"""MPS-24: right-rail products for SERP pages.
|
||||
|
||||
The shop's curated featured set (`shop.featured_product_ids`),
|
||||
filtered to public + ready products of this shop, deduped, capped.
|
||||
If the operator hasn't curated any, fall back to a random rotation
|
||||
of public ready products so the rail is never empty (discovery).
|
||||
Bounded — no unbounded query (CWE-407-safe).
|
||||
"""
|
||||
if shop is None:
|
||||
return []
|
||||
import random
|
||||
from sqlalchemy import func
|
||||
from ..models.product import get_product_by_id, Product
|
||||
|
||||
out = []
|
||||
seen = set()
|
||||
for pid in shop.featured_product_ids:
|
||||
if len(out) >= limit:
|
||||
break
|
||||
p = get_product_by_id(request.dbsession, pid)
|
||||
if (
|
||||
p is not None
|
||||
and p.shop_id == shop.id
|
||||
and p.visibility == 1
|
||||
and p.is_ready
|
||||
and p.id not in seen
|
||||
):
|
||||
seen.add(p.id)
|
||||
out.append(p)
|
||||
|
||||
if not out:
|
||||
pool = (
|
||||
shop.products.filter(Product.visibility == 1)
|
||||
.order_by(func.random())
|
||||
.limit(limit * 4)
|
||||
.all()
|
||||
)
|
||||
out = [p for p in pool if p.is_ready][:limit]
|
||||
return out
|
||||
|
||||
|
||||
def _build_home_layout_context(request, shop, products):
|
||||
"""
|
||||
MPS-24: derive home-layout context (chips/lanes/featured) for a shop home
|
||||
|
|
@ -368,9 +410,11 @@ def _build_home_layout_context(request, shop, products):
|
|||
"sort_options": SORT_OPTIONS,
|
||||
"price_min": min_cents,
|
||||
"price_max": max_cents,
|
||||
"featured_rail": [],
|
||||
}
|
||||
if shop is None:
|
||||
return ctx
|
||||
ctx["featured_rail"] = _featured_rail_products(request, shop)
|
||||
|
||||
layout = shop.home_layout or 0
|
||||
tag_limit = int(shop.home_layout_tag_limit or 8)
|
||||
|
|
@ -2144,6 +2188,7 @@ def shop_tag_detail(request):
|
|||
"sort_options": SORT_OPTIONS,
|
||||
"price_min": min_cents,
|
||||
"price_max": max_cents,
|
||||
"featured_rail": _featured_rail_products(request, shop),
|
||||
}
|
||||
return ctx
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue