Launch NowDocs

SEO and metadata

Page metadata, Open Graph images, JSON-LD, sitemap, robots.txt, web manifest and icons, all driven by siteConfig.

Launch Now ships a complete SEO setup built on the Next.js Metadata API. Every tag, the sitemap, robots.txt, the web manifest and the default Open Graph image read from one object, siteConfig, so renaming the product (the demo name is Pulse) updates all of them at once.

How it works

FileRole
lib/config/site-config.tssiteConfig: name, tagline, description, URL, social links, locale, theme color, geo tags.
lib/seo/metadata.tscreateSeoHead(), the helper every page uses to build its Metadata.
lib/seo/index.tsRe-exports createSeoHead, so you import from @/lib/seo.
lib/seo/json-ld.tsdefaultJsonLd(): site-wide Organization and WebSite structured data.
lib/seo/config.tsRe-export of siteConfig, kept for older imports.
app/layout.tsxRoot metadata (title template, Open Graph, Twitter), viewport theme color and the JSON-LD script.
app/opengraph-image.tsxGenerated 1200×630 Open Graph image.
app/sitemap.ts/sitemap.xml: marketing pages, legal documents, blog posts and docs pages.
app/robots.ts/robots.txt: allows everything except private app routes.
app/manifest.ts/manifest.webmanifest for installable PWAs.
app/favicon.ico, public/Favicon and icon files.

All absolute URLs are built from siteConfig.url, which reads NEXT_PUBLIC_APP_URL and falls back to http://localhost:3000.

Set NEXT_PUBLIC_APP_URL to your production domain in your hosting environment. Otherwise canonical URLs, the sitemap, robots.txt and Open Graph tags point at localhost.

Root metadata

app/layout.tsx defines defaults that apply to every route. The title uses a template, so a page titled Pricing renders as Pricing | Pulse:

app/layout.tsx
export const metadata: Metadata = {
  title: {
    default: siteConfig.name,
    template: `%s | ${siteConfig.name}`,
  },
  description: siteConfig.description,
  metadataBase: new URL(siteConfig.url),
  // ... openGraph, twitter
  robots: {
    index: true,
    follow: true,
  },
}
 
export const viewport: Viewport = {
  themeColor: siteConfig.themeColor,
}

The home page (app/page.tsx) opts out of the template with an absolute title, Pulse — <tagline>, and sets a canonical URL of /.

Page metadata with createSeoHead

Use createSeoHead() from @/lib/seo in any page or layout. Only title is required:

app/pricing/page.tsx
import { createSeoHead } from "@/lib/seo"
import type { Metadata } from "next"
 
export const metadata: Metadata = createSeoHead({
  title: "Pricing",
  description:
    "Simple, transparent pricing for every stage. Start free, upgrade when you grow.",
})

For dynamic routes, return it from generateMetadata:

app/legal/[document]/page.tsx
export async function generateMetadata({
  params,
}: PageParams<Params>): Promise<Metadata> {
  const doc = getLegalDocument((await params).document)
  if (!doc) return {}
  return createSeoHead({
    title: doc.title,
    description: doc.description,
    canonical: `/legal/${doc.slug}`,
  })
}

Options

OptionTypeDefaultEffect
titlestringrequiredPage title, Open Graph and Twitter title.
descriptionstringsiteConfig.descriptionMeta, Open Graph and Twitter description.
imagestringnonePath or absolute URL of a 1200×630 image. Overrides the generated app/opengraph-image for this page.
noIndexbooleanfalseSets robots to index: false, follow: false.
canonicalstringnonePath (/pricing) or absolute URL. Sets alternates.canonical and openGraph.url.
localestringsiteConfig.localeOpen Graph locale.
type"website" | "article" | "profile""website"Open Graph type.
publishedTimestringnoneOpen Graph publishedTime.
authorstringnoneAdds authors.
tagsstring[]noneOpen Graph tags (only when non-empty).

Every call also sets metadataBase, twitter.card (summary_large_image), twitter.creator from siteConfig.creator, and the geo meta tags (geo.region, geo.placename, geo.position, ICBM) from siteConfig.geo.

app/robots.ts already disallows the private app routes. For an extra guarantee on a specific page, pass noIndex, as the dashboard does:

app/(app)/(dash)/dashboard/page.tsx
export const metadata: Metadata = createSeoHead({
  title: "Dashboard",
  description: `Track your business metrics, revenue, orders, and user activity in real time with the ${siteConfig.name} dashboard.`,
  type: "website",
  noIndex: true,
})

Extend the result

createSeoHead returns a plain Metadata object, so you can spread it and add fields. The changelog page adds an RSS alternate this way:

app/changelog/page.tsx
const seo = createSeoHead({
  title: "Changelog",
  description: `New features, improvements and fixes shipped to ${siteConfig.name}.`,
  canonical: "/changelog",
})
 
export const metadata: Metadata = {
  ...seo,
  alternates: {
    ...seo.alternates,
    types: { "application/rss+xml": "/changelog/rss.xml" },
  },
}

Good to know: Blog posts and docs pages (app/blog/[...slug]/page.tsx, app/docs/[[...slug]]/page.tsx) return title and description from the MDX frontmatter in generateMetadata instead of calling createSeoHead. They inherit the rest from the root layout.

Open Graph image

app/opengraph-image.tsx uses the file-based metadata convention, so Next.js serves it for every route that does not pass its own image. It renders a dark 1200×630 PNG with ImageResponse from next/og: the first letter of siteConfig.name as a logo, the name, the tagline and the description.

app/opengraph-image.tsx
export const alt = `${siteConfig.name}: ${siteConfig.tagline}`
export const size = { width: 1200, height: 630 }
export const contentType = "image/png"

To customize it, edit the JSX and inline styles in that file (ImageResponse supports a subset of CSS with flexbox layout). To use a static image for one page instead, pass image to createSeoHead.

Structured data (JSON-LD)

The root layout renders one application/ld+json script built by defaultJsonLd(). It contains two schema.org objects:

  • Organization with name, url, description and sameAs (the non-empty values of siteConfig.links.twitter and siteConfig.links.github).
  • WebSite with name, url and description.

The layout escapes < in the serialized JSON so a value containing </script> cannot close the tag early. To add page-specific structured data, add another function to lib/seo/json-ld.ts and render it in the page the same way.

Sitemap

app/sitemap.ts generates /sitemap.xml from three sources:

GroupURLschangeFrequencypriority
Marketing/weekly1
/pricingmonthly0.8
/changelogweekly0.5
/statusdaily0.4
/blog, /docsweekly0.6
/legalyearly0.2
Legal/legal/<slug> for each entry in legalDocumentsyearly0.2
ContentEvery page from blog.getPages() and source.getPages() (except the /blog and /docs index pages)monthly0.5

New blog posts, docs pages and legal documents appear automatically. When you add a new public marketing route, add it to the marketing array.

robots.txt

app/robots.ts
export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: "*",
      allow: "/",
      disallow: ["/dashboard", "/account", "/orgs", "/admin", "/api"],
    },
    sitemap: new URL("/sitemap.xml", siteConfig.url).toString(),
  }
}

If you add private top-level routes (the generated resource routes, for example), add them to disallow.

Web manifest

app/manifest.ts serves the PWA manifest. It uses siteConfig.name for name and short_name, siteConfig.description, siteConfig.themeColor for both background_color and theme_color, display: "standalone", and two icons: /android-chrome-192x192.png and /android-chrome-512x512.png.

Icons and images

FileUsed by
app/favicon.icoBrowser favicon (Next.js file convention).
public/android-chrome-192x192.png, public/android-chrome-512x512.pngWeb manifest icons.
public/icon.svgLogo on the auth pages (app/auth/layout.tsx).
public/apple-touch-icon.png, public/favicon-16x16.png, public/favicon-32x32.png, public/logo_L.svgShipped in public/ but not referenced in code or metadata.

To rebrand, replace these files with your own at the same paths and sizes. If you want Next.js to emit <link> tags for the Apple touch icon or an SVG icon, either declare them in the root metadata.icons or move them into app/ as apple-icon.png and icon.svg, which Next.js picks up by file convention.

Customize for your product

Update siteConfig

Edit lib/config/site-config.ts: name, tagline, description, links, creator, locale, themeColor and geo. See Configuration for every field.

Set the production URL

Set NEXT_PUBLIC_APP_URL (for example https://yourdomain.com) in your production environment.

Replace the icons

Swap app/favicon.ico and the PNG and SVG files in public/.

Review the Open Graph image

Adjust colors and layout in app/opengraph-image.tsx, then open /opengraph-image in the browser to preview it.

Next steps