| name | seo-nextjs |
| description | Next.js 16 App Router SEO โ Metadata API, canonical/hreflang, OpenGraph, JSON-LD, sitemap/robots |
SEO โ Next.js 16 App Router
์์ค:
๊ฒ์ฆ์ผ: 2026-08-11
๊ฒ์ฆ ๋์ ๋ฒ์ : Next.js 16.3.0 (2026-08-03 ๋ฆด๋ฆฌ์ฆ, ํ์ฌ ์ต์ stable)
์ด ์คํฌ์ SEO ๋ฉํ/๊ตฌ์กฐํ ๋ฐ์ดํฐ ๋ฒ์๋ง ๋ค๋ฃฌ๋ค. App Router ์ผ๋ฐ(๋ฐ์ดํฐ ํ์นญยท์บ์ฑ ๋ชจ๋ธยทproxy ๋ฑ)์
frontend/nextjs, URL ์ ๊ทํยท๋ฆฌ๋ค์ด๋ ํธ๋ frontend/url-canonicalization-redirects ์คํฌ์ ์ฐธ์กฐํ๋ค.
0. Next.js 15 โ 16์์ SEO์ ์ํฅ ์๋ ๋ณ๊ฒฝ
| ํญ๋ชฉ | 15 | 16 |
|---|
params / searchParams | Promise (๋๊ธฐ ์ ๊ทผ ์์ ํ์ฉ) | Promise, ๋๊ธฐ ํธํ ์์ ์ ๊ฑฐ |
generateSitemaps์ id | number ๋๊ธฐ ์ ๋ฌ | Promise<string> โ await ํ์ (v16.0.0) |
opengraph-image / twitter-image / icon / apple-icon | paramsยทid ๋๊ธฐ | paramsยทid๊ฐ Promise (generateImageMetadata์ params๋ ๋๊ธฐ ์ ์ง) |
robots.ts | ํ์ค ํ๋๋ง | other ํ๋ ์ถ๊ฐ โ ๋นํ์ค per-agent ๋๋ ํฐ๋ธ (v16.3.0) |
| ์บ์ฑ | ์๋ฌต์ 4๊ณ์ธต | cacheComponents: true ์ generateMetadata๋ ์บ์ฑ ๊ท์น ์ ์ฉ (์๋ ยง7) |
| AMP | ์ง์ | ์์ ์ ๊ฑฐ โ next/ampยทuseAmpยทamp ์ค์ ์ญ์ |
1. Metadata API
์ ์ metadata
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: '์๋น์ค๋ช
',
description: '์๋น์ค ์ค๋ช
',
}
metadata ๊ฐ์ฒด์ generateMetadata ํจ์๋ Server Component์์๋ง ์ง์๋๋ฉฐ,
๊ฐ์ ๋ผ์ฐํธ ์ธ๊ทธ๋จผํธ์์ ๋์ ๋์์ exportํ ์ ์๋ค.
ํด๋ผ์ด์ธํธ ์ธํฐ๋์
์ด ํ์ํ๋ฉด page.tsx๋ Server Component๋ก ๋๊ณ ํ์ ํ์ผ๋ก ๋ถ๋ฆฌํ๋ค.
๋์ generateMetadata
params์ searchParams๋ ๋น๋๊ธฐ๋ค. ๋ฐ๋์ awaitํ๋ค.
import type { Metadata, ResolvingMetadata } from 'next'
type Props = {
params: Promise<{ id: string }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}
export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
const { id } = await params
const product = await fetch(`/api/products/${id}`).then(r => r.json())
const previousImages = (await parent).openGraph?.images || []
return {
title: product.name,
description: product.description,
: { : [, ...previousImages] },
}
}
ํ์
ํฌํผ (๊ถ์ฅ) โ npx next typegen์ผ๋ก ์์ฑ๋๋ PageProps<'/route'> / LayoutProps<'/route'>๋ฅผ ์ฒซ ์ธ์์ ์ฐ๋ฉด ๋ผ์ฐํธ์์ ํ๋ผ๋ฏธํฐ ํ์
์ด ์ถ๋ก ๋๋ค.
export async function generateMetadata(
props: PageProps<'/products/[id]'>
): Promise<Metadata> {
const { id } = await props.params
}
searchParams๋ page.js ์ธ๊ทธ๋จผํธ์์๋ง ์ฌ์ฉ ๊ฐ๋ฅํ๋ค (layout ๋ถ๊ฐ).
generateMetadata ๋ด๋ถ์ fetch๋ generateStaticParamsยทlayoutยทpage์ ์๋ memoize๋๋ค โ ์ค๋ณต ํธ์ถ์ ๊ฑฑ์ ํ์ง ์์๋ ๋๋ค.
fetch๋ฅผ ๋ชป ์ฐ๋ ๊ฒฝ์ฐ React cache()๋ก ๊ฐ์ผ๋ค.
ํ์ดํ ํ
ํ๋ฆฟ
export const metadata: Metadata = {
title: {
template: '%s | ์๋น์ค๋ช
',
default: '์๋น์ค๋ช
',
},
}
export const metadata: Metadata = {
title: 'About',
}
title.template์ ์์ ์ธ๊ทธ๋จผํธ์๋ง ์ ์ฉ๋๋ค. ๊ฐ์ ์ธ๊ทธ๋จผํธ์ page.js ํ์ดํ์๋ ์ ์ฉ๋์ง ์๋๋ค.
- ๋ถ๋ชจ template์ ๋ฌด์ํ๋ ค๋ฉด
title: { absolute: 'About' }.
page.js์ ์ ์ํ title.template์ ์๋ฌด ํจ๊ณผ๊ฐ ์๋ค (ํ์ด์ง๋ ํญ์ ์ข
๋จ ์ธ๊ทธ๋จผํธ).
๋ณํฉ ๊ท์น โ ์์ ๋ณํฉ(shallow merge)
๋ฉํ๋ฐ์ดํฐ๋ root โ page ์์ผ๋ก ํ๊ฐ๋๊ณ ์๊ฒ ๋ณํฉ๋๋ค. openGraphยทrobots์ฒ๋ผ ์ค์ฒฉ ํ๋๋ ํ์๊ฐ ์ ์ํ๋ฉด ์์ ํ๋ ์ ์ฒด๊ฐ ํต์งธ๋ก ๊ต์ฒด๋๋ค.
export const metadata = { openGraph: { title: 'Acme', description: 'Acme is a...' } }
export const metadata = { openGraph: { title: 'Blog' } }
๊ณตํต ํ๋๋ ๋ณ๋ ๋ณ์๋ก ๋ฝ์ spread ํ๋ค.
export const openGraphImage = { images: ['https://example.com/og.png'] }
export const metadata = { openGraph: { ...openGraphImage, title: 'About' } }
2. metadataBase โ ์๋ ๊ฒฝ๋ก ํ์ฉ
URL ๊ณ์ด ํ๋(canonical, alternates.languages, openGraph.images ๋ฑ)์ ์ ๋ URL ๋์ ์๋ ๊ฒฝ๋ก๋ฅผ ์ฐ๋ ค๋ฉด root layout์ metadataBase๋ฅผ ์ค์ ํ๋ค.
export const metadata: Metadata = {
metadataBase: new URL('https://example.com'),
alternates: {
canonical: '/',
languages: { 'en-US': '/en-US', 'ko-KR': '/ko-KR' },
},
openGraph: { images: '/og-image.png' },
}
<link rel="canonical" href="https://example.com" />
<link rel="alternate" hreflang="en-US" href="https://example.com/en-US" />
<meta property="og:image" content="https://example.com/og-image.png" />
์ฃผ์: metadataBase ์์ด ์๋ ๊ฒฝ๋ก๋ฅผ ์ฐ๋ฉด ๋น๋ ์๋ฌ๊ฐ ๋๋ค.
URL ํฉ์ฑ์ ๋๋ ํ ๋ฆฌ traversal ์๋ฏธ๋ก ์ด ์๋๋ผ ๊ฐ๋ฐ์ ์๋ ์ฐ์ ์ด๋ค โ /paymentsยท./paymentsยท../payments๊ฐ ๋ชจ๋ https://example.com/payments๋ก ํด์๋๋ค. ํ๋์ ์ ๋ URL์ ์ฃผ๋ฉด metadataBase๋ ๋ฌด์๋๋ค.
3. canonical / hreflang / alternates
export const metadata: Metadata = {
alternates: {
canonical: 'https://example.com/blog/post-1',
languages: {
'en-US': 'https://example.com/en-US/blog/post-1',
'ko-KR': 'https://example.com/ko-KR/blog/post-1',
},
media: { 'only screen and (max-width: 600px)': 'https://example.com/mobile' },
types: { 'application/rss+xml': 'https://example.com/rss' },
},
}
- ๋ชจ๋ ํ์ด์ง์ ์๊ธฐ์ฐธ์กฐ canonical์ ๊ธฐ๋ณธ์ผ๋ก ๋๋ค (ํ์ด์ง๋ค์ด์
ํฌํจ).
- canonical์ ์ ๋ URL ๋๋
metadataBase + ์๋ ๊ฒฝ๋ก๋ก ์์ฑํ๊ณ , trailingSlash ์ ์ฑ
๊ณผ ํํ๋ฅผ ์ผ์น์ํจ๋ค.
- ๋ค๊ตญ์ด ๋ผ์ฐํธ(
app/[lang]/...)์์ ์ธ์ด๊ฐ์ด ํ์ํ๋ฉด v16.3์ next/root-params๋ก prop drilling ์์ด ์ฝ์ ์ ์๋ค.
์์ธ ์ ๊ทํ ๊ท์น(301/308 ์ ํ, www/์ฌ๋์ ํต์ผ, ํธ๋ํน ํ๋ผ๋ฏธํฐ ์ฒ๋ฆฌ)์
frontend/url-canonicalization-redirects ์คํฌ์ ์ฐธ์กฐํ๋ค.
4. OpenGraph / Twitter Card
export const metadata: Metadata = {
openGraph: {
title: '์๋น์ค๋ช
',
description: '์๋น์ค ์ค๋ช
',
url: 'https://example.com',
siteName: '์๋น์ค๋ช
',
images: [
{
url: 'https://example.com/og.png',
width: 1200,
height: 630,
alt: 'OG ์ด๋ฏธ์ง',
},
],
locale: 'ko_KR',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: '์๋น์ค๋ช
',
description: '์๋น์ค ์ค๋ช
',
images: ['https://example.com/og.png'],
},
}
type: 'article'์ด๋ฉด ๊ธฐ์ฌ ์ ์ฉ ํ๋๋ฅผ ์ถ๊ฐํ ์ ์๋ค.
openGraph: {
type: 'article',
publishedTime: '2026-08-11T00:00:00.000Z',
authors: ['ํ๊ธธ๋'],
}
ํ์ ์ธ๊ทธ๋จผํธ๊ฐ openGraph๋ฅผ ์ ์ํ๋ฉด ์์์ OG ํ๋๋ ์ ๋ถ ๋์ฒด๋๋ค(ยง1 ๋ณํฉ ๊ท์น). ํ์ด์ง๋ณ๋ก ๋ช
์ ์ค์ ์ด ์์ ํ๋ค.
Twitter ์คํ์ X ์ธ ๋ค๋ฅธ ํ๋ซํผ๋ ์ฐธ์กฐํ๋ฏ๋ก ํจ๊ป ์ฑ์ฐ๋ ํธ์ด ์ข๋ค.
ํ์ผ ๊ธฐ๋ฐ OG ์ด๋ฏธ์ง โ v16์์ params/id๊ฐ Promise
opengraph-image.tsx ๊ฐ์ ํ์ผ ์ปจ๋ฒค์
์ config export์ ์ค์ ํ์ผ์ ๋๊ธฐํํ ํ์๊ฐ ์์ด ๋ ์์ ํ๋ค. ๋ค๋ง v16์์ ์๊ทธ๋์ฒ๊ฐ ๋ฐ๋์๋ค.
export async function generateImageMetadata({ params }) {
const { slug } = params
return [{ id: '1' }, { id: '2' }]
}
export default async function Image({ params, id }) {
const { slug } = await params
const imageId = await id
}
5. JSON-LD ๊ตฌ์กฐํ ๋ฐ์ดํฐ
Next.js ๊ณต์ ๊ถ์ฅ์ layout.js / page.js์์ ๋ค์ดํฐ๋ธ <script> ํ๊ทธ๋ก ๋ ๋ํ๋ ๋ฐฉ์์ด๋ค. Metadata API ํ๋๊ฐ ์๋๋ค.
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const product = await fetch(`/api/products/${id}`).then(r => r.json())
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
image: product.image,
description: product.description,
offers: {
'@type': 'Offer',
price: product.price,
priceCurrency: 'KRW',
},
}
return (
<section>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
// XSS ๋ฐฉ์ง: JSON.stringify๋ ์
์ฑ ๋ฌธ์์ด์ sanitizeํ์ง
// <
()(/</, '\\'),
}}
/>
)
}
next/script๋ฅผ ์ฐ์ง ์๋๋ค. next/script๋ JS ๋ก๋ฉ/์คํ ์ต์ ํ์ฉ์ด๊ณ , JSON-LD๋ ์คํ ์ฝ๋๊ฐ ์๋ ๊ตฌ์กฐํ ๋ฐ์ดํฐ์ด๋ฏ๋ก ๋ค์ดํฐ๋ธ <script>๊ฐ ์ฌ๋ฐ๋ฅธ ์ ํ์ด๋ค (๊ณต์ ๋ฌธ์ ๋ช
์).
ํ์
์์ ์ฑ: schema-dts ์ปค๋ฎค๋ํฐ ํจํค์ง๋ก ํ์ดํํ ์ ์๋ค.
import type { Product, WithContext } from 'schema-dts'
const jsonLd: WithContext<Product> = {
'@context': 'https://schema.org',
'@type': 'Product',
name: 'Next.js Sticker',
}
๊ฒ์ฆ: Google Rich Results Test ๋๋ Schema Markup Validator.
6. sitemap.ts
import type { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://example.com',
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 1,
},
{
url: 'https://example.com/about',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
]
}
๋ฐํ ํ์
:
type Sitemap = Array<{
url: string
lastModified?: string | Date
changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never'
priority?: number
alternates?: { languages?: Languages<string> }
}>
sitemap.js๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์บ์๋๋ ํน์ Route Handler๋ค. Request-time API๋ dynamic ์ค์ ์ ์ฐ๋ฉด ์บ์๋์ง ์๋๋ค.
๋ค๊ตญ์ด ์ฌ์ดํธ๋งต
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://example.com/about',
lastModified: new Date(),
alternates: {
languages: { ko: 'https://example.com/ko/about', en: 'https://example.com/en/about' },
},
},
]
}
์ด๋ฏธ์ง / ๋น๋์ค ์ฌ์ดํธ๋งต
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://example.com',
lastModified: '2026-08-11',
images: ['https://example.com/image.jpg'],
videos: [
{
title: 'example',
thumbnail_loc: 'https://example.com/thumb.jpg',
description: '์ค๋ช
',
},
],
},
]
}
๋ถํ โ generateSitemaps (โ ๏ธ v16์์ ์๊ทธ๋์ฒ ๋ณ๊ฒฝ)
Google ํ๋๋ ์ฌ์ดํธ๋งต๋น 50,000 URL์ด๋ค. ์ด๊ณผํ๋ฉด ๋ถํ ํ๋ค.
import type { MetadataRoute } from 'next'
export async function generateSitemaps() {
const total = await getProductCount()
return Array.from({ length: Math.ceil(total / 50000) }, (_, i) => ({ id: i }))
}
export default async function sitemap(props: {
id: Promise<string>
}): Promise<MetadataRoute.Sitemap> {
const id = await props.id
const start = Number(id) * 50000
const products = await getProducts({ offset: start, limit: 50000 })
return products.map(p => ({ : }))
}
์ฃผ์ (v16 breaking change): Next.js 15์ sitemap({ id }: { id: number }) ํํ๋ ๋ ์ด์ ๋์ํ์ง ์๋๋ค.
id๋ Promise<string>์ด๋ฏ๋ก await ํ Number()๋ก ๋ณํํด์ผ ๊ณฑ์
์ฐ์ฐ์ด ์ ์ ๋์ํ๋ค.
์์ฑ ๊ฒฐ๊ณผ๋ /product/sitemap/1.xml ํํ๋ก ์๋น๋๋ค.
๋ผ์ฐํธ ์ธ๊ทธ๋จผํธ๋ณ๋ก app/sitemap.xml, app/products/sitemap.xml์ฒ๋ผ ์ค์ฒฉ ๋ฐฐ์นํ๋ ๋ฐฉ์๋ ๊ฐ๋ฅํ๋ค.
7. robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/private/', '/admin/'],
},
],
sitemap: 'https://example.com/sitemap.xml',
}
}
rules๋ ๋จ์ผ ๊ฐ์ฒด ๋๋ ๋ฐฐ์ด ๋ ๋ค ๊ฐ๋ฅํ๋ค. ๋ด๋ณ๋ก ๋ค๋ฅด๊ฒ ์ฃผ๋ ค๋ฉด ๋ฐฐ์ด์ ์ด๋ค.
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{ userAgent: 'Googlebot', allow: ['/'], disallow: '/private/' },
{ userAgent: ['Applebot', 'Bingbot'], disallow: ['/'] },
],
sitemap: 'https://example.com/sitemap.xml',
}
}
๋ฐํ ํ์
:
type Robots = {
rules:
| { userAgent?: string | string[]; allow?: string | string[]; disallow?: string | string[]
crawlDelay?: number; other?: Record<string, string | number | Array<string | number>> }
| Array<{ userAgent: string | string[]; allow?: string | string[]; disallow?: string | string[]
crawlDelay?: number; other?: Record<string, string | number | Array<string | number>> }>
sitemap?: string | string[]
host?: string
}
๋นํ์ค ๋๋ ํฐ๋ธ โ other (v16.3.0 ์ ๊ท)
Robots Exclusion Standard์ ์๋ ๋๋ ํฐ๋ธ(Seznam Request-Rate, Yandex Clean-param ๋ฑ)๋ฅผ rule์ other ํ๋๋ก ๋ด๋ณด๋ธ๋ค. ํค์ ๋์๋ฌธ์๊ฐ ๋ณด์กด๋๊ณ , ๋ฐฐ์ด ๊ฐ์ ํญ๋ชฉ๋น ํ ์ค์ฉ ํด๋น User-Agent ๋ธ๋ก์ ์ถ๋ ฅ๋๋ค.
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{ userAgent: '*', allow: '/' },
{ userAgent: 'SeznamBot', allow: '/', other: { 'Request-Rate': '10/1m' } },
],
}
}
์ฃผ์: other ๊ฐ์ ๊ฒ์ฆ ์์ด ๊ทธ๋๋ก ์ถ๋ ฅ๋๋ค. ๋๋ ํฐ๋ธ ์ด๋ฆยท๋ฌธ๋ฒ์ ๋์ ๊ฒ์์์ง ๋ฌธ์๋ฅผ ์ง์ ํ์ธํด์ผ ํ๋ค.
8. ํฌ๋กค๋ฌ ๊ด๋ จ ๋ ๋๋ง ๋์ (SEO ํ์ ์ดํด)
Streaming metadata
Next.js๋ generateMetadata ์๋ฃ๋ฅผ ๊ธฐ๋ค๋ฆฌ์ง ์๊ณ ์ด๊ธฐ UI๋ฅผ ๋จผ์ ๋ณด๋ธ๋ค. ๋ฉํ๋ฐ์ดํฐ๊ฐ ๋์ค์ resolve๋๋ฉด ํ๊ทธ๊ฐ <body>์ append๋๋ค.
- JS๋ฅผ ์คํํ๊ณ DOM ์ ์ฒด๋ฅผ ๋ณด๋ ๋ด(Googlebot ๋ฑ) โ ์ ์ ํด์๋จ์ Vercel์ด ๊ฒ์ฆ
- HTML-limited ๋ด(
facebookexternalhit ๋ฑ, JS ๋ฏธ์คํ) โ ๋ฉํ๋ฐ์ดํฐ๊ฐ ๋ ๋๋ฅผ ๋ธ๋กํนํ๊ณ ๊ฒฐ๊ณผ๊ฐ <head>์ ๋ค์ด๊ฐ๋ค. Next.js๊ฐ User-Agent๋ก ์๋ ๊ฐ์งํ๋ค
๋ด ๋ชฉ๋ก์ ์กฐ์ ํ๊ฑฐ๋ streaming์ ์์ ํ ๋๋ ค๋ฉด:
const config: NextConfig = {
htmlLimitedBots: /.*/,
}
์ฃผ์: htmlLimitedBots ํ์ฅ์ ์๋ต ์๊ฐ์ ๋๋ฆฐ๋ค. ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์ถฉ๋ถํ ๊ฒฝ์ฐ๊ฐ ๋๋ถ๋ถ์ด๋ฉฐ, ๊ณ ๊ธ ๊ธฐ๋ฅ์ผ๋ก ๋ค๋ค์ผ ํ๋ค.
streaming metadata๋ TTFB๋ฅผ ์ค์ด๊ณ LCP ๊ฐ์ ์ ๊ธฐ์ฌํ๋ค.
Cache Components ์ฌ์ฉ ์ (cacheComponents: true)
generateMetadata๋ ๋ค๋ฅธ ์ปดํฌ๋ํธ์ ๋์ผํ ์บ์ฑ ๊ท์น์ ๋ฐ๋ฅธ๋ค.
- ๋ฐํ์ ๋ฐ์ดํฐ(
cookies(), headers(), params, searchParams)๋ฅผ ์ฝ๊ฑฐ๋ uncached fetch๋ฅผ ํ๋ฉด ์์ฒญ ์์ ์ผ๋ก ๋ฏธ๋ค์ง๋ค
- ํ์ด์ง์ ๋๋จธ์ง๊ฐ ์์ ํ prerender ๊ฐ๋ฅํ๋ฐ ๋ฉํ๋ฐ์ดํฐ๋ง ๋ฐํ์์ด๋ฉด ์๋ฌ๊ฐ ๋ฐ์ํ๋ค โ ์๋๋ฅผ ๋ช
์ํด์ผ ํ๋ค
export async function generateMetadata() {
'use cache'
const { title, description } = await db.query('site-metadata')
return { title, description }
}
๋ดยทํฌ๋กค๋ฌ์ static shell
Cache Components์์ ๋ธ๋ผ์ฐ์ ๋ static shell์ ์ฆ์ ๋ฐ์ง๋ง, ๋ด์ shell์ ๊ฑด๋๋ฐ๊ณ ์์ฒญ ์์ ์ ์ ์ฒด๋ฅผ ๋์ ๋ ๋ํ ๋ค ์์ฑ๋ HTML์ ๋ฐ๋๋ค.
์ฃผ์: shell์ด ๋น๋ ํ์์๋ง ์กด์ฌํ๋ ๋ฐ์ดํฐ์ ์์กดํ๋ฉด, ์ฌ๋์๊ฒ๋ ์ ์์ธ ํ์ด์ง๊ฐ ํฌ๋กค๋ฌ์๊ฒ๋ ๋ ๋ ์คํจํ ์ ์๋ค. shell์ด ์์กดํ๋ ๋ฐ์ดํฐ๋ ์์ฒญ ์์ ์๋ ์ ๊ทผ ๊ฐ๋ฅํด์ผ ํ๋ค.
9. ํํ ์ค์ ํจํด
export const metadata = { openGraph: { images: '/og.png' } }
'use client'
export const metadata = { title: 'X' }
export default async function sitemap({ id }: { id: number }) {
const start = id * 50000
}
__html: JSON.stringify(jsonLd)
__html: JSON.stringify(jsonLd).replace(/</g, '\\u003c')