Skip to main content
Version: App Router (1.5+)

SEO & Metadata

HeadstartWP provides seamless SEO support for Next.js App Router through the native Metadata API. All query functions automatically return SEO data that's compatible with Next.js metadata.

Basic Usage

generateMetadata Function

app/[...path]/page.tsx
import { queryPost } from '@headstartwp/next/app';
import { SafeHtml } from '@headstartwp/core/react';
import type { Metadata } from 'next';
import type { HeadstartWPRoute } from '@headstartwp/next/app';

export async function generateMetadata({ params }: HeadstartWPRoute): Promise<Metadata> {
const { seo } = await queryPost({
routeParams: await params,
params: {
postType: ['post', 'page'],
},
});

return seo.metadata;
}

export default async function SinglePostPage({ params }: HeadstartWPRoute) {
const { data, seo } = await queryPost({
routeParams: await params,
params: {
postType: ['post', 'page'],
},
});

return (
<>
{/* Yoast SEO meta tags are automatically included */}
<article>
<h1>{data.post.title.rendered}</h1>
<SafeHtml html={data.post.content.rendered} />
</article>
</>
);
}

Archive Pages

app/blog/page.tsx
import { queryPosts } from '@headstartwp/next/app';
import type { Metadata } from 'next';
import type { HeadstartWPRoute } from '@headstartwp/next/app';

export async function generateMetadata(): Promise<Metadata> {
return {
title: 'Blog Posts',
description: 'Latest blog posts and articles',
openGraph: {
title: 'Blog Posts',
description: 'Latest blog posts and articles',
type: 'website',
},
};
}

export default async function BlogPage({ params }: HeadstartWPRoute) {
const { data } = await queryPosts({
routeParams: await params,
params: {
postType: 'post',
per_page: 10,
},
});

return (
<main>
<h1>Blog Posts</h1>
{/* Render posts */}
</main>
);
}

Available Metadata

The seo.metadata object includes all standard Next.js metadata fields:

Basic Metadata

interface SEOMetadata {
title: string;
description: string;
keywords?: string[];
robots?: string;
canonical?: string;
}

Open Graph

interface OpenGraphData {
title: string;
description: string;
url: string;
siteName: string;
images: Array<{
url: string;
width: number;
height: number;
alt: string;
}>;
locale: string;
type: 'website' | 'article';
}

Twitter Cards

interface TwitterData {
card: 'summary' | 'summary_large_image';
title: string;
description: string;
images: string[];
creator?: string;
site?: string;
}

Advanced Examples

Custom Metadata Merging

app/blog/[slug]/page.tsx
import { queryPost } from '@headstartwp/next/app';
import type { Metadata } from 'next';
import type { HeadstartWPRoute } from '@headstartwp/next/app';

export async function generateMetadata({ params }: HeadstartWPRoute): Promise<Metadata> {
const { seo, data } = await queryPost({
routeParams: await params,
params: {
postType: 'post',
},
});

// Merge HeadstartWP SEO with custom metadata
return {
...seo.metadata,
// Override or add custom fields
keywords: [
...(seo.metadata.keywords || []),
'custom-keyword',
'blog',
],
authors: [
{
name: data.post.author?.name,
url: data.post.author?.link,
},
],
category: data.post.terms?.category?.[0]?.name,
};
}

Dynamic Metadata for Archives

app/category/[slug]/page.tsx
import { queryPosts } from '@headstartwp/next/app';
import type { Metadata } from 'next';
import type { HeadstartWPRoute } from '@headstartwp/next/app';

export async function generateMetadata({ params }: HeadstartWPRoute): Promise<Metadata> {
const resolvedParams = await params;

const { data } = await queryPosts({
routeParams: await params,
params: {
postType: 'post',
category: resolvedParams.slug,
per_page: 1, // Just to get category info
},
});

const category = data.queriedObject?.term;

if (!category) {
return {
title: 'Category Not Found',
description: 'The requested category was not found',
};
}

return {
title: `${category.name} - Blog Category`,
description: category.description || `Posts in the ${category.name} category`,
openGraph: {
title: category.name,
description: category.description || `Posts in the ${category.name} category`,
type: 'website',
},
alternates: {
canonical: category.link,
},
};
}

JSON-LD Schema

HeadstartWP automatically generates structured data for your content:

Article Schema

app/[...path]/page.tsx
import { JSONLD } from '@headstartwp/next/app';
import type { HeadstartWPRoute } from '@headstartwp/next/app';

export default async function PostPage({ params }: HeadstartWPRoute) {
const { data, seo } = await queryPost({
routeParams: await params,
params: { postType: 'post' },
});

return (
<article>
<h1>{data.post.title.rendered}</h1>
<SafeHtml html={data.post.content.rendered} />

{/* Render JSON-LD using the helper component */}
<JSONLD schema={seo.schema} />
</article>
);
}

Custom Schema

import type { HeadstartWPRoute } from '@headstartwp/next/app';

export default async function PostPage({ params }: HeadstartWPRoute) {
const { data, seo } = await queryPost({
routeParams: await params,
params: { postType: 'post' },
});

// Extend or customize the schema
const customSchema = {
...seo.schema,
'@context': 'https://schema.org',
'@type': 'BlogPosting',
// Add custom properties
wordCount: data.post.content.rendered.split(' ').length,
readingTime: `${Math.ceil(data.post.content.rendered.split(' ').length / 200)} min read`,
};

return (
<article>
<h1>{data.post.title.rendered}</h1>
<SafeHtml html={data.post.content.rendered} />

<JSONLD schema={customSchema} />
</article>
);
}

Multi-language SEO

Language Alternates

app/[...path]/page.tsx
import type { HeadstartWPRoute } from '@headstartwp/next/app';

export async function generateMetadata({ params }: HeadstartWPRoute): Promise<Metadata> {
const { seo, data } = await queryPost({
routeParams: await params,
params: { postType: ['post', 'page'] },
});

return {
...seo.metadata,
alternates: {
canonical: data.post.link,
languages: {
'en-US': '/en' + data.post.link,
'es-ES': '/es' + data.post.link,
'fr-FR': '/fr' + data.post.link,
},
},
};
}

WordPress SEO Plugin Integration

HeadstartWP automatically integrates with popular WordPress SEO plugins:

Yoast SEO

// Yoast metadata is automatically included in seo.metadata
import type { HeadstartWPRoute } from '@headstartwp/next/app';

export async function generateMetadata({ params }: HeadstartWPRoute): Promise<Metadata> {
const { seo } = await queryPost({
routeParams: await params,
params: { postType: 'post' },
});

// Includes Yoast title, description, focus keyword, etc.
return seo.metadata;
}