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 keyPUBLIC_HYDRA_PUBLISHABLE_KEY— your publishable key
Next steps
- SDK reference — full list of available methods
- Authentication — API key types and usage
- Pagination — cursor-based pagination for large catalogs