title | date |
---|---|
Collections |
2024-07-10 |
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.
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>
)
}
import { APIReference, File, Tokens } from 'mdxts'
<File name="index.ts" value="const a = 1;">
<Tokens />
</File>
<File path="./Button.tsx">
<APIReference />
</File>
import { CodeBlock } from 'mdxts'
export function useMDXComponents() {
return {
pre: (props) => {
const { value, language } = CodeBlock.parsePreProps(props)
return <CodeBlock value={value} language={language} />
},
}
}
import { CodeBlock } from 'mdxts'
export function useMDXComponents() {
return {
pre: (props) => {
const { value, language } = CodeBlock.parsePreProps(props)
return (
<CodeBlock
value={value}
language={language}
tsConfigFilePath="tsconfig.json"
/>
)
},
}
}