@faststore/api 4.7.0-dev.7 → 4.7.0-dev.9

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.
@@ -77,7 +77,15 @@ export async function getLocalizedProductEntry(
77
77
  categories: result.categories ?? [],
78
78
  availableLinkIds: result.availableLinkIds ?? {},
79
79
  }
80
- } catch {
80
+ } catch (err) {
81
+ // Without this the fallback is silent: the breadcrumb renders the IS
82
+ // category names, always in the default language, and the page can be
83
+ // frozen in that state by `revalidate: false`.
84
+ console.warn(
85
+ `[getLocalizedProductEntry] failed to load product ${productId} for locale ${locale}:`,
86
+ err
87
+ )
88
+
81
89
  return null
82
90
  }
83
91
  })()
@@ -314,7 +322,6 @@ export const StoreProduct: Record<string, GraphqlResolver<Root>> & {
314
322
  const productId = root.isVariantOf.productId
315
323
  const itemId = root.itemId
316
324
  const locale = ctx.storage.locale
317
- const defaultLocale = getDefaultLocale(ctx)
318
325
 
319
326
  // availableLinkIds returns localized slug for every locale,
320
327
  // we fetch for the current locale (reusing the request-scoped cache shared with the slug and
@@ -324,29 +331,56 @@ export const StoreProduct: Record<string, GraphqlResolver<Root>> & {
324
331
  if (!entry?.availableLinkIds) return null
325
332
 
326
333
  const { availableLinkIds } = entry
327
- const { linkText } = root.isVariantOf
328
334
 
329
335
  return configuredLocales
330
336
  .map((configuredLocale) => {
331
- // The default locale always uses the canonical IS linkText: it is always
332
- // present and matches the Query.product `slug.startsWith(linkText)` fast
333
- // path, so the fallback URL resolves cleanly even when the catalog has no
334
- // default-locale entry in availableLinkIds.
335
- if (configuredLocale === defaultLocale) {
336
- return { locale: configuredLocale, slug: getSlug(linkText, itemId) }
337
- }
338
-
339
- // Non-default locales only appear when they have a registered localized slug
340
- // in availableLinkIds. Untranslated locales are omitted so they are never
341
- // advertised as hreflang alternates — this keeps the hreflang cluster
342
- // symmetric across all locale variants of the product (every variant emits
343
- // the same set: default + translated locales). The LocalizationSelector
344
- // falls back to the default slug under the target prefix for omitted locales.
337
+ // availableLinkIds is identical whichever locale the Dataplane is queried
338
+ // with, so deriving every slug from it keeps the hreflang cluster
339
+ // reciprocal. The Intelligent Search linkText cannot stand in for a
340
+ // missing entry: it is localized to the locale being browsed, which would
341
+ // both point other locales at a 404 and make the advertised set depend on
342
+ // which locale served the request.
345
343
  const linkId = availableLinkIds[configuredLocale]
344
+
345
+ // Locales with no registered localized slug are omitted rather than
346
+ // guessed, so an alternate is only ever advertised for a slug the catalog
347
+ // actually resolves. A product with no translations advertises no
348
+ // alternates at all, from any locale. Navigating to an omitted locale is
349
+ // served by `defaultLocaleSlug`, which is free to guess precisely because
350
+ // it never reaches an hreflang annotation.
346
351
  return linkId
347
352
  ? { locale: configuredLocale, slug: getSlug(linkId, itemId) }
348
353
  : null
349
354
  })
350
355
  .filter((e): e is { locale: string; slug: string } => e !== null)
351
356
  },
357
+ defaultLocaleSlug: async (root, _args, ctx) => {
358
+ if (!isLocalizationEnabled(ctx)) return null
359
+
360
+ const defaultLocale = getDefaultLocale(ctx)
361
+
362
+ if (!defaultLocale) return null
363
+
364
+ const { productId, linkText } = root.isVariantOf
365
+ const itemId = root.itemId
366
+
367
+ // Reuses the request-scoped entry `otherLocales` already loads, so serving
368
+ // both fields costs a single Dataplane call.
369
+ const entry = await getLocalizedProductEntry(
370
+ ctx,
371
+ productId,
372
+ ctx.storage.locale
373
+ )
374
+
375
+ // linkText is localized to the locale being browsed, which is what makes it
376
+ // unusable for hreflang. It is acceptable here because an untranslated
377
+ // product carries the same slug in every locale, so linkText *is* the
378
+ // default-locale slug in the exact case where availableLinkIds is empty.
379
+ // A product translated for the browsed locale but not for the default one
380
+ // still yields a wrong guess; that URL 404s, which is the pre-existing
381
+ // behavior and still better than dropping the shopper on the locale root.
382
+ const linkId = entry?.availableLinkIds?.[defaultLocale] ?? linkText
383
+
384
+ return getSlug(linkId, itemId)
385
+ },
352
386
  }
@@ -109,10 +109,11 @@ const shouldFallbackToProductRoute = (error: unknown) =>
109
109
  * guards that the fetched sku is the one we actually asked for, throwing
110
110
  * SLUG_MISMATCH_ERROR (caught by the caller) when it isn't.
111
111
  *
112
- * When localization is enabled, the slug prefix may be a localized LinkId
113
- * that differs from the IS linkText (always in the default locale). In that
114
- * case we validate against the Catalog Dataplane API before rejecting the
115
- * slug.
112
+ * When localization is enabled, the slug prefix may be a localized LinkId that
113
+ * differs from the IS linkText. linkText follows the locale being browsed
114
+ * rather than the default one, so a mismatch is not by itself evidence of a
115
+ * wrong sku. We validate against the Catalog Dataplane API before rejecting
116
+ * the slug.
116
117
  */
117
118
  async function assertSkuMatchesSlug(
118
119
  ctx: GraphqlContext,
@@ -99,6 +99,14 @@ type StoreProduct {
99
99
  Only populated when localization is enabled.
100
100
  """
101
101
  otherLocales: [StoreProductLocale!]
102
+ """
103
+ Slug for the store's default locale, including the SKU ID suffix.
104
+ Navigation fallback for the locale selector when the target locale is absent
105
+ from `otherLocales`. Best-effort: it may point at a slug the catalog has not
106
+ registered for the default locale, so it must never be used to build hreflang
107
+ annotations. Only populated when localization is enabled.
108
+ """
109
+ defaultLocaleSlug: String
102
110
  }
103
111
 
104
112
  """