Installation Guide
Learn how to install and set up postfolio in your Next.js project.What is Postfolio?
postfolio is a plug-and-play MDX toolkit for Next.js portfolios. It handles bundling, frontmatter extraction, Table of Contents generation, and external blog integration so you can focus on writing and design.
It is powered by mdx-bundler with esbuild under the hood, giving you fast builds and the ability to embed React components directly in your markdown.
Installation
Install the core package:
$npm install postfolio
$pnpm add postfolio
$yarn add postfolio
Peer Dependencies
postfolio uses mdx-bundler which requires esbuild. Install it alongside:
$npm install mdx-bundler esbuild
postfolio is currently optimized for Next.js with the App Router.
Project Structure
text
my-project/
├── content/
│ └── blogs/
│ ├── my-first-post.mdx
│ └── another-post.mdx
├── app/
│ ├── page.tsx
│ └── posts/
│ └── [slug]/
│ └── page.tsx
├── components/
│ └── mdx-components.tsx
└── package.json
MDX Frontmatter
Add frontmatter to the top of your .mdx files:
mdx
---
title: "My Post Title"
description: "A brief description of the post."
date: "2026-06-15"
tags: ["nextjs", "react"]
author: "Your Name"
draft: false
cover: https://example.com/cover.jpg
---
Your content here...
Layout Components
Create an MDX components file to customize how your content renders:
tsx
// components/mdx-components.tsx
"use client";
import { Typography, Surface, Separator, Table, Button } from "@heroui/react";
import { generateSlug } from "postfolio/client";
import { Copy } from "@phosphor-icons/react";
export const components = {
h1: ({ children }) => (
<Typography.Heading level={1} id={generateSlug(children)} className="mt-8 mb-4">
{children}
</Typography.Heading>
),
h2: ({ children }) => (
<Typography.Heading level={2} id={generateSlug(children)} className="mt-8 mb-4">
{children}
</Typography.Heading>
),
p: ({ children }) => (
<Typography.Paragraph size="base" className="leading-relaxed mb-4">
{children}
</Typography.Paragraph>
),
pre: ({ children }) => (
<Surface variant="secondary" className="my-6 rounded-xl border border-border p-4 overflow-x-auto">
<pre className="text-sm font-mono leading-relaxed">{children}</pre>
</Surface>
),
table: ({ children }) => (
<Table>
<Table.ScrollContainer>
<Table.Content aria-label="Table" className="min-w-full">
{children}
</Table.Content>
</Table.ScrollContainer>
</Table>
),
hr: () => <Separator className="my-8" />,
};
Usage
List All Posts
tsx
// app/page.tsx
import { allPosts } from "postfolio/server";
export default async function HomePage() {
const posts = await allPosts("content/blogs");
return (
<div>
{posts.map((post) => (
<a key={post.slug} href={`/posts/${post.slug}`}>
<h2>{post.frontmatter.title}</h2>
<p>{post.frontmatter.description}</p>
</a>
))}
</div>
);
}
Render a Post
tsx
// app/posts/[slug]/page.tsx
import { MDXPost, generateTOC } from "postfolio/server";
import { Content } from "postfolio/client";
import { components } from "@/components/mdx-components";
export default async function Post({ params }) {
const { slug } = await params;
const post = await MDXPost(slug, { contentDir: "content/blogs" });
if (!post) return null;
const toc = generateTOC(post.raw);
return (
<article>
<h1>{post.frontmatter.title}</h1>
<nav>
{toc.map((item) => (
<a key={item.slug} href={`#${item.slug}`}>
{item.text}
</a>
))}
</nav>
<Content components={components} code={post.code} />
</article>
);
}
Generate Static Params
tsx
import { Slugs } from "postfolio/server";
export async function generateStaticParams() {
const slugs = await Slugs("content/blogs");
return slugs.map((slug) => ({ slug }));
}
External Posts (Dev.to)
tsx
import { externalPosts } from "postfolio/server";
const posts = await externalPosts([
"https://dev.to/api/articles/username/post-slug-12345",
{
url: "https://dev.to/api/articles/username/another-post-67890",
extraFrontmatter: { featured: true },
},
]);
API Reference
Server Functions (postfolio/server)
| Function | Parameters | Returns | Description |
|---|---|---|---|
| allPosts | (contentDir: string) | Promise of BlogPostSource array | Get all local MDX posts. Excludes drafts. |
| externalPosts | (inputs: ExternalPostInput array) | Promise of BlogPostSource array | Fetch posts from Dev.to API. Excludes drafts. |
| MDXPost | (slug: string, options?) | Promise of bundled post or undefined | Get a single post with bundled MDX code. |
| Post | (slug: string, contentDir: string) | BlogPostSource or undefined | Get a single local post (no bundling). |
| Slugs | (options?: string or object) | Promise of string array | Get all slugs from local and/or external sources. |
| generateTOC | (content: string) | TOCItem array | Generate Table of Contents from markdown headings. |
| generateSlug | (text: string) | string | Convert text to a URL-friendly slug. |
MDXPost Options
| Option | Type | Description |
|---|---|---|
| contentDir | string | Directory containing local MDX files |
| externalBlogs | ExternalPostInput array | Array of external post URLs |
Slugs Options
| Option | Type | Description |
|---|---|---|
| contentDir | string | Directory containing local MDX files |
| externalBlogs | ExternalPostInput array | Array of external post URLs |
ExternalPostInput
| Type | Description |
|---|---|
| string | URL to Dev.to article API endpoint |
| object with url and optional extraFrontmatter | URL with additional frontmatter to merge |
Client (postfolio/client)
| Export | Description |
|---|---|
| Content | Render bundled MDX content with custom components |
| useActiveHeading | Hook to track which heading is currently in view |
| generateSlug | Convert text to a URL-friendly slug |
Types
| Type | Fields |
|---|---|
| BlogFrontmatter | title, description, date, tags, author, draft, cover, cover_image |
| BlogPostSource | slug, filename, filePath, mdx, frontmatter |
| TOCItem | level, text, slug |