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 |
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
- Delete
src/app/opengraph-image.tsxandsrc/app/twitter-image.tsx - Add your image as
public/opengraph-image.png(1200x630px) - Update
siteConfig.ogImageto/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`,
};
}
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
- Updated
siteConfigwith production values - Set
NEXT_PUBLIC_SITE_URLenvironment variable - Customized OG image or replaced with static version
- Added metadata to all public pages
- Updated sitemap with all public pages
- Verified robots.txt blocks private routes
- Added Google Search Console verification
- Tested with Rich Results Test
- Verified social cards with preview tools
- Ran Lighthouse SEO audit (aim for 100)
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.