Putiikkipalvelu Docs
SDK

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:

ParamTypeRequiredDescription
optionsFetchOptionsNoFetch 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:

ParamTypeRequiredDescription
slugstringYesCategory URL slug
optionsFetchOptionsNoFetch options (caching, headers, etc.)

Returns: Promise<CategoryResponse>

Throws: NotFoundError if category not found


Fetch Options

All methods accept an optional options object:

OptionTypeDescription
signalAbortSignalCancel the request
cacheRequestCacheFetch cache mode
next.revalidatenumber | falseNext.js ISR revalidation time in seconds
next.tagsstring[]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

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;
  }
}

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;
}

On this page