Categories
Fetch category hierarchy using the Storefront SDK
Overview
The categories resource provides methods for fetching category data, including the full category tree with nested children and individual category details by slug.
Categories in Putiikkipalvelu form a hierarchical tree structure where each category can have child categories. The API supports up to 5 levels of nesting, making it suitable for complex navigation structures.
Methods
categories.list(options?)
Fetch all top-level categories with their nested children as a hierarchical tree.
const categories = await storefront.categories.list();Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
options | FetchOptions | No | Fetch options (caching, headers, etc.) |
Returns: Promise<Category[]>
The response includes only top-level categories (where parentId is null). Each category contains a children array with its subcategories, which in turn contain their own children, up to 5 levels deep.
categories.getBySlug(slug, options?)
Fetch a single category by its URL slug.
const { category } = await storefront.categories.getBySlug('korut');Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
slug | string | Yes | Category URL slug |
options | FetchOptions | No | Fetch options (caching, headers, etc.) |
Returns: Promise<CategoryResponse>
Throws: NotFoundError if category not found
Fetch Options
All methods accept an optional options object:
| Option | Type | Description |
|---|---|---|
signal | AbortSignal | Cancel the request |
cache | RequestCache | Fetch cache mode |
next.revalidate | number | false | Next.js ISR revalidation time in seconds |
next.tags | string[] | Next.js cache tags for on-demand revalidation |
Response Types
Category
Represents a category with its nested children.
interface Category {
/** Unique category identifier */
id: string;
/** Category display name */
name: string;
/** URL-friendly slug (unique per store) */
slug: string;
/** Store ID this category belongs to */
storeId: string;
/** Parent category ID (null for root categories) */
parentId: string | null;
/** Creation timestamp (ISO 8601) */
createdAt: string;
/** Child categories (recursive, up to 5 levels) */
children: Category[];
}CategoryResponse
Response from getBySlug():
interface CategoryResponse {
category: Category;
}Examples
Navigation Menu
Build a navigation menu from the category tree:
import { storefront } from '@/lib/storefront';
import type { Category } from '@putiikkipalvelu/storefront-sdk';
export async function getNavigation() {
const categories = await storefront.categories.list({
next: { revalidate: 3600, tags: ['categories'] } // Cache for 1 hour
});
return categories;
}
// React component
function CategoryMenu({ categories }: { categories: Category[] }) {
return (
<nav>
<ul>
{categories.map(category => (
<li key={category.id}>
<a href={`/category/${category.slug}`}>{category.name}</a>
{category.children.length > 0 && (
<ul>
{category.children.map(child => (
<li key={child.id}>
<a href={`/category/${child.slug}`}>{child.name}</a>
</li>
))}
</ul>
)}
</li>
))}
</ul>
</nav>
);
}Recursive Category Tree Component
Render a full category tree with unlimited depth:
import type { Category } from '@putiikkipalvelu/storefront-sdk';
function CategoryTree({ categories, depth = 0 }: {
categories: Category[];
depth?: number
}) {
if (categories.length === 0) return null;
return (
<ul style={{ marginLeft: depth * 16 }}>
{categories.map(category => (
<li key={category.id}>
<a href={`/category/${category.slug}`}>{category.name}</a>
<CategoryTree categories={category.children} depth={depth + 1} />
</li>
))}
</ul>
);
}Category Page with SEO Metadata
import { Metadata } from 'next';
import { storefront, NotFoundError } from '@/lib/storefront';
import { notFound } from 'next/navigation';
interface Props {
params: { slug: string }
}
// Generate metadata for SEO
export async function generateMetadata({ params }: Props): Promise<Metadata> {
try {
const { category } = await storefront.categories.getBySlug(params.slug);
return {
title: category.name,
description: `Browse ${category.name} products`,
};
} catch {
return { title: 'Category Not Found' };
}
}
// Page component
export default async function CategoryPage({ params }: Props) {
try {
const { category } = await storefront.categories.getBySlug(params.slug, {
next: { revalidate: 3600, tags: ['category', params.slug] }
});
// Fetch products for this category
const { products, totalCount } = await storefront.products.sorted({
slugs: [params.slug],
page: 1,
pageSize: 12
});
return (
<>
<h1>{category.name}</h1>
<p>{totalCount} products</p>
<ProductGrid products={products} />
</>
);
} catch (error) {
if (error instanceof NotFoundError) {
notFound();
}
throw error;
}
}Breadcrumb Navigation
Build breadcrumbs by traversing the category hierarchy:
import { storefront } from '@/lib/storefront';
import type { Category } from '@putiikkipalvelu/storefront-sdk';
// Helper to find a category and its ancestors
function findCategoryPath(
categories: Category[],
targetSlug: string,
path: Category[] = []
): Category[] | null {
for (const category of categories) {
const newPath = [...path, category];
if (category.slug === targetSlug) {
return newPath;
}
if (category.children.length > 0) {
const result = findCategoryPath(category.children, targetSlug, newPath);
if (result) return result;
}
}
return null;
}
export async function getBreadcrumbs(categorySlug: string) {
const categories = await storefront.categories.list({
next: { revalidate: 3600, tags: ['categories'] }
});
const path = findCategoryPath(categories, categorySlug);
return path || [];
}
// Usage in component
function Breadcrumbs({ path }: { path: Category[] }) {
return (
<nav aria-label="Breadcrumb">
<ol className="flex gap-2">
<li><a href="/">Home</a></li>
{path.map((category, index) => (
<li key={category.id}>
<span className="mx-2">/</span>
{index === path.length - 1 ? (
<span>{category.name}</span>
) : (
<a href={`/category/${category.slug}`}>{category.name}</a>
)}
</li>
))}
</ol>
</nav>
);
}Generate Static Params for Categories
Pre-render all category pages at build time:
import { storefront } from '@/lib/storefront';
import type { Category } from '@putiikkipalvelu/storefront-sdk';
// Flatten category tree to get all slugs
function getAllCategorySlugs(categories: Category[]): string[] {
const slugs: string[] = [];
function traverse(cats: Category[]) {
for (const cat of cats) {
slugs.push(cat.slug);
if (cat.children.length > 0) {
traverse(cat.children);
}
}
}
traverse(categories);
return slugs;
}
export async function generateStaticParams() {
const categories = await storefront.categories.list();
const slugs = getAllCategorySlugs(categories);
return slugs.map(slug => ({ slug }));
}Sitemap Generation
Include all categories in your sitemap:
import { storefront } from '@/lib/storefront';
import type { Category } from '@putiikkipalvelu/storefront-sdk';
function flattenCategories(categories: Category[]): Category[] {
const result: Category[] = [];
function traverse(cats: Category[]) {
for (const cat of cats) {
result.push(cat);
traverse(cat.children);
}
}
traverse(categories);
return result;
}
export default async function sitemap() {
const categories = await storefront.categories.list();
const allCategories = flattenCategories(categories);
return allCategories.map(category => ({
url: `https://mystore.com/category/${category.slug}`,
lastModified: new Date(category.createdAt),
changeFrequency: 'weekly' as const,
priority: 0.8,
}));
}Category Sidebar with Active State
'use client';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
import type { Category } from '@putiikkipalvelu/storefront-sdk';
function CategorySidebar({ categories }: { categories: Category[] }) {
const pathname = usePathname();
function isActive(slug: string): boolean {
return pathname.includes(`/category/${slug}`);
}
function CategoryItem({ category, depth = 0 }: {
category: Category;
depth?: number
}) {
const active = isActive(category.slug);
return (
<li>
<Link
href={`/category/${category.slug}`}
className={`
block py-2 px-4
${active ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'}
`}
style={{ paddingLeft: 16 + depth * 16 }}
>
{category.name}
</Link>
{category.children.length > 0 && (
<ul>
{category.children.map(child => (
<CategoryItem
key={child.id}
category={child}
depth={depth + 1}
/>
))}
</ul>
)}
</li>
);
}
return (
<aside>
<h2 className="font-bold text-lg px-4 py-2">Categories</h2>
<ul>
{categories.map(category => (
<CategoryItem key={category.id} category={category} />
))}
</ul>
</aside>
);
}Caching Recommendations
Categories typically don't change frequently, so aggressive caching is recommended:
// Long cache with tags for on-demand revalidation
const categories = await storefront.categories.list({
next: {
revalidate: 86400, // 24 hours
tags: ['categories']
}
});
// Individual category with tag
const { category } = await storefront.categories.getBySlug('korut', {
next: {
revalidate: 86400,
tags: ['category', 'korut']
}
});To revalidate when categories change in the dashboard, call:
import { revalidateTag } from 'next/cache';
// Revalidate all categories
revalidateTag('categories');
// Revalidate specific category
revalidateTag('korut');Error Handling
import {
NotFoundError,
AuthError,
StorefrontError
} from '@putiikkipalvelu/storefront-sdk';
try {
const { category } = await storefront.categories.getBySlug('unknown');
} catch (error) {
if (error instanceof NotFoundError) {
// Category not found (404)
return notFound();
}
if (error instanceof AuthError) {
// Invalid API key (401)
console.error('Check your STOREFRONT_API_KEY');
}
if (error instanceof StorefrontError) {
// Other API error
console.error(`API Error: ${error.message} (${error.code})`);
}
throw error;
}