Back to Help Center
Customization

SEO Guide

SEO Guide

This guide covers Search Engine Optimization (SEO) configuration for your SaaS application.

Table of Contents

  1. Built-in SEO Features
  2. Page Metadata
  3. Sitemap Configuration
  4. Robots.txt Configuration
  5. Open Graph & Social Sharing
  6. Structured Data
  7. Performance Optimization
  8. SEO Checklist

Built-in SEO Features

The boilerplate includes these SEO features out of the box:

| Feature | File | Description | |---------|------|-------------| | Sitemap | /app/sitemap.ts | Auto-generated XML sitemap | | Robots.txt | /app/robots.ts | Search engine crawl rules | | Meta tags | /app/layout.tsx | Global meta configuration | | OG Images | /app/opengraph-image.png | Social sharing images | | Page metadata | Each page.tsx | Per-page SEO settings |


Page Metadata

Global Metadata

Edit /app/layout.tsx for site-wide defaults:

import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: {
    default: 'Your SaaS Name',
    template: '%s | Your SaaS Name',  // Page titles become "Page | Your SaaS Name"
  },
  description: 'Your main site description for search engines (150-160 characters ideal).',
  keywords: ['saas', 'your', 'keywords', 'here'],
  authors: [{ name: 'Your Name' }],
  creator: 'Your Company',
  metadataBase: new URL('https://yourdomain.com'),
  openGraph: {
    type: 'website',
    locale: 'en_US',
    url: 'https://yourdomain.com',
    siteName: 'Your SaaS Name',
    title: 'Your SaaS Name',
    description: 'Your description for social sharing.',
    images: [
      {
        url: '/og.png',
        width: 1200,
        height: 630,
        alt: 'Your SaaS Name',
      },
    ],
  },
  twitter: {
    card: 'summary_large_image',
    title: 'Your SaaS Name',
    description: 'Your description for Twitter.',
    images: ['/og.png'],
    creator: '@yourusername',
  },
  robots: {
    index: true,
    follow: true,
  },
};

Per-Page Metadata

Each page can override the defaults:

// /app/pricing/page.tsx
export const metadata = {
  title: 'Pricing',  // Becomes "Pricing | Your SaaS Name"
  description: 'Simple, transparent pricing. Start free, upgrade when you need.',
  openGraph: {
    title: 'Pricing - Your SaaS Name',
    description: 'Choose the perfect plan for your needs.',
  },
};

export default function PricingPage() {
  // ...
}

Dynamic Metadata

For dynamic pages like blog posts:

// /app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
import { getPostContent } from '@/lib/mdx';

interface Props {
  params: { slug: string };
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const post = getPostContent(params.slug);

  if (!post) {
    return { title: 'Post Not Found' };
  }

  return {
    title: post.title,
    description: post.description,
    openGraph: {
      title: post.title,
      description: post.description,
      type: 'article',
      publishedTime: post.date,
      authors: [post.author.name],
      images: post.image ? [post.image] : undefined,
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.description,
    },
  };
}

Sitemap Configuration

The sitemap is automatically generated at /sitemap.xml.

Default Configuration

Edit /app/sitemap.ts to customize:

import { MetadataRoute } from 'next';
import { getAllPosts } from '@/lib/mdx';

export default function sitemap(): MetadataRoute.Sitemap {
  const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://yourdomain.com';

  // Get dynamic content (blog posts)
  const posts = getAllPosts();
  const blogUrls = posts.map((post) => ({
    url: `${baseUrl}/blog/${post.slug}`,
    lastModified: new Date(post.date),
    changeFrequency: 'monthly' as const,
    priority: 0.6,
  }));

  // Static pages
  const staticPages = [
    {
      url: baseUrl,
      lastModified: new Date(),
      changeFrequency: 'weekly' as const,
      priority: 1,
    },
    {
      url: `${baseUrl}/pricing`,
      lastModified: new Date(),
      changeFrequency: 'weekly' as const,
      priority: 0.9,
    },
    // Add more pages...
  ];

  return [...staticPages, ...blogUrls];
}

Priority Guidelines

| Priority | Use For | |----------|---------| | 1.0 | Homepage | | 0.9 | Key conversion pages (pricing) | | 0.8 | Important content (blog index) | | 0.6-0.7 | Regular content pages | | 0.5 | Utility pages (login, signup) |

Change Frequency

| Frequency | Use For | |-----------|---------| | daily | Frequently updated pages | | weekly | Regularly updated content | | monthly | Static content | | yearly | Rarely changed pages |


Robots.txt Configuration

Edit /app/robots.ts:

import { MetadataRoute } from 'next';

export default function robots(): MetadataRoute.Robots {
  const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://yourdomain.com';

  return {
    rules: [
      {
        userAgent: '*',
        allow: '/',
        disallow: [
          '/api/',           // API routes
          '/dashboard/',     // Private pages
          '/auth/callback',  // Auth routes
          '/auth/confirm',
          '/auth/error',
          '/protected/',
        ],
      },
    ],
    sitemap: `${baseUrl}/sitemap.xml`,
  };
}

What to Block

Always block:

  • API routes (/api/)
  • Authenticated areas (/dashboard/)
  • Auth callback URLs
  • Admin panels
  • User-specific pages

Open Graph & Social Sharing

Creating OG Images

  1. Static OG Image: Replace /public/og.png (1200x630 pixels)

  2. Dynamic OG Images: Use Next.js ImageResponse:

// /app/opengraph-image.tsx
import { ImageResponse } from 'next/og';

export const runtime = 'edge';
export const alt = 'Your SaaS Name';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';

export default async function Image() {
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 64,
          background: 'linear-gradient(to bottom, #000, #111)',
          color: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        Your SaaS Name
      </div>
    ),
    { ...size }
  );
}

Testing Social Sharing

Use these tools to preview how your pages appear when shared:


Structured Data

Add JSON-LD structured data for rich search results:

Organization Schema

// /app/layout.tsx
export default function RootLayout({ children }) {
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Organization',
    name: 'Your SaaS Name',
    url: 'https://yourdomain.com',
    logo: 'https://yourdomain.com/logo.png',
    sameAs: [
      'https://twitter.com/yourusername',
      'https://github.com/yourusername',
    ],
  };

  return (
    <html>
      <head>
        <script
          type="application/ld+json"
          dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

Product/SaaS Schema

// /app/pricing/page.tsx
const jsonLd = {
  '@context': 'https://schema.org',
  '@type': 'SoftwareApplication',
  name: 'Your SaaS Name',
  applicationCategory: 'BusinessApplication',
  offers: {
    '@type': 'AggregateOffer',
    lowPrice: '0',
    highPrice: '99',
    priceCurrency: 'USD',
    offerCount: '3',
  },
};

Blog Post Schema

// For blog posts
const jsonLd = {
  '@context': 'https://schema.org',
  '@type': 'BlogPosting',
  headline: post.title,
  description: post.description,
  datePublished: post.date,
  author: {
    '@type': 'Person',
    name: post.author.name,
  },
};

Performance Optimization

Page speed is an SEO ranking factor.

Built-in Optimizations

The boilerplate already includes:

  • Next.js Image Optimization: Use next/image for automatic optimization
  • Font Optimization: Google Fonts are self-hosted
  • Code Splitting: Automatic per-page code splitting
  • Static Generation: Marketing pages are pre-rendered

Additional Tips

  1. Optimize images before uploading:

    • Use WebP format when possible
    • Compress with TinyPNG
    • Use appropriate dimensions
  2. Lazy load below-fold content:

import dynamic from 'next/dynamic';

const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
  loading: () => <p>Loading...</p>,
});
  1. Monitor Core Web Vitals:

SEO Checklist

Before Launch

  • [ ] Set production URL in NEXT_PUBLIC_APP_URL
  • [ ] Update siteConfig with real company info
  • [ ] Create custom OG image (1200x630)
  • [ ] Replace favicon and app icons
  • [ ] Write unique meta descriptions for key pages
  • [ ] Verify sitemap at /sitemap.xml
  • [ ] Verify robots.txt at /robots.txt
  • [ ] Test OG tags with social sharing debuggers

After Launch

  • [ ] Submit sitemap to Google Search Console
  • [ ] Submit sitemap to Bing Webmaster Tools
  • [ ] Set up Google Analytics
  • [ ] Monitor Core Web Vitals
  • [ ] Check for crawl errors regularly

Ongoing

  • [ ] Create regular blog content
  • [ ] Update meta descriptions for new pages
  • [ ] Monitor search rankings
  • [ ] Fix any reported issues in Search Console
  • [ ] Keep sitemap up to date (automatic with Next.js)

File Locations Summary

| File | Purpose | |------|---------| | /app/layout.tsx | Global metadata, structured data | | /app/sitemap.ts | Sitemap generation | | /app/robots.ts | Crawl rules | | /app/opengraph-image.png | Default OG image | | /config/site.ts | Site name, description, URLs | | /public/favicon.ico | Browser favicon | | /public/og.png | Social sharing image |


Tools & Resources

Google Tools

Other Tools

Learning Resources