Putiikkipalvelu Docs
SDK

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:

ParamTypeRequiredDescription
slugstringYesPage slug (e.g. "about", "terms", "homepage")
optionsFetchOptionsNoFetch 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 }>;
  }
}

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

SlugDescriptionProtected
homepageStore landing page (homepage-only blocks allowed)Yes
aboutAbout the storeNo
termsTerms and conditionsYes
privacyPrivacy policyYes
contactContact informationNo
galleryPhoto galleryNo

Protected pages cannot be deleted from the dashboard but can be edited.

On this page