What features would you want on your blog? Here are the ones I chose:
Features
- [x] Static site that can be published to GitHub Pages
- [x] MDX support: I work on frontend code, so I need to embed React components in my posts
- [x] Search
- [x] Table of contents (Clerk style)
- [x] A Blog Post
- [x] List of Posts with Pagination
- [x] Categories
- [x] Series
- [x] Metadata with Social Images
- [x] Static (yes, repeating for emphasis)
Setup
The official FumaDocs documentation has an article on setting up a blog: Setup a Blog. Using that as a foundation, I built my blog with the features above.
You can proceed in two ways:
- Clone the current repo
- Follow the steps below to add these features to your existing Next.js site
Install and Configure Fuma Docs
Install Fuma Docs
You need a working Fuma Docs setup. If you don't have one, create it using the following command:
pnpm create fumadocs-app
Or you can configure FumaDocs manually.
At this point, you should have a basic setup ready with the files: source.config.ts
and lib/source.ts
.
Setup ShadCN Components
If you already have shadcn set up, you don't need to run the following command.
pnpm dlx shadcn@latest init
We need the following components:
- button
- popover
- badge
- card
You can add all of the above with:
pnpm dlx shadcn@latest add button popover badge card
We also need a book icon component, which is used when displaying series:
pnpm dlx shadcn@latest add "https://21st.dev/r/designali-in/book"
Define a Collection for Blog
Install zod
as we will use it to add a frontmatter schema.
zod
Now define a collection for the blog. Add the following:
import {
defineDocs,
defineConfig,
defineCollections,
frontmatterSchema,
} from "fumadocs-mdx/config";
import { z } from "zod";
export const blog = defineCollections({
type: "doc",
dir: "content/blog",
schema: frontmatterSchema.extend({
author: z.string(),
date: z
.string()
.or(z.date())
.transform((value, context) => {
try {
return new Date(value);
} catch {
context.addIssue({
code: z.ZodIssueCode.custom,
message: "Invalid date",
});
return z.NEVER;
}
}),
tags: z.array(z.string()).optional(),
image: z.string().optional(),
draft: z.boolean().optional().default(false),
series: z.string().optional(),
seriesPart: z.number().optional(),
}),
});
Add the following to lib/source.ts
:
import { docs, blog } from "@/.source";
import { loader } from "fumadocs-core/source";
import { createMDXSource } from "fumadocs-mdx";
export const blogSource = loader({
baseUrl: "/blog",
source: createMDXSource(blog),
});
export const {
getPage: getBlogPost,
getPages: getBlogPosts,
pageTree: pageBlogTree,
} = blogSource;
export type BlogPost = ReturnType<typeof getBlogPost>;
Add fumadocs-blog components
There are multiple features, so instead of adding files one by one, we'll copy entire folders using the giget command.
Add all the required components
npx giget gh:rjvim/rjvim.github.io/packages/fumadocs-blog/src fumadocs-blog --force
Add /blog route page
npx giget gh:rjvim/rjvim.github.io/apps/web/app/\(home\)/blog app/\(home\)/blog --force
Add /blog-og route page
npx giget gh:rjvim/rjvim.github.io/apps/web/app/blog-og app/blog-og --force
Correct the imports
The original components are built to work with a monorepo, so we need to correct the imports.
Run the following commands:
sed -i '' 's|@repo/fumadocs-blog/blog|@/fumadocs-blog|g' 'app/(home)/blog/[[...slug]]/page.tsx'
sed -i '' 's|@repo/fumadocs-blog/blog|@/fumadocs-blog|g' 'app/blog-og/[[...slug]]/route.tsx'
Add styles to global.css
```css title="global.css"
@import "../fumadocs-blog/styles/globals.css";
### Configure the blog
If fumadocs-blog was an npm package, all the above steps would be handled by that. Now, we need to configure the blog. Blog configuration drives following:
- Metadata
- Components
- Categories
- Series
- Others
_I will document a detailed guide, but as of now the following should give majority of the idea_
Add the following file "blog-configuration.tsx" in your repo at root path, we import this using `@/blog-configuration`.
```tsx
import type { Metadata } from "next/types";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Badge } from "@/components/ui/badge";
import { Book } from "@/components/ui/book";
import { Card } from "@/components/ui/card";
import type { BlogConstants, BlogConfiguration } from "@/fumadocs-blog";
import { PostCard } from "@/fumadocs-blog";
import {
Brain,
Book as LucideBook,
Code,
Cog,
Lightbulb,
Megaphone,
Rocket,
Users,
Wrench,
BookIcon,
} from "lucide-react";
// Blog text constants that can be customized
export const blogConstants: BlogConstants = {
// General
blogTitle: "Blog",
blogDescription: "Articles and thoughts",
siteName: "myblog.com",
defaultAuthorName: "My Name",
xUsername: "@my_x_username",
// Pagination
paginationTitle: (page: number) => `Blog - Page ${page}`,
paginationDescription: (page: number) =>
`Articles and thoughts - Page ${page}`,
categoryPaginationTitle: (category: string, page: number) =>
`${category.charAt(0).toUpperCase() + category.slice(1)} - Page ${page}`,
categoryPaginationDescription: (category: string, page: number) =>
`Articles in the ${category} category - Page ${page}`,
// URLs
blogBase: "/blog",
blogOgImageBase: "blog-og",
pageSize: 5,
};
export function createBlogMetadata(
override: Metadata,
blogConstants: BlogConstants
): Metadata {
// Derive values from the core properties
const siteUrl = `https://${blogConstants.siteName}`;
const author = {
name: blogConstants.defaultAuthorName,
url: siteUrl,
};
const creator = blogConstants.defaultAuthorName;
return {
...override,
authors: [author],
creator: creator,
openGraph: {
title: override.title ?? undefined,
description: override.description ?? undefined,
url: siteUrl,
siteName: blogConstants.siteName,
...override.openGraph,
},
twitter: {
card: "summary_large_image",
site: blogConstants.xUsername,
creator: blogConstants.xUsername,
title: override.title ?? undefined,
description: override.description ?? undefined,
...override.twitter,
},
alternates: {
canonical: "/",
types: {
"application/rss+xml": "/api/rss.xml",
},
...override.alternates,
},
};
}
export function getBlogConfiguration(): BlogConfiguration {
return {
PostCard: PostCard,
Button,
Popover,
PopoverContent,
PopoverTrigger,
Badge,
Book,
Card,
cn,
config: {
blogBase: blogConstants.blogBase,
blogOgImageBase: blogConstants.blogOgImageBase,
pageSize: 5,
},
};
}
export const useBlogConfiguration = getBlogConfiguration;
// Moved from lib/categories.ts
export const getCategoryBySlug = (slug: string) => {
const categories = {
idea: {
label: "Idea",
icon: Brain,
description:
"Exploratory thoughts and wild concepts for Teurons and beyond.",
},
opinions: {
label: "Opinions",
icon: Megaphone,
description:
"Subjective, wild, gut-hunch takes—less informed, out-of-box rants.",
},
};
return (
categories[slug as keyof typeof categories] || {
label: slug.toString().replace(/-/g, " ").toLowerCase(),
icon: BookIcon,
}
);
};
export const getSeriesBySlug = (slug: string) => {
const series = {
x: {
label: "Series X",
icon: LucideBook,
description: "A Sample Series",
},
// Add more series here as needed
};
return (
series[slug as keyof typeof series] || {
label: slug.charAt(0).toUpperCase() + slug.slice(1),
icon: LucideBook,
description: `Articles in the ${
slug.charAt(0).toUpperCase() + slug.slice(1)
} series.`,
}
);
};
Add following sample post
Add the sample .mdx
file below to test if everything is working. Place it at content/blog/idea/zero-trust-security.mdx
.
A few things to note: "idea" is the category of the blog, and "zero-trust-security" will be the URL of the blog post.
---
title: "Zero Trust Security"
description: "Why modern security architectures assume breach and verify everything"
author: lina
date: 2025-03-22
tags: [security, zero trust, cybersecurity, enterprise]
image: https://shadcnblocks.com/images/block/placeholder-5.svg
---
# Zero Trust Security
Traditional security models operated on the principle of "trust but verify" and focused on perimeter defense. Zero Trust flips this paradigm with a simple principle: never trust, always verify.
## Core Principles
Zero Trust is built on several foundational ideas:
### Assume Breach
Zero Trust architectures operate under the assumption that attackers are already present within the network.
### Verify Explicitly
Every access request must be fully authenticated, authorized, and encrypted:
1. Strong identity verification for all users
2. Device health validation
3. Just-in-time and just-enough access
4. Context-aware policies
## Implementation Strategies
Moving to Zero Trust requires systematic changes:
### Identity as the Control Plane
Modern security centers on identity rather than network location:
### Micro-Segmentation
Network security shifts from perimeter-based to fine-grained segmentation between workloads.
Test the blog
If you open http://localhost:3000/blog
, you'll see a list of posts—at this point, only one. You can add more, edit the content, and try it out. The pagination size is 5, so after 5 posts you'll see a paginator.
You can open a blog detail page and see details like tags, series, categories, etc.
If you want a post to be part of a series, add the following frontmatter to the .mdx
file:
series: building-react-component-library
seriesPart: 1
Future
This development is driven by my needs and the products I'm working on. Here are a few things in the pipeline:
- Newsletter subscription (But how do you do it for a static site?)
- Tags
- Make other components overridable so you can style them better
- Make this a clonable template
- The biggest problem I have with templates is that once you clone and start changing them, updating is not easy. You're off-track from the first second. So, I'm leaning towards a monorepo where I can balance open code and updates like a package.
Anything else? Hit me up on Twitter and follow for more updates.