SEO in SaaS Boilerplates: What Most Get Wrong in 2026
TL;DR
Most boilerplates focus on the app — auth, billing, dashboard. SEO for the marketing site is often incomplete or wrong. Key gaps: missing structured data, incorrect canonical URLs, no sitemap generation, poor meta descriptions, and app routes leaking into search indexes. These are fixable in a day but have a large long-term impact.
What SaaS Boilerplates Get Right
A well-configured Next.js boilerplate ships with:
// app/layout.tsx — global metadata
export const metadata: Metadata = {
title: {
default: 'YourSaaS — Short Description',
template: '%s | YourSaaS',
},
description: 'Compelling 150-160 character description with primary keyword.',
openGraph: {
type: 'website',
locale: 'en_US',
url: 'https://yoursaas.com',
siteName: 'YourSaaS',
},
twitter: {
card: 'summary_large_image',
creator: '@yoursaas',
},
};
This is the minimum. Most boilerplates stop here.
What Boilerplates Get Wrong
1. No Canonical URLs
Without canonicals, duplicate content (HTTP vs HTTPS, www vs non-www, trailing slashes) hurts rankings:
// app/layout.tsx — add canonical
export const metadata: Metadata = {
alternates: {
canonical: 'https://yoursaas.com',
},
};
// app/blog/[slug]/page.tsx — per-page canonical
export async function generateMetadata({ params }): Promise<Metadata> {
return {
alternates: {
canonical: `https://yoursaas.com/blog/${params.slug}`,
},
};
}
2. Missing Structured Data
Rich results (star ratings, FAQ accordions, breadcrumbs) require JSON-LD:
// components/json-ld.tsx
export function ArticleJsonLd({ post }: { post: Post }) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
description: post.description,
datePublished: post.publishedAt,
dateModified: post.updatedAt,
author: {
'@type': 'Organization',
name: 'YourSaaS',
url: 'https://yoursaas.com',
},
publisher: {
'@type': 'Organization',
name: 'YourSaaS',
logo: { '@type': 'ImageObject', url: 'https://yoursaas.com/logo.png' },
},
image: post.ogImage ?? 'https://yoursaas.com/og-default.png',
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
);
}
// For SaaS homepage — SoftwareApplication schema
export function SoftwareJsonLd() {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'YourSaaS',
applicationCategory: 'BusinessApplication',
operatingSystem: 'Web',
offers: {
'@type': 'Offer',
price: '29',
priceCurrency: 'USD',
},
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: '4.8',
ratingCount: '120',
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
);
}
3. App Routes in Search Index
Your /dashboard, /settings, and /api/* routes should not be indexed:
# public/robots.txt
User-agent: *
Allow: /
Disallow: /dashboard/
Disallow: /settings/
Disallow: /api/
Disallow: /auth/
Sitemap: https://yoursaas.com/sitemap.xml
// Or via Next.js metadata
export const metadata: Metadata = {
robots: {
index: false,
follow: false,
},
};
// Apply to app layout
// app/(app)/layout.tsx — dashboard routes
export const metadata: Metadata = {
robots: 'noindex, nofollow',
};
4. No Sitemap Generation
A sitemap tells search engines which pages to crawl. Dynamic pages (blog, comparison pages) need dynamic sitemaps:
// app/sitemap.ts
import { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await prisma.post.findMany({
where: { published: true },
select: { slug: true, updatedAt: true },
});
const blogUrls = posts.map((post) => ({
url: `https://yoursaas.com/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.7,
}));
return [
{
url: 'https://yoursaas.com',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 1.0,
},
{
url: 'https://yoursaas.com/pricing',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.9,
},
...blogUrls,
];
}
5. Poor OG Image Generation
Social previews drive link click-through. Boilerplates often have a static OG image that's the same for every page:
// app/blog/[slug]/opengraph-image.tsx — Dynamic OG images
import { ImageResponse } from 'next/og';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';
export default async function Image({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return new ImageResponse(
(
<div
style={{
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
padding: '80px',
justifyContent: 'center',
}}
>
<div style={{ fontSize: 48, fontWeight: 700, color: 'white', marginBottom: 24 }}>
{post.title}
</div>
<div style={{ fontSize: 24, color: '#94a3b8' }}>
{post.description}
</div>
<div style={{ position: 'absolute', bottom: 40, right: 80, color: '#64748b', fontSize: 20 }}>
YourSaaS.com
</div>
</div>
),
size
);
}
The Marketing Site SEO Checklist
Quick audit for any boilerplate:
[ ] Title tag: under 60 chars, includes primary keyword
[ ] Meta description: 150-160 chars, compelling, includes keyword
[ ] H1: one per page, matches page intent
[ ] Canonical URL: set on every page
[ ] robots.txt: excludes app routes
[ ] sitemap.xml: auto-generated, submitted to Google Search Console
[ ] OG tags: title, description, image for all shareable pages
[ ] JSON-LD: Article (blog), SoftwareApplication (homepage)
[ ] Core Web Vitals: LCP < 2.5s, FID < 100ms, CLS < 0.1
[ ] Internal links: blog posts link to pricing and product pages
[ ] Alt text: all images have descriptive alt text
Boilerplate SEO Out of the Box
| Boilerplate | Meta Tags | Sitemap | Structured Data | robots.txt |
|---|---|---|---|---|
| ShipFast | ✅ | ✅ | ❌ | ✅ |
| Makerkit | ✅ | ✅ | Partial | ✅ |
| T3 Stack | ❌ | ❌ | ❌ | ❌ |
| AstroWind | ✅ | ✅ | ✅ | ✅ |
| Open SaaS | ✅ | ✅ | ❌ | ✅ |
Find SEO-optimized boilerplates on StarterPick.
Check out this boilerplate
View Next.js on StarterPick →