Astro integration for the ShopSavvy Data API. Fetch live product data, price comparisons, price history, and shopping deals into your Astro site — at build time via Content Collections, on-demand via API endpoints, or in client islands.
Works with Astro 5+. Zero client-side JavaScript by default; only the DealFeed and PriceHistory components hydrate.
npm install astro-shopsavvy @shopsavvy/sdkThen run astro add to register the integration automatically:
npx astro add astro-shopsavvyOr add it manually in astro.config.mjs:
import { defineConfig } from 'astro/config'
import { shopsavvy } from 'astro-shopsavvy'
export default defineConfig({
integrations: [
shopsavvy({
apiKey: import.meta.env.SHOPSAVVY_API_KEY,
}),
],
})Set your API key in .env:
SHOPSAVVY_API_KEY=ss_live_your_api_key_here
Get your API key at shopsavvy.com/data.
Define a collection in src/content/config.ts using the shopsavvyLoader and the built-in Zod schema:
import { defineCollection } from 'astro:content'
import { shopsavvyLoader } from 'astro-shopsavvy'
import { ShopSavvySchema } from 'astro-shopsavvy'
export const collections = {
products: defineCollection({
loader: shopsavvyLoader({
// Free-text search queries — results are collected into the collection
queries: ['airpods pro', 'sony wh-1000xm5'],
// Direct lookups by ASIN, UPC, barcode, URL, or model number
identifiers: ['B09XS7JWHH'],
// Fetch current offers (prices) for each product — default true
includeOffers: true,
}),
schema: ShopSavvySchema,
}),
}Then use the collection in any page:
---
import { getCollection, getEntry } from 'astro:content'
const products = await getCollection('products')
const product = await getEntry('products', 'B09XS7JWHH')
---All components are server-rendered .astro files. Import them directly from the package — no client JS unless noted.
Zero-JS product card with image, title, brand, star rating, lowest price, and a buy button. Accepts an optional showOffers prop to display the full retailer list inline.
---
import { getCollection } from 'astro:content'
import ProductCard from 'astro-shopsavvy/components/ProductCard.astro'
const products = await getCollection('products')
---
<div class="product-grid">
{products.map(p => (
<ProductCard product={p.data} />
))}
</div>Props:
| Prop | Type | Default | Description |
|---|---|---|---|
product |
ProductEntry |
required | Product data from the collection or loader |
href |
string |
best offer URL | Override the card link |
showOffers |
boolean |
false |
Show inline retailer list below the price |
class |
string |
— | Additional CSS class(es) |
Server-rendered table of all available offers sorted by price. In-stock offers appear first; out-of-stock rows are dimmed.
---
import { getEntry } from 'astro:content'
import PriceComparisonTable from 'astro-shopsavvy/components/PriceComparisonTable.astro'
const product = await getEntry('products', 'B09XS7JWHH')
---
<PriceComparisonTable product={product.data} showOutOfStock={false} />Props:
| Prop | Type | Default | Description |
|---|---|---|---|
product |
ProductEntry |
required | Product data |
limit |
number |
all | Maximum rows to display |
showOutOfStock |
boolean |
true |
Include out-of-stock offers as dimmed rows |
class |
string |
— | Additional CSS class(es) |
Interactive deal feed with hot / new / top sort and load-more pagination. Hydrates when the component enters the viewport (client:visible). Fetches deals from your own API route, not directly from ShopSavvy — so your API key stays server-side.
---
import DealFeed from 'astro-shopsavvy/components/DealFeed.astro'
---
<DealFeed client:visible apiBase="/api/shopsavvy" limit={20} sort="hot" />Props:
| Prop | Type | Default | Description |
|---|---|---|---|
apiBase |
string |
/api/shopsavvy |
Base path for your API route |
limit |
number |
20 |
Deals per page |
sort |
'hot' | 'new' | 'top' |
'hot' |
Initial sort order |
category |
string |
— | Filter to a category slug |
class |
string |
— | Additional CSS class(es) |
Requires the API endpoint template below.
Line chart of historical prices across retailers, powered by Chart.js. Hydrates on page load (client:load). Chart.js is loaded from CDN by default; override chartJsSrc to self-host.
---
import PriceHistory from 'astro-shopsavvy/components/PriceHistory.astro'
---
<PriceHistory client:load identifier="B09XS7JWHH" days={90} apiBase="/api/shopsavvy" />Props:
| Prop | Type | Default | Description |
|---|---|---|---|
identifier |
string |
required | Product identifier (ASIN, UPC, barcode, URL, etc.) |
days |
number |
90 |
Days of history to display |
apiBase |
string |
/api/shopsavvy |
Base path for your API route |
chartJsSrc |
string |
jsDelivr CDN | Chart.js script URL |
height |
number |
300 |
Canvas height in pixels |
class |
string |
— | Additional CSS class(es) |
Create src/pages/api/shopsavvy/[...slug].ts to proxy requests through your server (keeps your API key out of the browser):
import type { APIRoute } from 'astro'
import { handleShopSavvyRequest } from 'astro-shopsavvy'
export const GET: APIRoute = (context) => handleShopSavvyRequest(context)The handler reads context.locals.shopsavvy (injected by the integration middleware) and routes requests automatically:
| Route | Description |
|---|---|
GET /api/shopsavvy/search?q=...&limit=10 |
Product search |
GET /api/shopsavvy/products/:id |
Product details |
GET /api/shopsavvy/products/:id/offers |
Current prices across retailers |
GET /api/shopsavvy/products/:id/history?days=90 |
Price history |
GET /api/shopsavvy/deals?sort=hot&limit=20&category=electronics |
Deals feed |
The shopsavvy() integration registers middleware automatically that injects a configured API client into Astro.locals.shopsavvy. Use it in any .astro page or API endpoint:
---
// src/pages/products/[id].astro
const { shopsavvy } = Astro.locals
const result = await shopsavvy.getProductDetails(Astro.params.id)
const product = result?.data?.[0]
---Export a deals feed using @astrojs/rss:
// src/pages/deals.xml.ts
import rss from '@astrojs/rss'
import type { APIRoute } from 'astro'
import { createClient, dealsToRssItems } from 'astro-shopsavvy'
export const GET: APIRoute = async (context) => {
const client = createClient({ apiKey: import.meta.env.SHOPSAVVY_API_KEY })
const response = await client.getDeals({ sort: 'hot', limit: 50 })
return rss({
title: 'Hot Deals',
description: 'Top shopping deals right now',
site: context.site!,
items: dealsToRssItems(response.deals ?? [], context.site!.toString()),
})
}Export a product feed:
import rss from '@astrojs/rss'
import type { APIRoute } from 'astro'
import { getCollection } from 'astro:content'
import { productToRssItem } from 'astro-shopsavvy'
export const GET: APIRoute = async (context) => {
const products = await getCollection('products')
return rss({
title: 'Product Prices',
description: 'Latest prices from ShopSavvy',
site: context.site!,
items: products.map(p => productToRssItem(p.data, '/products', context.site!.toString())),
})
}@astrojs/sitemap auto-discovers all static pages. To include dynamic product pages, generate them as static routes:
---
// src/pages/products/[id].astro
import { getCollection } from 'astro:content'
export async function getStaticPaths() {
const products = await getCollection('products')
return products.map(p => ({
params: { id: p.id },
props: { product: p.data },
}))
}
---@astrojs/sitemap picks these up automatically when site is set in astro.config.mjs.
Fetch product details at build time, render a ProductCard and PriceComparisonTable on each review page, and embed a PriceHistory island for context:
---
// src/pages/reviews/[slug].astro
import { getEntry } from 'astro:content'
import ProductCard from 'astro-shopsavvy/components/ProductCard.astro'
import PriceComparisonTable from 'astro-shopsavvy/components/PriceComparisonTable.astro'
import PriceHistory from 'astro-shopsavvy/components/PriceHistory.astro'
const product = await getEntry('products', Astro.params.slug)
---
<ProductCard product={product.data} showOffers />
<PriceComparisonTable product={product.data} />
<PriceHistory client:load identifier={product.data.amazon ?? product.id} days={180} />Build gift guides using static product grids. Query multiple search terms in the loader and group products by category on the page:
// src/content/config.ts
export const collections = {
'gift-tech': defineCollection({
loader: shopsavvyLoader({
queries: ['best headphones 2025', 'best laptop 2025', 'best smartwatch 2025'],
includeOffers: true,
}),
schema: ShopSavvySchema,
}),
}---
import { getCollection } from 'astro:content'
import ProductCard from 'astro-shopsavvy/components/ProductCard.astro'
const products = await getCollection('gift-tech')
---
<div class="gift-grid">
{products.map(p => <ProductCard product={p.data} />)}
</div>Use the DealFeed island as the homepage centrepiece, backed by the API endpoint:
---
// src/pages/index.astro
import DealFeed from 'astro-shopsavvy/components/DealFeed.astro'
---
<main>
<h1>Today's Best Deals</h1>
<DealFeed client:visible apiBase="/api/shopsavvy" limit={30} sort="hot" />
</main>Combine with the RSS helper to publish a syndicated deals feed at /deals.xml.
This integration works on every Astro adapter. The middleware and API endpoint helpers run in server mode; the Content Collection loader runs at build time.
| Platform | Adapter | Notes |
|---|---|---|
| Vercel | @astrojs/vercel |
Works with both static and SSR output modes |
| Netlify | @astrojs/netlify |
Edge and Node.js runtimes both supported |
| Cloudflare | @astrojs/cloudflare |
Use runtime.env.SHOPSAVVY_API_KEY for env vars |
| Node.js | @astrojs/node |
Set SHOPSAVVY_API_KEY in your process environment |
For static-only sites (output: 'static'), the middleware and API routes are not available — use the Content Collection loader and build-time data fetching only.
All types are exported from the package root:
import type {
ProductEntry,
ProductOffer,
PriceHistoryPoint,
DealEntry,
ShopSavvyProduct,
ShopSavvyDeal,
ShopSavvyIntegrationOptions,
ShopSavvyLocals,
} from 'astro-shopsavvy'Augment Astro.locals with the injected client type in src/env.d.ts:
/// <reference types="astro/client" />
declare namespace App {
interface Locals {
shopsavvy: import('astro-shopsavvy').ShopSavvyLocals['shopsavvy']
}
}MIT