Pages
Fetch CMS pages with typed content blocks for storefront rendering
Overview
The pages resource fetches published CMS pages by slug. Each page contains an ordered array of typed content blocks — a discriminated union on block.type that gives you full type safety in your template with zero casts.
Block types available on all pages: markdown, accordion, gallery, about
Homepage-only blocks: hero, latest_products, showcase, cta
Methods
pages.getBySlug(slug, options?)
Fetch a published CMS page by its slug.
const page = await storefront.pages.getBySlug("about");
console.log(page.title);
console.log(page.blocks.length);Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
slug | string | Yes | Page slug (e.g. "about", "terms", "homepage") |
options | FetchOptions | No | Fetch options (caching, etc.) |
Returns: Promise<StorePage>
Throws:
NotFoundError— Page not found or not published
Response Shape
interface StorePage {
id: string;
slug: string;
title: string;
description: string | null;
blocks: PageBlock[];
updatedAt: string;
storeName: string;
seo: PageSeo | null;
}
interface PageSeo {
seoTitle: string | null;
seoDescription: string | null;
domain: string | null;
openGraphImageUrl: string | null;
ogImageAlt: string | null;
twitterImageUrl: string | null;
twitterHandle: string | null;
}Block Types
PageBlock is a discriminated union on the type field. TypeScript narrows block.data automatically inside a switch statement.
markdown
Rich text HTML content (sanitized server-side).
{ type: "markdown", data: { content: string } }accordion
FAQ-style expandable Q&A sections.
{
type: "accordion",
data: {
title?: string;
description?: string;
items: Array<{ id: string; question: string; answer: string; order: number }>;
}
}gallery
Image gallery with ordered items.
{
type: "gallery",
data: {
items: Array<{ id: string; src: string; alt?: string; order: number }>;
}
}about
Image + text section with configurable layout direction.
{
type: "about",
data: {
title: string;
description: string; // HTML
imageUrl?: string;
imagePosition: "left" | "right";
}
}hero (homepage only)
Full-width hero section with optional background image and CTA.
{
type: "hero",
data: {
title: string;
subtitle?: string;
imageUrl?: string;
ctaText?: string;
ctaLink?: string;
}
}latest_products (homepage only)
Displays the latest N products from the store catalog.
{
type: "latest_products",
data: {
title?: string;
description?: string;
count: number; // 1–24, default 6
}
}showcase (homepage only)
Exactly 3 category showcase cards with images.
{
type: "showcase",
data: {
title?: string;
description?: string;
items: Array<{ // always 3 items
title: string;
description?: string;
imageUrl: string;
categorySlug: string;
}>;
}
}cta (homepage only)
Call-to-action section with primary and secondary buttons.
{
type: "cta",
data: {
title?: string;
description?: string;
primaryButtonText?: string;
primaryButtonLink?: string;
secondaryButtonText?: string;
secondaryButtonLink?: string;
}
}Usage Examples
Rendering Blocks with Type Safety
The SDK's discriminated union means TypeScript narrows block.data automatically — no casts needed:
import type { PageBlock } from "@putiikkipalvelu/storefront-sdk";
function BlockRenderer({ block }: { block: PageBlock }) {
switch (block.type) {
case "markdown":
// block.data.content is `string` — TypeScript knows
return <div dangerouslySetInnerHTML={{ __html: block.data.content }} />;
case "accordion":
return (
<div>
{block.data.title && <h2>{block.data.title}</h2>}
{block.data.items.map((item) => (
<details key={item.id}>
<summary>{item.question}</summary>
<div dangerouslySetInnerHTML={{ __html: item.answer }} />
</details>
))}
</div>
);
case "hero":
return (
<section>
<h1>{block.data.title}</h1>
{block.data.subtitle && <p>{block.data.subtitle}</p>}
{block.data.ctaText && (
<a href={block.data.ctaLink}>{block.data.ctaText}</a>
)}
</section>
);
// ... handle other block types
}
}CMS Page with Caching
const page = await storefront.pages.getBySlug("about", {
next: { revalidate: 60, tags: ["pages"] },
});Using SEO Data for Metadata
import type { Metadata } from "next";
export async function generateMetadata({ params }): Promise<Metadata> {
const page = await storefront.pages.getBySlug(params.slug);
return {
title: page.title,
description: page.description,
openGraph: page.seo?.openGraphImageUrl
? { images: [{ url: page.seo.openGraphImageUrl, alt: page.seo.ogImageAlt ?? "" }] }
: undefined,
};
}Error Handling
import { NotFoundError } from "@putiikkipalvelu/storefront-sdk";
import { notFound } from "next/navigation";
try {
const page = await storefront.pages.getBySlug(slug);
return <PageRenderer page={page} />;
} catch (error) {
if (error instanceof NotFoundError) {
notFound();
}
throw error;
}Common Slugs
| Slug | Description | Protected |
|---|---|---|
homepage | Store landing page (homepage-only blocks allowed) | Yes |
about | About the store | No |
terms | Terms and conditions | Yes |
privacy | Privacy policy | Yes |
contact | Contact information | No |
gallery | Photo gallery | No |
Protected pages cannot be deleted from the dashboard but can be edited.