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;
}