Next Starter Logo
Reference

SEO

How Next Starter handles metadata, Open Graph, Twitter Cards, sitemaps, robots.txt, and JSON-LD structured data from a single config layer in lib/config.ts.

SEO is how search engines and social apps understand your site. This starter handles it from one place. You set your app details once in lib/config.ts, and everything below is generated for you:

  • Metadata: the page title, description, and the Open Graph / Twitter tags that control how a link looks when shared.
  • Canonical URLs: tell search engines the one "real" address for a page, so duplicates don't compete.
  • Sitemap and robots.txt: a map of your pages for crawlers, plus rules for which pages to skip.
  • JSON-LD structured data: machine-readable facts about your site (its name, FAQ answers, price range) that help search engines show rich results.

All of it derives from APP_CONFIG and a few helpers in lib/config.ts.

APP_CONFIG

lib/config.ts exports APP_CONFIG, the single source of truth for SEO output. Edit these keys for your app:

KeyUsed for
nameApp name in titles, OG site_name, schema, publisher
descriptionDefault meta description and Organization schema
emailContact address surfaced elsewhere in the app
assets.images.defaultOpen Graph / Twitter image (/og-image.png, 1200×630)
assets.images.iconSchema image/logo (/icon-512.png)
assets.images.faviconFavicon + shortcut (/icon.svg)
assets.images.appleTouchIconApple touch icon
assets.images.maskIconSafari pinned-tab mask icon
social.twittertwitter:site handle + Organization sameAs
social.githubOrganization sameAs link
noIndexRoutesPaths to block from crawlers (see below)
theme.colors.backgroundLight/dark theme-color via generateViewport()
development.baseUrl / production.baseUrlBase URL for canonical, OG, sitemap, schema

getBaseUrl() returns the production base URL when NODE_ENV === "production", otherwise the development one. Every absolute URL in the SEO layer is built from it.

The asset config is a flat assets.images map of icon/OG paths. There is no branding block. Store your actual image files in public/ under the names referenced above (og-image.png, icon-512.png, icon.svg, apple-touch-icon.png, icon-mask.png).

Per-page metadata

Metadata is the set of tags in a page's <head> that describe it: the title, description, the Open Graph (OG) and Twitter tags for link previews, robots directives, the canonical URL, and icons.

Each page exports a metadata object built with generateMeta(). One call returns a complete Next.js Metadata object with all of those filled in.

// app/(site)/pricing/page.tsx
import { generateMeta } from "@/lib/config";
import type { Metadata } from "next";

export const metadata: Metadata = generateMeta({
  title: "Pricing",
  description: "Simple, transparent pricing for every team size.",
  pathname: "/pricing",
});

The root app/layout.tsx calls generateMeta({ pathname: "/" }) for site-wide defaults, and also exports viewport from generateViewport().

generateMeta options

OptionTypeDefaultNotes
pathnamestringrequiredBuilds the canonical and OG URL from the base URL
titlestringRendered as Title - App Name; omit for just the app name
descriptionstringAPP_CONFIG.descriptionMeta + OG + Twitter description
type"website" | "article""website"Use "article" for blog posts
imagestringassets.images.defaultRoot-relative path or absolute URL; relative paths get the base URL prefixed
noIndexbooleanfalseEmits noindex, nofollow robots directives
noCanonicalbooleanfalseOmits the canonical tag (e.g. paginated lists)
titleFirstbooleanfalseReverses to App Name - Title
alternateLanguagesRecord<string, string>{}hreflang alternate URLs

Canonical URLs are set automatically from pathname + base URL, with no manual work unless you pass noCanonical: true.

Dynamic metadata

For pages whose title comes from data (e.g. a blog post), export an async generateMetadata() and call generateMeta() inside it:

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);
  return generateMeta({
    title: post.title,
    description: post.excerpt,
    pathname: `/blog/${slug}`,
    type: "article",
    image: post.ogImage ?? undefined,
  });
}

Custom OG image per page: pass image with an absolute URL or root-relative path. The default 1200×630 image lives at public/og-image.png.

Sitemap and robots.txt

A sitemap is a list of your pages that search engines read to find and crawl them. robots.txt tells crawlers which paths to skip.

app/sitemap.ts uses Next.js split sitemaps (each sitemap has an id and its own file). The static publicPages array (path, priority, changeFrequency) is mapped to URLs against the base URL, served at /sitemap/pages.xml.

Add pages: append to publicPages. Add a second sitemap (e.g. blog): return another { id: "blog" } from generateSitemaps() and handle that id in the default export, fetching slugs and mapping them to URLs.

app/robots.ts generates robots.txt from APP_CONFIG.noIndexRoutes: each route is converted (/:path*/*) into a Disallow rule, with the sitemap URL appended.

Blocking routes from indexing

noIndexRoutes in APP_CONFIG is the single list that drives both crawler blocking surfaces:

  • app/robots.ts turns it into Disallow rules.
  • next.config.ts attaches an X-Robots-Tag: noindex, nofollow response header to each listed route.

Add a path here to block it everywhere at once. Defaults cover /auth/*, /api/*, /dashboard/*, /onboarding, /privacy, /terms, /500, /error, and /auth/error.

JSON-LD structured data

Structured data is machine-readable facts about a page, written in a format search engines understand (JSON-LD). It powers rich results: your site name in the knowledge panel, FAQ accordions in search, star-rated breadcrumbs. It doesn't change what users see on the page; it only describes the page to crawlers.

lib/config.ts exports four schema generators. Each returns a plain object you serialize into a <script type="application/ld+json"> tag.

GeneratorSchema typeWhere it rendersData source
generateOrganizationSchema()Organizationapp/layout.tsx (every page)APP_CONFIG name, url, icon, social
generateWebApplicationSchema()WebApplicationHome and pricing pagesAPP_CONFIG + lib/pricing.ts (price range, features)
generateBreadcrumbSchema(items)BreadcrumbListAbout, contact, pricing pagesitems: { name, href }[] you pass
generateFAQSchema(faqs)FAQPageHome and pricing pagesfaqs: { question, answer }[] you pass

Render one like this:

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
    __html: JSON.stringify(generateWebApplicationSchema()),
  }}
/>

generateWebApplicationSchema() pulls its offers price range (lowPrice/highPrice/offerCount) and featureList from lib/pricing.ts via getLowestPrice(), getHighestPrice(), getAllFeatures(), and PRICING_TIERS. Update your tiers there and the schema follows.

Add another structured-data type (e.g. Article, Product): write a new generator in lib/config.ts returning a schema object, then render it on the relevant page the same way. Use the existing generators as templates.

Viewport and theme color

generateViewport() (exported as viewport from app/layout.tsx) sets the viewport meta and a theme-color per color scheme, pulled from APP_CONFIG.theme.colors.background.light / .dark. This controls the mobile browser chrome color. Edit those two values to change it.

Where it lives

FileResponsibility
lib/config.tsAPP_CONFIG, generateMeta(), generateViewport(), the four schema generators, getBaseUrl()
lib/pricing.tsPrice range + feature list feeding the WebApplication schema
app/layout.tsxSite-wide metadata, viewport, Organization schema
app/sitemap.tsSplit sitemaps
app/robots.tsrobots.txt from noIndexRoutes
next.config.tsX-Robots-Tag headers from noIndexRoutes
public/OG image and icon assets

On this page