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
| Variable | Required | Default | Purpose |
|---|---|---|---|
| NEXT_PUBLIC_APP_URL | yes | — | Canonical base URL of your site, e.g. https://yourdomain.com. Used in all absolute URLs. |
5. Wire it in
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 */}
</>
)
}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),
})),
])
}import { robotsTxt } from '@/lib/seo'
export default function robots() { return robotsTxt() }6. Verify it works
- Visit
/sitemap.xmland confirm it returns valid XML with your page URLs. - Visit
/robots.txtand confirm the sitemap URL is present. - Open a product page and inspect the
<head>— confirm OG tags and JSON-LD are present. - 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.