← Tillbaka till artiklar

AURORA Commerce Pro: lägga till Sanity-stöd utan att röra butiksfönstret

Publicerad 1 april 2026 · 4 min läsning

nuxtsanityvuetailwindcss

Den här artikeln är automatiskt översatt från engelska. Visa på dev.to

När jag publicerade min första artikel om AURORA Commerce, lämnade en före detta kollega en kommentar som fastnade med mig:

"Idén är bra, men din repo-as-database-metod förutsätter en teknisk operatör.

Han hade rätt. Och det var exakt den begränsning som jag inte hade adresserat offentligt.

Så jag fixade det.Här är hur.


Begränsningen

Den ursprungliga AURORA Commerce-butiken läser produkter från YAML-filer via Nuxt Content:

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

För en kund som behöver uppdatera en produktbeskrivning på en tisdag eftermiddag utan att röra en terminal - det gör det inte.

Den naiva åtgärden skulle vara att hårdkod Sanity-frågor genom butikens sidor. Men det betyder att butiken blir kopplad till ett specifikt CMS.

Jag ville ha något renare.


Adapter för mönster

Tanken är enkel: introducera en enda lösare mellan API-rutterna och datakällan. Storefront-sidor frågar inte produkter direkt - de kallar API-rutter. API-rutter kallar adaptern. Adaptern vet var data kommer ifrån. Inget annat gör.

Storefront pages

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

getCatalogAdapter(event)

NuxtContentAdapter | SanityAdapter | YourCustomAdapter

CatalogProduct (normalized)

mapProductForApi(product, locale)

ProductApiItem (locale-aware response)

De viktigaste filerna:

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

Kontraktet

Två gränssnitt definierar hela systemet.

“CatalogAdapter” är det avtal som varje leverantör måste genomföra:

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

‘CatalogProduct’ är den normaliserade interna formen - samma fält, samma typer, oavsett källan.

‘ProductApiItem’ är vad API-rutterna faktiskt returnerar - locale-aware, med ‘priceCents’ redan beräknat:

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
}

Att “priceCents” konvertering sker i “map.ts”, inte i utcheckningsrutten.


Den lösa

Byte av leverantör är en miljövariabel:

# 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

Lösaren i ‘adapters/index.ts’ läser ‘CATALOG_PROVIDER’ och returnerar rätt adapter.

// 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 läser frågeparametern locale och returnerar 'fr'annoo 'en'. Den lokala upplösningen stannar på server-sidan - mapProductForApi väljer rätt fält (titleFr mot title, descriptionFr mot description, etc.) innan svaret skickas.

Ingen if/else spridd över hela kodbasen. Ingen Sanity importerar i butikssidor.


Adapter för hälsa

Sanity-adaptern är en uppsättning rena funktioner - ingen klass, ingen instansstatus:

// 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
  }
}

Slug-frågan hanterar både Sanity native ‘slug.current’ form och enkla strängslugs - användbart när du migrerar data från YAML.

Normalisera bilder

Sanity-bilder kan komma i tre olika former beroende på hur schemat är inställt:

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)
}

Om ditt Sanity-schema använder tillgångsreferenser utan URL-projektion lägger du till ‘url’ till din GROQ-fråga.

Fullständig produktstandardisering

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
})

Om Sanity ändrar sitt schema behöver endast den här funktionen uppdateras. butiksfönstret, kundvagnen och Stripe checkout vet inte.


Ytterligare en sak: image resilience

Sanity image URLs kan ibland misslyckas - CDN missar, opublicerade tillgångar. produktsidan hanterar nu detta graciöst:

// 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 || ''
}

Misslyckade bilder spåras i en ref. Den beräknade plockar nästa tillgängliga automatiskt. ingen trasig bild platshållare, ingen blixt - bara en tyst fallback.


Lägg till din egen adapter

Att lägga till Contentful, Hygraph eller ett anpassat REST API är fyra steg:

  1. Skapa ”server/verktyg/katalog/adapter/your-cms.ts”
  2. Exportera ett “CatalogAdapter” -objekt med “listProducts” och “getProductBySlug”
  3. Normalisera din CMS lönsamhet till ‘CatalogProduct’
  4. Registrera din leverantör i adapters/index.ts
  5. Ställ in ”CATALOG_PROVIDER=your-cms”

Butikssidorna, kundvagnen och Stripe checkout ändras inte.


Vad detta låser upp

Den ursprungliga Starter var ärlig om sin begränsning: YAML fungerar för en teknisk operatör, inte för en klient som behöver ett visuellt gränssnitt.

Samma butik, samma Stripe-integration, samma tvåspråkiga installation - men katalogkällan blir ett konfigurationsval, inte en arkitektonisk begränsning.

Live demoAURORA Commerce on Gumroad - Från och med €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.