Blog System

Create engaging content, improve SEO, and grow your audience with a powerful MDX-based blog system built for developers.

Blog System Overview

RankThis includes a complete blog system using MDX for rich content creation with code syntax highlighting, SEO optimization, and flexible customization.

šŸ“ Content Features

  • • MDX for rich markdown content
  • • Code syntax highlighting
  • • Custom React components
  • • Automatic slug generation
  • • Reading time estimation
  • • Category/tag support

šŸš€ SEO & Performance

  • • Automatic meta tags
  • • Open Graph images
  • • JSON-LD structured data
  • • Sitemap generation
  • • Static generation (SSG)
  • • Fast page loads

Blog Structure

Content Organization

MDX files are organized in a simple, scalable structure:

Blog structure
src/content/blog/
ā”œā”€ā”€ getting-started-with-rankthis.mdx
ā”œā”€ā”€ mastering-cursor-with-ranks-this.mdx
ā”œā”€ā”€ welcome-to-ranks-this.mdx
└── blog-design-guide.mdx
src/app/blog/
ā”œā”€ā”€ page.tsx # Blog listing page
ā”œā”€ā”€ [slug]/
│ └── page.tsx # Individual blog post
└── layout.tsx # Blog layout (optional)

Content Directory

  • • src/content/blog/ - All MDX files
  • • Filename becomes URL slug
  • • Frontmatter for metadata

Route Structure

  • • /blog - Blog index
  • • /blog/[slug] - Individual posts
  • • SEO-friendly URLs
MDX Configuration

Next.js configuration for MDX processing:

next.config.mjs
1// next.config.mjs
2import createMDX from '@next/mdx';
3
4/** @type {import('next').NextConfig} */
5const nextConfig = {
6 // Configure `pageExtensions` to include MDX files
7 pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
8 // Other config options here...
9};
10
11const withMDX = createMDX({
12 // Add markdown plugins here, as desired
13 options: {
14 remarkPlugins: [],
15 rehypePlugins: [],
16 },
17});
18
19// Wrap MDX and Next.js config with each other
20export default withMDX(nextConfig);
mdx-components.tsx
1// mdx-components.tsx
2import type { MDXComponents } from 'mdx/types';
3import { CodeBlock } from './src/components/docs/CodeBlock';
4
5export function useMDXComponents(components: MDXComponents): MDXComponents {
6 return {
7 // Built-in components
8 h1: ({ children }) => (
9 <h1 className="mb-6 text-4xl font-bold">{children}</h1>
10 ),
11 h2: ({ children }) => (
12 <h2 className="mb-4 mt-8 text-2xl font-semibold">{children}</h2>
13 ),
14 h3: ({ children }) => (
15 <h3 className="mb-3 mt-6 text-xl font-semibold">{children}</h3>
16 ),
17 p: ({ children }) => (
18 <p className="mb-4 leading-7 text-muted-foreground">{children}</p>
19 ),
20 ul: ({ children }) => (
21 <ul className="mb-4 list-disc pl-6 space-y-1">{children}</ul>
22 ),
23 ol: ({ children }) => (
24 <ol className="mb-4 list-decimal pl-6 space-y-1">{children}</ol>
25 ),
26
27 // Custom components
28 CodeBlock,
29 ...components,
30 };
31}

Content Creation

MDX Blog Post Template

Standard MDX frontmatter and content structure:

src/content/blog/getting-started.mdx
1---
2title: "Getting Started with RankThis SaaS Template"
3description: "Learn how to build and deploy your SaaS in under 30 minutes with our comprehensive template."
4date: "2024-03-15"
5author: "Your Name"
6category: "tutorial"
7tags: ["nextjs", "saas", "tutorial", "getting-started"]
8featured: true
9image: "/blog/getting-started-hero.jpg"
10---
11
12# Getting Started with RankThis
13
14Welcome to RankThis! This guide will help you get up and running with your new SaaS in no time.
15
16## What You'll Learn
17
18In this tutorial, you'll discover:
19
20- How to set up your development environment
21- Configure authentication with NextAuth.js
22- Set up Stripe payments
23- Deploy to production
24
25## Prerequisites
26
27Before we begin, make sure you have:
28
29- Node.js 18+ installed
30- A GitHub account
31- Basic knowledge of React and TypeScript
32
33## Step 1: Clone the Repository
34
35First, clone the RankThis template:
36
37<CodeBlock
38 code={`git clone https://github.com/your-repo/rankthis.git
39cd rankthis
40pnpm install`}
41 language="bash"
42/>
43
44## Step 2: Environment Setup
45
46Copy the environment template and fill in your values:
47
48<CodeBlock
49 code={`cp .env.example .env`}
50 language="bash"
51/>
52
53## Custom Components
54
55You can use custom React components in MDX:
56
57<Card>
58 <CardContent className="p-4">
59 <p>This is a custom card component embedded in MDX!</p>
60 </CardContent>
61</Card>
62
63## Conclusion
64
65You now have a fully functional SaaS template! Check out our other guides for advanced features.
Blog Metadata Utility

Utility functions for processing blog metadata:

src/lib/mdx.ts
1import fs from 'fs';
2import path from 'path';
3import matter from 'gray-matter';
4
5const BLOG_PATH = path.join(process.cwd(), 'src/content/blog');
6
7export interface BlogPost {
8 slug: string;
9 title: string;
10 description: string;
11 date: string;
12 author: string;
13 category: string;
14 tags: string[];
15 featured?: boolean;
16 image?: string;
17 content: string;
18 readingTime: number;
19}
20
21export function getAllBlogPosts(): BlogPost[] {
22 const files = fs.readdirSync(BLOG_PATH);
23
24 const posts = files
25 .filter(file => file.endsWith('.mdx'))
26 .map(file => {
27 const slug = file.replace('.mdx', '');
28 const fullPath = path.join(BLOG_PATH, file);
29 const fileContents = fs.readFileSync(fullPath, 'utf8');
30 const { data, content } = matter(fileContents);
31
32 // Calculate reading time (average 200 words per minute)
33 const wordCount = content.split(/\s+/).length;
34 const readingTime = Math.ceil(wordCount / 200);
35
36 return {
37 slug,
38 title: data.title,
39 description: data.description,
40 date: data.date,
41 author: data.author,
42 category: data.category,
43 tags: data.tags || [],
44 featured: data.featured || false,
45 image: data.image,
46 content,
47 readingTime,
48 };
49 })
50 .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
51
52 return posts;
53}
54
55export function getBlogPost(slug: string): BlogPost | null {
56 try {
57 const fullPath = path.join(BLOG_PATH, `${slug}.mdx`);
58 const fileContents = fs.readFileSync(fullPath, 'utf8');
59 const { data, content } = matter(fileContents);
60
61 const wordCount = content.split(/\s+/).length;
62 const readingTime = Math.ceil(wordCount / 200);
63
64 return {
65 slug,
66 title: data.title,
67 description: data.description,
68 date: data.date,
69 author: data.author,
70 category: data.category,
71 tags: data.tags || [],
72 featured: data.featured || false,
73 image: data.image,
74 content,
75 readingTime,
76 };
77 } catch {
78 return null;
79 }
80}
81
82export function getFeaturedPosts(): BlogPost[] {
83 return getAllBlogPosts().filter(post => post.featured);
84}
85
86export function getPostsByCategory(category: string): BlogPost[] {
87 return getAllBlogPosts().filter(post => post.category === category);
88}

Blog Pages

Blog Index Page

Blog listing with featured posts and categories:

src/app/blog/page.tsx
1import Link from "next/link";
2import { getAllBlogPosts, getFeaturedPosts } from "~/lib/mdx";
3import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
4import { Badge } from "~/components/ui/badge";
5
6export default function BlogPage() {
7 const allPosts = getAllBlogPosts();
8 const featuredPosts = getFeaturedPosts();
9 const recentPosts = allPosts.slice(0, 6);
10
11 return (
12 <div className="container mx-auto px-4 py-12">
13 <div className="mb-12 text-center">
14 <h1 className="mb-4 text-4xl font-bold">Blog</h1>
15 <p className="text-xl text-muted-foreground">
16 Insights, tutorials, and updates from the RankThis team
17 </p>
18 </div>
19
20 {/* Featured Posts */}
21 {featuredPosts.length > 0 && (
22 <section className="mb-16">
23 <h2 className="mb-8 text-2xl font-semibold">Featured Posts</h2>
24 <div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
25 {featuredPosts.map((post) => (
26 <Card key={post.slug} className="overflow-hidden">
27 {post.image && (
28 <div className="aspect-video bg-muted">
29 <img
30 src={post.image}
31 alt={post.title}
32 className="h-full w-full object-cover"
33 />
34 </div>
35 )}
36 <CardHeader>
37 <div className="flex items-center gap-2 text-sm text-muted-foreground">
38 <Badge variant="secondary">{post.category}</Badge>
39 <span>•</span>
40 <span>{post.readingTime} min read</span>
41 </div>
42 <CardTitle className="line-clamp-2">
43 <Link href={`/blog/${post.slug}`} className="hover:underline">
44 {post.title}
45 </Link>
46 </CardTitle>
47 </CardHeader>
48 <CardContent>
49 <p className="line-clamp-3 text-muted-foreground">
50 {post.description}
51 </p>
52 <div className="mt-4 flex items-center justify-between">
53 <span className="text-sm text-muted-foreground">
54 {new Date(post.date).toLocaleDateString()}
55 </span>
56 <Link
57 href={`/blog/${post.slug}`}
58 className="text-sm font-medium text-primary hover:underline"
59 >
60 Read more →
61 </Link>
62 </div>
63 </CardContent>
64 </Card>
65 ))}
66 </div>
67 </section>
68 )}
69
70 {/* Recent Posts */}
71 <section>
72 <h2 className="mb-8 text-2xl font-semibold">Recent Posts</h2>
73 <div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
74 {recentPosts.map((post) => (
75 <Card key={post.slug}>
76 <CardHeader>
77 <div className="flex items-center gap-2 text-sm text-muted-foreground">
78 <Badge variant="outline">{post.category}</Badge>
79 <span>•</span>
80 <span>{post.readingTime} min read</span>
81 </div>
82 <CardTitle className="line-clamp-2">
83 <Link href={`/blog/${post.slug}`} className="hover:underline">
84 {post.title}
85 </Link>
86 </CardTitle>
87 </CardHeader>
88 <CardContent>
89 <p className="line-clamp-3 text-muted-foreground">
90 {post.description}
91 </p>
92 <div className="mt-4 flex items-center justify-between">
93 <span className="text-sm text-muted-foreground">
94 {new Date(post.date).toLocaleDateString()}
95 </span>
96 <Link
97 href={`/blog/${post.slug}`}
98 className="text-sm font-medium text-primary hover:underline"
99 >
100 Read more →
101 </Link>
102 </div>
103 </CardContent>
104 </Card>
105 ))}
106 </div>
107 </section>
108 </div>
109 );
110}
Individual Blog Post

Dynamic blog post page with SEO and reading experience:

src/app/blog/[slug]/page.tsx
1import { notFound } from "next/navigation";
2import { Metadata } from "next";
3import { getBlogPost, getAllBlogPosts } from "~/lib/mdx";
4import { Badge } from "~/components/ui/badge";
5import { Separator } from "~/components/ui/separator";
6
7interface BlogPostPageProps {
8 params: {
9 slug: string;
10 };
11}
12
13export async function generateStaticParams() {
14 const posts = getAllBlogPosts();
15 return posts.map((post) => ({
16 slug: post.slug,
17 }));
18}
19
20export async function generateMetadata({
21 params
22}: BlogPostPageProps): Promise<Metadata> {
23 const post = getBlogPost(params.slug);
24
25 if (!post) {
26 return {};
27 }
28
29 return {
30 title: post.title,
31 description: post.description,
32 openGraph: {
33 title: post.title,
34 description: post.description,
35 type: "article",
36 publishedTime: post.date,
37 authors: [post.author],
38 images: post.image ? [{ url: post.image }] : [],
39 },
40 twitter: {
41 card: "summary_large_image",
42 title: post.title,
43 description: post.description,
44 images: post.image ? [post.image] : [],
45 },
46 };
47}
48
49export default async function BlogPostPage({ params }: BlogPostPageProps) {
50 const post = getBlogPost(params.slug);
51
52 if (!post) {
53 notFound();
54 }
55
56 return (
57 <article className="container mx-auto max-w-4xl px-4 py-12">
58 {/* Header */}
59 <header className="mb-12 text-center">
60 <div className="mb-4 flex items-center justify-center gap-2">
61 <Badge>{post.category}</Badge>
62 <span className="text-sm text-muted-foreground">•</span>
63 <span className="text-sm text-muted-foreground">
64 {post.readingTime} min read
65 </span>
66 </div>
67
68 <h1 className="mb-4 text-4xl font-bold md:text-5xl">
69 {post.title}
70 </h1>
71
72 <p className="mb-6 text-xl text-muted-foreground">
73 {post.description}
74 </p>
75
76 <div className="flex items-center justify-center gap-4 text-sm text-muted-foreground">
77 <span>By {post.author}</span>
78 <span>•</span>
79 <span>{new Date(post.date).toLocaleDateString()}</span>
80 </div>
81
82 {post.tags.length > 0 && (
83 <div className="mt-6 flex flex-wrap justify-center gap-2">
84 {post.tags.map((tag) => (
85 <Badge key={tag} variant="outline">
86 {tag}
87 </Badge>
88 ))}
89 </div>
90 )}
91 </header>
92
93 <Separator className="mb-12" />
94
95 {/* Content */}
96 <div className="prose prose-lg dark:prose-invert mx-auto">
97 {/* MDX content would be rendered here */}
98 <div dangerouslySetInnerHTML={{ __html: post.content }} />
99 </div>
100
101 {/* Footer */}
102 <footer className="mt-16 border-t pt-8">
103 <div className="text-center">
104 <p className="text-muted-foreground">
105 Published on {new Date(post.date).toLocaleDateString()}
106 </p>
107 </div>
108 </footer>
109 </article>
110 );
111}

SEO & Performance

SEO Optimization

Automatic SEO optimization for better search rankings:

SEO utilities
1// Automatic sitemap generation
2export default function sitemap() {
3 const posts = getAllBlogPosts();
4
5 const blogUrls = posts.map((post) => ({
6 url: `https://yourdomain.com/blog/${post.slug}`,
7 lastModified: new Date(post.date),
8 changeFrequency: 'weekly' as const,
9 priority: 0.7,
10 }));
11
12 return [
13 {
14 url: 'https://yourdomain.com/blog',
15 lastModified: new Date(),
16 changeFrequency: 'daily' as const,
17 priority: 0.8,
18 },
19 ...blogUrls,
20 ];
21}
22
23// JSON-LD structured data
24function generateBlogPostJsonLd(post: BlogPost) {
25 return {
26 '@context': 'https://schema.org',
27 '@type': 'BlogPosting',
28 headline: post.title,
29 description: post.description,
30 author: {
31 '@type': 'Person',
32 name: post.author,
33 },
34 datePublished: post.date,
35 dateModified: post.date,
36 image: post.image,
37 publisher: {
38 '@type': 'Organization',
39 name: 'Your Company',
40 logo: {
41 '@type': 'ImageObject',
42 url: 'https://yourdomain.com/logo.png',
43 },
44 },
45 };
46}
Performance Optimization

Built-in performance optimizations:

Static Generation

  • • All blog posts are pre-rendered
  • • Fast page loads
  • • Better SEO crawling
  • • Reduced server load

Optimizations

  • • Image optimization
  • • Code splitting
  • • Lazy loading
  • • Efficient bundling
šŸ“ Content Management Tips

Writing Best Practices

  • • Write compelling headlines
  • • Use clear, scannable structure
  • • Include relevant code examples
  • • Add helpful images and diagrams
  • • Optimize for featured snippets

Content Strategy

  • • Document your product features
  • • Share industry insights
  • • Create tutorial content
  • • Answer common questions
  • • Showcase customer success
šŸŽØ Advanced Customization

Custom Components

Create reusable React components for rich content like pricing tables, testimonials, or interactive demos that work seamlessly in MDX.

Content Categories

Organize posts by categories like "tutorials", "features", "updates" to help readers find relevant content quickly.

RSS Feeds

RSS generation
1// Generate RSS feed
2export function generateRSS() {
3 const posts = getAllBlogPosts();
4
5 const rssXml = `<?xml version="1.0" encoding="UTF-8"?>
6 <rss version="2.0">
7 <channel>
8 <title>Your Blog</title>
9 <description>Latest updates and tutorials</description>
10 <link>https://yourdomain.com/blog</link>
11 ${posts.map(post => `
12 <item>
13 <title>${post.title}</title>
14 <description>${post.description}</description>
15 <link>https://yourdomain.com/blog/${post.slug}</link>
16 <pubDate>${new Date(post.date).toUTCString()}</pubDate>
17 </item>
18 `).join('')}
19 </channel>
20 </rss>`;
21
22 return rssXml;
23}

šŸŽ‰ Core Features Complete!

Your blog system is ready for content creation! You've now completed all 8 core features. Your SaaS template is production-ready.