> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mavera.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Product Collection → Content Generation

> Generate brand-consistent marketing copy for Shopify collections using Mavera brand voices and generation

### Scenario

You maintain curated Shopify collections (e.g., "Summer Essentials"). You pull all products from a target collection, use their existing descriptions to create a Mavera brand voice, then generate fresh marketing copy — product descriptions, social posts, email subject lines — with voice consistency across the entire collection.

### Architecture

```mermaid theme={"dark"}
flowchart LR
A["GraphQL: collection products"] --> B["POST /api/v1/brand-voices"] --> C["POST /api/v1/generations per product"] --> D["Brand-consistent collection copy"]
```

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, time

  STORE = os.environ["SHOPIFY_STORE"]
  TOKEN = os.environ["SHOPIFY_ACCESS_TOKEN"]
  MV = os.environ["MAVERA_API_KEY"]
  SH = f"https://{STORE}.myshopify.com/admin/api/2024-10/graphql.json"
  SH_H = {"X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json"}
  MB = "https://app.mavera.io/api/v1"
  MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
  HANDLE = "summer-essentials"

  QUERY = """query ($handle: String!) {
    collectionByHandle(handle: $handle) { title
      products(first: 50) { edges { node { title descriptionHtml productType
        priceRangeV2 { minVariantPrice { amount currencyCode } }
      }}}
    }
  }"""

  resp = requests.post(SH, json={"query": QUERY, "variables": {"handle": HANDLE}}, headers=SH_H)
  resp.raise_for_status()
  coll = resp.json()["data"]["collectionByHandle"]
  products = [e["node"] for e in coll["products"]["edges"]]
  print(f"Collection: {coll['title']} — {len(products)} products")

  samples = [p["descriptionHtml"] for p in products if p.get("descriptionHtml")][:10]
  voice_resp = requests.post(f"{MB}/brand-voices", json={"name": f"Shopify — {HANDLE}", "samples": samples}, headers=MH)
  voice_resp.raise_for_status()
  voice_id = voice_resp.json()["id"]
  print(f"Brand voice: {voice_id}")

  for p in products:
      price = p["priceRangeV2"]["minVariantPrice"]
      gen = requests.post(f"{MB}/generations", json={
          "brand_voice_id": voice_id,
          "prompt": f"Write: 1) 2-sentence product description 2) Instagram caption with hashtags 3) Email subject line\n\nProduct: {p['title']}\nType: {p['productType']}\nPrice: {price['amount']} {price['currencyCode']}\nCurrent: {p.get('descriptionHtml', 'N/A')}",
      }, headers=MH)
      gen.raise_for_status()
      print(f"\n--- {p['title']} ---\n{gen.json()['text']}")
      time.sleep(0.3)
  ```

  ```javascript JavaScript theme={"dark"}
  const STORE = process.env.SHOPIFY_STORE, TOKEN = process.env.SHOPIFY_ACCESS_TOKEN, MV = process.env.MAVERA_API_KEY;
  const SH = `https://${STORE}.myshopify.com/admin/api/2024-10/graphql.json`;
  const MB = "https://app.mavera.io/api/v1";
  const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
  const HANDLE = "summer-essentials";

  const QUERY = `query($handle:String!){collectionByHandle(handle:$handle){title products(first:50){edges{node{title descriptionHtml productType priceRangeV2{minVariantPrice{amount currencyCode}}}}}}}`;

  (async () => {
    const resp = await fetch(SH, { method: "POST", headers: { "X-Shopify-Access-Token": TOKEN, "Content-Type": "application/json" }, body: JSON.stringify({ query: QUERY, variables: { handle: HANDLE } }) });
    const coll = (await resp.json()).data.collectionByHandle;
    const products = coll.products.edges.map(e => e.node);
    console.log(`Collection: ${coll.title} — ${products.length} products`);

    const samples = products.map(p => p.descriptionHtml).filter(Boolean).slice(0, 10);
    const vResp = await fetch(`${MB}/brand-voices`, { method: "POST", headers: MH, body: JSON.stringify({ name: `Shopify — ${HANDLE}`, samples }) });
    const voiceId = (await vResp.json()).id;
    console.log(`Brand voice: ${voiceId}`);

    for (const p of products) {
      const price = p.priceRangeV2.minVariantPrice;
      const gen = await fetch(`${MB}/generations`, { method: "POST", headers: MH, body: JSON.stringify({
        brand_voice_id: voiceId,
        prompt: `Write: 1) 2-sentence description 2) Instagram caption 3) Email subject\n\nProduct: ${p.title}\nType: ${p.productType}\nPrice: ${price.amount} ${price.currencyCode}\nCurrent: ${p.descriptionHtml || "N/A"}`
      }) });
      console.log(`\n--- ${p.title} ---\n${(await gen.json()).text}`);
      await new Promise(r => setTimeout(r, 300));
    }
  })();
  ```
</CodeGroup>

### Example Output

```text theme={"dark"}
Collection: Summer Essentials — 24 products
Brand voice: bv_9c4e2a1f

--- Linen Breeze Shirt ---
1. Lightweight linen that breathes from morning coffee to sunset cocktails. Relaxed fit with mother-of-pearl buttons.
2. Your summer uniform, perfected. The Linen Breeze Shirt in sea salt. #SummerEssentials #LinenLife
3. The shirt that makes summer effortless — now in 4 new colors
```

### Error Handling

<AccordionGroup>
  <Accordion title="Collection handle not found">
    If `collectionByHandle` returns `null`, the handle doesn't exist or is unpublished. Verify in Shopify Admin → Products → Collections → URL handle. Handles are case-sensitive and use hyphens. Query `collections(first: 10)` to list available handles.
  </Accordion>

  <Accordion title="Brand voice — insufficient samples">
    Mavera requires at least 3 text samples to create a brand voice. If your collection has fewer products with descriptions, combine from multiple collections or add sample copy manually.
  </Accordion>
</AccordionGroup>
