Content · Starter

SEO Toolkit

Metadata, JSON-LD schemas, sitemap, and robots helpers for Next.js 14+ — covers product pages, articles, breadcrumbs, OG images, Twitter Cards, hreflang, and noindex.

Read the Getting Access guideif you haven't yet.

1. Get the file

Sign in at marrowstack.dev, open the SEO Toolkit block, and click Copy all files. Paste lib/seo.ts into your project. No runtime dependencies beyond Next.js.

2. Prerequisites

  • Next.js 14 or 15, App Router
  • TypeScript 5+

3. Install

No additional packages required. The block uses only Next.js built-ins.

4. Environment

VariableRequiredDefaultPurpose
NEXT_PUBLIC_APP_URLyesCanonical base URL of your site, e.g. https://yourdomain.com. Used in all absolute URLs.

5. Wire it in

app/products/[slug]/page.tsx
import { buildMetadata, buildProductJsonLd } from '@/lib/seo'
import type { Metadata } from 'next'

export async function generateMetadata({ params }): Promise<Metadata> {
  const product = await getProduct(params.slug)
  return buildMetadata({
    title: product.name,
    description: product.description,
    ogImage: product.imageUrl,
    canonical: `/products/${params.slug}`,
  })
}

export default async function ProductPage({ params }) {
  const product = await getProduct(params.slug)
  const jsonLd = buildProductJsonLd({
    name: product.name,
    description: product.description,
    price: product.price,
    currency: 'USD',
    imageUrl: product.imageUrl,
    url: `/products/${params.slug}`,
  })

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      {/* page content */}
    </>
  )
}
app/sitemap.ts
import { generateSitemap } from '@/lib/seo'
import { getAllProducts } from '@/lib/db'

export default async function sitemap() {
  const products = await getAllProducts()
  return generateSitemap([
    { url: '/', lastModified: new Date() },
    { url: '/products', lastModified: new Date() },
    ...products.map((p) => ({
      url: `/products/${p.slug}`,
      lastModified: new Date(p.updatedAt),
    })),
  ])
}
app/robots.ts
import { robotsTxt } from '@/lib/seo'
export default function robots() { return robotsTxt() }

6. Verify it works

  1. Visit /sitemap.xml and confirm it returns valid XML with your page URLs.
  2. Visit /robots.txt and confirm the sitemap URL is present.
  3. Open a product page and inspect the <head> — confirm OG tags and JSON-LD are present.
  4. Paste a page URL into the Google Rich Results Test and confirm it detects the schema.

7. Failure modes & fixes

Sitemap shows localhost URLs

Cause: NEXT_PUBLIC_APP_URL is set to http://localhost:3000.

Fix: Set NEXT_PUBLIC_APP_URL to your production domain on Vercel/your host. The sitemap uses this as the base URL.

OG image not rendering in social previews

Cause: The ogImage URL is relative. Social crawlers can't resolve relative URLs.

Fix: Pass absolute URLs to buildMetadata(). The block prepends NEXT_PUBLIC_APP_URL automatically when given a relative path.

JSON-LD not picked up by Google

Cause: The script tag is in the body instead of the head, or contains invalid JSON.

Fix: Place the script tag in the page component (not layout). Next.js App Router moves script tags to the head automatically.