Next.js Integration
Build a storefront with Next.js App Router using the Skakio Catalog API.
Setup
1. Configure Environment Variables
Create .env.local:
SKAKIO_API_KEY=pk_live_your_key_here
SKAKIO_STORE_ID=store_abc123
2. Create API Client
Create lib/skakio.ts:
const API_BASE = 'https://api.skakio.com';
interface TokenData {
token: string;
expires_in: number;
}
class SkakioClient {
private apiKey: string;
private token: string | null = null;
private tokenExpiresAt: number = 0;
constructor(apiKey: string) {
this.apiKey = apiKey;
}
private async getToken(): Promise<string> {
const now = Date.now();
const buffer = 60 * 1000;
if (!this.token || now > this.tokenExpiresAt - buffer) {
const res = await fetch(`${API_BASE}/auth/token`, {
method: 'POST',
headers: { 'X-API-Key': this.apiKey },
cache: 'no-store',
});
if (!res.ok) throw new Error('Failed to get token');
const { data }: { data: TokenData } = await res.json();
this.token = data.token;
this.tokenExpiresAt = now + data.expires_in * 1000;
}
return this.token!;
}
async fetch<T>(path: string, options?: { revalidate?: number }): Promise<T> {
const token = await this.getToken();
const res = await fetch(`${API_BASE}${path}`, {
headers: { Authorization: `Bearer ${token}` },
next: { revalidate: options?.revalidate ?? 60 },
});
if (!res.ok) {
throw new Error(`API error: ${res.status}`);
}
const { data } = await res.json();
return data;
}
}
export const skakio = new SkakioClient(process.env.SKAKIO_API_KEY!);
export const STORE_ID = process.env.SKAKIO_STORE_ID!;
Page Examples
Store Page (App Router)
Create app/page.tsx:
import { skakio, STORE_ID } from '@/lib/skakio';
import Link from 'next/link';
import Image from 'next/image';
interface Store {
id: string;
name: string;
description: string;
logo?: { url: string };
}
interface Listing {
id: string;
name: string;
publicationCount: number;
}
export default async function HomePage() {
const [store, listings] = await Promise.all([
skakio.fetch<Store>(`/api/store/${STORE_ID}`),
skakio.fetch<Listing[]>(`/api/store/${STORE_ID}/listings`),
]);
return (
<main className="container mx-auto px-4 py-8">
<header className="text-center mb-12">
{store.logo && (
<Image
src={store.logo.url}
alt={store.name}
width={120}
height={120}
className="mx-auto rounded-full"
/>
)}
<h1 className="text-4xl font-bold mt-4">{store.name}</h1>
<p className="text-gray-600 mt-2">{store.description}</p>
</header>
<section>
<h2 className="text-2xl font-semibold mb-6">Categories</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{listings.map((listing) => (
<Link
key={listing.id}
href={`/category/${listing.id}`}
className="p-6 border rounded-lg hover:shadow-lg transition"
>
<h3 className="font-semibold">{listing.name}</h3>
<p className="text-sm text-gray-500">
{listing.publicationCount} products
</p>
</Link>
))}
</div>
</section>
</main>
);
}
Category Page
Create app/category/[id]/page.tsx:
import { skakio } from '@/lib/skakio';
import Link from 'next/link';
import Image from 'next/image';
interface Publication {
id: string;
title: string;
price: { amount: number; currency: string };
media?: { url: string }[];
}
interface Listing {
id: string;
name: string;
description: string;
publications?: Publication[];
}
export default async function CategoryPage({
params,
}: {
params: { id: string };
}) {
const listing = await skakio.fetch<Listing>(
`/api/listing/${params.id}?expand=publication`
);
return (
<main className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold">{listing.name}</h1>
<p className="text-gray-600 mt-2">{listing.description}</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 mt-8">
{listing.publications?.map((product) => (
<Link
key={product.id}
href={`/product/${product.id}`}
className="group"
>
<div className="aspect-square relative overflow-hidden rounded-lg bg-gray-100">
{product.media?.[0] && (
<Image
src={product.media[0].url}
alt={product.title}
fill
className="object-cover group-hover:scale-105 transition"
/>
)}
</div>
<h3 className="mt-2 font-medium truncate">{product.title}</h3>
<p className="text-green-600 font-semibold">
{product.price.currency} {product.price.amount.toFixed(2)}
</p>
</Link>
))}
</div>
</main>
);
}
Product Page
Create app/product/[id]/page.tsx:
import { skakio } from '@/lib/skakio';
import Image from 'next/image';
interface Publication {
id: string;
title: string;
description: string;
price: { amount: number; currency: string };
quantity: { available: number };
media?: { url: string; alt?: string }[];
}
interface Review {
id: string;
rating: number;
title: string;
content: string;
author?: { name: string };
}
export default async function ProductPage({
params,
}: {
params: { id: string };
}) {
const [product, reviewsData] = await Promise.all([
skakio.fetch<Publication>(`/api/publication/${params.id}`),
skakio.fetch<Review[]>(`/api/publication/${params.id}/reviews?limit=5`),
]);
return (
<main className="container mx-auto px-4 py-8">
<div className="grid md:grid-cols-2 gap-8">
{/* Images */}
<div className="space-y-4">
{product.media?.map((m, i) => (
<div key={i} className="aspect-square relative rounded-lg overflow-hidden">
<Image
src={m.url}
alt={m.alt || product.title}
fill
className="object-cover"
priority={i === 0}
/>
</div>
))}
</div>
{/* Info */}
<div>
<h1 className="text-3xl font-bold">{product.title}</h1>
<p className="text-2xl text-green-600 font-bold mt-4">
{product.price.currency} {product.price.amount.toFixed(2)}
</p>
<p className="mt-4 text-gray-700">{product.description}</p>
<div className="mt-6">
{product.quantity.available > 0 ? (
<span className="text-green-600">
In Stock ({product.quantity.available} available)
</span>
) : (
<span className="text-red-600">Out of Stock</span>
)}
</div>
</div>
</div>
{/* Reviews */}
<section className="mt-12">
<h2 className="text-2xl font-semibold mb-6">Reviews</h2>
{reviewsData.length > 0 ? (
<div className="space-y-4">
{reviewsData.map((review) => (
<div key={review.id} className="border-b pb-4">
<div className="text-yellow-500">
{'★'.repeat(review.rating)}
{'☆'.repeat(5 - review.rating)}
</div>
<h4 className="font-medium mt-1">{review.title}</h4>
<p className="text-gray-600">{review.content}</p>
<span className="text-sm text-gray-400">
{review.author?.name}
</span>
</div>
))}
</div>
) : (
<p className="text-gray-500">No reviews yet.</p>
)}
</section>
</main>
);
}
Search with Client Component
Create app/search/page.tsx:
'use client';
import { useState, useTransition } from 'react';
import Link from 'next/link';
import { searchProducts } from './actions';
export default function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<any>(null);
const [isPending, startTransition] = useTransition();
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
startTransition(async () => {
const data = await searchProducts(query);
setResults(data);
});
};
return (
<main className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Search</h1>
<form onSubmit={handleSearch} className="flex gap-2 mb-8">
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
className="flex-1 px-4 py-2 border rounded-lg"
/>
<button
type="submit"
disabled={isPending}
className="px-6 py-2 bg-blue-600 text-white rounded-lg disabled:opacity-50"
>
{isPending ? 'Searching...' : 'Search'}
</button>
</form>
{results?.publications && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{results.publications.map((product: any) => (
<Link key={product.id} href={`/product/${product.id}`}>
<h3 className="font-medium">{product.title}</h3>
<p className="text-green-600">
{product.price.currency} {product.price.amount}
</p>
</Link>
))}
</div>
)}
</main>
);
}
Create app/search/actions.ts:
'use server';
import { skakio } from '@/lib/skakio';
export async function searchProducts(query: string) {
if (!query) return null;
return skakio.fetch(
`/api/marketplace/search?q=${encodeURIComponent(query)}&limit=20`,
{ revalidate: 0 }
);
}
Static Generation
Generate static pages at build time:
// app/category/[id]/page.tsx
import { skakio, STORE_ID } from '@/lib/skakio';
export async function generateStaticParams() {
const listings = await skakio.fetch<{ id: string }[]>(
`/api/store/${STORE_ID}/listings`
);
return listings.map((listing) => ({
id: listing.id,
}));
}
Caching Strategies
// Static data (revalidate every hour)
const store = await skakio.fetch('/api/store/123', { revalidate: 3600 });
// Dynamic data (no cache)
const search = await skakio.fetch('/api/marketplace/search?q=bike', {
revalidate: 0,
});
// On-demand revalidation
import { revalidatePath } from 'next/cache';
revalidatePath('/category/123');