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

$npm install postfolio mdx-bundler esbuild
$pnpm add postfolio mdx-bundler esbuild
$yarn add postfolio mdx-bundler esbuild

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...
FieldTypeDescription
titlestringPost title
descriptionstringShort description
datestringPublication date
tagsstring arrayPost tags
authorstringAuthor name
draftbooleanSet true to exclude from listings
coverstringCover image URL
cover_imagestringAlternative 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)

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 MDXPostResult or undefinedGet a single post with compiled content.
Post(slug: string, contentDir: string)BlogPostSource or undefinedGet a single local post (no compilation).
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

MDXPostResult

FieldTypeDescription
slugstringURL-friendly post identifier
codestring or undefinedCompiled MDX code (local posts only)
markdownstring or undefinedRaw markdown (external posts only)
frontmatterBlogFrontmatterPost metadata
rawstringOriginal markdown/MDX source

Slugs Options

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

ExternalPostInput

TypeDescription
stringURL to Dev.to article API endpoint
objecturl string + optional extraFrontmatter object

Client Components (postfolio/renderer)

ExportDescription
ContentRender MDX or markdown content with custom components. Accepts code, markdown, and components props.

Client Hooks (postfolio/client)

ExportDescription
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
ExternalPostInputstring or object with url and extraFrontmatter
TOCItemlevel, text, slug