Documentation

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.

app/[locale]/page.tsx
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:

app/layout.tsx
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:

app/blog/[slug]/page.tsx
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:

app/blog/[slug]/page.tsx
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:

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:

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
});