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.

Most of what search engines and social apps learn about your site passes through one file. lib/config.ts holds APP_CONFIG plus the helpers that turn it into page metadata and Open Graph tags, canonical URLs, the sitemap, robots.txt, and JSON-LD structured data.

APP_CONFIG

lib/config.ts exports APP_CONFIG, the defaults the SEO helpers read from. Edit these keys for your app:

KeyUsed for
nameApp name in titles, OG site_name, schema, publisher
descriptionDefault meta description and Organization schema
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 (/apple-touch-icon.png)
assets.images.maskIconSafari pinned-tab mask icon (/icon-mask.png), also the maskable manifest icon
social.twittertwitter:site handle + Organization sameAs
social.githubOrganization sameAs link
noIndexRoutesPaths that get an X-Robots-Tag: noindex header (see below)
theme.colors.backgroundLight/dark theme-color via generateViewport(), plus the manifest colors
theme.colors.primaryColor attached to the Safari mask-icon link
development.baseUrl / production.baseUrlBase URL for canonical, OG, sitemap, schema

Each image path above names a file that already ships in public/, so the quickest rebrand is to overwrite those files and leave the config alone.

getBaseUrl() returns the production base URL when NODE_ENV === "production" and the development one otherwise. Canonical, Open Graph, sitemap and schema URLs are built on it. production.baseUrl ships as a placeholder, so change it before you deploy or every one of those URLs points at the example domain.

Per-page metadata

Each page exports a metadata object built with generateMeta(). One call fills in the title, description, Open Graph and Twitter tags, robots directives, canonical URL, and icons.

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

export const metadata: Metadata = generateMeta({
  title: "Pricing",
  description:
    "Pick the plan that fits, upgrade when you outgrow it, and cancel whenever you want.",
  pathname: "/pricing",
});

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

generateMeta options

OptionTypeDefaultNotes
pathnamestringrequiredBuilds the canonical and OG URL from the base URL
titlestringnoneRendered 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.defaultAbsolute URL, or a root-relative path that gets the base URL prefixed (anything not starting with http)
noIndexbooleanfalseEmits noindex, follow robots directives (the page stays out of the index, its links still get crawled)
noCanonicalbooleanfalseOmits the canonical tag (e.g. paginated lists)
titleFirstbooleanfalseReverses to App Name - Title
alternateLanguagesRecord<string, string>{}hreflang alternate URLs

Route every page through generateMeta(). Next merges child metadata over the parent's one key at a time, so a page that exports a bare { title, description } inherits whatever canonical the root layout set, which is /. That page then tells search engines it's a duplicate of your homepage, and nothing in the build warns you.

Dynamic metadata

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

// a route you add, e.g. app/(site)/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,
  });
}

Sitemap and robots.txt

app/sitemap.ts uses Next.js split sitemaps (each sitemap has an id and its own URL). It maps the static publicPages array (path, priority, changeFrequency) to URLs against the base URL, and Next serves the result at /sitemap/pages.xml. Out of the box it lists four marketing pages: /, /about, /contact, and /pricing.

To add pages, append to publicPages. For a second sitemap, say a blog, return another { id: "blog" } from generateSitemaps() and handle that id in the default export, where you fetch the slugs and map them to URLs.

app/robots.ts generates a robots.txt with a single Allow: / rule for every user agent and the /sitemap/pages.xml URL. There are no Disallow rules, and that's on purpose. A route crawlers can't fetch can't show them its noindex header either, and a blocked-but-linked URL can still end up indexed without content.

Blocking routes from indexing

noIndexRoutes in APP_CONFIG is the list of route patterns that next.config.ts gives an X-Robots-Tag: noindex response header. Crawlers stay free to fetch those routes, read the header, and follow the links on them; they just keep the pages out of search results.

Add a path here to keep it out of the index. The shipped list is in lib/config.ts: auth, API, dashboard, onboarding, plus /privacy and /terms, so drop those two if you want your legal pages indexed.

The list doesn't touch page metadata, though. To put a noindex tag in one page's <head>, pass noIndex: true to that page's generateMeta() call.

JSON-LD structured data

Structured data is what makes a page eligible for rich results: your site name in the knowledge panel, FAQ accordions under a search listing, a breadcrumb trail where the raw URL would be. lib/config.ts exports four generators for it. 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. Its description and applicationCategory, though, live inline in the generator instead of reading from APP_CONFIG, so edit those in lib/config.ts when you rebrand.

For another structured-data type, say Article or Product, write a new generator in lib/config.ts that returns a schema object, then render it on the relevant page the same way.

Viewport, theme color, and manifest

generateViewport() (exported as viewport from app/layout.tsx) sets the viewport meta plus a theme-color for each color scheme, taken from APP_CONFIG.theme.colors.background.light and .dark. Those two values are what tint the mobile browser chrome.

app/manifest.ts serves /manifest.webmanifest, which browsers read when someone installs your site to a home screen. It reuses background.light for both background_color and theme_color, and declares three icons. Two come from assets.images. The third, /icon-192.png, is hardcoded, so it has to sit in public/ even though no config key points at it. next.config.ts caches the manifest response for a day.

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 (allow all, sitemap URL)
app/manifest.ts/manifest.webmanifest for installable/home-screen use
next.config.tsX-Robots-Tag headers from noIndexRoutes
public/OG image and icon assets

On this page