> For the complete Lynkow documentation index in agent-friendly format, see [llms.txt](/llms.txt).

# Pages & Site Configuration

**Publié le** : 2026-08-14
**Catégorie** : Features

This guide covers how to fetch your site configuration, render global layout elements (header, footer), display pages with dynamic data, handle SEO metadata, and build navigation -- everything you need to turn Lynkow into the backbone of your Next.js site.

> **Reference** - This guide is a tutorial. For exhaustive, always-current signatures and field lists, see the auto-generated reference:
> 
> - SDK: [`PagesService`](/docs/sdk/services/sdk-pages-service)
> - SDK: [`SiteService`](/docs/sdk/services/sdk-site-service)
> - API: [Storefront: Pages](/docs/storefront-api/storefront-pages)

## Prerequisites

You should have the Lynkow SDK installed and a client initialized. If not, see [Guide 1: Installation](/docs/sdk/getting-started/quick-start).

```typescript
// lib/lynkow.ts
import { createClient } from 'lynkow'

export const lynkow = createClient({
  siteId: process.env.NEXT_PUBLIC_LYNKOW_SITE_ID!,
  fetchOptions: {
    next: { revalidate: 60 },
  },
})
```

---

## 1. Fetch site config for your layout

The `globals.siteConfig()` method returns everything you need to build a consistent layout: site metadata, the active locale, and all global blocks (header, footer, or any custom blocks you have defined in the admin).

```typescript
const { data } = await lynkow.globals.siteConfig()
```

### Response shape

The layout example below reads these fields:

```typescript
{
  data: {
    site: { name: string; favicon: string | null }
    locale: string                        // Active locale, e.g. "en"
    globals: Record<string, GlobalBlock>  // Each block exposes a `data` object
  }
}
```

See the full `SiteConfig` and `GlobalBlock` shapes in the [SDK Types reference](/docs/sdk/types).

The `data` field structure depends on how you have configured the block schema in the Lynkow admin. For example, a header block might have `navigation`, `logo`, and `cta` fields, while a footer block might have `columns`, `social`, and `copyright`.

---

## 2. Layout component

Use `globals.siteConfig()` in your root layout to fetch everything in a single request. This is a Server Component, so the data is fetched at build time (or revalidated on schedule with ISR).

```tsx
// app/layout.tsx
import { lynkow } from '@/lib/lynkow'
import { Header } from '@/components/header'
import { Footer } from '@/components/footer'

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const { data } = await lynkow.globals.siteConfig()

  const header = data.globals['header']
  const footer = data.globals['footer']

  return (
    <html lang={data.locale}>
      <head>
        {data.site.favicon && (
          <link rel="icon" href={data.site.favicon} />
        )}
      </head>
      <body>
        {header && <Header data={header.data} siteName={data.site.name} />}
        <main>{children}</main>
        {footer && <Footer data={footer.data} />}
      </body>
    </html>
  )
}
```

### Header component

```tsx
// components/header.tsx
import Link from 'next/link'
import Image from 'next/image'

interface HeaderData {
  logo?: { url: string; alt: string } | null
  navigation?: Array<{
    label: string
    url: string
    children?: Array<{ label: string; url: string }>
  }>
  cta?: { label: string; url: string; variant: 'primary' | 'secondary' } | null
}

export function Header({
  data,
  siteName,
}: {
  data: HeaderData
  siteName: string
}) {
  return (
    <header className="site-header">
      <Link href="/">
        {data.logo ? (
          <Image
            src={data.logo.url}
            alt={data.logo.alt || siteName}
            width={150}
            height={50}
            priority
          />
        ) : (
          <span>{siteName}</span>
        )}
      </Link>

      <nav>
        <ul>
          {data.navigation?.map((item, i) => (
            <li key={i}>
              <Link href={item.url}>{item.label}</Link>
              {item.children && item.children.length > 0 && (
                <ul className="submenu">
                  {item.children.map((child, j) => (
                    <li key={j}>
                      <Link href={child.url}>{child.label}</Link>
                    </li>
                  ))}
                </ul>
              )}
            </li>
          ))}
        </ul>
      </nav>

      {data.cta && (
        <Link href={data.cta.url} className={`btn btn-${data.cta.variant}`}>
          {data.cta.label}
        </Link>
      )}
    </header>
  )
}
```

### Footer component

```tsx
// components/footer.tsx
import Link from 'next/link'

interface FooterData {
  columns?: Array<{
    title: string
    links: Array<{ label: string; url: string }>
  }>
  social?: Array<{ platform: string; url: string }>
  copyright?: string
  legalLinks?: Array<{ label: string; url: string }>
}

export function Footer({ data }: { data: FooterData }) {
  return (
    <footer className="site-footer">
      {data.columns && data.columns.length > 0 && (
        <div className="footer-columns">
          {data.columns.map((column, i) => (
            <div key={i} className="footer-column">
              <h4>{column.title}</h4>
              <ul>
                {column.links.map((link, j) => (
                  <li key={j}>
                    <Link href={link.url}>{link.label}</Link>
                  </li>
                ))}
              </ul>
            </div>
          ))}
        </div>
      )}

      {data.social && data.social.length > 0 && (
        <div className="social-links">
          {data.social.map((s, i) => (
            <a key={i} href={s.url} target="_blank" rel="noopener noreferrer">
              {s.platform}
            </a>
          ))}
        </div>
      )}

      <div className="footer-bottom">
        {data.copyright && <p>{data.copyright}</p>}
        {data.legalLinks?.map((link, i) => (
          <Link key={i} href={link.url}>
            {link.label}
          </Link>
        ))}
      </div>
    </footer>
  )
}
```

---

## 3. Render a page

Create a catch-all route to render any Lynkow page by its URL path.

### Using `pages.getByPath()`

The recommended approach for dynamic routing. The SDK resolves the URL path to the correct page, including nested paths like `/services/consulting`.

```tsx
// app/[[...slug]]/page.tsx
import { lynkow } from '@/lib/lynkow'
import { notFound } from 'next/navigation'
import { isLynkowError } from 'lynkow'

export default async function Page({
  params,
}: {
  params: Promise<{ slug?: string[] }>
}) {
  const { slug } = await params
  const path = slug ? `/${slug.join('/')}` : '/'

  try {
    const page = await lynkow.pages.getByPath(path)

    return <PageRenderer slug={page.slug} data={page.data} />
  } catch (error) {
    if (isLynkowError(error) && error.code === 'NOT_FOUND') {
      notFound()
    }
    throw error
  }
}
```

### Using `pages.getBySlug()`

If you know the exact slug (not the URL path), you can fetch directly:

```typescript
const page = await lynkow.pages.getBySlug('homepage')
```

Both methods return the same `Page` object.

### The `Page` response

The renderer reads `slug` and `data`; the SEO and locale sections below read `seo` and `alternates`.

```typescript
{
  slug: string                    // "homepage"
  data: Record<string, unknown>   // Resolved page data (see section 4)
  seo: PageSeo | null             // Open Graph, Twitter, indexing (section 7)
  alternates: Array<{ locale: string; path: string; current: boolean }>
}
```

See the full `Page` and `PageSeo` shapes in the [SDK Types reference](/docs/sdk/types).

---

## 4. Handle DataSource data

The `page.data` field contains all the structured data defined in the page's schema, with DataSources already resolved by the API. This means if your page schema includes a "latest reviews" DataSource, the actual reviews are embedded directly in `page.data`.

```tsx
// components/page-renderer.tsx
import { Homepage } from './pages/homepage'
import { AboutPage } from './pages/about'
import { ContactPage } from './pages/contact'
import { GenericPage } from './pages/generic'

interface PageRendererProps {
  slug: string
  data: Record<string, unknown>
}

export function PageRenderer({ slug, data }: PageRendererProps) {
  switch (slug) {
    case 'homepage':
      return <Homepage data={data} />
    case 'about':
      return <AboutPage data={data} />
    case 'contact':
      return <ContactPage data={data} />
    default:
      return <GenericPage data={data} />
  }
}
```

Example: a homepage with a hero section, features list, and a DataSource for testimonials:

```tsx
// components/pages/homepage.tsx
import Image from 'next/image'
import Link from 'next/link'

interface HomepageData {
  hero: {
    title: string
    subtitle: string
    backgroundImage: { url: string; alt: string } | null
    cta: { label: string; url: string } | null
  }
  features: Array<{
    icon: string
    title: string
    description: string
  }>
  // DataSource: resolved automatically by the API
  testimonials: {
    items: Array<{
      id: string
      authorName: string
      rating: number
      content: string
    }>
    meta: { totalCount: number; hasMore: boolean }
  }
}

export function Homepage({ data }: { data: HomepageData }) {
  return (
    <>
      <section className="hero">
        {data.hero.backgroundImage && (
          <Image
            src={data.hero.backgroundImage.url}
            alt={data.hero.backgroundImage.alt}
            fill
            priority
          />
        )}
        <h1>{data.hero.title}</h1>
        <p>{data.hero.subtitle}</p>
        {data.hero.cta && (
          <Link href={data.hero.cta.url} className="btn btn-primary">
            {data.hero.cta.label}
          </Link>
        )}
      </section>

      {data.features?.length > 0 && (
        <section className="features">
          {data.features.map((feature, i) => (
            <div key={i} className="feature-card">
              <span className="feature-icon">{feature.icon}</span>
              <h3>{feature.title}</h3>
              <p>{feature.description}</p>
            </div>
          ))}
        </section>
      )}

      {data.testimonials?.items?.length > 0 && (
        <section className="testimonials">
          <h2>What our customers say</h2>
          {data.testimonials.items.map((review) => (
            <blockquote key={review.id}>
              <div className="stars">
                {'★'.repeat(review.rating)}
                {'☆'.repeat(5 - review.rating)}
              </div>
              <p>{review.content}</p>
              <cite>{review.authorName}</cite>
            </blockquote>
          ))}
        </section>
      )}
    </>
  )
}
```

> **Tip:** If a DataSource fails to resolve (e.g., the reviews service is temporarily down), the data will be `null` or empty and the page will include a warning in `page._warnings`. Always use optional chaining when accessing DataSource fields.

---

## 5. Navigation

Build your site navigation from `pages.list()`. This returns all published pages with their paths, which you can use to create menus, breadcrumbs, or sitemaps.

```tsx
// components/page-nav.tsx
import Link from 'next/link'
import { lynkow } from '@/lib/lynkow'

export async function PageNav() {
  const { data: pages } = await lynkow.pages.list()

  return (
    <nav aria-label="Pages">
      <ul>
        {pages.map((page) => (
          <li key={page.slug}>
            <Link href={page.path || `/${page.slug}`}>
              {page.name}
            </Link>
          </li>
        ))}
      </ul>
    </nav>
  )
}
```

### Static generation with `generateStaticParams`

Pre-generate all pages at build time for maximum performance:

```typescript
// app/[[...slug]]/page.tsx
export async function generateStaticParams() {
  const { data: pages } = await lynkow.pages.list()

  return pages.map((page) => ({
    slug: page.path === '/'
      ? undefined
      : page.path?.split('/').filter(Boolean),
  }))
}
```

---

## 6. Legal pages

Use the `tag` filter to fetch pages grouped by purpose. Pages tagged as `legal` in the Lynkow admin (privacy policy, terms of service, cookie policy, etc.) can be fetched separately:

```typescript
const { data: legalPages } = await lynkow.pages.list({ tag: 'legal' })
```

Display them in your footer:

```tsx
// components/legal-links.tsx
import Link from 'next/link'
import { lynkow } from '@/lib/lynkow'

export async function LegalLinks() {
  const { data: legalPages } = await lynkow.pages.list({ tag: 'legal' })

  if (legalPages.length === 0) return null

  return (
    <nav aria-label="Legal">
      <ul className="legal-links">
        {legalPages.map((page) => (
          <li key={page.slug}>
            <Link href={page.path || `/${page.slug}`}>
              {page.name}
            </Link>
          </li>
        ))}
      </ul>
    </nav>
  )
}
```

You can use the same pattern for any tag: `pages.list({ tag: 'resources' })`, `pages.list({ tag: 'product' })`, etc.

---

## 7. SEO meta tags

Use Next.js `generateMetadata` to set page-level SEO from `page.seo`. The SDK returns all Open Graph, Twitter Card, and indexing directives you need.

```typescript
// app/[[...slug]]/page.tsx
import type { Metadata } from 'next'
import { lynkow } from '@/lib/lynkow'
import { isLynkowError } from 'lynkow'

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug?: string[] }>
}): Promise<Metadata> {
  const { slug } = await params
  const path = slug ? `/${slug.join('/')}` : '/'

  try {
    const page = await lynkow.pages.getByPath(path)

    if (!page.seo) return {}

    return {
      title: page.seo.metaTitle,
      description: page.seo.metaDescription,

      openGraph: {
        title: page.seo.ogTitle || page.seo.metaTitle,
        description: page.seo.ogDescription || page.seo.metaDescription,
        images: page.seo.ogImage
          ? [{ url: page.seo.ogImage.url, alt: page.seo.ogImage.alt }]
          : [],
      },

      twitter: page.seo.twitterCard
        ? { card: page.seo.twitterCard as 'summary' | 'summary_large_image' }
        : undefined,

      alternates: {
        canonical: page.seo.canonicalUrl || undefined,
        languages: Object.fromEntries(
          page.alternates.map((alt) => [alt.locale, alt.path])
        ),
      },

      robots: page.seo.noIndex ? { index: false } : undefined,
    }
  } catch (error) {
    if (isLynkowError(error) && error.code === 'NOT_FOUND') {
      return {}
    }
    throw error
  }
}
```

---

## 8. JSON-LD structured data

The API generates Schema.org JSON-LD for each page (Organization, WebPage, BreadcrumbList, etc.). Inject it into the page head for search engine rich results.

### Fetch JSON-LD

```typescript
const jsonLd = await lynkow.pages.getJsonLd('homepage')
// Returns an array of schema objects, e.g.:
// [
//   { "@type": "Organization", "name": "...", "url": "..." },
//   { "@type": "WebPage", "name": "Home Page", "description": "..." },
//   { "@type": "BreadcrumbList", "itemListElement": [...] }
// ]
```

### JsonLd component

```tsx
// components/json-ld.tsx
export function JsonLd({ schemas }: { schemas: object[] }) {
  if (!schemas || schemas.length === 0) return null

  return (
    <>
      {schemas.map((schema, i) => (
        <script
          key={i}
          type="application/ld+json"
          dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
        />
      ))}
    </>
  )
}
```

### Use in the page component

```tsx
// app/[[...slug]]/page.tsx
import { JsonLd } from '@/components/json-ld'

export default async function Page({
  params,
}: {
  params: Promise<{ slug?: string[] }>
}) {
  const { slug } = await params
  const path = slug ? `/${slug.join('/')}` : '/'

  try {
    const page = await lynkow.pages.getByPath(path)
    const jsonLd = await lynkow.pages.getJsonLd(page.slug)

    return (
      <>
        <JsonLd schemas={jsonLd} />
        <PageRenderer slug={page.slug} data={page.data} />
      </>
    )
  } catch (error) {
    if (isLynkowError(error) && error.code === 'NOT_FOUND') {
      notFound()
    }
    throw error
  }
}
```

---

## 9. Locale switcher

Each page includes an `alternates` array listing all available translations with their paths. Use this to build a language switcher.

```tsx
// components/locale-switcher.tsx
import Link from 'next/link'

interface Alternate {
  locale: string
  path: string
  current: boolean
}

const LOCALE_LABELS: Record<string, string> = {
  en: 'English',
  fr: 'Francais',
  es: 'Espanol',
  de: 'Deutsch',
  pt: 'Portugues',
}

export function LocaleSwitcher({ alternates }: { alternates: Alternate[] }) {
  if (alternates.length <= 1) return null

  return (
    <nav aria-label="Language">
      <ul className="locale-switcher">
        {alternates.map((alt) => (
          <li key={alt.locale}>
            {alt.current ? (
              <span aria-current="page">
                {LOCALE_LABELS[alt.locale] || alt.locale}
              </span>
            ) : (
              <Link href={alt.path} hrefLang={alt.locale}>
                {LOCALE_LABELS[alt.locale] || alt.locale}
              </Link>
            )}
          </li>
        ))}
      </ul>
    </nav>
  )
}
```

Pass alternates from your page component:

```tsx
export default async function Page({ params }) {
  const { slug } = await params
  const path = slug ? `/${slug.join('/')}` : '/'
  const page = await lynkow.pages.getByPath(path)

  return (
    <>
      <LocaleSwitcher alternates={page.alternates} />
      <PageRenderer slug={page.slug} data={page.data} />
    </>
  )
}
```

---

## 10. 404 handling

When a page is not found, the SDK throws a `LynkowError` with code `NOT_FOUND`. Use the `isLynkowError` type guard to detect this and trigger Next.js's built-in 404 page.

```tsx
// app/[[...slug]]/page.tsx
import { lynkow } from '@/lib/lynkow'
import { notFound } from 'next/navigation'
import { isLynkowError } from 'lynkow'

export default async function Page({
  params,
}: {
  params: Promise<{ slug?: string[] }>
}) {
  const { slug } = await params
  const path = slug ? `/${slug.join('/')}` : '/'

  try {
    const page = await lynkow.pages.getByPath(path)
    return <PageRenderer slug={page.slug} data={page.data} />
  } catch (error) {
    if (isLynkowError(error) && error.code === 'NOT_FOUND') {
      notFound()
    }
    // Re-throw other errors (500, network issues, etc.)
    // so they are caught by the nearest error.tsx boundary
    throw error
  }
}
```

Create a custom 404 page:

```tsx
// app/not-found.tsx
import Link from 'next/link'

export default function NotFound() {
  return (
    <div className="not-found">
      <h1>404</h1>
      <p>The page you are looking for does not exist.</p>
      <Link href="/">Go back home</Link>
    </div>
  )
}
```

---

## Complete example: full page route

Here is the complete catch-all page route combining all the patterns from this guide:

```tsx
// app/[[...slug]]/page.tsx
import type { Metadata } from 'next'
import { lynkow } from '@/lib/lynkow'
import { notFound } from 'next/navigation'
import { isLynkowError } from 'lynkow'
import { JsonLd } from '@/components/json-ld'
import { LocaleSwitcher } from '@/components/locale-switcher'
import { PageRenderer } from '@/components/page-renderer'

// Static generation: pre-build all known pages
export async function generateStaticParams() {
  const { data: pages } = await lynkow.pages.list()

  return pages.map((page) => ({
    slug: page.path === '/'
      ? undefined
      : page.path?.split('/').filter(Boolean),
  }))
}

// SEO metadata
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug?: string[] }>
}): Promise<Metadata> {
  const { slug } = await params
  const path = slug ? `/${slug.join('/')}` : '/'

  try {
    const page = await lynkow.pages.getByPath(path)
    if (!page.seo) return {}

    return {
      title: page.seo.metaTitle,
      description: page.seo.metaDescription,
      openGraph: {
        title: page.seo.ogTitle || page.seo.metaTitle,
        description: page.seo.ogDescription || page.seo.metaDescription,
        images: page.seo.ogImage
          ? [{ url: page.seo.ogImage.url, alt: page.seo.ogImage.alt }]
          : [],
      },
      twitter: page.seo.twitterCard
        ? { card: page.seo.twitterCard as 'summary' | 'summary_large_image' }
        : undefined,
      alternates: {
        canonical: page.seo.canonicalUrl || undefined,
        languages: Object.fromEntries(
          page.alternates.map((alt) => [alt.locale, alt.path])
        ),
      },
      robots: page.seo.noIndex ? { index: false } : undefined,
    }
  } catch {
    return {}
  }
}

// Page component
export default async function Page({
  params,
}: {
  params: Promise<{ slug?: string[] }>
}) {
  const { slug } = await params
  const path = slug ? `/${slug.join('/')}` : '/'

  try {
    const page = await lynkow.pages.getByPath(path)
    const jsonLd = await lynkow.pages.getJsonLd(page.slug)

    return (
      <>
        <JsonLd schemas={jsonLd} />
        <LocaleSwitcher alternates={page.alternates} />
        <PageRenderer slug={page.slug} data={page.data} />
      </>
    )
  } catch (error) {
    if (isLynkowError(error) && error.code === 'NOT_FOUND') {
      notFound()
    }
    throw error
  }
}
```

---

## Cache behavior

| Method | SDK internal TTL |
| --- | --- |
| `globals.siteConfig()` | 10 minutes |
| `pages.list()` | 5 minutes |
| `pages.getBySlug()` | 5 minutes |
| `pages.getByPath()` | 5 minutes |
| `pages.getJsonLd()` | 5 minutes |


To manually clear the SDK's internal cache:

```typescript
lynkow.globals.clearCache()
lynkow.pages.clearCache()
```

> **Note:** These TTLs are for the SDK's internal cache. The Next.js data cache is controlled separately via `fetchOptions.next.revalidate` set when creating the client (see [Installation guide](/docs/sdk/getting-started/quick-start)).