Collections are a way to group and organize related files. They can be used to generate static pages, create navigations, and more. At their core, they abstract directories and files into a Source
, allowing you to analyze and render them programmatically.
Collection
A collection is created by calling createCollection
with a glob pattern and optional options:
import { createCollection, type SourceOf } from 'mdxts'
export const PostsCollection = createCollection<{
frontmatter: {
title: string
description: string
}
}>('@/posts/*.mdx', {
tsConfigFilePath: 'tsconfig.json',
})
export type PostSource = SourceOf<typeof Posts>
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.
import { PostsCollection, type PostSource } from '@/collections'
export default async function Page({ params }: { params: { slug: string } }) {
const Post = await PostsCollection.getSource(params.path)
if (!Post) notFound()
const frontmatter = await Post.getNamedExport('frontmatter').getValue()
const Content = await Post.getDefaultExport().getValue()
return (
<>
<h1>{frontmatter.title}</h1>
<p>{frontmatter.description}</p>
<Content />
</>
)
}
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:
import { PostsCollection } from '@/collections'
export function generateStaticParams() {
return PostsCollection.getSources().map((Source) => ({
path: Source.getPath(),
}))
}
To generate navigations we can use the getSources
and getSiblings
methods to loop through each Source
and generate a list or tree of links.
Use getSources
to render a list of the immediate sources in the collection:
export default async function Page() {
return (
<>
<h1>All Posts</h1>
<ul>
{PostsCollection.getSources().map((Source) => (
<Post key={Source.getPath()} Source={Source} />
))}
</ul>
</>
)
}
To paginate the sources, we can use the getSources
method to retrieve all sources, sort them, and paginate them:
const LIMIT = 10
export default async function Page({
searchParams,
}: {
searchParams: { page: string; order: 'asc' | 'desc' }
}) {
const page = parseInt(searchParams.page, 10) || 0
const allSources = await PostsCollection.getSources()
// Retrieve the frontmatter for sorting
const sourcesWithfrontmatter = await Promise.all(
allSources.map(async (source) => {
const frontmatter = await source.getNamedExport('frontmatter').getValue()
return { source, frontmatter }
})
)
// Sort the sources based on the order
sourcesWithfrontmatter.sort((a, b) => {
if (searchParams.order === 'asc') {
return new Date(a.frontmatter.date) - new Date(b.frontmatter.date)
}
return new Date(b.frontmatter.date) - new Date(a.frontmatter.date)
})
// Paginate the sources
const offset = page * LIMIT
const totalPages = Math.ceil(allSources.length / LIMIT)
const startIndex = offset
const endIndex = startIndex + LIMIT
const paginatedSources = sourcesWithfrontmatter.slice(startIndex, endIndex)
return (
<>
<h1>Posts</h1>
<nav>
<ul>
{paginatedSources.map(({ source }) => (
<Post key={source.getPath()} Source={source} />
))}
</ul>
</nav>
<nav>
<ul>
{Array.from(Array(totalPages).keys()).map((index) => (
<li key={index}>
<Link href={`/posts/page/${index}`}>{index + 1}</Link>
</li>
))}
</ul>
</nav>
</>
)
}
Similar to list navigation, we can use getSources
recursively to render a tree of links:
import { PostsCollection } from '@/collections'
export default async function Layout() {
return (
<nav>
<ul>
<TreeNavigation Source={PostsCollection} />
</ul>
</nav>
)
}
async function TreeNavigation({ Source }: { Source: PostSource }) {
const Sources = Source.getSources()
const path = Source.getPath()
const depth = Source.getDepth()
const frontmatter = await Source.getNamedExport('frontmatter').getValue()
if (Sources.length === 0) {
return (
<li style={{ paddingLeft: `${depth}rem` }}>
<Link href={path} style={{ color: 'white' }}>
{frontmatter.title}
</Link>
</li>
)
}
const childrenSources = Sources.map((ChildSource) => (
<TreeNavigation key={ChildSource.getPath()} Source={ChildSource} />
))
if (depth > 0) {
return (
<li style={{ paddingLeft: `${depth}rem` }}>
<Link href={path} style={{ color: 'white' }}>
{frontmatter.title}
</Link>
<ul>{childrenSources}</ul>
</li>
)
}
return <ul>{childrenSources}</ul>
}
export default async function Page({ params }) {
const PostFile = Posts.getSource(params.slug)
if (!PostFile) notFound()
const Post = await PostFile.getDefaultExport().getValue()
const frontmatter = await PostFile.getNamedExport('frontmatter').getValue()
const [Previous, Next] = PostFile.getSiblings()
return (
<>
<h1>{frontmatter.title}</h1>
<p>{frontmatter.description}</p>
<Post />
{Previous ? <Sibling Source={Previous} direction="previous" /> : null}
{Next ? <Sibling Source={Next} direction="next" /> : null}
</>
)
}
function Sibling({
Source,
direction,
}: {
Source: ReturnType<typeof Posts.getSource>
direction: 'previous' | 'next'
}) {
const frontmatter = await Source.getNamedExport('frontmatter').getValue()
return (
<a href={Source.getPath()}>
<span>{direction === 'previous' ? 'Previous' : 'Next'}</span>
{frontmatter.title}
</a>
)
}
import type { MDXContent, SourceOf } from 'mdxts'
import { createCollection } from 'mdxts'
import { getSiteMetadata } from '@/utils'
export const Posts = createCollection<{
default: MDXContent
frontmatter: {
title: string
description: string
}
}>('@/posts/*.mdx', {
tsConfigFilePath: 'tsconfig.json',
})
export type PostSource = SourceOf<typeof Posts>
export function generateStaticParams() {
return Posts.getSources().map((Source) => ({
slug: Source.getPath(),
}))
}
export async function generateMetadata({ params }) {
const Post = Posts.getSource(params.slug)
const frontmatter = await Post.getNamedExport('frontmatter').getValue()
return getSiteMetadata({
title: `${frontmatter.title} - MDXTS`,
description: frontmatter.description,
})
}
export default async function Page({ params }) {
const Post = await Posts.getSource(params.slug)
if (!Post) notFound()
const Content = await Post.getDefaultExport().getValue()
const frontmatter = await Post.getNamedExport('frontmatter').getValue()
return (
<>
<h1>{frontmatter.title}</h1>
<p>{frontmatter.description}</p>
<Content />
</>
)
}
import { Posts, type PostSource } from './[slug]/page'
function Navigation({ Source }: { Source: PostSource }) {
const Sources = 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>
)
}
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.getNamedExport('frontmatter').getValue()
return (
<li>
<a href={Source.getPath()}>
<h2>{frontmatter.title}</h2>
<p>{frontmatter.description}</p>
</a>
</li>
)
}
import { createCollection } from 'mdxts'
export const Components = createCollection('@/components/**/index.{ts,tsx}')
export const ComponentsMDX = createCollection('@/components/**/README.mdx')
export function generateStaticParams() {
return Components.getSources().map((Component) => ({
slug: Component.getPath(),
}))
}
export default async function Page({ params }) {
const Component = Components.getSource(params.slug)
const ComponentMDX = ComponentsMDX.getSource(params.slug)
if (!Component && !ComponentMDX) notFound()
const Content = await ComponentMDX.getDefaultExport().getValue()
return (
<>
<h1>{ComponentFile.getLabel()}</h1>
<Content />
<Component>
<APIReference />
</Component>
</>
)
}
import { createCollection, Tokens } from 'mdxts'
export const ComponentExamples = createCollection<
Record<string, React.ComponentType>
>('@/components/**/*.examples.tsx', {
resolveBasePathname: (pathname) => pathname.replace('.examples', ''),
})
export function generateStaticParams() {
return ComponentExamples.getSources().map((Component) => {
const componentPath = Component.getPath()
return Component.getNamedExports().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.getNamedExport(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>
)
}
import { APIReference, createCollection, type CollectionOptions } from '#mdxts'
const sharedOptions = {
tsConfigFilePath: '../packages/mdxts/tsconfig.json',
} satisfies CollectionOptions
export const Packages = createCollection('src/**/index.{ts,tsx}', sharedOptions)
export const PackagesMDX = createCollection('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.getDefaultExport().getValue()
return (
<>
<h1>{PackageFile.getLabel()}</h1>
<PackageDocs />
<PackageFile>
<APIReference />
</PackageFile>
</>
)
}
import { createCollection } from 'mdxts'
const Posts = createCollection('@/posts/*.mdx')
export default async function Page({ params }) {
const PostFile = Posts.getSource(params.slug)
if (!PostFile) notFound()
const frontmatter = await PostFile.getNamedExport('frontmatter').getValue()
const Post = await PostFile.getDefaultExport().getValue()
const [Previous, Next] = PostFile.getSiblings()
return (
<>
<h1>{frontmatter.title}</h1>
<p>{frontmatter.description}</p>
<Post />
{Previous ? <Sibling Source={Previous} direction="previous" /> : null}
{Next ? <Sibling Source={Next} direction="next" /> : null}
</>
)
}
function Sibling({
Source,
direction,
}: {
Source: ReturnType<typeof Posts.getSource>
direction: 'previous' | 'next'
}) {
const frontmatter = await Source.getNamedExport('frontmatter').getValue()
return (
<a href={Source.getPath()}>
<span>{direction === 'previous' ? 'Previous' : 'Next'}</span>
{frontmatter.title}
</a>
)
}
typeof createCollection
Creates a collection of sources based on a specified file pattern.
Note, an import getter for each file extension will be generated at the root of the project in a .renoun/collections.ts
file.
CollectionOptions<AllExports> | undefined
(source: unknown) => source is ExportSource<any>
unknown
(source: unknown) => source is ExportSource<any>
unknown
(source: unknown) => source is ExportSource<any>
unknown
(source: unknown) => source is FileSystemSource<any>
unknown
(source: unknown) => source is CollectionSource<any>
unknown
MDXContent
The type of the default export of an MDX module.
MDXProps
FilePatterns<Extension>
`${string}${Extension}`
`${string}${Extension}${string}`
BaseSource
() => string
The full path to the source formatted to be URL-friendly, taking the
collection baseDirectory
and basePath
configuration into account.
() => string[]
An array of path segments to the source excluding the collection basePath
if configured.
() => string
The path to the source on the local filesystem in development and the git repository in production if configured.
BaseSourceWithGetters<Exports>
(path?: string | string[]) => FileSystemSource<Exports> | undefined
Retrieves a source in the immediate directory or sub-directory by its path.
string | Array<string> | undefined
<Depth extends number>(options?: { depth?: PositiveIntegerOrInfinity<Depth>; }) => Promise<FileSystemSource<Exports>[]>
Retrieves sources in the immediate directory and possibly sub-directories based on the provided depth
.
Defaults to a depth of Infinity
which will return all sources.
{ depth?: PositiveIntegerOrInfinity<Depth>; } | undefined
() => string
The full path to the source formatted to be URL-friendly, taking the
collection baseDirectory
and basePath
configuration into account.
() => string[]
An array of path segments to the source excluding the collection basePath
if configured.
() => string
The path to the source on the local filesystem in development and the git repository in production if configured.
ExportSource<Value>
() => string
The name of the exported source. If the default export name cannot be derived, the file name will be used.
() => Promise<ReturnType<typeof resolveType>>
The resolved type of the exported source based on the TypeScript type if it exists.
() => string
The name of the exported source formatted as a title.
() => string | undefined
The description of the exported source based on the JSDoc comment if it exists.
() => { tagName: string; text?: string; }[] | undefined
The tags of the exported source based on the JSDoc comment if it exists.
() => string
The URL-friendly slug of the export name.
() => string
A text representation of the exported source if it is statically analyzable.
() => Promise<Value>
The runtime value of the export loaded from a dynamic import map generated in the .renoun
directory at the root of the project.
Note, any side-effects in modules of targeted files will be run.
() => "server" | "client" | "isomorphic" | "unknown"
The execution environment of the export source.
() => DeclarationPosition
The lines and columns where the export starts and ends.
() => Promise<[previous?: ExportSource<Value>, next?: ExportSource<Value>]>
The previous and next export sources within the same file.
() => boolean
Whether the export is considered the main export of the file based on the name matching the file name or directory name.
() => string
The full path to the source formatted to be URL-friendly, taking the
collection baseDirectory
and basePath
configuration into account.
() => string[]
An array of path segments to the source excluding the collection basePath
if configured.
() => string
The path to the source on the local filesystem in development and the git repository in production if configured.
FileSystemSource<Exports>
() => string
The base file name or directory name.
() => string
The file name formatted as a title.
() => string
Order of the source in the collection based on its position in the file system.
() => number
Depth of source starting from the collection.
() => Promise<Date | undefined>
Date the source was first created.
() => Promise<Date | undefined>
Date the source was last updated.
() => Promise<string[]>
Authors who have contributed to the source.
(options?: { depth?: number; }) => Promise<[previous?: FileSystemSource<Exports>, next?: FileSystemSource<Exports>]>
The previous and next sources in the collection if they exist. Defaults to a depth of Infinity
which considers all descendants.
{ depth?: number; } | undefined
() => ExportSource<Exports["default"]>
The default export source.
<Name extends Exclude<keyof Exports, "default">>(name: Name) => ExportSource<Exports[Name]>
A single named export source of the file.
string | number | symbol
() => ExportSource<Exports[keyof Exports]> | undefined
The main export source of the file based on the file name or directory name.
() => ExportSource<Exports[keyof Exports]>[]
All exported sources of the file.
() => boolean
If the source is a file.
() => boolean
If the source is a directory.
(path?: string | string[]) => FileSystemSource<Exports> | undefined
Retrieves a source in the immediate directory or sub-directory by its path.
string | Array<string> | undefined
<Depth extends number>(options?: { depth?: PositiveIntegerOrInfinity<Depth> | undefined; } | undefined) => Promise<FileSystemSource<Exports>[]>
Retrieves sources in the immediate directory and possibly sub-directories based on the provided depth
.
Defaults to a depth of Infinity
which will return all sources.
{ depth?: PositiveIntegerOrInfinity<Depth> | undefined; } | undefined
() => string
The full path to the source formatted to be URL-friendly, taking the
collection baseDirectory
and basePath
configuration into account.
() => string[]
An array of path segments to the source excluding the collection basePath
if configured.
() => string
The path to the source on the local filesystem in development and the git repository in production if configured.
CollectionSource<Exports>
(path?: string | string[]) => FileSystemSource<Exports> | undefined
Retrieves a source in the immediate directory or sub-directory by its path.
string | Array<string> | undefined
<Depth extends number>(options?: { depth?: PositiveIntegerOrInfinity<Depth> | undefined; } | undefined) => Promise<FileSystemSource<Exports>[]>
Retrieves sources in the immediate directory and possibly sub-directories based on the provided depth
.
Defaults to a depth of Infinity
which will return all sources.
{ depth?: PositiveIntegerOrInfinity<Depth> | undefined; } | undefined
() => string
The full path to the source formatted to be URL-friendly, taking the
collection baseDirectory
and basePath
configuration into account.
CollectionOptions<Exports>
string | undefined
The title used for the collection when rendered for a page.
string | undefined
The label used for the collection when rendered as a navigation item. Defaults to the title.
string | undefined
The base directory used when calculating source paths. This is useful in monorepos where source files can be located outside of the workspace.
string | undefined
The base pathname used when calculating navigation paths. This includes everything after
the hostname (e.g. /docs
in https://renoun.com/docs
).
string | undefined
The path to the TypeScript config file.
((source: FileSystemSource<Exports> | ExportSource<any>) => boolean) | undefined
A filter function to only include specific file system sources. If tsConfigFilePath
is defined,
all files matching paths in ignore
will always be filtered out.
((a: FileSystemSource<Exports>, b: FileSystemSource<Exports>) => Promise<number>) | undefined
A custom sort function for ordering file system sources.
{ [Name in keyof Exports]?: ((value: Exports[Name]) => Exports[Name]) | undefined; } | undefined
Validate and transform exported values from source files.