Add Blog and SEO to Your SaaS Boilerplate 2026
TL;DR
Adding a blog to ShipFast or T3 Stack takes 2-3 days. The core: MDX content directory, dynamic routes, meta tags, sitemap.xml, and OG images. For boilerplates that include blog (Makerkit, Supastarter, Open SaaS), you configure rather than build. This guide covers adding blog + SEO from scratch to any Next.js boilerplate.
Why Your SaaS Needs a Blog
The blog-for-SEO model is well-understood and well-proven: publish articles that rank for problem-aware searches in your niche, those articles drive organic traffic, a percentage of visitors convert to signups. For SaaS products, the payback period on a well-optimized blog post is typically 6–12 months, after which it generates free leads indefinitely.
The mistake most founders make is not adding the technical foundation early. Once you have 5–10 articles planned, you want the infrastructure ready so you're not publishing to a blog that doesn't generate link equity. Set it up correctly from the start.
What "set up correctly" means for SaaS blogs:
- MDX for code-heavy technical content (fenced code blocks, custom components)
- Static generation with
generateStaticParams— not server-rendered per request generateMetadatafor per-post meta tags- Dynamic
sitemap.tsthat includes blog posts - OG image generation for social sharing
- Correct canonical URLs, especially if you syndicate content
The full setup takes 2–3 days of focused work. This guide walks through each component.
Boilerplates with Blog Built-In
Before building from scratch, check whether your boilerplate already has blog infrastructure:
| Boilerplate | Blog Included | MDX Support | OG Images | Sitemap |
|---|---|---|---|---|
| Makerkit | ✅ | ✅ | ✅ | ✅ |
| Supastarter | ✅ | ✅ | ✅ | ✅ |
| Open SaaS | ✅ | ✅ | ❌ | ✅ |
| ShipFast | ❌ Build it | — | — | — |
| T3 Stack | ❌ Build it | — | — | — |
| Bedrock | ❌ Build it | — | — | — |
If your boilerplate already includes a blog, you're configuring rather than building. Skip to the SEO checklist at the end.
Step 1: MDX Setup
MDX combines Markdown with React components — essential for technical SaaS blogs with code examples, custom callouts, and embedded demos.
npm install @next/mdx @mdx-js/loader @mdx-js/react rehype-highlight remark-gfm gray-matter
// next.config.js
const withMDX = require('@next/mdx')({
extension: /\.mdx?$/,
options: {
remarkPlugins: [require('remark-gfm')],
rehypePlugins: [require('rehype-highlight')],
},
});
module.exports = withMDX({
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
});
Alternatively, use next-mdx-remote for more flexibility with remote content sources:
npm install next-mdx-remote
next-mdx-remote lets you pass custom component mappings to MDX content, which is useful for custom callout boxes, embedded demos, or product screenshots.
Step 2: Blog Content Structure
content/
└── blog/
├── first-post.mdx
├── product-update-march-2026.mdx
└── feature-announcement.mdx
Each MDX file uses frontmatter for metadata:
---
title: "Post Title"
description: "150-160 character meta description with target keyword in first 80 chars"
date: "2026-03-08"
author: "Team Name"
tags: ["tag1", "tag2"]
image: "/blog/first-post-cover.jpg"
---
# Post Title
Content goes here in markdown.
The description field becomes the <meta name="description"> and Open Graph description. Write it as 150–160 characters with the target keyword in the first 80 characters.
Step 3: Blog List and Post Routes
// lib/blog.ts — content loading utilities
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
const BLOG_DIR = path.join(process.cwd(), 'content/blog');
export type BlogPost = {
slug: string;
title: string;
description: string;
date: string;
author: string;
tags: string[];
image?: string;
content: string;
};
export function getBlogPosts(): BlogPost[] {
const files = fs.readdirSync(BLOG_DIR).filter(f => f.endsWith('.mdx'));
return files
.map(file => {
const slug = file.replace('.mdx', '');
const raw = fs.readFileSync(path.join(BLOG_DIR, file), 'utf-8');
const { data, content } = matter(raw);
return { slug, content, ...data } as BlogPost;
})
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
}
export function getBlogPost(slug: string): BlogPost | null {
const filepath = path.join(BLOG_DIR, `${slug}.mdx`);
if (!fs.existsSync(filepath)) return null;
const raw = fs.readFileSync(filepath, 'utf-8');
const { data, content } = matter(raw);
return { slug, content, ...data } as BlogPost;
}
// app/blog/page.tsx — blog list
import { getBlogPosts } from '~/lib/blog';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Blog — YourSaaS',
description: 'Insights on [your topic] from the YourSaaS team',
};
export default function BlogPage() {
const posts = getBlogPosts();
return (
<main>
<h1>Blog</h1>
<div className="grid gap-6">
{posts.map(post => (
<PostCard key={post.slug} post={post} />
))}
</div>
</main>
);
}
// app/blog/[slug]/page.tsx — individual post
import { getBlogPost, getBlogPosts } from '~/lib/blog';
import { MDXRemote } from 'next-mdx-remote/rsc';
import { notFound } from 'next/navigation';
export async function generateStaticParams() {
return getBlogPosts().map(post => ({ slug: post.slug }));
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = getBlogPost(params.slug);
if (!post) return {};
return {
title: `${post.title} — YourSaaS`,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
type: 'article',
publishedTime: post.date,
},
};
}
export default function BlogPostPage({ params }: { params: { slug: string } }) {
const post = getBlogPost(params.slug);
if (!post) notFound();
return (
<article>
<header>
<h1>{post.title}</h1>
<time>{new Date(post.date).toLocaleDateString()}</time>
</header>
<MDXRemote source={post.content} />
</article>
);
}
Step 4: Sitemap Generation
A dynamic sitemap ensures Google discovers new blog posts automatically:
// app/sitemap.ts — dynamic sitemap
import { MetadataRoute } from 'next';
import { getBlogPosts } from '~/lib/blog';
export default function sitemap(): MetadataRoute.Sitemap {
const posts = getBlogPosts();
const baseUrl = 'https://yoursaas.com';
const staticRoutes = [
{ url: baseUrl, changeFrequency: 'weekly', priority: 1.0 },
{ url: `${baseUrl}/pricing`, changeFrequency: 'monthly', priority: 0.8 },
{ url: `${baseUrl}/blog`, changeFrequency: 'weekly', priority: 0.7 },
{ url: `${baseUrl}/about`, changeFrequency: 'monthly', priority: 0.5 },
] as MetadataRoute.Sitemap;
const blogRoutes: MetadataRoute.Sitemap = posts.map(post => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.date),
changeFrequency: 'monthly',
priority: 0.6,
}));
return [...staticRoutes, ...blogRoutes];
}
After deploying, submit the sitemap URL (https://yoursaas.com/sitemap.xml) to Google Search Console.
Step 5: OG Image Generation
Open Graph images are what appear when someone shares your post on Twitter/LinkedIn. Next.js can generate them server-side from a JSX template:
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og';
import { getBlogPost } from '~/lib/blog';
export const runtime = 'edge';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';
export default async function OGImage({ params }: { params: { slug: string } }) {
const post = getBlogPost(params.slug);
if (!post) return new Response('Not found', { status: 404 });
return new ImageResponse(
(
<div
style={{
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: '60px',
}}
>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: 24 }}>
YourSaaS Blog
</div>
<div style={{ color: 'white', fontSize: 64, fontWeight: 'bold', lineHeight: 1.1 }}>
{post.title}
</div>
<div style={{ color: 'rgba(255,255,255,0.8)', fontSize: 24 }}>
{new Date(post.date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}
</div>
</div>
),
{ ...size }
);
}
This generates a unique OG image for each post automatically. No need to manually create images in Figma.
Step 6: Root Layout Meta Tags
// app/layout.tsx — global SEO defaults
import type { Metadata } from 'next';
export const metadata: Metadata = {
metadataBase: new URL('https://yoursaas.com'),
title: {
default: 'YourSaaS — What You Do',
template: '%s | YourSaaS',
},
description: 'Default meta description (150-160 characters, includes keyword)',
robots: { index: true, follow: true },
openGraph: {
type: 'website',
locale: 'en_US',
url: 'https://yoursaas.com',
siteName: 'YourSaaS',
},
twitter: {
card: 'summary_large_image',
site: '@yoursaas',
creator: '@yourname',
},
};
The metadataBase is required for relative URL resolution in Open Graph tags. Without it, social media crawlers can't resolve image URLs.
SEO Quality Checklist
After setup, verify these before publishing:
Technical:
-
sitemap.xmlreturns all blog posts and resolves correctly -
robots.txtdoesn't block/blog/ - Each post page has a unique
<title>and<meta name="description"> -
<link rel="canonical">on each post (Next.js generates this ifmetadataBaseis set) - OG image resolves for each post (test with opengraph.xyz)
Content:
- Title: 30–55 characters, includes primary keyword
- Description: 150–160 characters, keyword in first 80 characters
- Slug: matches the primary keyword (e.g.,
how-to-add-stripe-nextjs-2026) - H1 matches the title (or is very close)
- Internal links: at least 3 per post to other relevant pages
After publishing:
- Sitemap submitted to Google Search Console
- First post URL submitted for indexing via Search Console
- Post shared on social to get initial crawl signals
Content Strategy: What to Write First
Setting up the blog infrastructure is day one. The harder, longer work is writing content that actually ranks and converts. Most SaaS founders build the blog, write 3 posts, then abandon it. The ones who succeed treat it as a long-term compound investment.
The content types that work best for SaaS blogs, in order of ROI:
Comparison posts: "[Your Product] vs [Competitor]" posts convert incredibly well because they target purchase-intent keywords. Someone searching "ShipFast vs Makerkit" is close to buying. Write honest comparisons that acknowledge your weaknesses — readers trust balanced content and it performs better in search.
Integration guides: "How to use [Your Product] with [Popular Tool]" — "how to add Stripe to Next.js", "how to connect Supabase to Prisma". These rank for long-tail keywords with strong buying intent, and they pre-qualify users who already use the tools you integrate with.
Problem-first guides: "How to [Specific Problem]" where the problem is something your product solves. Not feature documentation — write about the problem that leads users to your solution.
Category pages: For directory-style products, "Best X for Y" pages aggregate many solutions. These are Tier 2 articles (feature matrices, multiple tools compared) and take more effort but have strong rankings potential for high-volume comparison queries.
The publication cadence that works: 2–4 substantive articles per month, every month, for 6–12 months before expecting significant organic traffic. Short articles published frequently outperform infrequent long articles in most niches.
Internal Linking Strategy
Internal linking is one of the most underused SEO tactics for SaaS blogs. Every article you publish should link to 3+ other pages on your site: one to a comparison page, one to a related blog post, one to a product/category page.
The goals of internal linking:
- Pass link equity from high-authority pages to deeper pages
- Keep readers on your site longer (lower bounce rate signals)
- Guide readers from educational content toward product pages (conversion flow)
Build your internal link structure deliberately. Create a spreadsheet with your articles on one axis and target pages on the other. For each article, identify which product pages, comparison pages, and related posts should receive links. Update older articles when new related content is published.
For directory sites specifically, every blog post is an opportunity to link to relevant category pages and individual product listings. A guide about "how to add Stripe to Next.js" should link to your Stripe-enabled boilerplates comparison page.
Time Budget
| Component | Duration |
|---|---|
| MDX setup + content utilities | 0.5 day |
| Blog list + post routes | 0.5 day |
| Sitemap generation | 0.5 day |
| OG image generation | 0.5 day |
| Meta tags in layout | 0.5 day |
| Styling the blog | 0.5 day |
| Total | ~3 days |
Measuring Blog Performance
Once you have 10+ posts published and a few months of data, the analysis reveals what's working. The metrics that matter for SaaS blog SEO:
Organic sessions by landing page: Which specific posts are driving traffic? The 80/20 rule applies — a few posts will drive most organic traffic. Identify them early and update them first when the content becomes stale.
Keyword rankings via Google Search Console: GSC shows which queries your pages appear for, average position, and click-through rate. An article ranking at position 8 for a high-volume keyword is a top priority to optimize (get it to position 4–5 and traffic roughly doubles).
Conversion rate by post: Which posts lead to signups? Typically problem-aware content ("how to do X") converts better than informational content ("what is X"). Add UTM parameters to internal links so you can trace conversions back to specific posts in your analytics.
Content decay: Articles published 12+ months ago often start to decline as fresher content outranks them. Set a quarterly calendar reminder to update your top-10 posts with current information, new screenshots, and updated comparisons.
The blog compounds over time — posts published today generate traffic for years. But only if you publish consistently and update the high-performing content when it ages.
Related Resources
For boilerplates that include blog infrastructure out of the box, see best boilerplates with blog built-in. If you're adding the blog as part of a broader App Router migration, how to migrate Pages Router to App Router has the full migration context. For the content marketing strategy to go with your new blog, adding SEO to SaaS boilerplate covers the keyword targeting approach.
Methodology
Setup instructions tested against Next.js 14.2 and 15.x with next-mdx-remote v4 and the @next/mdx package. SEO best practices based on current Google Search documentation and Search Console behavior as of Q1 2026.
Find boilerplates with blog built-in on StarterPick.
Check out this boilerplate
View ShipFaston StarterPick →