Skip to content

Commit f89d73b

Browse files
authored
Add hidden paths (#137)
* Add hidden paths prop * Update changelog * Fix skeleton display
1 parent e0299b0 commit f89d73b

11 files changed

Lines changed: 153 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Hidden Paths property.
13+
14+
### Fixed
15+
16+
- Skeleton display.
17+
1018
## [2.21.0] - 2026-07-02
1119

1220
### Added

docs/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ Follow these steps to use the Recommendation Shelf app in your store:
4545
"blocks": ["list-context.product-list-static"],
4646
"props": {
4747
"campaignVrn": "vrn:recommendations:biggy:rec-similar-v2:00318b68-cb1b-4d5a-8b0f-cc7fbcdd014b",
48-
"title": "Similar items"
48+
"title": "Similar items",
49+
"hiddenPaths": ["/checkout/cart", "/produto/*"]
4950
}
5051
},
5152
"list-context.product-list-static": {
@@ -101,6 +102,7 @@ Configure the `recommendation-shelf` block using the following properties:
101102
| `campaignVrn` | `string` | VRN for the recommendation campaign (the recommendation **list ID** from Admin. See [Obtaining the VRN](#obtaining-the-vrn)). | - |
102103
| `displayTitle` | `boolean` | Whether to show the shelf title (`true`) or hide it (`false`). | `true` |
103104
| `itemsContext` | `array` | Context source for items in the recommendation request (`PDP` or `CART`). Useful for enabling shelves on the cart page with `CROSS_SELL`. | `['PDP']` |
105+
| `hiddenPaths` | `array` | URL paths where the shelf should not be displayed. Supports exact paths (e.g. `/checkout/cart`) and prefix wildcards with `*` (e.g. `/produto/*`). Useful when the shelf is placed in a global section (such as the footer) but should be hidden on specific pages. | `[]` |
104106

105107
### Obtaining the VRN
106108

messages/context.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@
77
"admin/editor.recommendation-shelf.items-context": "Items Context",
88
"admin/editor.recommendation-shelf.items-context.description": "From where to retrieve the items that will be used as context in the Recommendation request",
99
"admin/editor.recommendation-shelf.items-context.cart": "Cart",
10-
"admin/editor.recommendation-shelf.items-context.pdp": "Product Page"
10+
"admin/editor.recommendation-shelf.items-context.pdp": "Product Page",
11+
"admin/editor.recommendation-shelf.hidden-paths": "Hidden paths",
12+
"admin/editor.recommendation-shelf.hidden-paths.description": "URL paths where the shelf should not be displayed. Use exact paths (e.g. /checkout/cart) or prefix wildcards with * (e.g. /produto/*)."
1113
}

messages/en.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@
77
"admin/editor.recommendation-shelf.items-context": "Items Context",
88
"admin/editor.recommendation-shelf.items-context.description": "From where to retrieve the items that will be used as context in the Recommendation request",
99
"admin/editor.recommendation-shelf.items-context.cart": "Cart",
10-
"admin/editor.recommendation-shelf.items-context.pdp": "Product Page"
10+
"admin/editor.recommendation-shelf.items-context.pdp": "Product Page",
11+
"admin/editor.recommendation-shelf.hidden-paths": "Hidden paths",
12+
"admin/editor.recommendation-shelf.hidden-paths.description": "URL paths where the shelf should not be displayed. Use exact paths (e.g. /checkout/cart) or prefix wildcards with * (e.g. /produto/*)."
1113
}

messages/es.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@
77
"admin/editor.recommendation-shelf.items-context": "Contexto de los artículos",
88
"admin/editor.recommendation-shelf.items-context.description": "Contexto de los artículos",
99
"admin/editor.recommendation-shelf.items-context.cart": "Carrito",
10-
"admin/editor.recommendation-shelf.items-context.pdp": "Página de producto"
10+
"admin/editor.recommendation-shelf.items-context.pdp": "Página de producto",
11+
"admin/editor.recommendation-shelf.hidden-paths": "Rutas ocultas",
12+
"admin/editor.recommendation-shelf.hidden-paths.description": "Rutas de URL donde la estantería no debe mostrarse. Use rutas exactas (p. ej. /checkout/cart) o comodines de prefijo con * (p. ej. /produto/*)."
1113
}

messages/pt.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@
77
"admin/editor.recommendation-shelf.items-context": "Contexto dos Itens",
88
"admin/editor.recommendation-shelf.items-context.description": "De onde recuperar os itens que serão usados como contexto na requisição de recomendação",
99
"admin/editor.recommendation-shelf.items-context.cart": "Carrinho",
10-
"admin/editor.recommendation-shelf.items-context.pdp": "Página de Produto"
10+
"admin/editor.recommendation-shelf.items-context.pdp": "Página de Produto",
11+
"admin/editor.recommendation-shelf.hidden-paths": "Caminhos ocultos",
12+
"admin/editor.recommendation-shelf.hidden-paths.description": "Caminhos de URL onde a vitrine não deve ser exibida. Use caminhos exatos (ex.: /checkout/cart) ou curingas de prefixo com * (ex.: /produto/*)."
1113
}

react/RecommendationShelf.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import React from 'react'
22
import { defineMessages } from 'react-intl'
3+
import { useRuntime } from 'vtex.render-runtime'
34

45
import { RecommendationShelfErrorBoundary } from './components/RecommendationShelfErrorBoundary'
56
import { RecommendationShelfContainer } from './components/RecommendationShelfContainer'
7+
import { isPathHidden } from './utils/hiddenPaths'
68

79
defineMessages({
810
shelf: {
@@ -43,23 +45,40 @@ defineMessages({
4345
id: 'admin/editor.recommendation-shelf.items-context.pdp',
4446
defaultMessage: 'Product Page',
4547
},
48+
hiddenPaths: {
49+
id: 'admin/editor.recommendation-shelf.hidden-paths',
50+
defaultMessage: 'Hidden paths',
51+
},
52+
hiddenPathsDescription: {
53+
id: 'admin/editor.recommendation-shelf.hidden-paths.description',
54+
defaultMessage:
55+
'URL paths where the shelf should not be displayed. Use exact paths (e.g. /checkout/cart) or prefix wildcards with * (e.g. /produto/*).',
56+
},
4657
})
4758

4859
type Props = {
4960
campaignVrn: string
5061
title?: string
5162
displayTitle: boolean
5263
itemsContext: ItemContextType[]
64+
hiddenPaths?: string[]
5365
}
5466

5567
const RecommendationShelf: StorefrontFunctionComponent<Props> = ({
5668
campaignVrn,
5769
title,
5870
displayTitle,
5971
itemsContext,
72+
hiddenPaths,
6073
}) => {
74+
const { route } = useRuntime()
75+
6176
if (!campaignVrn) return null
6277

78+
const currentPath = route?.canonicalPath ?? route?.path ?? '/'
79+
80+
if (isPathHidden(currentPath, hiddenPaths)) return null
81+
6382
return (
6483
<RecommendationShelfErrorBoundary>
6584
<RecommendationShelfContainer
@@ -105,6 +124,15 @@ RecommendationShelf.schema = {
105124
},
106125
default: ['PDP'],
107126
},
127+
hiddenPaths: {
128+
title: 'admin/editor.recommendation-shelf.hidden-paths',
129+
description: 'admin/editor.recommendation-shelf.hidden-paths.description',
130+
type: 'array',
131+
items: {
132+
type: 'string',
133+
},
134+
default: [],
135+
},
108136
},
109137
}
110138

react/components/ShelfSkeleton/index.tsx

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,28 @@
11
import React from 'react'
22
import { useCssHandles } from 'vtex.css-handles'
3+
import { useRuntime } from 'vtex.render-runtime'
34

45
import { SkeletonPiece } from './SkeletonPiece'
56

67
const CSS_HANDLES = ['recommendationShelfContainer']
78

8-
function checkDevice(): 'DESKTOP' | 'MOBILE' | 'TABLET' {
9-
if (typeof window === 'undefined') {
10-
return 'MOBILE'
11-
}
12-
13-
if (window.innerWidth <= 768) {
14-
return 'MOBILE'
15-
}
16-
17-
if (window.innerWidth <= 1024) {
18-
return 'TABLET'
19-
}
20-
21-
if (window.innerWidth > 1024) {
22-
return 'DESKTOP'
23-
}
24-
25-
return 'MOBILE'
26-
}
27-
9+
// Number of skeleton tiles per device type. The device is resolved by
10+
// render-runtime from the User-Agent on the server, so it is consistent
11+
// across SSR and client hydration (unlike window.innerWidth).
2812
const DEVICE_MAP = {
29-
DESKTOP: 5,
30-
MOBILE: 2,
31-
TABLET: 3,
32-
}
13+
phone: 2,
14+
tablet: 3,
15+
desktop: 5,
16+
} as const
3317

3418
export function ShelfSkeleton() {
3519
const handles = useCssHandles(CSS_HANDLES)
36-
const device = checkDevice()
37-
const skeletonCount = DEVICE_MAP[device] || DEVICE_MAP.DESKTOP
20+
const { deviceInfo } = useRuntime()
21+
22+
const skeletonCount =
23+
DEVICE_MAP[deviceInfo?.type as keyof typeof DEVICE_MAP] ??
24+
DEVICE_MAP.desktop
25+
3826
const skeletonPieces = Array.from({ length: skeletonCount }, (_, index) => (
3927
<SkeletonPiece key={index} />
4028
))

react/utils/hiddenPaths.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { isPathHidden, normalizePath } from './hiddenPaths'
2+
3+
describe('normalizePath', () => {
4+
it('adds leading slash when missing', () => {
5+
expect(normalizePath('checkout')).toBe('/checkout')
6+
})
7+
8+
it('removes trailing slash except for root', () => {
9+
expect(normalizePath('/checkout/')).toBe('/checkout')
10+
expect(normalizePath('/')).toBe('/')
11+
})
12+
13+
it('returns root for empty path', () => {
14+
expect(normalizePath('')).toBe('/')
15+
})
16+
})
17+
18+
describe('isPathHidden', () => {
19+
it('returns false when hiddenPaths is empty or undefined', () => {
20+
expect(isPathHidden('/checkout', undefined)).toBe(false)
21+
expect(isPathHidden('/checkout', [])).toBe(false)
22+
})
23+
24+
it('matches exact paths', () => {
25+
expect(isPathHidden('/checkout/cart', ['/checkout/cart'])).toBe(true)
26+
expect(isPathHidden('/checkout/cart/', ['/checkout/cart'])).toBe(true)
27+
expect(isPathHidden('/checkout', ['/checkout/cart'])).toBe(false)
28+
})
29+
30+
it('matches wildcard prefix paths', () => {
31+
expect(isPathHidden('/produto', ['/produto/*'])).toBe(true)
32+
expect(isPathHidden('/produto/sku-123', ['/produto/*'])).toBe(true)
33+
expect(isPathHidden('/produtos', ['/produto/*'])).toBe(false)
34+
})
35+
36+
it('matches root path exactly', () => {
37+
expect(isPathHidden('/', ['/'])).toBe(true)
38+
expect(isPathHidden('/home', ['/'])).toBe(false)
39+
})
40+
})

react/utils/hiddenPaths.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export function normalizePath(path: string): string {
2+
if (!path) return '/'
3+
4+
const withLeadingSlash = path.startsWith('/') ? path : `/${path}`
5+
6+
if (withLeadingSlash !== '/' && withLeadingSlash.endsWith('/')) {
7+
return withLeadingSlash.slice(0, -1)
8+
}
9+
10+
return withLeadingSlash
11+
}
12+
13+
export function isPathHidden(
14+
currentPath: string,
15+
hiddenPaths?: string[]
16+
): boolean {
17+
if (!hiddenPaths?.length) return false
18+
19+
const normalizedCurrent = normalizePath(currentPath)
20+
21+
return hiddenPaths.some((hiddenPath) => {
22+
const isWildcard = hiddenPath.endsWith('*')
23+
const pathWithoutWildcard = isWildcard
24+
? hiddenPath.slice(0, -1)
25+
: hiddenPath
26+
27+
const normalizedHidden = normalizePath(pathWithoutWildcard)
28+
29+
if (isWildcard) {
30+
return (
31+
normalizedCurrent === normalizedHidden ||
32+
normalizedCurrent.startsWith(`${normalizedHidden}/`)
33+
)
34+
}
35+
36+
return normalizedCurrent === normalizedHidden
37+
})
38+
}

0 commit comments

Comments
 (0)