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
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:
- Gelato’s product UID mismatch between API and dashboard
- Attribute slug inconsistencies (using “a-2” vs “a2”)
- 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
-
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
-
Configure WooCommerce Webhook
Use the WooCommerce admin interface:
- WooCommerce → Settings → Advanced → Webhooks
- Add webhook for
product.createdtopic - Add webhook for
product.updatedtopic - 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
.envto.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
- Webhook reliability: Monitor webhook deliveries in WooCommerce to catch any failures
- Build times: Builds take longer as product count increases. Current setup handles 100+ products comfortably
- Variant complexity: Variable products with multiple attributes require careful client-side JavaScript to match the right variation
- Cross-domain checkout: Cookie configuration in WordPress is important for cart persistence across the static site and checkout subdomain
- 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.
