Hydra is now in beta|Get started free|Follow our journey on X.com

Astro
On this page

Astro + Hydra

Build a fast, content-driven storefront using Astro and the Hydra SDK. Astro renders static pages by default with interactive islands for dynamic features like the shopping cart.

Prerequisites

  • Node.js 18+
  • A Hydra project with API keys (get them from your admin panel)

1. Create your project

npm create astro@latest my-store
cd my-store
npm install @gethydra/sdk

2. Environment variables

Create a .env file in your project root:

# Server-side only — used in frontmatter and API routes
HYDRA_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Client-side — prefixed with PUBLIC_ so Astro exposes it to the browser
PUBLIC_HYDRA_PUBLISHABLE_KEY=pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Secret keys (sk_live_*) are only available in .astro frontmatter and server endpoints. Publishable keys (pk_live_*) are safe for client-side scripts — they only allow read access and cart operations.

3. SDK setup

Create a server-side SDK instance for use in frontmatter scripts and API routes.

// src/lib/hydra.ts
import { Hydra } from '@gethydra/sdk';

export const hydra = new Hydra({
  secret_key: import.meta.env.HYDRA_SECRET_KEY,
  base_url: 'https://api.hydrajs.dev',
});

4. Product listing page

Fetch products in the frontmatter block. The page renders as static HTML with zero client-side JavaScript.

---
// src/pages/products/index.astro
import Layout from '../../layouts/Layout.astro';
import { hydra } from '../../lib/hydra';

const { data: products } = await hydra.products.list({ limit: 20 });
---

<Layout title="Products">
  <h1>Products</h1>
  <div class="product-grid">
    {products.map((product) => (
      <a href={`/products/${product.handle}`} class="product-card">
        {product.images[0] && (
          <img
            src={product.images[0].src}
            alt={product.images[0].alt ?? product.title}
            width={product.images[0].width}
            height={product.images[0].height}
            loading="lazy"
          />
        )}
        <h2>{product.title}</h2>
        <p>${(product.variants[0].price / 100).toFixed(2)}</p>
      </a>
    ))}
  </div>
</Layout>

<style>
  .product-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 1.5rem;
  }
  .product-card {
    text-decoration: none;
    color: inherit;
  }
  .product-card img {
    width: 100%;
    height: auto;
    border-radius: 8px;
  }
</style>

5. Product detail page

Use dynamic routes to generate pages for each product.

---
// src/pages/products/[handle].astro
import Layout from '../../layouts/Layout.astro';
import { hydra } from '../../lib/hydra';

const { handle } = Astro.params;

let product;
try {
  const response = await hydra.products.getByHandle(handle!, {
    expand: 'variants,images',
  });
  product = response.data;
} catch {
  return Astro.redirect('/404');
}
---

<Layout title={product.title}>
  <h1>{product.title}</h1>

  {product.images[0] && (
    <img
      src={product.images[0].src}
      alt={product.images[0].alt ?? product.title}
      width={product.images[0].width}
      height={product.images[0].height}
    />
  )}

  <div set:html={product.body_html} />

  <div class="variants">
    {product.variants.map((variant) => (
      <div class="variant-row">
        <span>{variant.title}</span>
        <span>${(variant.price / 100).toFixed(2)}</span>
        <button
          class="add-to-cart"
          data-variant-id={variant.id}
        >
          Add to cart
        </button>
      </div>
    ))}
  </div>
</Layout>

<script>
  import { Hydra } from '@gethydra/sdk';

  const hydraClient = new Hydra({
    publishable_key: import.meta.env.PUBLIC_HYDRA_PUBLISHABLE_KEY,
    base_url: 'https://api.hydrajs.dev',
  });

  function getCartId(): string | null {
    return localStorage.getItem('hydra_cart_id');
  }

  function setCartId(id: string) {
    localStorage.setItem('hydra_cart_id', id);
  }

  document.querySelectorAll('.add-to-cart').forEach((button) => {
    button.addEventListener('click', async (e) => {
      const target = e.currentTarget as HTMLButtonElement;
      const variantId = target.dataset.variantId!;
      target.disabled = true;
      target.textContent = 'Adding...';

      try {
        let cartId = getCartId();

        if (!cartId) {
          const { data: cart } = await hydraClient.cart.create({
            items: [{ variant_id: variantId, quantity: 1 }],
          });
          setCartId(cart.id);
        } else {
          await hydraClient.cart.addItem(cartId, {
            variant_id: variantId,
            quantity: 1,
          });
        }

        target.textContent = 'Added!';
        setTimeout(() => {
          target.textContent = 'Add to cart';
          target.disabled = false;
        }, 1500);
      } catch (error) {
        console.error('Failed to add to cart:', error);
        target.textContent = 'Add to cart';
        target.disabled = false;
      }
    });
  });
</script>

<style>
  .variants {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    margin-top: 1.5rem;
  }
  .variant-row {
    display: flex;
    align-items: center;
    gap: 1rem;
  }
</style>

6. Cart page

For the cart page, use a <script> tag to fetch and render cart data on the client.

---
// src/pages/cart.astro
import Layout from '../layouts/Layout.astro';
---

<Layout title="Cart">
  <h1>Cart</h1>
  <div id="cart-container">
    <p>Loading cart...</p>
  </div>
</Layout>

<script>
  import { Hydra } from '@gethydra/sdk';

  const hydraClient = new Hydra({
    publishable_key: import.meta.env.PUBLIC_HYDRA_PUBLISHABLE_KEY,
    base_url: 'https://api.hydrajs.dev',
  });

  const container = document.getElementById('cart-container')!;
  const cartId = localStorage.getItem('hydra_cart_id');

  if (!cartId) {
    container.innerHTML = '<p>Your cart is empty.</p>';
  } else {
    const { data: cart } = await hydraClient.cart.get(cartId);

    if (cart.items.length === 0) {
      container.innerHTML = '<p>Your cart is empty.</p>';
    } else {
      container.innerHTML = `
        <ul>
          ${cart.items
            .map(
              (item) =>
                `<li>${item.title} x ${item.quantity} — $${(item.price / 100).toFixed(2)}</li>`
            )
            .join('')}
        </ul>
        <p><strong>Total: $${(cart.total / 100).toFixed(2)}</strong></p>
        <button id="checkout-btn">Checkout</button>
      `;

      document.getElementById('checkout-btn')?.addEventListener('click', async () => {
        const btn = document.getElementById('checkout-btn') as HTMLButtonElement;
        btn.disabled = true;
        btn.textContent = 'Redirecting...';

        const { data: checkout } = await hydraClient.checkout.create({
          cart_id: cart.id,
        });
        window.location.href = checkout.checkout_url;
      });
    }
  }
</script>

7. Static generation (optional)

To pre-render all product pages at build time, export getStaticPaths:

---
// src/pages/products/[handle].astro (add to frontmatter)
export async function getStaticPaths() {
  const { data: products } = await hydra.products.list({ limit: 100 });
  return products.map((product) => ({
    params: { handle: product.handle },
  }));
}
---

Static generation works well for catalogs that change infrequently. For stores with frequent updates, use output: 'server' in your Astro config and deploy with an SSR adapter.

8. Deploy

Netlify

npm install @astrojs/netlify
npx astro add netlify
netlify deploy --prod

Cloudflare Pages

npm install @astrojs/cloudflare
npx astro add cloudflare
npx wrangler pages deploy dist/

Set your environment variables in the hosting dashboard:

  • HYDRA_SECRET_KEY — your secret key
  • PUBLIC_HYDRA_PUBLISHABLE_KEY — your publishable key

Next steps