Collections

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.

Routing

Creating a 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.

Rendering a Page

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 />
    </>
  )
}

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:

import { PostsCollection } from '@/collections'

export function generateStaticParams() {
  return PostsCollection.getSources().map((Source) => ({
    path: Source.getPath(),
  }))
}

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:

export default async function Page() {
  return (
    <>
      <h1>All Posts</h1>
      <ul>
        {PostsCollection.getSources().map((Source) => (
          <Post key={Source.getPath()} Source={Source} />
        ))}
      </ul>
    </>
  )
}

Paginated Navigation

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>
    </>
  )
}

Tree Navigation

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>
}

Sibling Navigation

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>
  )
}

Blogs

blog/[slug]/page.tsx

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 />
    </>
  )
}

blog/layout.tsx

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>
  )
}

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.getNamedExport('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 { 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>
    </>
  )
}

components/[slug]/[example]/page.tsx

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>
  )
}

Custom TypeScript Configuration

packages/[slug]/page.tsx

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>
    </>
  )
}

post/[slug].tsx

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>
  )
}

API Reference

createCollection

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.

Parameters

options

CollectionOptions<AllExports> | undefined
Return
CollectionSource<AllExports>

isExportSource

(source: unknown) => source is ExportSource<any>
Parameters

source *

unknown
Return
boolean

isNamedExportSource

(source: unknown) => source is ExportSource<any>
Parameters

source *

unknown
Return
boolean

isDefaultExportSource

(source: unknown) => source is ExportSource<any>
Parameters

source *

unknown
Return
boolean

isFileSystemSource

(source: unknown) => source is FileSystemSource<any>
Parameters

source *

unknown
Return
boolean

isCollectionSource

(source: unknown) => source is CollectionSource<any>
Parameters

source *

unknown
Return
boolean

MDXContent

MDXContent

The type of the default export of an MDX module.

Parameters
MDXProps
Return
Element

FilePatterns

FilePatterns<Extension>

`${string}${Extension}`

`${string}${Extension}${string}`

BaseSource

BaseSource

Properties

getPath *

() => string

The full path to the source formatted to be URL-friendly, taking the collection baseDirectory and basePath configuration into account.

getPathSegments *

() => string[]

An array of path segments to the source excluding the collection basePath if configured.

getEditPath *

() => string

The path to the source on the local filesystem in development and the git repository in production if configured.

BaseSourceWithGetters

BaseSourceWithGetters<Exports>

Properties

getSource *

(path?: string | string[]) => FileSystemSource<Exports> | undefined

Retrieves a source in the immediate directory or sub-directory by its path.

path

string | Array<string> | undefined

getSources *

<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.

options

{ depth?: PositiveIntegerOrInfinity<Depth>; } | undefined

getPath *

() => string

The full path to the source formatted to be URL-friendly, taking the collection baseDirectory and basePath configuration into account.

getPathSegments *

() => string[]

An array of path segments to the source excluding the collection basePath if configured.

getEditPath *

() => string

The path to the source on the local filesystem in development and the git repository in production if configured.

ExportSource

ExportSource<Value>

Properties

getName *

() => string

The name of the exported source. If the default export name cannot be derived, the file name will be used.

getType *

() => Promise<ReturnType<typeof resolveType>>

The resolved type of the exported source based on the TypeScript type if it exists.

getTitle *

() => string

The name of the exported source formatted as a title.

getDescription *

() => string | undefined

The description of the exported source based on the JSDoc comment if it exists.

getTags *

() => { tagName: string; text?: string; }[] | undefined

The tags of the exported source based on the JSDoc comment if it exists.

getSlug *

() => string

The URL-friendly slug of the export name.

getText *

() => string

A text representation of the exported source if it is statically analyzable.

getValue *

() => 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.

getEnvironment *

() => "server" | "client" | "isomorphic" | "unknown"

The execution environment of the export source.

getPosition *

() => DeclarationPosition

The lines and columns where the export starts and ends.

getSiblings *

() => Promise<[previous?: ExportSource<Value>, next?: ExportSource<Value>]>

The previous and next export sources within the same file.

isMainExport *

() => boolean

Whether the export is considered the main export of the file based on the name matching the file name or directory name.

getPath *

() => string

The full path to the source formatted to be URL-friendly, taking the collection baseDirectory and basePath configuration into account.

getPathSegments *

() => string[]

An array of path segments to the source excluding the collection basePath if configured.

getEditPath *

() => string

The path to the source on the local filesystem in development and the git repository in production if configured.

FileSystemSource

FileSystemSource<Exports>

Properties

getName *

() => string

The base file name or directory name.

getTitle *

() => string

The file name formatted as a title.

getOrder *

() => string

Order of the source in the collection based on its position in the file system.

getDepth *

() => number

Depth of source starting from the collection.

getCreatedAt *

() => Promise<Date | undefined>

Date the source was first created.

getUpdatedAt *

() => Promise<Date | undefined>

Date the source was last updated.

getAuthors *

() => Promise<string[]>

Authors who have contributed to the source.

getSiblings *

(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.

options

{ depth?: number; } | undefined

getDefaultExport *

() => ExportSource<Exports["default"]>

The default export source.

getNamedExport *

<Name extends Exclude<keyof Exports, "default">>(name: Name) => ExportSource<Exports[Name]>

A single named export source of the file.

name *

string | number | symbol

getMainExport *

() => ExportSource<Exports[keyof Exports]> | undefined

The main export source of the file based on the file name or directory name.

getExports *

() => ExportSource<Exports[keyof Exports]>[]

All exported sources of the file.

isFile *

() => boolean

If the source is a file.

isDirectory *

() => boolean

If the source is a directory.

getSource *

(path?: string | string[]) => FileSystemSource<Exports> | undefined

Retrieves a source in the immediate directory or sub-directory by its path.

path

string | Array<string> | undefined

getSources *

<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.

options

{ depth?: PositiveIntegerOrInfinity<Depth> | undefined; } | undefined

getPath *

() => string

The full path to the source formatted to be URL-friendly, taking the collection baseDirectory and basePath configuration into account.

getPathSegments *

() => string[]

An array of path segments to the source excluding the collection basePath if configured.

getEditPath *

() => string

The path to the source on the local filesystem in development and the git repository in production if configured.

CollectionSource

CollectionSource<Exports>

Properties

getSource *

(path?: string | string[]) => FileSystemSource<Exports> | undefined

Retrieves a source in the immediate directory or sub-directory by its path.

path

string | Array<string> | undefined

getSources *

<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.

options

{ depth?: PositiveIntegerOrInfinity<Depth> | undefined; } | undefined

getPath *

() => string

The full path to the source formatted to be URL-friendly, taking the collection baseDirectory and basePath configuration into account.

CollectionOptions

CollectionOptions<Exports>

Properties

title

string | undefined

The title used for the collection when rendered for a page.

label

string | undefined

The label used for the collection when rendered as a navigation item. Defaults to the title.

baseDirectory

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.

basePath

string | undefined

The base pathname used when calculating navigation paths. This includes everything after the hostname (e.g. /docs in https://renoun.com/docs).

tsConfigFilePath

string | undefined

The path to the TypeScript config file.

filter

((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.

sort

((a: FileSystemSource<Exports>, b: FileSystemSource<Exports>) => Promise<number>) | undefined

A custom sort function for ordering file system sources.

schema

{ [Name in keyof Exports]?: ((value: Exports[Name]) => Exports[Name]) | undefined; } | undefined

Validate and transform exported values from source files.