Blog + SEO Module
MDX blog with automatic sitemap, OG images, RSS feed, and structured data.
Pro tier only. This module is included in the Pro plan.
Overview
The Blog + SEO module gives you a complete content marketing setup:
- MDX blog — write posts in Markdown with React components
- Sitemap — auto-generated at
/sitemap.xml - OG images — dynamic Open Graph images for social sharing
- RSS feed — auto-generated at
/feed.xml - Structured data — JSON-LD for search engines
- SEO metadata — per-page title, description, and keywords
Blog content lives in content/blog/ and blog routes live in src/app/(marketing)/blog/.
Configuration
Blog content directory
Posts are MDX files in content/blog/:
content/blog/
├── hello-world.mdx
├── building-with-ai.mdx
└── shipping-fast.mdx
Each post has frontmatter:
---
title: Hello World
description: Our first blog post — what we're building and why.
date: 2026-01-15
author: Your Name
image: /blog/hello-world.png
tags: ["announcement", "launch"]
---
Welcome to our blog. We built this SaaS because...
SEO metadata
Global metadata is set in the root layout:
import type { Metadata } from "next";
export const metadata: Metadata = {
title: {
default: "Your SaaS — Tagline here",
template: "%s | Your SaaS",
},
description: "A one-sentence description of your product.",
openGraph: {
siteName: "Your SaaS",
url: "https://yourdomain.com",
},
twitter: {
card: "summary_large_image",
},
};
Per-page metadata is set in each page file using generateMetadata.
Usage
Write a blog post
Create a new MDX file in content/blog/:
---
title: My New Post
description: A short summary for SEO and social cards.
date: 2026-03-15
author: Your Name
image: /blog/my-new-post.png
tags: ["tutorial"]
---
## Introduction
Start writing your post here. You can use standard Markdown.
### Code examples
```typescript
const greeting = "Hello from the blog!";
React components
You can embed React components in MDX:
<Callout type="info">
This is a callout component rendered inside a blog post.
</Callout>
Posts are automatically available at /blog/my-new-post based on the filename.
Sitemap generation
The sitemap is auto-generated at build time:
import { getAllPosts } from "@/lib/blog";
export default async function sitemap() {
const posts = await getAllPosts();
return [
{ url: "https://yourdomain.com", lastModified: new Date() },
{ url: "https://yourdomain.com/pricing", lastModified: new Date() },
{ url: "https://yourdomain.com/blog", lastModified: new Date() },
...posts.map((post) => ({
url: `https://yourdomain.com/blog/${post.slug}`,
lastModified: new Date(post.date),
})),
];
}
Dynamic OG images
Each blog post gets a dynamic Open Graph image generated at request time:
import { ImageResponse } from "next/og";
import { getPost } from "@/lib/blog";
export default async function OGImage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return new ImageResponse(
<div style={{ display: "flex", flexDirection: "column", padding: 48 }}>
<h1 style={{ fontSize: 48 }}>{post.title}</h1>
<p style={{ fontSize: 24, color: "#666" }}>{post.description}</p>
</div>,
{ width: 1200, height: 630 }
);
}
RSS feed
An RSS feed is generated at /feed.xml:
import { getAllPosts } from "@/lib/blog";
export async function GET() {
const posts = await getAllPosts();
// Generate RSS XML from posts
return new Response(rssXml, {
headers: { "Content-Type": "application/xml" },
});
}
Customization
Change blog layout
Edit the blog list page and post layout:
src/app/(marketing)/blog/page.tsx— blog index with post cardssrc/app/(marketing)/blog/[slug]/page.tsx— individual post layout
Add categories
- Add a
categoryfield to post frontmatter - Create a category filter on the blog index page
- Optionally add
/blog/category/[category]routes
Custom OG image template
Edit src/app/blog/[slug]/opengraph-image.tsx to change fonts, colors, layout, or add your logo.
Structured data
JSON-LD is added automatically for blog posts:
export default function BlogPost({ post }) {
const jsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
description: post.description,
datePublished: post.date,
author: { "@type": "Person", name: post.author },
};
return (
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
<article>{/* post content */}</article>
</>
);
}
Removing this module
- Delete
content/blog/directory - Delete
src/app/(marketing)/blog/directory - Delete
src/app/sitemap.ts(or remove blog entries from it) - Delete
src/app/feed.xml/directory - Remove blog-related MDX dependencies:
pnpm remove next-mdx-remote gray-matter