@faststore/api 4.7.1-dev.0 → 4.7.1-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.js +58 -47
- package/dist/cjs/index.js.map +1 -1
- package/dist/es/index.mjs +988 -977
- package/dist/es/index.mjs.map +1 -1
- package/dist/src/__generated__/schema.d.ts +12 -1
- package/dist/src/__generated__/schema.d.ts.map +1 -1
- package/dist/src/platforms/vtex/resolvers/collection.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__generated__/schema.ts +12 -1
- package/src/platforms/vtex/resolvers/collection.ts +81 -68
- package/src/platforms/vtex/typeDefs/collection.graphql +12 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GraphqlResolver } from '..'
|
|
1
|
+
import type { GraphqlContext, GraphqlResolver } from '..'
|
|
2
2
|
import type { Brand } from '../clients/commerce/types/Brand'
|
|
3
3
|
import type { CategoryTree } from '../clients/commerce/types/CategoryTree'
|
|
4
4
|
import type { CollectionPageType } from '../clients/commerce/types/Portal'
|
|
@@ -39,6 +39,41 @@ export type LegacyStoreCollectionRoot =
|
|
|
39
39
|
*/
|
|
40
40
|
export type Root = ByLinkIdRoot | LegacyStoreCollectionRoot
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Lowercases a catalog slug so one spelling is announced per locale.
|
|
44
|
+
*
|
|
45
|
+
* Merchants register linkIds in whatever casing they typed ("Elettronica",
|
|
46
|
+
* "Eletronicos"), and by-linkid resolves any casing, so without this every
|
|
47
|
+
* variant would be a legitimate URL advertising itself as canonical. Accents
|
|
48
|
+
* are preserved: they are part of the registered slug, not a casing artifact.
|
|
49
|
+
*/
|
|
50
|
+
const canonicalizeSlug = (slug: string): string => slug.toLowerCase()
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Loads the by-linkid entity for every level of a collection slug, so
|
|
54
|
+
* "vestuario/camisetas" resolves both "vestuario" and "vestuario/camisetas".
|
|
55
|
+
* The collectionLoader cache means a segment already fetched by another
|
|
56
|
+
* resolver of the same request costs nothing here.
|
|
57
|
+
*/
|
|
58
|
+
const loadSegmentEntities = (
|
|
59
|
+
ctx: GraphqlContext,
|
|
60
|
+
slug: string
|
|
61
|
+
): Promise<ByLinkIdRoot[]> => {
|
|
62
|
+
const segments = slug.split('/').filter(Boolean)
|
|
63
|
+
const {
|
|
64
|
+
loaders: { collectionLoader },
|
|
65
|
+
} = ctx
|
|
66
|
+
|
|
67
|
+
return Promise.all(
|
|
68
|
+
segments.map((_, index) =>
|
|
69
|
+
collectionLoader.load({
|
|
70
|
+
slug: segments.slice(0, index + 1).join('/'),
|
|
71
|
+
locale: getCatalogLocale(ctx),
|
|
72
|
+
})
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
42
77
|
const slugifyRoot = (root: ByLinkIdRoot): string => {
|
|
43
78
|
if (isCategory(root)) {
|
|
44
79
|
// root.slug is the full accumulated input slug (e.g. "vestuario/camisetas"),
|
|
@@ -86,24 +121,13 @@ export const StoreCollection: Record<string, GraphqlResolver<ByLinkIdRoot>> = {
|
|
|
86
121
|
}
|
|
87
122
|
|
|
88
123
|
// For categories, IS expects the canonical (default-locale) slug in selectedFacets
|
|
89
|
-
// regardless of which locale the current request uses.
|
|
90
|
-
//
|
|
124
|
+
// regardless of which locale the current request uses. `linkId` carries the slug
|
|
125
|
+
// registered for the *requested* locale, so it is the wrong one here whenever the
|
|
126
|
+
// shopper is browsing anything but the default locale; availableLinkIds is keyed by
|
|
127
|
+
// locale and gives us the default-locale slug directly.
|
|
91
128
|
const { defaultLocale } = getLocalizationConfig(ctx)
|
|
92
129
|
|
|
93
|
-
const
|
|
94
|
-
const segmentSlugs = segments.map((_, i) =>
|
|
95
|
-
segments.slice(0, i + 1).join('/')
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
const {
|
|
99
|
-
loaders: { collectionLoader },
|
|
100
|
-
} = ctx
|
|
101
|
-
|
|
102
|
-
const entities = await Promise.all(
|
|
103
|
-
segmentSlugs.map((s) =>
|
|
104
|
-
collectionLoader.load({ slug: s, locale: getCatalogLocale(ctx) })
|
|
105
|
-
)
|
|
106
|
-
)
|
|
130
|
+
const entities = await loadSegmentEntities(ctx, slug)
|
|
107
131
|
|
|
108
132
|
return {
|
|
109
133
|
selectedFacets: entities.map((entity, index) => ({
|
|
@@ -115,27 +139,7 @@ export const StoreCollection: Record<string, GraphqlResolver<ByLinkIdRoot>> = {
|
|
|
115
139
|
}
|
|
116
140
|
},
|
|
117
141
|
breadcrumbList: async (root, _, ctx) => {
|
|
118
|
-
const
|
|
119
|
-
loaders: { collectionLoader },
|
|
120
|
-
} = ctx
|
|
121
|
-
|
|
122
|
-
const slug = slugifyRoot(root)
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Split slug into segments so each breadcrumb level gets its own
|
|
126
|
-
* by-linkid result. For "vestuario/camisetas" this produces two loader
|
|
127
|
-
* calls: one for "vestuario" and one for "vestuario/camisetas".
|
|
128
|
-
*/
|
|
129
|
-
const segments = slug.split('/').filter(Boolean)
|
|
130
|
-
const slugs = segments.map((_, index) =>
|
|
131
|
-
segments.slice(0, index + 1).join('/')
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
const collections = await Promise.all(
|
|
135
|
-
slugs.map((s) =>
|
|
136
|
-
collectionLoader.load({ slug: s, locale: getCatalogLocale(ctx) })
|
|
137
|
-
)
|
|
138
|
-
)
|
|
142
|
+
const collections = await loadSegmentEntities(ctx, slugifyRoot(root))
|
|
139
143
|
|
|
140
144
|
return {
|
|
141
145
|
itemListElement: collections.map((collection, index) => ({
|
|
@@ -156,31 +160,14 @@ export const StoreCollection: Record<string, GraphqlResolver<ByLinkIdRoot>> = {
|
|
|
156
160
|
|
|
157
161
|
if (configuredLocales.length === 0) return null
|
|
158
162
|
|
|
159
|
-
const currentLocale = ctx.storage.locale
|
|
160
163
|
const slug = slugifyRoot(root)
|
|
161
|
-
const segments = slug.split('/').filter(Boolean)
|
|
162
164
|
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
const {
|
|
166
|
-
loaders: { collectionLoader },
|
|
167
|
-
} = ctx
|
|
168
|
-
|
|
169
|
-
// Build per-level slug paths: ["vestuario", "vestuario/camisetas"].
|
|
170
|
-
// The collectionLoader DataLoader cache means any segment already fetched
|
|
171
|
-
// by breadcrumbList or meta costs nothing here.
|
|
172
|
-
const segmentSlugs = segments.map((_, i) =>
|
|
173
|
-
segments.slice(0, i + 1).join('/')
|
|
174
|
-
)
|
|
165
|
+
if (slug.split('/').filter(Boolean).length === 0) return null
|
|
175
166
|
|
|
176
167
|
let entities: ByLinkIdRoot[]
|
|
177
168
|
|
|
178
169
|
try {
|
|
179
|
-
entities = await
|
|
180
|
-
segmentSlugs.map((s) =>
|
|
181
|
-
collectionLoader.load({ slug: s, locale: getCatalogLocale(ctx) })
|
|
182
|
-
)
|
|
183
|
-
)
|
|
170
|
+
entities = await loadSegmentEntities(ctx, slug)
|
|
184
171
|
} catch (err) {
|
|
185
172
|
console.warn('[otherLocales] failed to load collection entities:', err)
|
|
186
173
|
|
|
@@ -189,16 +176,14 @@ export const StoreCollection: Record<string, GraphqlResolver<ByLinkIdRoot>> = {
|
|
|
189
176
|
|
|
190
177
|
return configuredLocales
|
|
191
178
|
.map((configuredLocale) => {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
// If any segment is missing an entry for this locale, omit the whole URL
|
|
201
|
-
// to keep the hreflang cluster symmetric.
|
|
179
|
+
// Every locale, including the one being rendered, is read from
|
|
180
|
+
// availableLinkIds, which holds registered translations only. by-linkid
|
|
181
|
+
// resolves an untranslated segment by falling back to a slug that
|
|
182
|
+
// exists in another locale, so trusting its `linkId` here would
|
|
183
|
+
// advertise an alternate the target locale never registered and break
|
|
184
|
+
// the reciprocity hreflang requires. The page still declares itself
|
|
185
|
+
// canonical through `canonicalSlug`, which is free to fall back
|
|
186
|
+
// precisely because it is not an hreflang annotation.
|
|
202
187
|
const parts: string[] = []
|
|
203
188
|
|
|
204
189
|
for (const entity of entities) {
|
|
@@ -210,9 +195,37 @@ export const StoreCollection: Record<string, GraphqlResolver<ByLinkIdRoot>> = {
|
|
|
210
195
|
}
|
|
211
196
|
|
|
212
197
|
return parts.length > 0
|
|
213
|
-
? {
|
|
198
|
+
? {
|
|
199
|
+
locale: configuredLocale,
|
|
200
|
+
slug: canonicalizeSlug(parts.join('/')),
|
|
201
|
+
}
|
|
214
202
|
: null
|
|
215
203
|
})
|
|
216
204
|
.filter((e): e is { locale: string; slug: string } => e !== null)
|
|
217
205
|
},
|
|
206
|
+
|
|
207
|
+
canonicalSlug: async (root, _, ctx) => {
|
|
208
|
+
const slug = slugifyRoot(root)
|
|
209
|
+
|
|
210
|
+
if (slug.split('/').filter(Boolean).length === 0) return null
|
|
211
|
+
|
|
212
|
+
let entities: ByLinkIdRoot[]
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
entities = await loadSegmentEntities(ctx, slug)
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.warn('[canonicalSlug] failed to load collection entities:', err)
|
|
218
|
+
|
|
219
|
+
return null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const parts = entities.map((entity) => entity.linkId)
|
|
223
|
+
|
|
224
|
+
// A segment with no linkId leaves nothing better than the visited slug to
|
|
225
|
+
// announce, so the page keeps describing itself by the path it was reached
|
|
226
|
+
// through.
|
|
227
|
+
return parts.every((part): part is string => Boolean(part))
|
|
228
|
+
? canonicalizeSlug(parts.join('/'))
|
|
229
|
+
: canonicalizeSlug(slug)
|
|
230
|
+
},
|
|
218
231
|
}
|
|
@@ -95,9 +95,20 @@ type StoreCollection {
|
|
|
95
95
|
type: StoreCollectionType!
|
|
96
96
|
"""
|
|
97
97
|
Localized versions of this collection for all available locales.
|
|
98
|
-
Only
|
|
98
|
+
Only announces slugs the catalog has registered for each locale, so it is safe
|
|
99
|
+
to build hreflang annotations from. Only populated when localization is
|
|
100
|
+
enabled.
|
|
99
101
|
"""
|
|
100
102
|
otherLocales: [StoreCollectionLocale!]
|
|
103
|
+
"""
|
|
104
|
+
Slug the catalog resolves for the requested locale, lowercased.
|
|
105
|
+
Meant for the page's canonical URL: `slug` echoes the path the shopper
|
|
106
|
+
visited, and by-linkid matches case-insensitively, so without this every
|
|
107
|
+
casing variant would claim to be canonical. Unlike `otherLocales`, it may fall
|
|
108
|
+
back to a slug registered for another locale, so it must never be used to
|
|
109
|
+
build hreflang annotations.
|
|
110
|
+
"""
|
|
111
|
+
canonicalSlug: String
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
"""
|