QL
Abdul Quddus
2026-06-15
Installation Guide

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)

FunctionParametersReturnsDescription
allPosts(contentDir: string)Promise of BlogPostSource arrayGet all local MDX posts. Excludes drafts.
externalPosts(inputs: ExternalPostInput array)Promise of BlogPostSource arrayFetch posts from Dev.to API. Excludes drafts.
MDXPost(slug: string, options?)Promise of bundled post or undefinedGet a single post with bundled MDX code.
Post(slug: string, contentDir: string)BlogPostSource or undefinedGet a single local post (no bundling).
Slugs(options?: string or object)Promise of string arrayGet all slugs from local and/or external sources.
generateTOC(content: string)TOCItem arrayGenerate Table of Contents from markdown headings.
generateSlug(text: string)stringConvert text to a URL-friendly slug.

MDXPost Options

OptionTypeDescription
contentDirstringDirectory containing local MDX files
externalBlogsExternalPostInput arrayArray of external post URLs

Slugs Options

OptionTypeDescription
contentDirstringDirectory containing local MDX files
externalBlogsExternalPostInput arrayArray of external post URLs

ExternalPostInput

TypeDescription
stringURL to Dev.to article API endpoint
object with url and optional extraFrontmatterURL with additional frontmatter to merge

Client (postfolio/client)

ExportDescription
ContentRender bundled MDX content with custom components
useActiveHeadingHook to track which heading is currently in view
generateSlugConvert text to a URL-friendly slug

Types

TypeFields
BlogFrontmattertitle, description, date, tags, author, draft, cover, cover_image
BlogPostSourceslug, filename, filePath, mdx, frontmatter
TOCItemlevel, text, slug