SEO
Learn how to optimize your Sushify Next.js application for search engines
Sushify Next.js includes comprehensive SEO features built on Next.js 15's App Router, providing excellent search engine optimization out of the box.
Features
- Metadata API: Type-safe metadata configuration
- Sitemap Generation: Automatic sitemap creation
- Robots.txt: Search engine crawler configuration
- Open Graph: Social media sharing optimization
- Canonical URLs: Prevent duplicate content
- Image Optimization: Automatic image optimization
Metadata Configuration
Basic Metadata
The metadata utility is located at apps/web/modules/seo/metadata.ts.
import { createMetadata } from "@seo";
export const metadata = createMetadata({
title: "Home",
description: "Welcome to our amazing SaaS platform",
keywords: ["saas", "nextjs", "productivity"],
path: "/",
locale: "en", // Optional: specify locale
});Root Layout Metadata
For root layout files, use the isRootLayout flag to set up the title template:
import { createMetadata } from "@seo";
export const metadata = createMetadata({
isRootLayout: true, // This sets up the title template
});This generates a title template that will be used across all pages: %s | Your App Name.
Dynamic Metadata
For dynamic pages with server-side data, you can use the generateDynamicMetadata helper:
import { generateDynamicMetadata } from "@seo";
export async function generateMetadata({ params }) {
return generateDynamicMetadata(
async () => {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
image: post.coverImage,
keywords: post.tags,
};
},
{
path: `/blog/${params.slug}`,
locale: params.locale,
}
);
}Or use createMetadata directly:
import { createMetadata } from "@seo";
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
return createMetadata({
title: post.title,
description: post.excerpt,
image: post.coverImage,
path: `/blog/${params.slug}`,
});
}Open Graph
Configure Open Graph tags for social sharing. The image parameter supports multiple formats:
String format (simple):
export const metadata = createMetadata({
title: "Amazing Feature",
description: "Discover our latest feature",
image: "https://yourdomain.com/og-image.png",
path: "/features/amazing",
});Object format (with dimensions):
export const metadata = createMetadata({
title: "Amazing Feature",
description: "Discover our latest feature",
image: {
url: "https://yourdomain.com/og-image.png",
width: 1200,
height: 630,
alt: "Amazing Feature Preview",
},
path: "/features/amazing",
});Array format (multiple images):
export const metadata = createMetadata({
title: "Amazing Feature",
description: "Discover our latest feature",
image: [
{
url: "https://yourdomain.com/og-image-1.png",
width: 1200,
height: 630,
alt: "Feature Overview",
},
{
url: "https://yourdomain.com/og-image-2.png",
width: 1200,
height: 630,
alt: "Feature Details",
},
],
path: "/features/amazing",
});This generates:
<meta property="og:title" content="Amazing Feature" />
<meta property="og:description" content="Discover our latest feature" />
<meta property="og:image" content="https://yourdomain.com/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://yourdomain.com/features/amazing" />Twitter Cards
Twitter card metadata is automatically included:
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Amazing Feature" />
<meta name="twitter:description" content="Discover our latest feature" />
<meta name="twitter:image" content="https://yourdomain.com/og-image.png" />Sitemap
Automatic Sitemap Generation
Create a sitemap at app/sitemap.ts:
import { MetadataRoute } from "next";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://yourdomain.com";
// Static pages
const staticPages = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: "daily" as const,
priority: 1,
},
{
url: `${baseUrl}/pricing`,
lastModified: new Date(),
changeFrequency: "weekly" as const,
priority: 0.8,
},
];
// Dynamic pages (blog posts, etc.)
const posts = await db.post.findMany({
select: { slug: true, updatedAt: true },
});
const dynamicPages = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: "monthly" as const,
priority: 0.6,
}));
return [...staticPages, ...dynamicPages];
}Robots.txt
Basic Configuration
Create 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/", "/admin/", "/app/"],
},
],
sitemap: `${baseUrl}/sitemap.xml`,
};
}Canonical URLs
Prevent duplicate content with canonical URLs:
export const metadata = createMetadata({
title: "Page Title",
description: "Description",
path: "/canonical-path", // Canonical URL
});