By Kris Black | Published on 9/30/2024

Learn how to integrate WordPress and WooCommerce with Astro to create a high-performance, SEO-friendly e-commerce site.

Introduction

Combining the robust content management capabilities of WordPress and the powerful e-commerce features of WooCommerce with the high-performance static site generation of Astro can create an exceptional online store. In this article, we will explore the steps to integrate WordPress and WooCommerce with Astro, leveraging GraphQL and APIs to deliver a seamless and efficient user experience.

Why Choose Astro for Your E-commerce Site?

Astro is a modern web framework that focuses on delivering static HTML with minimal JavaScript, resulting in fast load times and improved performance. Here are some key reasons to choose Astro for your e-commerce site:

  • Performance: Astro's static site generation ensures fast load times and a smooth user experience.
  • SEO Optimization: By generating static HTML, Astro helps improve your site's SEO rankings.
  • Flexibility: Astro can be integrated with various APIs and content management systems, including WordPress and WooCommerce.
  • Scalability: Astro's architecture allows your site to handle increased traffic and data loads efficiently.

Integrating WordPress with Astro

To integrate WordPress with Astro, we will use the WordPress REST API or GraphQL API to fetch content and render it in Astro components. Here are the steps to set up this integration:

  • Set Up WordPress: Ensure you have a WordPress site set up and running. Install necessary plugins like WPGraphQL to enable GraphQL API.
  • Fetch Data: Use Astro's data fetching capabilities to pull content from your WordPress site using the REST API or GraphQL.
  • Render Content: Create Astro components to render the fetched content. Utilize Astro's templating system to structure your pages and posts.

Integrating WooCommerce with Astro

WooCommerce can be integrated with Astro to manage products, orders, and other e-commerce functionalities. Here are the steps to integrate WooCommerce with Astro:

  • Set Up WooCommerce: Ensure you have WooCommerce installed and configured on your WordPress site. Install necessary plugins like WooGraphQL to enable GraphQL API for WooCommerce.
  • Fetch Products: Use Astro to fetch product data from the WooCommerce API. You can use GraphQL to query product information efficiently.
  • Render Products: Create Astro components to display product listings, product details, and other e-commerce features on your site.

Advantages of Using GraphQL with Astro

Using GraphQL with Astro provides several benefits:

  • Efficient Data Fetching: GraphQL allows you to request only the data you need, reducing the amount of data transferred and improving performance.
  • Flexible Queries: GraphQL enables complex queries to fetch related data in a single request, making it easier to manage and display content.
  • Improved Developer Experience: GraphQL's schema and type system provide better tooling and error handling, enhancing the development process.

Conclusion

Integrating WordPress and WooCommerce with Astro enables you to build high-performance, SEO-friendly e-commerce sites that leverage the strengths of each platform. By using GraphQL and APIs, you can efficiently fetch and display data, providing a seamless user experience. Whether you're looking to enhance your existing site or build a new one from scratch, Astro offers a powerful solution for modern web development.

Step 2: WordPress and GraphQL Setup

First, let's configure WordPress and WooCommerce for headless operation. Install and activate these plugins:

  • WPGraphQL (wp-graphql)
  • WPGraphQL for WooCommerce (wp-graphql-woocommerce)
  • WPGraphQL CORS (wp-graphql-cors)

Add this to your WordPress wp-config.php file:

// Enable GraphQL debugging
define('GRAPHQL_DEBUG', true);

// Allow CORS for your Astro domain
define('GRAPHQL_CORS_ALLOW_ORIGIN', 'http://localhost:3000');

Step 3: Configure Astro for GraphQL

Create a GraphQL client utility file:

// src/lib/graphql.ts
export async function gqlQuery(query: string, variables = {}) {
  const response = await fetch('https://your-wordpress-site.com/graphql', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query,
      variables,
    }),
  });

  const { data } = await response.json();
  return data;
}

Step 4: Fetch Products from WooCommerce

Create a products page in Astro:

---
// src/pages/products.astro
import { gqlQuery } from '../lib/graphql';

const PRODUCTS_QUERY = `
  query GetProducts {
    products(first: 12) {
      nodes {
        id
        name
        price
        slug
        image {
          sourceUrl
          altText
        }
      }
    }
  }
`;

const { products } = await gqlQuery(PRODUCTS_QUERY);
---

<div class="products-grid">
  {products.nodes.map((product) => (
    <div class="product-card">
      <img src={product.image.sourceUrl} alt={product.image.altText} />
      <h3>{product.name}</h3>
      <p class="price">{product.price}</p>
      <button data-product-id={product.id}>Add to Cart</button>
    </div>
  ))}
</div>

<style>
  .products-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 2rem;
    padding: 2rem;
  }
  .product-card {
    border: 1px solid #eee;
    padding: 1rem;
    border-radius: 8px;
  }
</style>

Step 5: Single Product Page

Create dynamic product pages:

---
// src/pages/product/[slug].astro
import { gqlQuery } from '../../lib/graphql';

export async function getStaticPaths() {
  const PATHS_QUERY = `
    query GetProductSlugs {
      products(first: 100) {
        nodes {
          slug
        }
      }
    }
  `;

  const { products } = await gqlQuery(PATHS_QUERY);
  
  return products.nodes.map(({ slug }) => ({
    params: { slug },
  }));
}

const { slug } = Astro.params;

const PRODUCT_QUERY = `
  query GetProduct($slug: ID!) {
    product(id: $slug, idType: SLUG) {
      id
      name
      description
      price
      image {
        sourceUrl
        altText
      }
    }
  }
`;

const { product } = await gqlQuery(PRODUCT_QUERY, { slug });
---

<div class="product-detail">
  <img src={product.image.sourceUrl} alt={product.image.altText} />
  <div class="product-info">
    <h1>{product.name}</h1>
    <p class="price">{product.price}</p>
    <div class="description" set:html={product.description} />
    <button data-product-id={product.id}>Add to Cart</button>
  </div>
</div>

Step 6: Shopping Cart Implementation

Create a cart component using client-side JavaScript:

---
// src/components/Cart.astro
---

<div id="cart" class="cart-drawer">
  <h2>Shopping Cart</h2>
  <div id="cart-items"></div>
  <div id="cart-total"></div>
</div>

<script>
class Cart {
  constructor() {
    this.items = new Map();
    this.loadCart();
    this.bindEvents();
  }

  loadCart() {
    const savedCart = localStorage.getItem('cart');
    if (savedCart) {
      const cartData = JSON.parse(savedCart);
      cartData.forEach(([id, item]) => this.items.set(id, item));
    }
    this.updateCart();
  }

  addItem(product) {
    const existing = this.items.get(product.id);
    if (existing) {
      existing.quantity += 1;
    } else {
      this.items.set(product.id, { ...product, quantity: 1 });
    }
    this.saveCart();
    this.updateCart();
  }

  updateCart() {
    const cartEl = document.getElementById('cart-items');
    cartEl.innerHTML = Array.from(this.items.values())
      .map(item => `
        <div class="cart-item">
          <img src="${item.image}" alt="${item.name}" />
          <h3>${item.name}</h3>
          <p>Quantity: ${item.quantity}</p>
          <p>Price: ${item.price}</p>
        </div>
      `).join('');
  }

  saveCart() {
    localStorage.setItem('cart', 
      JSON.stringify(Array.from(this.items.entries())));
  }

  bindEvents() {
    document.addEventListener('click', e => {
      if (e.target.matches('[data-product-id]')) {
        const productId = e.target.dataset.productId;
        // Fetch product data and add to cart
        this.addItem(productData);
      }
    });
  }
}

new Cart();
</script>

<style>
.cart-drawer {
  position: fixed;
  right: 0;
  top: 0;
  height: 100vh;
  width: 300px;
  background: white;
  box-shadow: -2px 0 5px rgba(0,0,0,0.1);
  padding: 1rem;
}
</style>

Step 7: Deployment Considerations

When deploying your Astro + WordPress site, consider these important factors:

  • Configure CORS headers on your WordPress site to allow requests from your Astro domain
  • Set up proper caching for GraphQL requests
  • Use environment variables for API endpoints
// src/env.d.ts
interface ImportMetaEnv {
  readonly WORDPRESS_API_URL: string;
  readonly WORDPRESS_AUTH_TOKEN?: string;
}

// astro.config.mjs
export default defineConfig({
  vite: {
    define: {
      'process.env.WORDPRESS_API_URL': 
        JSON.stringify(process.env.WORDPRESS_API_URL),
    },
  },
});

Project Structure

Here's how we'll organize our Astro + WordPress e-commerce project:

my-astro-store/
├── src/
│   ├── components/
│   │   ├── Cart.astro
│   │   ├── ProductCard.astro
│   │   └── ProductGrid.astro
│   ├── layouts/
│   │   └── Store.astro
│   ├── lib/
│   │   ├── graphql.ts
│   │   └── woocommerce.ts
│   └── pages/
│       ├── products/
│       │   ├── index.astro
│       │   └── [slug].astro
│       └── cart.astro
├── astro.config.mjs
└── package.json

Environment Setup

Create a .env file in your project root:

WORDPRESS_URL=https://your-wordpress-site.com
WORDPRESS_AUTH_TOKEN=your_jwt_token
PUBLIC_SITE_URL=http://localhost:3000

Update your astro.config.mjs:

import { defineConfig } from 'astro/config';

export default defineConfig({
  site: process.env.PUBLIC_SITE_URL,
  integrations: [
    // Add any required integrations
  ],
  vite: {
    plugins: [
      // Add any Vite plugins
    ],
    define: {
      'process.env.WORDPRESS_URL': JSON.stringify(process.env.WORDPRESS_URL),
    },
  },
});

Creating Reusable Product Components

ProductCard Component

---
// src/components/ProductCard.astro
interface Props {
  product: {
    id: string;
    name: string;
    price: string;
    image: {
      sourceUrl: string;
      altText: string;
    };
    slug: string;
  };
}

const { product } = Astro.props;
---

<div class="product-card">
  <a href={'/products/' + product.slug}>
    <img 
      src={product.image.sourceUrl} 
      alt={product.image.altText}
      loading="lazy"
      width="300"
      height="300"
    />
    <h3>{product.name}</h3>
    <p class="price">{product.price}</p>
  </a>
  <button 
    class="add-to-cart"
    data-product-id={product.id}
    data-product-name={product.name}
    data-product-price={product.price}
    data-product-image={product.image.sourceUrl}
  >
    Add to Cart
  </button>
</div>

<style>
  .product-card {
    border: 1px solid #eee;
    border-radius: 8px;
    padding: 1rem;
    transition: transform 0.2s;
  }

  .product-card:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 6px rgba(0,0,0,0.1);
  }

  .product-card img {
    width: 100%;
    height: auto;
    border-radius: 4px;
  }

  .price {
    font-weight: bold;
    color: #2563eb;
  }

  .add-to-cart {
    width: 100%;
    padding: 0.5rem;
    background: #2563eb;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    transition: background 0.2s;
  }

  .add-to-cart:hover {
    background: #1d4ed8;
  }
</style>

Enhanced Cart Implementation

---
// src/components/Cart.astro
---

<div id="cart" class="cart-drawer">
  <div class="cart-header">
    <h2>Shopping Cart</h2>
    <button id="close-cart">&times;</button>
  </div>
  <div id="cart-items"></div>
  <div id="cart-total"></div>
  <button id="checkout" class="checkout-button">Proceed to Checkout</button>
</div>

<script>
interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
  image: string;
}

class Cart {
  private items: Map<string, CartItem>;
  private isOpen: boolean;

  constructor() {
    this.items = new Map();
    this.isOpen = false;
    this.loadCart();
    this.bindEvents();
  }

  private loadCart() {
    const savedCart = localStorage.getItem('cart');
    if (savedCart) {
      const cartData = JSON.parse(savedCart);
      cartData.forEach(([id, item]: [string, CartItem]) => 
        this.items.set(id, item)
      );
    }
    this.updateCart();
  }

  private addItem(productData: CartItem) {
    const existing = this.items.get(productData.id);
    if (existing) {
      existing.quantity += 1;
    } else {
      this.items.set(productData.id, { 
        ...productData, 
        quantity: 1 
      });
    }
    this.saveCart();
    this.updateCart();
    this.openCart();
  }

  private removeItem(id: string) {
    this.items.delete(id);
    this.saveCart();
    this.updateCart();
  }

  private updateQuantity(id: string, quantity: number) {
    const item = this.items.get(id);
    if (item) {
      item.quantity = quantity;
      if (quantity <= 0) {
        this.removeItem(id);
      } else {
        this.saveCart();
        this.updateCart();
      }
    }
  }

  private updateCart() {
    const cartEl = document.getElementById('cart-items');
    const totalEl = document.getElementById('cart-total');
    if (!cartEl || !totalEl) return;

    cartEl.innerHTML = Array.from(this.items.values())
      .map(item => `
        <div class="cart-item" data-id="${item.id}">
          <img src="${item.image}" alt="${item.name}" />
          <div class="cart-item-details">
            <h3>${item.name}</h3>
            <div class="quantity-controls">
              <button class="quantity-btn minus">-</button>
              <input type="number" value="${item.quantity}" 
                min="1" class="quantity-input" />
              <button class="quantity-btn plus">+</button>
            </div>
            <p class="price">$${(item.price * item.quantity).toFixed(2)}</p>
          </div>
          <button class="remove-item">&times;</button>
        </div>
      `).join('');

    const total = Array.from(this.items.values())
      .reduce((sum, item) => sum + (item.price * item.quantity), 0);
    
    totalEl.innerHTML = `
      <div class="cart-total">
        <span>Total:</span>
        <span>$${total.toFixed(2)}</span>
      </div>
    `;
  }

  private saveCart() {
    localStorage.setItem('cart', 
      JSON.stringify(Array.from(this.items.entries()))
    );
  }

  private openCart() {
    const cart = document.getElementById('cart');
    if (cart) {
      cart.classList.add('open');
      this.isOpen = true;
    }
  }

  private closeCart() {
    const cart = document.getElementById('cart');
    if (cart) {
      cart.classList.remove('open');
      this.isOpen = false;
    }
  }

  private bindEvents() {
    // Add to cart button clicks
    document.addEventListener('click', e => {
      const target = e.target as HTMLElement;
      
      if (target.matches('[data-product-id]')) {
        const button = target as HTMLButtonElement;
        const productData = {
          id: button.dataset.productId!,
          name: button.dataset.productName!,
          price: parseFloat(button.dataset.productPrice!),
          image: button.dataset.productImage!
        };
        this.addItem(productData);
      }
    });

    // Cart item quantity controls
    document.addEventListener('click', e => {
      const target = e.target as HTMLElement;
      
      if (target.matches('.quantity-btn')) {
        const cartItem = target.closest('.cart-item');
        if (!cartItem) return;

        const id = cartItem.dataset.id!;
        const input = cartItem.querySelector('.quantity-input') as HTMLInputElement;
        const currentQty = parseInt(input.value);

        if (target.classList.contains('plus')) {
          this.updateQuantity(id, currentQty + 1);
        } else if (target.classList.contains('minus')) {
          this.updateQuantity(id, currentQty - 1);
        }
      }
    });

    // Remove item button clicks
    document.addEventListener('click', e => {
      const target = e.target as HTMLElement;
      
      if (target.matches('.remove-item')) {
        const cartItem = target.closest('.cart-item');
        if (cartItem) {
          this.removeItem(cartItem.dataset.id!);
        }
      }
    });

    // Close cart button
    document.getElementById('close-cart')?.addEventListener('click', () => {
      this.closeCart();
    });

    // Checkout button
    document.getElementById('checkout')?.addEventListener('click', () => {
      // Implement checkout logic here
      console.log('Proceeding to checkout with items:', 
        Array.from(this.items.values()));
    });
  }
}

// Initialize cart
new Cart();
</script>

<style>
  .cart-drawer {
    position: fixed;
    right: -400px;
    top: 0;
    height: 100vh;
    width: 400px;
    background: white;
    box-shadow: -2px 0 5px rgba(0,0,0,0.1);
    padding: 1rem;
    transition: right 0.3s ease;
    z-index: 1000;
  }

  .cart-drawer.open {
    right: 0;
  }

  .cart-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 1rem;
    padding-bottom: 1rem;
    border-bottom: 1px solid #eee;
  }

  .cart-item {
    display: grid;
    grid-template-columns: auto 1fr auto;
    gap: 1rem;
    padding: 1rem;
    border-bottom: 1px solid #eee;
  }

  .cart-item img {
    width: 60px;
    height: 60px;
    object-fit: cover;
    border-radius: 4px;
  }

  .quantity-controls {
    display: flex;
    align-items: center;
    gap: 0.5rem;
  }

  .quantity-input {
    width: 50px;
    text-align: center;
  }

  .quantity-btn {
    width: 24px;
    height: 24px;
    border-radius: 50%;
    border: 1px solid #ddd;
    background: white;
    cursor: pointer;
  }

  .checkout-button {
    width: 100%;
    padding: 1rem;
    background: #2563eb;
    color: white;
    border: none;
    border-radius: 4px;
    margin-top: 1rem;
    cursor: pointer;
  }

  .checkout-button:hover {
    background: #1d4ed8;
  }
</style>

Enhanced GraphQL Integration

Create a more robust GraphQL client with caching and error handling:

// src/lib/graphql.ts
interface GraphQLResponse<T> {
  data?: T;
  errors?: Array<{
    message: string;
    locations: Array<{
      line: number;
      column: number;
    }>;
  }>;
}

class GraphQLClient {
  private endpoint: string;
  private cache: Map<string, any>;
  private cacheTimeout: number;

  constructor(endpoint: string, cacheTimeout = 5 * 60 * 1000) {
    this.endpoint = endpoint;
    this.cache = new Map();
    this.cacheTimeout = cacheTimeout;
  }

  async query<T>(query: string, variables = {}, useCache = true): Promise<T> {
    const cacheKey = JSON.stringify({ query, variables });
    
    if (useCache) {
      const cached = this.cache.get(cacheKey);
      if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
        return cached.data;
      }
    }

    try {
      const response = await fetch(this.endpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          // Add any authentication headers here
        },
        body: JSON.stringify({
          query,
          variables,
        }),
      });

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const result: GraphQLResponse<T> = await response.json();

      if (result.errors) {
        throw new Error(result.errors[0].message);
      }

      if (!result.data) {
        throw new Error('No data returned from GraphQL query');
      }

      if (useCache) {
        this.cache.set(cacheKey, {
          data: result.data,
          timestamp: Date.now(),
        });
      }

      return result.data;
    } catch (error) {
      console.error('GraphQL query error:', error);
      throw error;
    }
  }

  clearCache() {
    this.cache.clear();
  }
}

// Create a singleton instance
export const graphql = new GraphQLClient(
  import.meta.env.WORDPRESS_URL + '/graphql'
);

// Export common queries
export const PRODUCT_QUERIES = {
  ALL_PRODUCTS: `
    query GetProducts($first: Int = 12, $after: String) {
      products(first: $first, after: $after) {
        pageInfo {
          hasNextPage
          endCursor
        }
        nodes {
          id
          name
          price
          slug
          shortDescription
          image {
            sourceUrl
            altText
          }
          ... on SimpleProduct {
            price
            regularPrice
            salePrice
          }
          ... on VariableProduct {
            variations {
              nodes {
                price
                regularPrice
                salePrice
              }
            }
          }
        }
      }
    }
  `,
  
  SINGLE_PRODUCT: `
    query GetProduct($slug: ID!) {
      product(id: $slug, idType: SLUG) {
        id
        name
        description
        shortDescription
        price
        regularPrice
        salePrice
        slug
        image {
          sourceUrl
          altText
        }
        galleryImages {
          nodes {
            sourceUrl
            altText
          }
        }
        ... on SimpleProduct {
          stockStatus
          stockQuantity
        }
        ... on VariableProduct {
          variations {
            nodes {
              id
              name
              price
              regularPrice
              salePrice
              stockStatus
              stockQuantity
              attributes {
                nodes {
                  name
                  value
                }
              }
            }
          }
        }
      }
    }
  `,
};

Final Thoughts

By combining Astro with WordPress and WooCommerce, you get the best of both worlds: WordPress's content management and e-commerce capabilities with Astro's performance benefits. This architecture provides:

  • Blazing-fast page loads with static site generation
  • Robust content management through WordPress
  • Powerful e-commerce features via WooCommerce
  • Improved SEO through static HTML generation

Additional Resources

Relevant Hashtags

#AstroDev #WordPress #WooCommerce #GraphQL #WebDev #JAMstack #Ecommerce #HeadlessCMS #WebPerformance #JavaScript