Collection Recipes
Learn how to query information from files, directories, and module exports in various contexts.
In this guide, we’ll explore the power and flexibility of collections, demonstrating how to query information from files, directories, and module exports in various contexts.
By organizing content into structured collections, we can easily generate static pages and manage complex routing and navigations. The following will walk through the process of setting up collections, rendering pages, and building various navigations like lists, paginated views, and hierarchical trees.
Routing
Creating a Collection
A collection is created by calling the collection
function with a glob pattern and optional options:
This will create a collection of files and directories normalized as a Source
that can be used to generate static pages, render pages, and more.
Rendering a Page
Generating Static Params
To loop through each Source
use the getSources
method and return an object with the path
as the key. This will be used to generate static multi-dimensional routes as it returns an array of path segments:
Navigation
Generating Navigations
To generate navigations we can use the getSources
and getSiblings
methods to loop through each Source
and generate a list or tree of links.
List Navigation
Use getSources
to render a list of the immediate sources in the collection:
Paginated Navigation
To paginate the sources, we can use the getSources
method to retrieve all sources, sort them, and paginate them:
Tree Navigation
Similar to list navigation, we can use getSources
recursively to render a tree of links:
Sibling Navigation
Metadata
Generating Metadata
To generate metadata for file, export a metadata
constant that returns an object with the any metadata properties you want to include:
Using Metadata
To use the metadata, we can get the value of the metadata
constant from the file and use it to set the page title and description:
Generating Metadata for Collections
To generate metadata for collections, we can use the root directory in the targeted file pattern to set the metadata:
Using Metadata for Collections
Similar to above, we can get the value of the metadata
constant for a collection and use it to set the page title and description:
This works by finding the index file related to the root directory we’re targeting in the component’s collection and extracting the metadata from it.
Blogs
blog/[slug]/page.tsx
import type { MDXContent, SourceOf } from 'renoun'
import { Collection } from 'renoun/collections'
import { notFound } from 'next/navigation'
import { getSiteMetadata } from '@/utils'
export const Posts = new Collection<{
default: MDXContent
frontmatter: {
title: string
description: string
}
}>({ filePattern: '@/posts/*.mdx' })
export type PostSource = SourceOf<typeof Posts>
export function generateStaticParams() {
return Posts.getSources().map((source) => ({
slug: source.getPath(),
}))
}
export async function generateMetadata({ params }) {
const source = await Posts.getSource((await params).slug)
if (!source) notFound()
const frontmatter = await source.getExport('frontmatter').getValue()
return getSiteMetadata({
title: `${frontmatter.title} - MDXTS`,
description: frontmatter.description,
})
}
export default async function Page({ params }) {
const source = await Posts.getSource((await params).slug)
if (!source) notFound()
const frontmatter = await source.getExport('frontmatter').getValue()
const Content = await source.getExport('default').getValue()
return (
<>
<h1>{frontmatter.title}</h1>
<p>{frontmatter.description}</p>
<Content />
</>
)
}
blog/layout.tsx
import { Posts, type PostSource } from './[slug]/page'
function Navigation({ source }: { source: PostSource }) {
const sources = await source.getSources()
if (sources.length === 0) return null
return (
<ul>
{sources.map((sourceItem) => (
<li key={sourceItem.getPath()}>
{sourceItem.getPath()}
<Navigation source={sourceItem} />
</li>
))}
</ul>
)
}
export default function BlogLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div>
<aside>
<h2>Posts</h2>
<Navigation source={Posts} />
</aside>
<main>{children}</main>
</div>
)
}
blog/page.tsx
import { Posts, type PostSource } from './[slug]/page'
export default async function BlogPage() {
return (
<>
<h1>All Posts</h1>
<ul>
{Posts.getSources().map((source) => (
<BlogPost key={source.getPath()} source={source} />
))}
</ul>
</>
)
}
async function BlogPost({ source }: { source: PostSource }) {
const frontmatter = await source.getExport('frontmatter').getValue()
return (
<li>
<a href={source.getPath()}>
<h2>{frontmatter.title}</h2>
<p>{frontmatter.description}</p>
</a>
</li>
)
}
API Documentation
components/[slug]/page.tsx
import { collection } from 'renoun'
export const Components = collection('@/components/**/index.{ts,tsx}')
export const ComponentsMDX = collection('@/components/**/README.mdx')
export function generateStaticParams() {
return Components.getSources().map((Component) => ({
slug: Component.getPath(),
}))
}
export default async function Page({ params }) {
const Component = Components.getSource((await params).slug)
const ComponentMDX = ComponentsMDX.getSource((await params).slug)
if (!Component && !ComponentMDX) notFound()
const Content = await ComponentMDX.getExport('default').getValue()
return (
<>
<h1>{ComponentFile.getLabel()}</h1>
<Content />
<Component>
<APIReference />
</Component>
</>
)
}
components/[slug]/[example]/page.tsx
import { Collection } from 'renoun/collections'
import { Tokens } from 'renoun/components'
import { notFound } from 'next/navigation'
export const ComponentExamples = new Collection<
Record<string, React.ComponentType>
>({
filePattern: '@/components/**/*.examples.tsx',
})
export function generateStaticParams() {
return ComponentExamples.getSources().map((Component) => {
const componentPath = Component.getPath()
return Component.getExports().map(([exportName]) => ({
component: componentPath,
example: exportName,
}))
})
}
export default async function Page({
params,
}: {
params: { component: string; example: string }
}) {
const ExampleSource = ComponentExamples.getSource(params.component)
if (!ExampleSource) notFound()
const ExportedSource = ExampleSource.getExport(params.example)
if (!ExportedSource) notFound()
const name = ExportedSource.getName()
const Example = await ExportedSource.getValue()
return (
<div>
{name}
<ExampleSource>
{/* show all examples and highlight the focused example */}
<Tokens
focus={[[ExportedSource.getStart(), ExportedSource.getEnd()]]}
/>
{/* alternatively, pass the source */}
<Tokens focus={[ExportedSource]} />
</ExampleSource>
<ExportedSource>
{/* display highlighted example source */}
<Tokens />
</ExportedSource>
{/* render the example */}
<Example />
</div>
)
}
TypeScript Configuration
packages/[slug]/page.tsx
import { APIReference, collection, type CollectionOptions } from 'renoun'
import { notFound } from 'next/navigation'
const sharedOptions = {
tsConfigFilePath: '../packages/mdxts/tsconfig.json',
} satisfies CollectionOptions
export const Packages = collection('src/**/index.{ts,tsx}', sharedOptions)
export const PackagesMDX = collection('src/**/README.mdx', sharedOptions)
export function generateStaticParams() {
return Packages.getSources().map((file) => ({
component: file.getPath(),
}))
}
export default async function Page({ params }) {
const [PackageFile, PackageMDXFile] = await Promise.all([
Packages.getSource(params.component),
PackagesMDX.getSource(params.component),
])
if (!PackageFile && !PackageMDXFile) notFound()
const PackageDocs = await PackageMDXFile.getExport('default').getValue()
return (
<>
<h1>{PackageFile.getLabel()}</h1>
<PackageDocs />
<PackageFile>
<APIReference />
</PackageFile>
</>
)
}