Blog System
Create engaging content, improve SEO, and grow your audience with a powerful MDX-based blog system built for developers.
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
MDX files are organized in a simple, scalable structure:
src/content/blog/āāā getting-started-with-rankthis.mdxāāā mastering-cursor-with-ranks-this.mdxāāā welcome-to-ranks-this.mdxāāā blog-design-guide.mdxsrc/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
Next.js configuration for MDX processing:
1// next.config.mjs2import createMDX from '@next/mdx';34/** @type {import('next').NextConfig} */5const nextConfig = {6 // Configure `pageExtensions` to include MDX files7 pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],8 // Other config options here...9};1011const withMDX = createMDX({12 // Add markdown plugins here, as desired13 options: {14 remarkPlugins: [],15 rehypePlugins: [],16 },17});1819// Wrap MDX and Next.js config with each other20export default withMDX(nextConfig);
1// mdx-components.tsx2import type { MDXComponents } from 'mdx/types';3import { CodeBlock } from './src/components/docs/CodeBlock';45export function useMDXComponents(components: MDXComponents): MDXComponents {6 return {7 // Built-in components8 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 ),2627 // Custom components28 CodeBlock,29 ...components,30 };31}
Content Creation
Standard MDX frontmatter and content structure:
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: true9image: "/blog/getting-started-hero.jpg"10---1112# Getting Started with RankThis1314Welcome to RankThis! This guide will help you get up and running with your new SaaS in no time.1516## What You'll Learn1718In this tutorial, you'll discover:1920- How to set up your development environment21- Configure authentication with NextAuth.js22- Set up Stripe payments23- Deploy to production2425## Prerequisites2627Before we begin, make sure you have:2829- Node.js 18+ installed30- A GitHub account31- Basic knowledge of React and TypeScript3233## Step 1: Clone the Repository3435First, clone the RankThis template:3637<CodeBlock38 code={`git clone https://github.com/your-repo/rankthis.git39cd rankthis40pnpm install`}41 language="bash"42/>4344## Step 2: Environment Setup4546Copy the environment template and fill in your values:4748<CodeBlock49 code={`cp .env.example .env`}50 language="bash"51/>5253## Custom Components5455You can use custom React components in MDX:5657<Card>58 <CardContent className="p-4">59 <p>This is a custom card component embedded in MDX!</p>60 </CardContent>61</Card>6263## Conclusion6465You now have a fully functional SaaS template! Check out our other guides for advanced features.
Utility functions for processing blog metadata:
1import fs from 'fs';2import path from 'path';3import matter from 'gray-matter';45const BLOG_PATH = path.join(process.cwd(), 'src/content/blog');67export 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}2021export function getAllBlogPosts(): BlogPost[] {22 const files = fs.readdirSync(BLOG_PATH);2324 const posts = files25 .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);3132 // Calculate reading time (average 200 words per minute)33 const wordCount = content.split(/\s+/).length;34 const readingTime = Math.ceil(wordCount / 200);3536 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());5152 return posts;53}5455export 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);6061 const wordCount = content.split(/\s+/).length;62 const readingTime = Math.ceil(wordCount / 200);6364 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}8182export function getFeaturedPosts(): BlogPost[] {83 return getAllBlogPosts().filter(post => post.featured);84}8586export function getPostsByCategory(category: string): BlogPost[] {87 return getAllBlogPosts().filter(post => post.category === category);88}
Blog Pages
Blog listing with featured posts and categories:
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";56export default function BlogPage() {7 const allPosts = getAllBlogPosts();8 const featuredPosts = getFeaturedPosts();9 const recentPosts = allPosts.slice(0, 6);1011 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 team17 </p>18 </div>1920 {/* 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 <img30 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 <Link57 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 )}6970 {/* 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 <Link97 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}
Dynamic blog post page with SEO and reading experience:
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";67interface BlogPostPageProps {8 params: {9 slug: string;10 };11}1213export async function generateStaticParams() {14 const posts = getAllBlogPosts();15 return posts.map((post) => ({16 slug: post.slug,17 }));18}1920export async function generateMetadata({21 params22}: BlogPostPageProps): Promise<Metadata> {23 const post = getBlogPost(params.slug);2425 if (!post) {26 return {};27 }2829 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}4849export default async function BlogPostPage({ params }: BlogPostPageProps) {50 const post = getBlogPost(params.slug);5152 if (!post) {53 notFound();54 }5556 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 read65 </span>66 </div>6768 <h1 className="mb-4 text-4xl font-bold md:text-5xl">69 {post.title}70 </h1>7172 <p className="mb-6 text-xl text-muted-foreground">73 {post.description}74 </p>7576 <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>8182 {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>9293 <Separator className="mb-12" />9495 {/* 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>100101 {/* 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
Automatic SEO optimization for better search rankings:
1// Automatic sitemap generation2export default function sitemap() {3 const posts = getAllBlogPosts();45 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 }));1112 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}2223// JSON-LD structured data24function 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}
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
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
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
1// Generate RSS feed2export function generateRSS() {3 const posts = getAllBlogPosts();45 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>`;2122 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.