SEO Overview

Built-in SEO features in Nebutra-Sailor — Next.js App Router metadata, OG image generation, structured data, and i18n hreflang support.

What is built in

Nebutra-Sailor ships with a complete SEO foundation out of the box. You do not need to install third-party SEO libraries; everything is built on the Next.js 16 App Router metadata system.

FeatureStatusLocation
Static metadataBuilt inexport const metadata in page files
Dynamic metadataBuilt ingenerateMetadata() in page files
OG image generationBuilt in@vercel/og via opengraph-image.tsx convention
Robots.txtBuilt inapps/landing/src/app/robots.ts
XML sitemapBuilt inapps/landing/src/app/sitemap.ts
Canonical URLsBuilt inSet via metadata.alternates.canonical
Structured data (JSON-LD)Built inInjected via <script type="application/ld+json"> in layout
i18n hreflangBuilt innext-intl + metadata.alternates.languages
Blog SEOBuilt inSanity content with auto-generated OG images

Next.js App Router metadata API

Next.js 16 provides two ways to export metadata from any page.tsx or layout.tsx:

Static metadata

For pages with fixed titles and descriptions, export a metadata object:

import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "Nebutra — AI-native SaaS Platform",
  description: "Build and ship AI-powered SaaS products faster with Nebutra.",
};

Dynamic metadata

For pages that depend on route parameters (e.g., a blog post slug), use generateMetadata:

import type { Metadata } from "next";
import { getPost } from "@/lib/sanity";

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string; lang: string }>;
}): Promise<Metadata> {
  const { slug, lang } = await params;
  const post = await getPost(slug, lang);
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      images: [post.ogImage],
    },
  };
}

OG image generation

Nebutra uses Vercel's @vercel/og library to generate Open Graph images at the edge. OG images are defined using the Next.js file-system convention:

apps/landing/src/app/[lang]/blog/[slug]/opengraph-image.tsx

The opengraph-image.tsx file exports an ImageResponse that renders a React component to a PNG image at request time, using the Satori renderer.

import { ImageResponse } from "next/og";

export const runtime = "edge";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";

export default async function OgImage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  // fetch post title, etc.
  return new ImageResponse(
    <div style={{ /* layout */ }}>
      {/* content */}
    </div>,
    { ...size }
  );
}

The runtime = "edge" export is required for @vercel/og. Without it, OG image generation runs on the Node.js runtime, which is significantly slower.

Structured data (JSON-LD)

Structured data is injected in apps/landing/src/app/[lang]/layout.tsx as a <script> tag. The root layout includes Organization and Website schemas. Individual pages (e.g., blog posts) can inject additional schemas using the same pattern.

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const jsonLd = {
    "@context": "https://schema.org",
    "@type": "Organization",
    name: "Nebutra",
    url: "https://nebutra.com",
  };

  return (
    <html>
      <body>
        <script
          type="application/ld+json"
          // biome-ignore lint/security/noDangerouslySetInnerHtml: structured data is safe static JSON
          dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
        />
        {children}
      </body>
    </html>
  );
}

i18n SEO with hreflang

The landing page supports 7 locales via next-intl. Each page automatically generates alternates.languages metadata so search engines understand the language relationship between URLs.

export async function generateMetadata({
  params,
}: {
  params: Promise<{ lang: string }>;
}): Promise<Metadata> {
  const { lang } = await params;
  return {
    alternates: {
      canonical: `https://nebutra.com/${lang}`,
      languages: {
        "en": "https://nebutra.com/en",
        "zh": "https://nebutra.com/zh",
        "ja": "https://nebutra.com/ja",
        // ... other locales
      },
    },
  };
}

Search engines use these hreflang annotations to serve the correct language version to users.

Blog SEO with Sanity

When FEATURE_BLOG=true, blog posts are fetched from Sanity CMS. Each post document includes:

  • title and excerpt fields used for <title> and <meta name="description">
  • An ogImage asset reference used for OG image <meta> tags
  • A publishedAt field used for article:published_time

Sanity-powered blog posts automatically include Article structured data.

How is this guide?

Edit on GitHub

Last updated on

On this page