Examples
Complete queries for pages most sites have. They use the example schema and the Next.js App Router, and assume a pages root without i18n unless noted.
A page for every url
A catch-all route that renders whatever entry lives at the requested url, and prerenders all of them at build time:
import {cms} from '@/cms'
import {Query} from 'alinea'
import {notFound} from 'next/navigation'
interface PageProps {
params: Promise<{slug?: Array<string>}>
}
export async function generateStaticParams() {
const urls = await cms.find({
root: cms.workspaces.main.pages,
select: Query.url
})
return urls.map(url => ({slug: url.split('/').filter(Boolean)}))
}
export default async function Page({params}: PageProps) {
const {slug = []} = await params
const page = await cms.first({
root: cms.workspaces.main.pages,
url: `/${slug.join('/')}`,
select: {type: Query.type, title: Query.title}
})
if (!page) notFound()
return <h1>{page.title}</h1>
}Render a different component per page.type, each fetching the data it needs by id or url.
Blog overview with pagination
import {cms} from '@/cms'
import {BlogPost} from '@/schema'
import {Query} from 'alinea'
const perPage = 10
interface BlogPageProps {
searchParams: Promise<{page?: string}>
}
export default async function BlogPage({searchParams}: BlogPageProps) {
const page = Math.max(1, Number((await searchParams).page) || 1)
const [posts, total] = await Promise.all([
cms.find({
type: BlogPost,
select: {
title: Query.title,
url: Query.url,
date: BlogPost.publishDate,
intro: BlogPost.intro
},
orderBy: {desc: BlogPost.publishDate},
skip: (page - 1) * perPage,
take: perPage
}),
cms.count({type: BlogPost})
])
const pageCount = Math.ceil(total / perPage)
return (
<main>
{posts.map(post => (
<a key={post.url} href={post.url}>
<h2>{post.title}</h2>
<p>{post.intro}</p>
</a>
))}
<p>
Page {page} of {pageCount}
</p>
</main>
)
}Related posts
Posts that share at least one category with the current post, newest first, without the post itself:
import {cms} from '@/cms'
import {BlogPost} from '@/schema'
import {Query} from 'alinea'
export async function relatedPosts(postId: string) {
const post = await cms.get({
type: BlogPost,
id: postId,
select: {categories: BlogPost.categories}
})
const categoryIds = post.categories.map(category => category.entryId)
if (categoryIds.length === 0) return []
return cms.find({
type: BlogPost,
id: {isNot: postId},
filter: {categories: {includes: {_entry: {in: categoryIds}}}},
select: {title: Query.title, url: Query.url},
orderBy: {desc: BlogPost.publishDate},
take: 3
})
}To require all categories instead of any, combine one includes per category with and:
import {cms} from '@/cms'
import {BlogPost} from '@/schema'
export function postsInAllCategories(categoryIds: Array<string>) {
return cms.find({
type: BlogPost,
filter: {
and: categoryIds.map(id => ({categories: {includes: {_entry: id}}}))
}
})
}Filtering a listing from the url
Optional filters from search params, skipped when they're empty:
import {cms} from '@/cms'
import {BlogPost} from '@/schema'
import {Query} from 'alinea'
interface Filters {
category?: string
q?: string
}
export function filteredPosts({category, q}: Filters) {
return cms.find({
type: BlogPost,
search: q || undefined,
filter: {
categories: category ? {includes: {_entry: category}} : undefined
},
select: {title: Query.title, url: Query.url},
// Sort by date unless the visitor searched, then by relevance
orderBy: q ? undefined : {desc: BlogPost.publishDate}
})
}Redirect old urls
When an editor changes the path of a published document, or moves it, its previous url is kept in its metadata aliases. Look it up when nothing matches, and redirect:
import {cms} from '@/cms'
import {Query} from 'alinea'
import {notFound, permanentRedirect} from 'next/navigation'
export async function findPage(url: string) {
const page = await cms.first({url, select: {title: Query.title}})
if (page) return page
const moved = await cms.first({alias: url, select: Query.url})
if (moved) permanentRedirect(moved)
notFound()
}Sitemap
Every page with its translations, for app/sitemap.ts. This one uses a translated pages root:
import {cms} from '@/cms'
import {Query} from 'alinea'
import type {MetadataRoute} from 'next'
const baseUrl = 'https://example.com'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const pages = await cms.find({
root: cms.workspaces.main.pages,
locale: 'en',
select: {
url: Query.url,
updatedAt: Query.updatedAt,
translations: Query.translations({
includeSelf: true,
select: {locale: Query.locale, url: Query.url}
})
}
})
return pages.map(page => ({
url: baseUrl + page.url,
lastModified: page.updatedAt ? new Date(page.updatedAt * 1000) : undefined,
alternates: {
languages: Object.fromEntries(
page.translations.map(t => [t.locale, baseUrl + t.url])
)
}
}))
}Site settings
Settings that appear on every page, like the footer, fit in a seeded entry in their own root (see Roots). There's exactly one, so query it by type with get:
import {cms} from '@/cms'
import {Settings} from '@/schema'
export async function Footer() {
const settings = await cms.get({type: Settings})
return <footer>{settings.title}</footer>
}In a translated settings root, pass the locale too, otherwise get returns whichever translation comes first.