
Building Scalable E-commerce with Astro and Saleor
Learn how to build a fast, SEO-optimized e-commerce storefront using Astro static generation capabilities combined with Saleor headless commerce API.
Building Scalable E-commerce with Astro and Saleor
E-commerce performance matters more than ever. With Core Web Vitals affecting SEO rankings and user expectations at an all-time high, choosing the right tech stack can make or break your online store. Today, I’ll show you how Astro’s static generation capabilities combined with Saleor’s headless commerce API create a powerhouse combination for modern e-commerce.
Why Astro + Saleor?
Astro’s Advantages
- Zero JavaScript by default - Only ship JS when needed
- Partial hydration - Interactive components load progressively
- Framework agnostic - Use React, Vue, or Svelte components
- Built-in optimizations - Image optimization, code splitting, and more
Saleor’s Strengths
- GraphQL-first API - Efficient data fetching
- Headless architecture - Complete frontend flexibility
- Multi-tenant support - Handle multiple stores
- Rich commerce features - Everything from inventory to payments
Project Setup
Let’s start by setting up our Astro project with the necessary dependencies:
npm create astro@latest astro-saleor-store
cd astro-saleor-store
npm install graphql graphql-request @astrojs/tailwind
Setting Up the Saleor Client
First, we’ll create a GraphQL client to interact with Saleor’s API:
// src/lib/saleor.ts
import { GraphQLClient } from 'graphql-request';
const SALEOR_API_URL = import.meta.env.PUBLIC_SALEOR_API_URL || 'https://demo.saleor.io/graphql/';
export const saleorClient = new GraphQLClient(SALEOR_API_URL, {
headers: {
'Content-Type': 'application/json',
},
});
// GraphQL queries
export const GET_PRODUCTS = `
query GetProducts($first: Int, $channel: String) {
products(first: $first, channel: $channel) {
edges {
node {
id
name
slug
description
thumbnail {
url
alt
}
pricing {
priceRange {
start {
gross {
amount
currency
}
}
}
}
}
}
}
}
`;
export const GET_PRODUCT_BY_SLUG = `
query GetProductBySlug($slug: String!, $channel: String) {
product(slug: $slug, channel: $channel) {
id
name
slug
description
images {
url
alt
}
variants {
id
name
pricing {
price {
gross {
amount
currency
}
}
}
}
}
}
`;
Creating Product Pages with Static Generation
Now let’s create dynamic product pages that are statically generated at build time:
---
// src/pages/products/[slug].astro
import { saleorClient, GET_PRODUCT_BY_SLUG, GET_PRODUCTS } from '../../lib/saleor';
import Layout from '../../layouts/Layout.astro';
export async function getStaticPaths() {
const data = await saleorClient.request(GET_PRODUCTS, {
first: 100,
channel: 'default-channel'
});
return data.products.edges.map(({ node }) => ({
params: { slug: node.slug },
props: { product: node }
}));
}
const { slug } = Astro.params;
const product = await saleorClient.request(GET_PRODUCT_BY_SLUG, {
slug,
channel: 'default-channel'
});
---
<Layout title={product.product.name}>
<main class="container mx-auto px-4 py-8">
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Product Images -->
<div class="space-y-4">
{product.product.images.map((image) => (
<img
src={image.url}
alt={image.alt}
class="w-full rounded-lg shadow-lg"
/>
))}
</div>
<!-- Product Info -->
<div class="space-y-6">
<h1 class="text-3xl font-bold">{product.product.name}</h1>
<p class="text-gray-600">{product.product.description}</p>
<!-- Variants -->
<div class="space-y-4">
{product.product.variants.map((variant) => (
<div class="border rounded-lg p-4">
<h3 class="font-semibold">{variant.name}</h3>
<p class="text-2xl font-bold text-green-600">
${variant.pricing.price.gross.amount}
</p>
<button class="add-to-cart bg-blue-600 text-white px-6 py-2 rounded-lg mt-2">
Add to Cart
</button>
</div>
))}
</div>
</div>
</div>
</main>
</Layout>
<script>
// Add interactive cart functionality
document.querySelectorAll('.add-to-cart').forEach(button => {
button.addEventListener('click', (e) => {
// Add to cart logic here
console.log('Added to cart!');
});
});
</script>
Building a Product Listing Page
Create a fast-loading product catalog:
---
// src/pages/products/index.astro
import { saleorClient, GET_PRODUCTS } from '../../lib/saleor';
import Layout from '../../layouts/Layout.astro';
import ProductCard from '../../components/ProductCard.astro';
const data = await saleorClient.request(GET_PRODUCTS, {
first: 50,
channel: 'default-channel'
});
const products = data.products.edges.map(edge => edge.node);
---
<Layout title="Products">
<main class="container mx-auto px-4 py-8">
<h1 class="text-4xl font-bold mb-8">Our Products</h1>
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6">
{products.map((product) => (
<ProductCard product={product} />
))}
</div>
</main>
</Layout>
Optimizing Performance
Image Optimization
---
// src/components/ProductCard.astro
import { Image } from 'astro:assets';
const { product } = Astro.props;
---
<div class="product-card border rounded-lg overflow-hidden shadow-lg hover:shadow-xl transition-shadow">
<a href={`/products/${product.slug}`}>
<Image
src={product.thumbnail.url}
alt={product.thumbnail.alt}
width={300}
height={300}
class="w-full h-48 object-cover"
loading="lazy"
/>
<div class="p-4">
<h3 class="font-semibold text-lg">{product.name}</h3>
<p class="text-gray-600 text-sm">{product.description}</p>
<p class="font-bold text-xl text-green-600 mt-2">
${product.pricing.priceRange.start.gross.amount}
</p>
</div>
</a>
</div>
Partial Hydration for Cart
---
// src/components/Cart.astro
---
<div id="cart" class="cart-container">
<!-- Static cart UI -->
<div class="cart-icon">
🛒 <span id="cart-count">0</span>
</div>
</div>
<!-- Only hydrate the cart component -->
<script>
import { CartManager } from '../scripts/cart.js';
// Initialize cart functionality
const cart = new CartManager();
cart.init();
</script>
SEO and Meta Tags
---
// src/components/SEO.astro
const { title, description, image, url } = Astro.props;
---
<head>
<title>{title}</title>
<meta name="description" content={description} />
<!-- Open Graph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
<meta property="og:url" content={url} />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
<!-- Structured Data -->
<script type="application/ld+json">
{JSON.stringify({
"@context": "https://schema.org",
"@type": "Product",
"name": title,
"description": description,
"image": image
})}
</script>
</head>
Performance Results
With this setup, you can expect:
- Lighthouse scores of 95+ across all metrics
- First Contentful Paint < 1.5s
- Time to Interactive < 2s
- Cumulative Layout Shift = 0
Deployment Strategy
For optimal performance, deploy to edge locations:
# Vercel (recommended)
npm run build
vercel deploy
# Netlify
npm run build
netlify deploy --prod
# Cloudflare Pages
npm run build
wrangler pages publish dist
Conclusion
The combination of Astro and Saleor delivers exceptional performance while maintaining developer experience and business flexibility. By leveraging static generation for product pages and selective hydration for interactive components, you get the best of both worlds: fast loading times and rich user interactions.
This architecture scales beautifully and provides a solid foundation for enterprise e-commerce solutions.
Ready to build your own lightning-fast e-commerce store? Check out the complete source code on GitHub.