← Retour aux articles

AURORA Commerce Pro : ajouter le support Sanity sans toucher le magasin

Publié le 1 avril 2026 · 4 min de lecture

nuxtsanityvuetailwindcss

Cet article est traduit automatiquement depuis l'anglais. Voir sur dev.to

Lorsque j’ai publié mon premier article sur AURORA Commerce, un ancien collègue a laissé un commentaire qui m’a retenu :

« L’idée est excellente, mais votre approche de repo-as-database suppose un opérateur technique.

Il avait raison, et c’était exactement la limitation que je n’avais pas abordée publiquement.

Je l’ai réparé, voilà comment.


Les contraintes

La boutique originale AURORA Commerce lit les produits des fichiers YAML via Nuxt Content :

const { data: products } = await useAsyncData('products', () =>
  queryCollection('products')
    .where('active', '=', true)
    .all()
)

Pour un client qui a besoin de mettre à jour une description du produit un mardi après-midi sans toucher un terminal - ce n’est pas le cas.

La solution naïve serait de faire des requêtes de code dur Sanity sur toutes les pages de la boutique. mais cela signifie que la boutique devient couplée à un CMS spécifique.

Je voulais quelque chose de plus propre.


Le modèle d’adaptateur

L’idée est simple: introduisez un seul résolveur entre les itinéraires API et la source de données. Les pages Storefront ne demandent pas directement les produits - ils appellent les itinéraires API. Les itinéraires API appellent l’adaptateur. L’adaptateur sait d’où viennent les données. Rien d’autre ne le fait.

Storefront pages

API routes (/api/products, /api/products/[slug])

getCatalogAdapter(event)

NuxtContentAdapter | SanityAdapter | YourCustomAdapter

CatalogProduct (normalized)

mapProductForApi(product, locale)

ProductApiItem (locale-aware response)

Les principaux fichiers :

server/utils/catalog/
  types.ts         # CatalogProduct, ProductApiItem, CatalogAdapter
  map.ts           # mapProductForApi + resolveLocale
  adapters/
    index.ts       # Resolver — reads CATALOG_PROVIDER env var
    content.ts     # Nuxt Content (YAML) implementation
    sanity.ts      # Sanity implementation

Le contrat

Deux interfaces définissent l’ensemble du système.

« CatalogAdapter » est le contrat que chaque fournisseur doit mettre en œuvre :

// server/utils/catalog/types.ts
export interface CatalogAdapter {
  listProducts: (event: any) => Promise<CatalogProduct[]>
  getProductBySlug: (event: any, slug: string) => Promise<CatalogProduct | null>
}

‘CatalogProduct’ est la forme interne normalisée - les mêmes champs, les mêmes types, indépendamment de la source.

‘ProductApiItem’ est ce que les itinéraires API rendent réellement - locale-aware, avec ‘priceCents’ déjà calculés:

export interface ProductApiItem {
  id: number
  name: string        // resolved from title / titleFr
  slug: string
  description: string // resolved from description / descriptionFr
  priceCents: number  // Math.round(price * 100) — euros in YAML, cents to the client
  imageUrl: string
  imageUrls: string[]
  sizes: string[]
  // ... rest of the fields
}

Que la conversion de “priceCents” se produit dans “map.ts”, pas dans l’itinéraire de paiement. Un endroit, aucune surprise.


La résolution

Le changement de fournisseur est une variable environnementale :

# Use Nuxt Content YAML (default)
CATALOG_PROVIDER=content

# Use Sanity
CATALOG_PROVIDER=sanity
SANITY_PROJECT_ID=your_project_id
SANITY_DATASET=production
SANITY_API_VERSION=2025-01-01
SANITY_TOKEN=optional_read_token

Le résolveur dans ‘adapters/index.ts’ lit ‘CATALOG_PROVIDER’ et renvoie l’adaptateur correct.

// server/api/products.get.ts
export default defineEventHandler(async (event) => {
  const adapter = getCatalogAdapter(event)
  const locale = resolveLocale(event) // reads ?locale= query param
  const products = await adapter.listProducts(event)
  return products.map((p) => mapProductForApi(p, locale))
})

‘resolveLocale’ lit le paramètre de la requête ‘locale’ et renvoie '‘fr’annoo ‘en’’.La résolution locale reste du côté du serveur - ‘mapProductForApi’ choisit le champ droit (‘titleFr’ contre ‘title’, ‘descriptionFr’ contre ‘description’, etc.) avant d’envoyer la réponse.

Aucun ‘if/else’ dispersé dans la base de codes. Aucun Sanity n’importe dans les pages de la boutique. L’itinéraire de paiement est inchangé.


L’adaptateur de santé

L’adaptateur Sanity est un ensemble de fonctions pures - pas de classe, pas d’état d’instance:

// server/utils/catalog/adapters/sanity.ts
export const sanityAdapter: CatalogAdapter = {
  async listProducts() {
    const client = createSanityClient()
    const raw = await client.fetch(`*[_type == "product"]`)
    return raw.map(normalizeSanityProduct)
  },

  async getProductBySlug(_, slug) {
    const client = createSanityClient()
    const raw = await client.fetch(
      `*[_type == "product" && (slug.current == $slug || slug == $slug)][0]`,
      { slug }
    )
    return raw ? normalizeSanityProduct(raw) : null
  }
}

La requête slug traite à la fois la forme native ‘slug.current’ de Sanity et les slugs de chaîne simples - utiles lors de la migration de données à partir de YAML.

Normalisation des images

Les images sanitaires peuvent être présentées sous trois formes différentes en fonction de la configuration du schéma :

const normalizeImages = (value: unknown): string[] => {
  if (!Array.isArray(value)) return []

  return value
    .map((image) => {
      // Plain URL string
      if (typeof image === 'string') return image

      // Sanity asset reference with resolved URL
      if (image?.asset?.url) return String(image.asset.url)

      // Direct URL field
      if (image?.url) return String(image.url)

      return ''
    })
    .filter(Boolean)
}

Si votre schéma Sanity utilise des références d’actifs sans projection d’URL, ajoutez ‘url’ à votre requête GROQ.

Normalisation complète des produits

const normalizeSanityProduct = (entry: any): CatalogProduct => ({
  productId: Number(entry.productId || 0),
  title: String(entry.title || ''),
  titleFr: optionalString(entry.titleFr),
  slug: normalizeSlug(entry.slug),
  price: Number(entry.price || 0),
  category: String(entry.category || 'tailoring') as CatalogProduct['category'],
  images: normalizeImages(entry.images),
  sizes: toStringArray(entry.sizes),
  sizeChart: Array.isArray(entry.sizeChart) ? entry.sizeChart : [],
  reviews: Array.isArray(entry.reviews) ? entry.reviews : [],
  // bilingual fields follow the same optionalString pattern
})

Si Sanity modifie son schéma, seule cette fonctionnalité a besoin d’être mise à jour. Le magasin, le panier et le checkout Stripe ne le savent pas.


Encore une chose : la résilience de l’image

Les URL d’image sanitaires peuvent occasionnellement échouer - le CDN manque, les actifs non publiés.

// app/pages/produit/[slug].vue
const failedPrimaryImages = ref<string[]>([])

const primaryImageSrc = computed(() => {
  if (selectedImage.value) return selectedImage.value
  return product.value?.imageUrls.find(
    (img) => !failedPrimaryImages.value.includes(img)
  ) || product.value?.imageUrl || ''
})

const handlePrimaryImageError = () => {
  const current = primaryImageSrc.value
  if (current) failedPrimaryImages.value.push(current)
  const next = product.value?.imageUrls.find(
    (img) => !failedPrimaryImages.value.includes(img)
  )
  selectedImage.value = next || ''
}

Les images manquantes sont suivies dans un ref. Le calcul choisit automatiquement la prochaine image disponible. Pas de place d’image cassée, pas de flash - juste un retour silencieux.


Utiliser son propre adaptateur

L’ajout de Contentful, Hygraph ou une API REST personnalisée est de quatre étapes:

  1. Créer « serveur / utilitaires / catalogue / adaptateurs / your-cms.ts »
  2. Exporter un objet “CatalogAdapter” avec “listProducts” et “getProductBySlug”
  3. Normalisez la charge utile de votre CMS en ‘CatalogProduct’
  4. Inscrivez votre fournisseur dans « adaptateurs / index.ts »
  5. Sélectionnez « CATALOG_PROVIDER=your-cms»

Les pages de la boutique, le panier et le chèque Stripe ne changent pas.


Ce qui déverrouille

Le Starter original était honnête sur sa contrainte : YAML fonctionne pour un opérateur technique, pas pour un client qui a besoin d’une interface visuelle.

La même boutique, la même intégration Stripe, la même configuration bilingue - mais la source de catalogue devient un choix de configuration, pas une contrainte architecturale.

démonstration en directAURORA Commerce sur Gumroad - À partir de €29 / Pro €59


Thanks to Robin for the feedback that drove this iteration.

Built with Nuxt 4, Vue 3, Nuxt Content v3, Sanity, Pinia, Tailwind CSS, Stripe, and Bun.