# 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 in one file.
## What's included
**Metadata**
- `buildMetadata(page, path?)` — returns a fully typed Next.js `Metadata` object with OG, Twitter Card, canonical, hreflang alternates, and robots directives
- `noIndexMetadata(title?)` — minimal metadata that tells crawlers to skip the page; use on dashboards, auth pages, admin routes
**JSON-LD schemas**
- `productJsonLd(opts)` — `Product` schema with `Offer`, optional `AggregateRating`; `priceValidUntil` auto-set to one year from build time
- `organizationJsonLd(opts?)` — `Organization` schema with `sameAs` links for Twitter and GitHub
- `articleJsonLd(opts)` — `TechArticle` schema with author, publisher, publish/update dates
- `breadcrumbJsonLd(items)` — `BreadcrumbList` from an array of `{ name, url }` pairs; relative URLs are prefixed with `BASE_URL` automatically
- `softwareAppJsonLd(opts)` — `SoftwareApplication` schema; useful for product landing pages
**Components & helpers**
- `JsonLd({ schema })` — injects a `<script type="application/ld+json">` tag; accepts output from any JSON-LD function above
- `buildSitemapEntry(path, opts?)` — returns a typed sitemap entry object with `url`, `lastModified`, `changeFrequency`, `priority`
**Template strings**
- `SITEMAP_TEMPLATE` — paste-ready content for `app/sitemap.ts`
- `ROBOTS_TEMPLATE` — paste-ready content for `app/robots.ts`
## Setup
### 1. Install dependencies
No extra packages — uses the Next.js 14 Metadata API only.
### 2. Environment variables
```
NEXT_PUBLIC_APP_URL=https://yourapp.com
NEXT_PUBLIC_SITE_NAME=YourApp
NEXT_PUBLIC_TWITTER_HANDLE=@yourhandle
```
### 3. Add sitemap and robots
Copy `SITEMAP_TEMPLATE` to `app/sitemap.ts` and `ROBOTS_TEMPLATE` to `app/robots.ts`, then adjust the paths array to match your routes.
## Usage examples
```tsx
// app/blocks/[id]/page.tsx — product page with full SEO
import { buildMetadata, productJsonLd, breadcrumbJsonLd, JsonLd } from '@/blocks/seo'
export function generateMetadata({ params }) {
return buildMetadata(
{
title: 'Auth System — $19',
description: 'Full NextAuth.js setup with RBAC, OAuth, and Supabase.',
image: '/api/og?title=Auth+System&price=19',
keywords: ['nextjs auth', 'nextauth supabase', 'rbac nextjs'],
type: 'website',
},
`/blocks/${params.id}`,
)
}
export default function BlockPage() {
return (
<>
<JsonLd schema={productJsonLd({ name: 'Auth System', description: '...', price: 19, currency: 'USD' })} />
<JsonLd schema={breadcrumbJsonLd([
{ name: 'Blocks', url: '/blocks' },
{ name: 'Auth System', url: '/blocks/auth' },
])} />
{/* page content */}
</>
)
}
```
```tsx
// app/dashboard/page.tsx — prevent indexing
import { noIndexMetadata } from '@/blocks/seo'
export const metadata = noIndexMetadata('Dashboard')
```
```tsx
// app/blog/[slug]/page.tsx — article with JSON-LD
import { buildMetadata, articleJsonLd, JsonLd } from '@/blocks/seo'
export function generateMetadata({ params }) {
return buildMetadata({
title: post.title,
description: post.excerpt,
type: 'article',
publishedAt: post.createdAt,
updatedAt: post.updatedAt,
authors: [post.author],
}, `/blog/${params.slug}`)
}
export default function BlogPost({ post }) {
return (
<>
<JsonLd schema={articleJsonLd({
headline: post.title,
description: post.excerpt,
publishedAt: post.createdAt,
author: post.author,
url: `/blog/${post.slug}`,
})} />
{/* content */}
</>
)
}
```
## Notes
- `BASE_URL`, `SITE_NAME`, and `TWITTER_HANDLE` are read from env vars at module load time — if those vars aren't set at build time the fallback strings (`yourapp.com`, etc.) will end up in your production metadata
- `JsonLd` returns `as any` to satisfy TypeScript — Next.js doesn't type `<script>` inside RSC returns; this is intentional, not a bug
- `SITEMAP_TEMPLATE` imports from `@/lib/blocksData` — update that import path (or replace with your own data source) before using it
- `productJsonLd` sets `priceValidUntil` to one year from `new Date()` at render time, not build time — fine for SSR, but if you statically generate product pages you may want to pass an explicit date