# Menus, site-wide content and lists

> Read menus, site-wide entries such as a header or footer, index pages with pagination, filters and search, entries without an address such as FAQs, and references between entries, with the SDK.

Source: https://docs.bifrostorbit.com/cms/developers/menus-globals-and-lists/

Pages are only part of a website. This page covers the rest: menus, site-wide content, lists of entries and references between them. It uses the `cms` client from [Connect a website](https://docs.bifrostorbit.com/cms/developers/connect-a-website/). Every call here reads published content, and takes `preview: true` to read drafts while previewing.

## Menus

Editors build menus in **Menus**, each with a key such as `main` or `footer`.

```tsx
const menu = await cms.findNavigation({ key: 'main' })
```

| Method | When the menu does not exist |
| --- | --- |
| `getNavigation({ key })` | Throws `CmsNotFoundError` |
| `findNavigation({ key })` | Answers `null` |

Both take `region`, `locale`, `preview` and `signal`. A menu can be shared by every region or belong to one; ask with the region you are showing and the right one answers.

A menu has `key`, `name` and `items`. Each item has:

| Property | Holds |
| --- | --- |
| `label` | The text to show |
| `href` | Where it goes: a page's current address, any address, or `null` for an item that only groups others |
| `target` | `_blank` to open in a new tab, or `null` |
| `icon` | An icon name editors chose, or `null`. Map it to your own icons. |
| `badge` | A short label such as "New", or `null` |
| `description` | A line of text for larger menus, such as a mega menu, or `null` |
| `children` | Sub-items, in the same shape |

Items that link to an unpublished page are left out, and hidden items never arrive.

A menu with one level of sub-menus:

```tsx
// components/site-menu.tsx
import Link from 'next/link'
import type { CmsNavItem } from '@bifrost-orbit-cms/sdk'

function Item({ item }: { item: CmsNavItem }) {
  const label = (
    <>
      {item.label}
      {item.badge ? <span className="badge">{item.badge}</span> : null}
    </>
  )
  return (
    <li>
      {item.href ? <Link href={item.href} target={item.target ?? undefined}>{label}</Link> : <span>{label}</span>}
      {item.children.length > 0 ? (
        <ul>
          {item.children.map((child) => (
            <Item key={`${child.label}-${child.href}`} item={child} />
          ))}
        </ul>
      ) : null}
    </li>
  )
}

export function SiteMenu({ items }: { items: CmsNavItem[] }) {
  return (
    <nav aria-label="Main">
      <ul>
        {items.map((item) => (
          <Item key={`${item.label}-${item.href}`} item={item} />
        ))}
      </ul>
    </nav>
  )
}
```

To mark the current page, compare `item.href` with the address in a client component using `usePathname()` from `next/navigation`.

Mark the menu for the preview so editors can open it from there: `<header data-cms-edit="navigation:main" data-cms-edit-label="Menu">`. See [Set up the live preview](https://docs.bifrostorbit.com/cms/developers/preview/).

Changing a menu sends `navigation.changed`, which refreshes `cms:nav:<key>` and every page that shows it.

## Site-wide content

Content used on every page, such as the footer text, contact details, social links or the cookie notice, lives in global entries. Each has a key, such as `footer`.

```ts
const footer = await cms.findGlobal({ key: 'footer' })
```

| Method | When the entry does not exist |
| --- | --- |
| `getGlobal({ key })` | Throws `CmsNotFoundError` |
| `findGlobal({ key })` | Answers `null` |

Both take `region`, `locale`, `strict`, `preview`, `include` and `signal`. A global entry follows the language fallback, but not the region fallback.

A global has `key`, `name`, `contentType`, `locale` (the language that answered), `fields` and `components`. Type the fields with the generated type for its content type:

```ts
import type { FooterFields } from '@/cms/types'

const footer = await cms.findGlobal({ key: 'footer' })
const fields = footer?.fields as FooterFields | undefined
```

Draw its components with the same registry as pages:

```tsx
<CmsBlocks blocks={footer?.components} registry={registry} />
```

Read several at once so the layout waits for the slowest, not the sum:

```ts
const [header, footer, menu] = await Promise.all([
  cms.findGlobal({ key: 'header' }),
  cms.findGlobal({ key: 'footer' }),
  cms.findNavigation({ key: 'main' }),
])
```

`find…` answers `null` when the entry is missing, but still throws when the CMS cannot be reached. To keep the website up during an outage, catch per call: `cms.findGlobal({ key: 'footer' }).catch(() => null)`, or use `Promise.allSettled`.

Publishing any entry without an address refreshes `cms:global`, so every global is fetched fresh on the next visit.

## Lists and index pages

`list` reads entries of one content type, such as blog posts or products, for an index page:

```ts
const posts = await cms.list({ type: 'blogPost', sort: '-publishedAt', perPage: 12, page: 1 })
```

| Option | Default | Means |
| --- | --- | --- |
| `type` | | The content type key. Required. |
| `page` | `1` | Which page of results |
| `perPage` | `20` | Up to 100. Larger values are capped. |
| `sort` | `sortOrder` | `sortOrder` (the order editors set), `name`, `publishedAt`, `updatedAt`, each with `-` in front for the reverse |
| `filter` | | Field key to value, such as `{ activity: 'hiking' }`. Exact, as text; a list field matches when it contains the value. All must match. |
| `tag` | | Entries whose `tags` field contains this value |
| `category` | | Entries whose `category` field equals this value |
| `q` | | Entries whose name contains this text, ignoring case |
| `include` | | `['fields']` adds each entry's fields; `['references']` also expands references in them |
| `region`, `locale`, `preview`, `signal` | | As for pages. Lists never fall back to another language. |

The answer:

```ts
{
  items: [{ id, url, name, contentType, publishedAt, seo, fields? }],
  total: 42,     // matching entries across all pages
  page: 1,
  hasMore: true, // ask for page 2
}
```

To keep every entry of a type under one address, set **Address** on the content type in **Schema › Content types**, such as `/blog`. Every entry then lives at `/blog/<slug>`, whatever folder it is in, including new ones editors create. It can be set only when every entry already lives there, so no address changes, and removed only when the type has no entries. Make the index page the hub page of a folder with the same address, so editors find it beside the entries.

Items are summaries: no body. Link to each item's `url`, and render the full entry on its own page. An entry without an address has `url` and `seo` set to `null`. Global entries never appear in lists.

### A paginated index page

```tsx
// app/blog/page.tsx
import Link from 'next/link'
import { cms } from '@/lib/cms'

export const revalidate = 60

type Props = { searchParams: Promise<{ page?: string }> }

export default async function Blog({ searchParams }: Props) {
  const page = Math.max(1, Number((await searchParams).page) || 1)
  const posts = await cms.list({ type: 'blogPost', sort: '-publishedAt', perPage: 12, page })

  return (
    <main>
      <h1>Blog</h1>
      <ul>
        {posts.items.map((post) => (
          <li key={post.id}>{post.url ? <Link href={post.url}>{post.name}</Link> : post.name}</li>
        ))}
      </ul>
      <nav>
        {page > 1 ? <Link href={`/blog?page=${page - 1}`}>Newer</Link> : null}
        {posts.hasMore ? <Link href={`/blog?page=${page + 1}`}>Older</Link> : null}
      </nav>
    </main>
  )
}
```

A folder such as `app/blog` takes priority over the catch-all page route, so `/blog` is this index while `/blog/my-post` still comes from the CMS. Reading `searchParams` renders the page on each request, but the CMS answer is still cached under `cms:list`. To keep the index fully cached, put the page number in the address instead, such as `app/blog/page/[n]/page.tsx`.

If editors also build the index page itself in the CMS, with an intro and components, render it from the catch-all route and add the list as a component that fetches it; see "Components that need more data" in [Render components](https://docs.bifrostorbit.com/cms/developers/render-blocks/).

### Every entry, across pages

```ts
async function allEntries(type: string) {
  const items = []
  for (let page = 1; ; page += 1) {
    const result = await cms.list({ type, page, perPage: 100 })
    items.push(...result.items)
    if (!result.hasMore) return items
  }
}
```

### Entries without an address

Some entries are never pages, such as FAQ entries, team members or testimonials. Read them with their fields:

```ts
import type { FaqEntryFields } from '@/cms/types'

const faqs = await cms.list<FaqEntryFields>({ type: 'faqEntry', category: 'pricing', include: ['fields'] })
faqs.items.map((item) => item.fields?.question)
```

The type parameter types `fields`. Fields arrive as on a page: images as objects, links with current addresses, rich text as HTML.

### Filters and search

```ts
await cms.list({ type: 'product', filter: { activity: 'hiking', season: 'winter' } })
await cms.list({ type: 'blogPost', tag: 'hiking' })
await cms.list({ type: 'faqEntry', category: 'pricing', include: ['fields'] })
await cms.list({ type: 'product', q: 'boots' })
```

- Filter values are text: pass `'60'`, not `60`. A field key must start with a letter and contain only letters, digits and underscores.
- `q` matches entry names only. For full-text search across bodies, send the entries to a search service, such as Algolia or Meilisearch, from your webhook route when content is published.
- For a filter driven by visitors, read the choices from a taxonomy: `cms.getTaxonomy({ key: 'activity' })` answers its `terms`, each with `value`, `label` and `sortOrder`.

## References

Reference and References fields point at other entries, such as the author of a post or the questions in an FAQ block. By default they arrive as entry ids. Ask for the entries instead with `include`:

```ts
const result = await cms.resolve({ path, preview, include: ['references'] })
```

`resolve`, `getPage`, `getGlobal` and `list` take it. Every Reference field on the entry and in any of its components then holds the entry, with `id`, `url`, `name`, `contentType`, `publishedAt` and `fields`:

```tsx
export function FaqList({ block }: BlockProps<'faqList'>) {
  return (
    <section data-cms-id={block.id}>
      {block.questions?.map((entry) => (
        <details key={entry.id}>
          <summary>{entry.fields.question}</summary>
          <RichText value={entry.fields.answer} />
        </details>
      ))}
    </section>
  )
}
```

- **Type it:** generate types with `--include references`, so `block.questions` is typed as `CmsEntry<FaqEntryFields>[]` rather than `string[]`. Read every page with `include: ['references']` so the types are always true.
- **One level only:** references inside a referenced entry stay ids.
- **Unpublished entries** are left out of a References list, and a Reference becomes `null`. Previews show drafts of referenced entries too.
- **Language:** each entry is read in the visitor's language, with the same fallback as the page.
- **Refreshing:** answers with references are tagged `cms:references`, and every content change refreshes that tag, so a page showing an FAQ entry updates when the entry is published.
- **Live preview:** `CmsLivePage` asks the editor for references too when the page was read with them.

Without `include`, fetch referenced entries yourself with `list` and `filter`, or keep the ids for linking.

## Regions, products and other content

- Regions and their languages: `cms.getRegions()` and `cms.getLocales(region)`; see [Regions and languages](https://docs.bifrostorbit.com/cms/developers/regions-and-languages/).
- Banners, popups and countdowns: see [Render banners, popups and countdowns](https://docs.bifrostorbit.com/cms/developers/render-campaigns/).
- Products: `cms.getProducts({ page, perPage, filter })` answers products with prices and stock from the shop catalogue, in the same shape as lists.
- Anything else, in one request: [GraphQL](https://docs.bifrostorbit.com/cms/developers/graphql/) through `cms.graphql(query, { variables })`. GraphQL answers are never cached by the SDK; wrap them in your own cache if you use them on public pages.
