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 content bundling, frontmatter extraction, Table of Contents generation, and external blog integration from Dev.to — so you can focus on writing and design.
Local posts are compiled via mdx-bundler (supports React components in markdown). External posts are rendered via react-markdown with GFM support. Both use the same <Content /> component.
Installation
Install the package and its peer dependencies:
postfolio is 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...
| Field | Type | Description |
|---|---|---|
| title | string | Post title |
| description | string | Short description |
| date | string | Publication date |
| tags | string array | Post tags |
| author | string | Author name |
| draft | boolean | Set true to exclude from listings |
| cover | string | Cover image URL |
| cover_image | string | Alternative cover image field (used by Dev.to) |
MDX Components
Create a components file to customize how your content renders. This works for both local MDX and external markdown posts.
tsx
// components/mdx-components.tsx
"use client";
import { Typography, Surface, Separator, Button } from "@heroui/react";
import { generateSlug } from "postfolio/client";
import { CopyIcon } from "@phosphor-icons/react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
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 }) => {
const code = children?.props?.children || "";
const language = children?.props?.className?.replace("language-", "") || "text";
return (
<div className="my-6 overflow-hidden rounded-xl border border-border group relative">
<div className="flex items-center justify-between px-4 py-2 bg-default-50 border-b border-separator">
<Typography className="text-xs font-mono text-muted">{language}</Typography>
</div>
<SyntaxHighlighter language={language} style={oneDark} customStyle={{ margin: 0, padding: "1rem", borderRadius: 0, fontSize: "0.875rem" }}>
{code}
</SyntaxHighlighter>
</div>
);
},
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 (Local + External)
The Content component handles both MDX (local) and markdown (external) content through a single API.
tsx
// app/posts/[slug]/page.tsx
import { MDXPost, Slugs, generateTOC } from "postfolio/server";
import { Content } from "postfolio/renderer";
import { components } from "@/components/mdx-components";
const externalPostUrls = [
"https://dev.to/api/articles/username/post-slug-12345",
];
export async function generateStaticParams() {
const slugs = await Slugs({ contentDir: "content/blogs", externalBlogs: externalPostUrls });
return slugs.map((slug) => ({ slug }));
}
export default async function Page({ params }) {
const { slug } = await params;
const post = await MDXPost(slug, {
contentDir: "content/blogs",
externalBlogs: externalPostUrls,
});
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 code={post.code} markdown={post.markdown} components={components} />
</article>
);
}
External Posts (Dev.to)
Fetch posts from Dev.to and display them alongside your local content.
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 },
},
]);
External posts are rendered using react-markdown with GFM support (tables, strikethrough, task lists). Local posts use mdx-bundler for full MDX/JSX support. Both share the same <Content /> component and custom component overrides.
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 MDXPostResult or undefined | Get a single post with compiled content. |
| Post | (slug: string, contentDir: string) | BlogPostSource or undefined | Get a single local post (no compilation). |
| 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 |
MDXPostResult
| Field | Type | Description |
|---|---|---|
| slug | string | URL-friendly post identifier |
| code | string or undefined | Compiled MDX code (local posts only) |
| markdown | string or undefined | Raw markdown (external posts only) |
| frontmatter | BlogFrontmatter | Post metadata |
| raw | string | Original markdown/MDX source |
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 | url string + optional extraFrontmatter object |
Client Components (postfolio/renderer)
| Export | Description |
|---|---|
| Content | Render MDX or markdown content with custom components. Accepts code, markdown, and components props. |
Client Hooks (postfolio/client)
| Export | Description |
|---|---|
| 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 |
| ExternalPostInput | string or object with url and extraFrontmatter |
| TOCItem | level, text, slug |