By Charity Shot 10 min read

Building a Headless eCommerce Stack: Astro + WooCommerce + Gelato + Cloudflare

How I built a static-first photography shop using Astro as the frontend, WooCommerce as a headless backend, Gelato for print-on-demand fulfillment, and automated deployments via Cloudflare Pages

Share:

A Technical Deep Dive: This is a departure from my usual photography posts. As a software engineer who loves automation, I wanted to share the technical architecture behind this site. If you’re here for street photography, check out the latest blog posts instead—but if you’re curious about building modern eCommerce platforms, read on!

When redesigning Charity Shot, I wanted the performance and SEO benefits of a static site while maintaining the robust product management and checkout capabilities of WooCommerce. Here’s how I architected a headless eCommerce solution that automatically syncs products from Gelato’s print-on-demand service and triggers rebuilds when inventory changes.

Architecture Overview

The system combines four main components into an automated pipeline:

graph TB
    subgraph "Product Creation Flow"
        G[Gelato API] -->|POST products| WC[WooCommerce]
        WC -->|Store products| DB[(MySQL Database)]
    end

    subgraph "Build & Deploy Flow"
        WC -->|Product updated webhook| CF_HOOK[Cloudflare Deploy Hook]
        CF_HOOK -->|Trigger build| CF_PAGES[Cloudflare Pages]
        CF_PAGES -->|Fetch via REST API| WC
        CF_PAGES -->|Build static site| ASTRO[Astro Site]
    end

    subgraph "User Journey"
        USER[User] -->|Browse products| ASTRO
        ASTRO -->|Add to cart| WC_CHECKOUT[WooCommerce Checkout]
        WC_CHECKOUT -->|Order placed| GELATO[Gelato Fulfillment]
        GELATO -->|Ship print| USER
    end

    style G fill:#6366f1
    style WC fill:#7c3aed
    style CF_PAGES fill:#f97316
    style ASTRO fill:#ef4444

Component Breakdown

1. Gelato → WooCommerce Product Sync

Gelato’s API automatically creates and updates products in WooCommerce. Each photograph becomes a product with multiple variants (A2, A3, A4 sizes).

Gelato Product Structure

{
  "title": "Moody Urban Street Scene",
  "variants": [
    {
      "title": "A2 Print",
      "sku": "CHARITY-001-A2",
      "price": "29.99",
      "gelato_product_uid": "poster_210x297mm_white"
    },
    {
      "title": "A3 Print",
      "sku": "CHARITY-001-A3",
      "price": "19.99",
      "gelato_product_uid": "poster_297x420mm_white"
    }
  ]
}

WooCommerce Variable Product Mapping

When Gelato posts to WooCommerce, it creates:

  • Simple products for single-size items
  • Variable products with attributes for multi-size prints
// WooCommerce product structure created by Gelato
$product = [
    'name' => 'Moody Urban Street Scene',
    'type' => 'variable',
    'attributes' => [
        [
            'name' => 'Size',
            'options' => ['A2', 'A3', 'A4'],
            'variation' => true
        ]
    ],
    'variations' => [
        [
            'attributes' => ['Size' => 'A2'],
            'sku' => 'CHARITY-001-A2',
            'regular_price' => '29.99',
            'meta_data' => [
                ['key' => '_gelato_product_uid', 'value' => 'poster_210x297mm_white']
            ]
        ]
    ]
];

Troubleshooting variant sync: I discovered that A2 and A3 variants sometimes fail to sync properly while A4 works fine. The issue often stems from:

  1. Gelato’s product UID mismatch between API and dashboard
  2. Attribute slug inconsistencies (using “a-2” vs “a2”)
  3. Variation attribute values not matching parent attributes exactly

2. Astro Static Site Generation

Astro fetches products at build time via WooCommerce REST API, generating fully static pages with optimal performance.

Product Data Fetching

// src/lib/woocommerce.js
const WC_URL = import.meta.env.WC_URL;
const WC_KEY = import.meta.env.WC_CONSUMER_KEY;
const WC_SECRET = import.meta.env.WC_CONSUMER_SECRET;

const auth = btoa(`${WC_KEY}:${WC_SECRET}`);

export async function getProducts(params = {}) {
  const queryParams = new URLSearchParams({
    per_page: '100',
    ...params
  });

  const response = await fetch(`${WC_URL}/wp-json/wc/v3/products?${queryParams}`, {
    headers: {
      'Authorization': `Basic ${auth}`
    }
  });

  if (!response.ok) {
    throw new Error(`Failed to fetch products: ${response.statusText}`);
  }

  return response.json();
}

export async function getProduct(slug) {
  const response = await fetch(`${WC_URL}/wp-json/wc/v3/products?slug=${slug}`, {
    headers: {
      'Authorization': `Basic ${auth}`
    }
  });

  if (!response.ok) {
    throw new Error(`Failed to fetch product: ${response.statusText}`);
  }

  const products = await response.json();
  return products[0];
}

Dynamic Routes for Products

Astro generates static pages for each product at build time using getStaticPaths():

// src/pages/shop/[slug].astro
export async function getStaticPaths() {
  const products = await getProducts();

  return await Promise.all(products.map(async (product) => {
    let variations = [];

    if (product.type === 'variable') {
      variations = await getProductVariations(product.id);
    }

    return {
      params: { slug: product.slug },
      props: { product, variations }
    };
  }));
}

The product page includes:

  • Image gallery with lightbox zoom
  • Variant selection for variable products (sizes, frames)
  • Real-time price updates via client-side JavaScript
  • Direct checkout redirect to WooCommerce

3. Checkout Redirect to WooCommerce

Rather than rebuilding checkout functionality, I redirect users to the WooCommerce site for cart and payment processing. This maintains PCI compliance and leverages WooCommerce’s mature checkout flow.

Add-to-Cart Redirect Strategy

For simple products, the “Buy Now” button constructs a checkout URL:

const checkoutUrl = `${WC_URL}/checkout?add-to-cart=${productId}&redirect_to_checkout=1`;

For variable products, it includes the variation details:

const url = `${WC_URL}/checkout?add-to-cart=${productId}&variation_id=${variationId}&attribute_pa_size=${size}&redirect_to_checkout=1`;

Users click “Buy Now” and land directly on the checkout page with their selected product ready to purchase.

4. Automated Deployments via Cloudflare

The critical piece: whenever products change in WooCommerce (created, updated, or deleted), the site automatically rebuilds.

sequenceDiagram
    participant Gelato
    participant WC as WooCommerce
    participant Hook as Cloudflare Deploy Hook
    participant CF as Cloudflare Pages
    participant Astro as Astro Build Process

    Gelato->>WC: POST new product via API
    WC->>WC: Save product to database
    WC->>Hook: Trigger webhook (product.updated)
    Hook->>CF: Start new deployment
    CF->>Astro: Clone repo & run build
    Astro->>WC: Fetch updated products via REST API
    Astro->>Astro: Generate static pages
    Astro->>CF: Return built assets
    CF->>CF: Deploy to edge network
    Note over CF: New product live in ~2-4 minutes

Setting Up the Deploy Hook

  1. Create Cloudflare Pages Deploy Hook

    • Navigate to your Cloudflare Pages project
    • Settings → Builds & Deployments → Deploy Hooks
    • Create hook (name: “WooCommerce Product Updates”)
    • Copy the webhook URL: https://api.cloudflare.com/client/v4/pages/webhooks/deploy/xxx
  2. Configure WooCommerce Webhook

Use the WooCommerce admin interface:

  • WooCommerce → Settings → Advanced → Webhooks
  • Add webhook for product.created topic
  • Add webhook for product.updated topic
  • Set Delivery URL to your Cloudflare deploy hook
  • Set Status to “Active”

Now whenever you add or update a product in WooCommerce, it automatically triggers a rebuild of your Astro site on Cloudflare Pages.

Securing Deploy Hooks

Deploy hooks are essentially public URLs that trigger builds. Here’s how to prevent abuse:

1. Cloudflare’s Built-in Rate Limiting

Cloudflare Pages automatically rate-limits deploy hook requests, but you can add extra protection:

  • Use Cloudflare WAF rules to restrict access by IP range
  • Set up rate limiting rules in Cloudflare dashboard
  • Monitor deployment logs for unusual activity

2. WooCommerce Webhook Security

WooCommerce webhooks include HMAC signatures you can verify:

// In a custom plugin or functions.php
add_filter('woocommerce_webhook_payload', function($payload, $resource, $resource_id, $webhook_id) {
    // Add verification token
    $payload['verify_token'] = hash_hmac('sha256', $webhook_id, WP_SALT);
    return $payload;
}, 10, 4);

3. Monitor Build Frequency

Set up alerts for:

  • More than X builds per hour (Cloudflare can email you)
  • Failed builds (may indicate malicious payloads)
  • Unusual deployment patterns

4. Use Separate Hooks

Create different deploy hooks for:

  • Automated (WooCommerce webhooks)
  • Manual (your use only, kept private)
  • Emergency (rollback/hotfix)

This limits exposure if one hook URL leaks.

5. Cloudflare Access Control

For sensitive projects, put deploy hooks behind Cloudflare Access (requires paid plan) to require authentication.

Practical Impact:

Deploy hooks don’t expose sensitive data or allow code execution - they only trigger builds. The worst case is someone forcing frequent rebuilds (annoying, but not destructive). Cloudflare’s rate limiting handles 99% of abuse cases.

5. The Complete Flow

Here’s what happens when Gelato adds a new product:

graph LR
    A[Upload photo to Gelato] --> B[Gelato creates product]
    B --> C[Gelato API posts to WooCommerce]
    C --> D[WooCommerce saves product]
    D --> E[WooCommerce webhook fires]
    E --> F[Cloudflare receives webhook]
    F --> G[Cloudflare starts build]
    G --> H[Astro fetches products from WC API]
    H --> I[Astro generates static pages]
    I --> J[Deploy to Cloudflare edge]
    J --> K[New product live on site]

    style A fill:#6366f1
    style D fill:#7c3aed
    style J fill:#f97316
    style K fill:#10b981

Typical timing:

  • Gelato → WooCommerce: A few seconds
  • Webhook trigger: Instant
  • Cloudflare build: 1-3 minutes
  • Total time to live: 2-4 minutes

Environment Configuration

Environment Variables

CRITICAL: Never commit your .env file to Git!

Create a .env file locally with your WooCommerce API credentials:

WC_URL=https://your-woocommerce-site.com
WC_CONSUMER_KEY=ck_your_key_here
WC_CONSUMER_SECRET=cs_your_secret_here

Set the same variables in Cloudflare Pages (Settings → Environment variables).

Security best practices:

  • Add .env to .gitignore (should already be there)
  • Generate read-only API credentials in WooCommerce (Settings → Advanced → REST API)
  • Rotate credentials if ever exposed
  • Use separate credentials for development and production

Performance Benefits

Static site generation gives us excellent performance:

  • Fast page loads: Pre-rendered HTML served from Cloudflare’s global CDN
  • Optimized images: Astro’s built-in Sharp integration optimizes all images at build time
  • Minimal JavaScript: Only essential JS for variant selection and checkout redirects
  • SEO-friendly: Static HTML is instantly indexable by search engines

The trade-off is that product updates require a full rebuild (1-3 minutes), but this is acceptable for a photography print shop where inventory doesn’t change frequently.

Debugging & Monitoring

Webhook Delivery Logs

WooCommerce logs all webhook deliveries:

  • WooCommerce → Status → Logs
  • Filter by webhook ID to see success/failure

Cloudflare Build Logs

Check deployment status:

# Using Wrangler CLI
npx wrangler pages deployment list

# View specific deployment logs
npx wrangler pages deployment tail

Manual Deploy Trigger

Test the integration:

# Trigger Cloudflare deploy manually
curl -X POST https://api.cloudflare.com/client/v4/pages/webhooks/deploy/YOUR_HOOK_ID

# Verify products are fetching correctly
curl -X GET "https://your-wc-site.com/wp-json/wc/v3/products" \
  -u "ck_xxx:cs_xxx"

Lessons Learned

  1. Webhook reliability: Monitor webhook deliveries in WooCommerce to catch any failures
  2. Build times: Builds take longer as product count increases. Current setup handles 100+ products comfortably
  3. Variant complexity: Variable products with multiple attributes require careful client-side JavaScript to match the right variation
  4. Cross-domain checkout: Cookie configuration in WordPress is important for cart persistence across the static site and checkout subdomain
  5. Static limitations: Since everything is pre-rendered, real-time stock levels aren’t possible. Product updates require a rebuild.

Future Enhancements

  • Webhook authentication: Add HMAC verification to secure deploy hooks
  • Partial rebuilds: Only regenerate pages for changed products
  • Real-time cart: Implement a lightweight cart UI using WooCommerce Store API
  • Analytics: Track conversion funnel from Astro product page → WooCommerce checkout

Conclusion

This architecture delivers excellent performance and developer experience: the speed and SEO benefits of a static Astro site, combined with the robust product management and checkout capabilities of WooCommerce. Automated deployments via webhooks mean product updates flow from WooCommerce to the live site automatically, while Gelato handles print fulfillment.

The initial setup took some time to configure all the pieces (Astro, WooCommerce API integration, Cloudflare Pages, webhooks), but now it runs smoothly. When I add or update products in WooCommerce, they appear on the site after the automatic rebuild completes.

Tech Stack Summary:

  • Frontend: Astro (static generation)
  • Backend: WooCommerce (headless)
  • Fulfillment: Gelato (print-on-demand)
  • Hosting: Cloudflare Pages (free tier)
  • Automation: WooCommerce webhooks → Cloudflare deploy hooks

Monthly hosting costs are minimal - just the WooCommerce hosting on Pikapods. Cloudflare Pages, Gelato integration, and deployments are all on free tiers.


Questions about this setup? Reach out via the contact page and I’m happy to help!

Enjoyed this post?

Check out the prints from Portsmouth and beyond, all captured with charity shop cameras.

Related Posts

Ed Leeman

About Ed Leeman

Street and urban photographer capturing Portsmouth and Hampshire with charity shop cameras. Software engineer who loves automation—building tools to streamline everything from photo workflows to print sales. Finding beauty in the overlooked.