SEO

Starterbase includes a comprehensive SEO system with dynamic Open Graph images, structured data, and metadata helpers.

Overview

Feature Description
Dynamic OG Images Auto-generated branded images for social sharing
Structured Data JSON-LD schemas for rich search results
Metadata Helpers One-liner page SEO configuration
Sitemap & Robots Dynamic generation with Next.js
Canonical URLs Automatic duplicate content prevention
💡 Quick Start

Update src/lib/seo/config.ts with your app details and everything propagates automatically.

File Structure

src/
├── app/
│   ├── opengraph-image.tsx    # Dynamic OG image (1200x630)
│   ├── twitter-image.tsx      # Dynamic Twitter card image
│   ├── sitemap.ts             # Dynamic sitemap.xml
│   ├── robots.ts              # Dynamic robots.txt
│   ├── manifest.ts            # PWA manifest
│   └── layout.tsx             # Root metadata + structured data
└── lib/seo/
    ├── config.ts              # Central SEO configuration
    ├── metadata.ts            # Page metadata helpers
    ├── structured-data.tsx    # JSON-LD schema generators
    └── index.ts               # Unified exports

Configuration

All SEO settings are centralized in src/lib/seo/config.ts:

export const siteConfig = {
  // App identity
  name: process.env.NEXT_PUBLIC_APP_NAME || 'Starterbase',
  tagline: 'Ship your app faster',
  description: 'Production-ready application template...',

  // URLs
  url: process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com',
  ogImage: '/opengraph-image',

  // Social (leave empty if not used)
  twitterHandle: '',
  social: {
    twitter: '',
    github: '',
    linkedin: '',
  },

  // Locale
  locale: 'en_US',
  locales: ['en_US'],

  // Theme
  themeColor: {
    light: '#ffffff',
    dark: '#09090b',
  },

  // Contact
  email: '',
  creator: { name: '', url: '' },
};

Environment Variables

NEXT_PUBLIC_APP_NAME=Your App Name
NEXT_PUBLIC_SITE_URL=https://yourapp.com

Open Graph Images

Dynamic OG image generation using Next.js ImageResponse API at /opengraph-image.

Customizing the Design

Edit src/app/opengraph-image.tsx:

export default async function OGImage() {
  return new ImageResponse(
    (
      <div style={{
        height: '100%',
        width: '100%',
        display: 'flex',
        backgroundColor: '#09090b', // Your brand color
      }}>
        {/* Your custom design */}
      </div>
    ),
    { width: 1200, height: 630 }
  );
}

Using a Static Image

  1. Delete src/app/opengraph-image.tsx and src/app/twitter-image.tsx
  2. Add your image as public/opengraph-image.png (1200x630px)
  3. Update siteConfig.ogImage to /opengraph-image.png

Page Metadata

Using the Metadata Helper

// src/app/pricing/page.tsx
import { generatePageMetadata } from '@/lib/seo/metadata';

export const metadata = generatePageMetadata({
  title: 'Pricing',
  description: 'Choose the perfect plan for your needs.',
  path: '/pricing',
});

Available Options

Option Type Description
title string Page title (appended with site name)
description string Meta description (150-160 chars)
path string URL path for canonical link
ogImage string Custom OG image path
ogTitle string Override OG title
ogDescription string Override OG description
publishedTime string Article publish date
modifiedTime string Article update date
authors string[] Article authors
keywords string[] Page keywords
noIndex boolean Prevent indexing (default: false)

Using Presets

import { pageMetadata } from '@/lib/seo/metadata';

// No-argument presets
export const metadata = pageMetadata.home();
export const metadata = pageMetadata.login();      // noIndex: true
export const metadata = pageMetadata.register();   // noIndex: true
export const metadata = pageMetadata.contact();
export const metadata = pageMetadata.privacy();
export const metadata = pageMetadata.terms();

// Optional description override
export const metadata = pageMetadata.pricing('Custom description');
export const metadata = pageMetadata.about('Custom about text');
export const metadata = pageMetadata.blog('Latest articles');

Dynamic Pages (Blog, Products)

// src/app/blog/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
  const post = await getPost(params.slug);

  return generatePageMetadata({
    title: post.title,
    description: post.excerpt,
    path: `/blog/${post.slug}`,
    ogImage: post.coverImage,
    publishedTime: post.publishedAt,
    modifiedTime: post.updatedAt,
    authors: [post.author.name],
  });
}

Structured Data (JSON-LD)

Available Schemas

Function Schema Type Use Case
getOrganizationSchema() Organization Company info
getWebsiteSchema() WebSite Site info
getWebPageSchema() WebPage Individual pages
getBreadcrumbSchema() BreadcrumbList Navigation path
getFAQSchema() FAQPage FAQ sections
getSoftwareApplicationSchema() SoftwareApplication Web applications

Adding to Pages

import { JsonLd, getFAQSchema } from '@/lib/seo/structured-data';

const faqs = [
  { question: 'How does billing work?', answer: 'We charge monthly...' },
  { question: 'Can I cancel anytime?', answer: 'Yes, you can cancel...' },
];

export default function FAQPage() {
  return (
    <>
      <JsonLd data={getFAQSchema(faqs)} />
      <div>Your FAQ content...</div>
    </>
  );
}

Adding Breadcrumbs

import { JsonLd, getBreadcrumbSchema } from '@/lib/seo/structured-data';

export default function ProductPage() {
  return (
    <>
      <JsonLd data={getBreadcrumbSchema([
        { name: 'Home', path: '/' },
        { name: 'Products', path: '/products' },
        { name: 'Pro Plan' }, // Current page (no path)
      ])} />
      <div>Product content...</div>
    </>
  );
}

Default Schemas

The root layout automatically includes Organization and WebSite schemas using values from siteConfig.

Sitemap

Dynamic sitemap at /sitemap.xml. Edit src/app/sitemap.ts:

export default function sitemap(): MetadataRoute.Sitemap {
  const baseUrl = siteConfig.url;

  return [
    { url: baseUrl, lastModified: new Date(), changeFrequency: 'monthly', priority: 1 },
    { url: `${baseUrl}/login`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.5 },
    { url: `${baseUrl}/register`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.5 },
    // Add more public pages...
  ];
}

Dynamic Sitemap (Blog, Products)

export default async function sitemap(): MetadataRoute.Sitemap {
  const baseUrl = siteConfig.url;
  const posts = await getPosts();

  const blogUrls = posts.map((post) => ({
    url: `${baseUrl}/blog/${post.slug}`,
    lastModified: new Date(post.updatedAt),
    changeFrequency: 'weekly' as const,
    priority: 0.6,
  }));

  return [
    { url: baseUrl, priority: 1 },
    { url: `${baseUrl}/blog`, priority: 0.8 },
    ...blogUrls,
  ];
}

Robots.txt

Dynamic robots file at /robots.txt. Edit src/app/robots.ts:

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
      disallow: [
        '/dashboard',
        '/dashboard/*',
        '/settings',
        '/settings/*',
        '/back-office',
        '/back-office/*',
        '/accept-invite',
        '/accept-invite/*',
      ],
    },
    sitemap: `${siteConfig.url}/sitemap.xml`,
  };
}
⚠️ Protected Routes

Always block authenticated routes (/dashboard, /settings, /back-office) to prevent indexing.

Search Console Verification

Add to src/app/layout.tsx metadata:

export const metadata: Metadata = {
  verification: {
    google: 'your-verification-code',
    yandex: 'yandex-code',  // optional
    bing: 'bing-code',      // optional
  },
};

Testing

Tool Purpose
Rich Results Test Test structured data
Meta Tags Preview Preview social cards
Facebook Debugger Debug OG tags
Twitter Card Validator Test Twitter cards
Lighthouse SEO audit

Local Testing

npm run build && npm run start

Verify: /sitemap.xml, /robots.txt, /opengraph-image, view page source for meta tags.

Pre-Launch Checklist

Common Issues

OG Image Not Updating: Social platforms cache aggressively. Use debug tools to force refresh:

Structured Data Errors: Use Rich Results Test. Common issues: missing required fields, invalid date formats (use ISO 8601), URL mismatches.

Pages Not Indexed: Check robots.txt isn't blocking, verify page is in sitemap, ensure noIndex isn't set, submit URL directly in Search Console.