@masters-ws/react-seo
v1.4.1
Published
Professional high-performance SEO package for React/Next.js. Zero-dependency core for Next.js App Router, optional Helmet components for React.
Downloads
58
Maintainers
Readme
@masters-ws/react-seo
Professional high-performance SEO package for React and Next.js. Zero-dependency core for Next.js App Router + optional Helmet components for universal React support.
Key Features
- ✅ Zero-Dependency Core — Pure functions for Next.js (no react-helmet-async needed!)
- ✅ SSR-First Architecture — All metadata rendered server-side for Google crawlers
- ✅ Clean JSON-LD — Automatic removal of
undefined/nullvalues from schemas - ✅ @graph Support — Combine multiple schemas into a single
<script>tag (Google recommended) - ✅ Full Metadata Support — Title, Description, OG, Twitter Cards, Canonical, Robots
- ✅ Intelligent Pagination — Automatic
rel="prev"andrel="next"handling - ✅ 30+ Schema Types — Product, Article, FAQ, Event, LocalBusiness, Video, Recipe, etc.
- ✅ Rich Product Schema — Multi-image, reviews, return policy, shipping, variants
- ✅ Convenience Helpers — One-call setup: Product, Article, Category pages
- ✅ Development Warnings — Console warnings for missing required schema fields
- ✅ i18n Ready — No hardcoded strings, all labels are configurable
- ✅ Multilingual Support — Easy
hreflangmanagement - ✅ Performance Optimized — DNS Prefetch, Preconnect, Preload support
Installation
For Next.js App Router (Zero Dependencies):
npm install @masters-ws/react-seoFor React / Next.js Pages Router (With Components):
npm install @masters-ws/react-seo react-helmet-async🚀 Quick Start: Next.js App Router (Recommended)
Product Page (One-Call Setup)
// app/products/[slug]/page.tsx — Server Component (NO 'use client')
import { generateProductMetadata, JsonLd } from '@masters-ws/react-seo/core';
const siteConfig = {
name: "My Store",
url: "https://store.com",
logo: "https://store.com/logo.png",
description: "Best online store",
language: "en_US",
};
// ✅ Server-side metadata — Google sees it immediately!
export async function generateMetadata({ params }: { params: { slug: string } }) {
const product = await fetchProduct(params.slug);
const { metadata } = generateProductMetadata({
name: product.name,
description: product.short_description,
image: [product.main_image, ...product.gallery], // Multi-image support
price: product.price,
currency: "USD",
sku: product.sku,
brand: product.brand?.name,
availability: product.in_stock
? "https://schema.org/InStock"
: "https://schema.org/OutOfStock",
url: `https://store.com/products/${params.slug}`,
metaTitle: product.meta_title,
metaDescription: product.meta_description,
// Individual reviews (optional)
reviews: product.reviews?.map(r => ({
author: r.user.name,
ratingValue: r.rating,
reviewBody: r.comment,
datePublished: r.created_at,
})),
// Return policy (optional)
returnPolicy: {
returnPolicyCategory: 'MerchantReturnFiniteReturnWindow',
returnWithin: 30,
returnMethod: 'ReturnByMail',
returnFees: 'FreeReturn',
},
// Shipping (optional)
shipping: {
shippingRate: { value: 5.99, currency: "USD" },
shippingDestination: "US",
deliveryTime: { minDays: 3, maxDays: 7 },
},
// Product variants (optional — generates AggregateOffer)
variants: product.variants?.map(v => ({
name: v.name,
sku: v.sku,
price: v.price,
})),
breadcrumbs: [
{ name: "Home", item: "https://store.com" },
{ name: "Shop", item: "https://store.com/shop" },
{ name: product.category?.name, item: `https://store.com/categories/${product.category?.slug}` },
{ name: product.name, item: `https://store.com/products/${params.slug}` },
],
}, siteConfig);
return metadata;
}
// ✅ Server-rendered JSON-LD schemas with @graph pattern
export default async function ProductPage({ params }: { params: { slug: string } }) {
const product = await fetchProduct(params.slug);
const { schemas } = generateProductMetadata({ /* same data */ }, siteConfig);
return (
<>
<JsonLd schema={schemas} graph />
<ProductDetailClient product={product} />
</>
);
}What Google sees in the HTML source:
<!-- ✅ Server-rendered <head> tags -->
<title>Product Name | My Store</title>
<meta name="description" content="Product description..." />
<meta property="og:title" content="Product Name" />
<meta property="og:image" content="https://store.com/product.jpg" />
<meta property="og:type" content="product" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="canonical" href="https://store.com/products/my-product" />
<!-- ✅ Server-rendered JSON-LD with @graph -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{ "@type": "Product", "name": "...", "offers": {...}, "review": [...] },
{ "@type": "BreadcrumbList", "itemListElement": [...] },
{ "@type": "Organization", "name": "...", "logo": "..." },
{ "@type": "WebSite", "name": "...", "potentialAction": {...} }
]
}
</script>Article Page (One-Call Setup)
// app/news/[slug]/page.tsx — Server Component
import { generateArticleMetadata, JsonLd } from '@masters-ws/react-seo/core';
export async function generateMetadata({ params }) {
const article = await fetchArticle(params.slug);
const { metadata } = generateArticleMetadata({
title: article.title,
description: article.excerpt,
image: article.cover_image,
publishedTime: article.published_at,
modifiedTime: article.updated_at,
author: { name: article.author.name, url: article.author.url },
url: `https://mysite.com/news/${params.slug}`,
category: article.category,
tags: article.tags,
readingTime: article.reading_time,
wordCount: article.word_count,
}, siteConfig);
return metadata;
}
export default async function ArticlePage({ params }) {
const article = await fetchArticle(params.slug);
const { schemas } = generateArticleMetadata({ /* same data */ }, siteConfig);
return (
<>
<JsonLd schema={schemas} graph />
<article>...</article>
</>
);
}Category Page with Pagination
// app/categories/[slug]/page.tsx — Server Component
import { generateCategoryMetadata, JsonLd } from '@masters-ws/react-seo/core';
export async function generateMetadata({ params, searchParams }) {
const page = Number(searchParams.page) || 1;
const category = await fetchCategory(params.slug);
const { metadata } = generateCategoryMetadata({
name: category.name,
description: category.description,
url: `https://store.com/categories/${params.slug}`,
image: category.image,
page,
totalPages: category.totalPages,
// Products on this page → generates ItemList schema
items: category.products.map((p, i) => ({
name: p.name,
url: `https://store.com/products/${p.slug}`,
image: p.image,
position: i + 1,
})),
}, siteConfig);
return metadata;
}
export default async function CategoryPage({ params, searchParams }) {
const page = Number(searchParams.page) || 1;
const category = await fetchCategory(params.slug);
const { schemas } = generateCategoryMetadata({ /* same data */ }, siteConfig);
return (
<>
<JsonLd schema={schemas} graph />
<ProductGrid products={category.products} />
</>
);
}📰 Use Case: Blog / News Site
A complete guide for setting up SEO on a blog or news website.
Blog Homepage
// app/page.tsx — Server Component
import { generateHomepageMetadata, JsonLd } from '@masters-ws/react-seo/core';
const siteConfig = {
name: "Tech Blog",
url: "https://techblog.com",
logo: "https://techblog.com/logo.png",
description: "Latest technology news, tutorials, and insights",
language: "en_US",
twitterHandle: "@techblog",
socialLinks: [
"https://twitter.com/techblog",
"https://facebook.com/techblog",
"https://linkedin.com/company/techblog",
],
};
export async function generateMetadata() {
const { metadata } = generateHomepageMetadata({
title: "Tech Blog — Latest Technology News & Tutorials",
description: "Stay updated with the latest tech news, programming tutorials, and expert insights.",
ogImage: "https://techblog.com/og-cover.jpg",
}, siteConfig);
return metadata;
}
export default function HomePage() {
const { schemas } = generateHomepageMetadata({
title: "Tech Blog — Latest Technology News & Tutorials",
description: "Stay updated with the latest tech news, programming tutorials, and expert insights.",
}, siteConfig);
return (
<>
<JsonLd schema={schemas} graph />
<main>
<h1>Welcome to Tech Blog</h1>
{/* Latest articles grid */}
</main>
</>
);
}Article / Blog Post Page
// app/blog/[slug]/page.tsx — Server Component
import { generateArticleMetadata, JsonLd } from '@masters-ws/react-seo/core';
export async function generateMetadata({ params }: { params: { slug: string } }) {
const article = await fetchArticle(params.slug);
const { metadata } = generateArticleMetadata({
title: article.title,
description: article.excerpt,
image: article.cover_image,
publishedTime: article.published_at, // ISO 8601
modifiedTime: article.updated_at,
author: {
name: article.author.name,
url: `https://techblog.com/authors/${article.author.slug}`,
},
url: `https://techblog.com/blog/${params.slug}`,
category: article.category.name, // e.g. "JavaScript"
tags: article.tags.map(t => t.name), // ["React", "Next.js", "SEO"]
readingTime: article.reading_time, // e.g. 5 (minutes)
wordCount: article.word_count,
breadcrumbs: [
{ name: "Home", item: "https://techblog.com" },
{ name: article.category.name, item: `https://techblog.com/category/${article.category.slug}` },
{ name: article.title, item: `https://techblog.com/blog/${params.slug}` },
],
}, siteConfig);
return metadata;
}
export default async function ArticlePage({ params }: { params: { slug: string } }) {
const article = await fetchArticle(params.slug);
const { schemas } = generateArticleMetadata({ /* same data as above */ }, siteConfig);
return (
<>
<JsonLd schema={schemas} graph />
<article>
<h1>{article.title}</h1>
<p>By {article.author.name} · {article.reading_time} min read</p>
<div dangerouslySetInnerHTML={{ __html: article.content }} />
</article>
</>
);
}What Google sees:
<title>How to Master SEO in Next.js | Tech Blog</title>
<meta name="description" content="A comprehensive guide to..." />
<meta property="og:type" content="article" />
<meta property="article:published_time" content="2024-06-15T10:00:00Z" />
<meta property="article:author" content="John Doe" />
<meta property="article:section" content="JavaScript" />
<meta property="article:tag" content="React,Next.js,SEO" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{ "@type": "NewsArticle", "headline": "...", "author": { "@type": "Person", "name": "..." } },
{ "@type": "BreadcrumbList", "itemListElement": [...] },
{ "@type": "Organization", ... },
{ "@type": "WebSite", "potentialAction": { "@type": "SearchAction", ... } }
]
}
</script>Blog Category Page (with Pagination)
// app/category/[slug]/page.tsx — Server Component
import { generateCategoryMetadata, JsonLd } from '@masters-ws/react-seo/core';
export async function generateMetadata({ params, searchParams }) {
const page = Number(searchParams.page) || 1;
const category = await fetchCategory(params.slug);
const { metadata } = generateCategoryMetadata({
name: category.name,
description: `All articles about ${category.name}`,
url: `https://techblog.com/category/${params.slug}`,
image: category.cover_image,
page,
totalPages: category.totalPages,
// Articles on this page → generates ItemList schema
items: category.articles.map((a, i) => ({
name: a.title,
url: `https://techblog.com/blog/${a.slug}`,
image: a.cover_image,
position: (page - 1) * 10 + i + 1,
})),
}, siteConfig);
return metadata;
}
export default async function CategoryPage({ params, searchParams }) {
const page = Number(searchParams.page) || 1;
const category = await fetchCategory(params.slug);
const { schemas } = generateCategoryMetadata({ /* same data */ }, siteConfig);
return (
<>
<JsonLd schema={schemas} graph />
<h1>{category.name}</h1>
<ArticleGrid articles={category.articles} />
<Pagination current={page} total={category.totalPages} />
</>
);
}Pagination SEO is automatic! The helper generates:
rel="canonical"(without?page=1for page 1)rel="prev"/rel="next"links- Page number in title (e.g. "JavaScript - Page 2")
Author Page
// app/authors/[slug]/page.tsx — Server Component
import {
toNextMetadata,
generateWebPageSchema,
generateOrganizationSchema,
JsonLd
} from '@masters-ws/react-seo/core';
export async function generateMetadata({ params }) {
const author = await fetchAuthor(params.slug);
return toNextMetadata({
title: `${author.name} — Author at Tech Blog`,
description: author.bio,
image: author.avatar,
type: 'profile',
canonical: `https://techblog.com/authors/${params.slug}`,
}, siteConfig);
}
export default async function AuthorPage({ params }) {
const author = await fetchAuthor(params.slug);
const schemas = [
{
"@context": "https://schema.org",
"@type": "Person",
"name": author.name,
"description": author.bio,
"image": author.avatar,
"jobTitle": author.job_title,
"url": `https://techblog.com/authors/${params.slug}`,
"sameAs": author.social_links,
},
generateOrganizationSchema(siteConfig),
];
return (
<>
<JsonLd schema={schemas} graph />
<h1>{author.name}</h1>
<p>{author.bio}</p>
<ArticleGrid articles={author.articles} />
</>
);
}Tag Page
// app/tag/[slug]/page.tsx — Server Component
import { generateCategoryMetadata, JsonLd } from '@masters-ws/react-seo/core';
export async function generateMetadata({ params, searchParams }) {
const page = Number(searchParams.page) || 1;
const tag = await fetchTag(params.slug);
// Reuse generateCategoryMetadata — tags are just another type of collection!
const { metadata } = generateCategoryMetadata({
name: `Tag: ${tag.name}`,
description: tag.description || `All articles tagged with "${tag.name}"`,
url: `https://techblog.com/tag/${params.slug}`,
page,
totalPages: tag.totalPages,
noindex: tag.totalPages <= 1, // noindex thin tag pages
}, siteConfig);
return metadata;
}Article with FAQ Section
// app/blog/[slug]/page.tsx — If the article has an FAQ section
import { generateArticleMetadata, generateFAQSchema, JsonLd } from '@masters-ws/react-seo/core';
export default async function ArticlePage({ params }) {
const article = await fetchArticle(params.slug);
const { schemas: articleSchemas } = generateArticleMetadata({ /* ... */ }, siteConfig);
// Add FAQ schema if article has FAQ section
const allSchemas = [...articleSchemas];
if (article.faqs?.length > 0) {
allSchemas.push(generateFAQSchema(
article.faqs.map(f => ({ q: f.question, a: f.answer }))
));
}
return (
<>
<JsonLd schema={allSchemas} graph />
<article>
<h1>{article.title}</h1>
<div>{article.content}</div>
{article.faqs?.length > 0 && (
<section>
<h2>Frequently Asked Questions</h2>
{article.faqs.map(faq => (
<details key={faq.question}>
<summary>{faq.question}</summary>
<p>{faq.answer}</p>
</details>
))}
</section>
)}
</article>
</>
);
}🏢 Use Case: Showcase / Branding Website
A complete guide for corporate sites, portfolios, landing pages, and service-based businesses.
Homepage
// app/page.tsx — Server Component
import { generateHomepageMetadata, JsonLd } from '@masters-ws/react-seo/core';
const siteConfig = {
name: "Acme Solutions",
url: "https://acme.com",
logo: "https://acme.com/logo.png",
description: "Digital solutions for modern businesses",
language: "en_US",
twitterHandle: "@acme",
socialLinks: [
"https://twitter.com/acme",
"https://linkedin.com/company/acme",
"https://github.com/acme",
],
};
export async function generateMetadata() {
const { metadata } = generateHomepageMetadata({
title: "Acme Solutions — Digital Solutions for Modern Businesses",
description: "We help businesses grow with cutting-edge web development, mobile apps, and cloud solutions.",
ogImage: "https://acme.com/og-image.jpg",
// Add LocalBusiness if you have a physical office
localBusiness: {
name: "Acme Solutions HQ",
description: "Software development company",
telephone: "+1-555-123-4567",
address: {
street: "123 Tech Avenue",
city: "San Francisco",
region: "CA",
postalCode: "94105",
country: "US",
},
geo: { lat: 37.7749, lng: -122.4194 },
openingHours: ["Mo-Fr 09:00-18:00"],
},
}, siteConfig);
return metadata;
}
export default function HomePage() {
const { schemas } = generateHomepageMetadata({ /* same data */ }, siteConfig);
return (
<>
<JsonLd schema={schemas} graph />
<main>
<section id="hero">
<h1>Build. Scale. Succeed.</h1>
<p>Digital solutions for modern businesses</p>
</section>
<section id="services">...</section>
<section id="portfolio">...</section>
<section id="contact">...</section>
</main>
</>
);
}What Google sees for a local business:
{
"@context": "https://schema.org",
"@graph": [
{ "@type": "WebPage", "name": "Acme Solutions", "url": "https://acme.com" },
{ "@type": "Organization", "name": "Acme Solutions", "logo": "...", "sameAs": [...] },
{ "@type": "WebSite", "potentialAction": { "@type": "SearchAction", ... } },
{
"@type": "LocalBusiness",
"name": "Acme Solutions HQ",
"telephone": "+1-555-123-4567",
"address": { "@type": "PostalAddress", "streetAddress": "123 Tech Avenue", ... },
"geo": { "@type": "GeoCoordinates", "latitude": 37.7749, "longitude": -122.4194 },
"openingHours": ["Mo-Fr 09:00-18:00"]
}
]
}About Page
// app/about/page.tsx — Server Component
import {
toNextMetadata,
generateWebPageSchema,
generateOrganizationSchema,
generateBreadcrumbSchema,
JsonLd
} from '@masters-ws/react-seo/core';
export async function generateMetadata() {
return toNextMetadata({
title: "About Us — Our Story & Mission",
description: "Learn about Acme Solutions, our mission, team, and the values that drive us to build amazing digital products.",
image: "https://acme.com/about-og.jpg",
type: 'website',
canonical: "https://acme.com/about",
}, siteConfig);
}
export default function AboutPage() {
const schemas = [
generateWebPageSchema({
name: "About Acme Solutions",
description: "Our story, mission, and team",
url: "https://acme.com/about",
image: "https://acme.com/team-photo.jpg",
}, siteConfig),
generateBreadcrumbSchema([
{ name: "Home", item: "https://acme.com" },
{ name: "About Us", item: "https://acme.com/about" },
]),
generateOrganizationSchema(siteConfig),
];
return (
<>
<JsonLd schema={schemas} graph />
<main>
<h1>About Us</h1>
<p>Founded in 2020, Acme Solutions...</p>
</main>
</>
);
}Services Page
// app/services/page.tsx — Server Component
import {
toNextMetadata,
generateWebPageSchema,
generateBreadcrumbSchema,
generateFAQSchema,
JsonLd
} from '@masters-ws/react-seo/core';
export async function generateMetadata() {
return toNextMetadata({
title: "Our Services — Web Development, Mobile Apps & Cloud",
description: "Explore our professional services: custom web development, mobile app design, cloud infrastructure, and digital consulting.",
image: "https://acme.com/services-og.jpg",
type: 'website',
canonical: "https://acme.com/services",
}, siteConfig);
}
export default function ServicesPage() {
const schemas = [
generateWebPageSchema({
name: "Our Services",
description: "Professional digital services",
url: "https://acme.com/services",
}, siteConfig),
generateBreadcrumbSchema([
{ name: "Home", item: "https://acme.com" },
{ name: "Services", item: "https://acme.com/services" },
]),
// Add FAQ for common service questions → shows in Google rich results!
generateFAQSchema([
{ q: "What technologies do you use?", a: "We specialize in React, Next.js, Node.js, and cloud platforms like AWS and GCP." },
{ q: "How long does a typical project take?", a: "Most projects take 4-12 weeks depending on complexity." },
{ q: "Do you offer ongoing maintenance?", a: "Yes, we offer monthly maintenance plans starting at $500/month." },
]),
];
return (
<>
<JsonLd schema={schemas} graph />
<main>
<h1>Our Services</h1>
<ServiceCard title="Web Development" description="..." />
<ServiceCard title="Mobile Apps" description="..." />
<ServiceCard title="Cloud Solutions" description="..." />
<section>
<h2>Frequently Asked Questions</h2>
{/* FAQ content */}
</section>
</main>
</>
);
}Individual Service Page
// app/services/[slug]/page.tsx — Server Component
import {
toNextMetadata,
generateWebPageSchema,
generateBreadcrumbSchema,
generateHowToSchema,
generateFAQSchema,
JsonLd
} from '@masters-ws/react-seo/core';
export async function generateMetadata({ params }) {
const service = await fetchService(params.slug);
return toNextMetadata({
title: `${service.name} — Professional ${service.name} Services`,
description: service.meta_description,
image: service.og_image,
type: 'website',
canonical: `https://acme.com/services/${params.slug}`,
}, siteConfig);
}
export default async function ServicePage({ params }) {
const service = await fetchService(params.slug);
const schemas = [
generateWebPageSchema({
name: service.name,
description: service.description,
url: `https://acme.com/services/${params.slug}`,
}, siteConfig),
generateBreadcrumbSchema([
{ name: "Home", item: "https://acme.com" },
{ name: "Services", item: "https://acme.com/services" },
{ name: service.name, item: `https://acme.com/services/${params.slug}` },
]),
];
// Add HowTo schema if the service has a process
if (service.process_steps) {
schemas.push(generateHowToSchema({
name: `How We Deliver ${service.name}`,
description: `Our step-by-step process for ${service.name}`,
steps: service.process_steps.map(s => ({
name: s.title,
text: s.description,
})),
}));
}
// Add FAQ if available
if (service.faqs?.length > 0) {
schemas.push(generateFAQSchema(
service.faqs.map(f => ({ q: f.question, a: f.answer }))
));
}
return (
<>
<JsonLd schema={schemas} graph />
<main>
<h1>{service.name}</h1>
<div>{service.content}</div>
</main>
</>
);
}Contact Page
// app/contact/page.tsx — Server Component
import {
toNextMetadata,
generateWebPageSchema,
generateBreadcrumbSchema,
generateLocalBusinessSchema,
JsonLd
} from '@masters-ws/react-seo/core';
export async function generateMetadata() {
return toNextMetadata({
title: "Contact Us — Get In Touch",
description: "Have a project in mind? Contact Acme Solutions for a free consultation. We respond within 24 hours.",
type: 'website',
canonical: "https://acme.com/contact",
}, siteConfig);
}
export default function ContactPage() {
const schemas = [
generateWebPageSchema({
name: "Contact Us",
description: "Get in touch with our team",
url: "https://acme.com/contact",
}, siteConfig),
generateBreadcrumbSchema([
{ name: "Home", item: "https://acme.com" },
{ name: "Contact", item: "https://acme.com/contact" },
]),
generateLocalBusinessSchema({
name: "Acme Solutions",
description: "Digital solutions company",
telephone: "+1-555-123-4567",
address: {
street: "123 Tech Avenue",
city: "San Francisco",
region: "CA",
postalCode: "94105",
country: "US",
},
geo: { lat: 37.7749, lng: -122.4194 },
openingHours: ["Mo-Fr 09:00-18:00"],
}),
];
return (
<>
<JsonLd schema={schemas} graph />
<main>
<h1>Contact Us</h1>
<ContactForm />
<Map />
</main>
</>
);
}Portfolio / Case Study Page
// app/portfolio/[slug]/page.tsx
import {
toNextMetadata,
generateWebPageSchema,
generateBreadcrumbSchema,
JsonLd,
cleanSchema,
} from '@masters-ws/react-seo/core';
export async function generateMetadata({ params }) {
const project = await fetchProject(params.slug);
return toNextMetadata({
title: `${project.name} — Case Study`,
description: project.summary,
image: project.cover_image,
type: 'website',
canonical: `https://acme.com/portfolio/${params.slug}`,
}, siteConfig);
}
export default async function CaseStudyPage({ params }) {
const project = await fetchProject(params.slug);
const schemas = [
generateWebPageSchema({
name: project.name,
description: project.summary,
url: `https://acme.com/portfolio/${params.slug}`,
image: project.cover_image,
datePublished: project.completed_at,
}, siteConfig),
generateBreadcrumbSchema([
{ name: "Home", item: "https://acme.com" },
{ name: "Portfolio", item: "https://acme.com/portfolio" },
{ name: project.name, item: `https://acme.com/portfolio/${params.slug}` },
]),
// Custom CreativeWork schema for portfolio items
cleanSchema({
"@context": "https://schema.org",
"@type": "CreativeWork",
"name": project.name,
"description": project.summary,
"image": project.cover_image,
"dateCreated": project.completed_at,
"creator": {
"@type": "Organization",
"name": siteConfig.name,
},
"url": `https://acme.com/portfolio/${params.slug}`,
}),
];
return (
<>
<JsonLd schema={schemas} graph />
<main>
<h1>{project.name}</h1>
<p>{project.summary}</p>
</main>
</>
);
}Careers / Job Listing Page
// app/careers/[slug]/page.tsx
import {
toNextMetadata,
generateJobPostingSchema,
generateBreadcrumbSchema,
JsonLd
} from '@masters-ws/react-seo/core';
export async function generateMetadata({ params }) {
const job = await fetchJob(params.slug);
return toNextMetadata({
title: `${job.title} — Join Our Team`,
description: `We're hiring a ${job.title}. ${job.summary}`,
type: 'website',
canonical: `https://acme.com/careers/${params.slug}`,
}, siteConfig);
}
export default async function JobPage({ params }) {
const job = await fetchJob(params.slug);
const schemas = [
generateJobPostingSchema({
title: job.title,
description: job.full_description,
datePosted: job.posted_at,
validThrough: job.expires_at,
employmentType: job.type, // "FULL_TIME", "CONTRACTOR", etc.
remote: job.is_remote,
hiringOrganization: {
name: siteConfig.name,
sameAs: siteConfig.url,
logo: siteConfig.logo,
},
jobLocation: {
streetAddress: "123 Tech Avenue",
addressLocality: "San Francisco",
addressRegion: "CA",
addressCountry: "US",
},
baseSalary: job.salary ? {
currency: "USD",
value: { minValue: job.salary.min, maxValue: job.salary.max },
unitText: "YEAR",
} : undefined,
}),
generateBreadcrumbSchema([
{ name: "Home", item: "https://acme.com" },
{ name: "Careers", item: "https://acme.com/careers" },
{ name: job.title, item: `https://acme.com/careers/${params.slug}` },
]),
];
return (
<>
<JsonLd schema={schemas} graph />
<main>
<h1>{job.title}</h1>
<JobDetails job={job} />
<ApplyButton />
</main>
</>
);
}🛒 Use Case: E-Commerce Store
See the Quick Start section above for complete examples:
- Product Page — with reviews, return policy, shipping, variants
- Category Page — with pagination and ItemList schema
Manual Approach (More Control)
Use individual functions for granular control:
import {
toNextMetadata,
generateProductSchema,
generateBreadcrumbSchema,
generateOrganizationSchema,
generateWebPageSchema,
JsonLd
} from '@masters-ws/react-seo/core';
export async function generateMetadata({ params }) {
const product = await fetchProduct(params.slug);
return toNextMetadata({
title: product.meta_title || product.name,
description: product.meta_description,
image: product.main_image?.url,
type: 'product',
canonical: `https://store.com/products/${params.slug}`,
product: {
sku: product.sku,
brand: product.brand?.name,
price: product.price,
currency: "USD",
availability: "https://schema.org/InStock",
}
}, siteConfig);
}
export default async function ProductPage({ params }) {
const product = await fetchProduct(params.slug);
const schemas = [
generateProductSchema({
name: product.name,
description: product.description,
image: [product.main_image, ...product.gallery],
price: product.price,
currency: "USD",
url: `https://store.com/products/${params.slug}`,
reviews: [
{ author: "John", ratingValue: 5, reviewBody: "Excellent product!" },
],
returnPolicy: {
returnPolicyCategory: 'MerchantReturnFiniteReturnWindow',
returnWithin: 14,
},
shipping: {
shippingRate: { value: 0, currency: "USD" },
deliveryTime: { minDays: 2, maxDays: 5 },
},
}),
generateBreadcrumbSchema([
{ name: "Home", item: "https://store.com" },
{ name: product.name, item: `https://store.com/products/${params.slug}` },
]),
];
return (
<>
<JsonLd schema={schemas} graph />
<ProductDetailClient product={product} />
</>
);
}<JsonLd> Component
Server-safe JSON-LD renderer. Works in Server Components (no 'use client' needed).
Props
| Prop | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| schema | object \| object[] | — | Schema object(s) to render |
| graph | boolean | false | Combine schemas into a single @graph block |
Separate Scripts (default)
<JsonLd schema={[productSchema, breadcrumbSchema]} />
// Renders 2 separate <script type="application/ld+json"> tags@graph Pattern (recommended for multiple schemas)
<JsonLd schema={[productSchema, breadcrumbSchema, orgSchema]} graph />
// Renders 1 <script> with @context + @graph containing all schemasUsage: React / Pages Router (With Components)
// _app.tsx
import { SEOProvider } from '@masters-ws/react-seo';
const config = { name: "My Site", url: "https://mysite.com", description: "Awesome site" };
export default function App({ Component, pageProps }) {
return (
<SEOProvider config={config}>
<Component {...pageProps} />
</SEOProvider>
);
}// pages/products/[id].tsx
import { SeoProduct } from '@masters-ws/react-seo';
export default function ProductPage({ product }) {
return (
<>
<SeoProduct item={{
name: product.name,
description: product.description,
image: product.image,
price: product.price,
currency: "USD",
sku: product.sku,
brand: product.brand,
}} />
<main>...</main>
</>
);
}Core Functions Reference
Convenience Helpers (Recommended — One-Call Setup)
| Function | Description |
| :--- | :--- |
| generateProductMetadata(product, config) | Generates Metadata + Product, Breadcrumb, Organization, WebSite schemas |
| generateArticleMetadata(article, config) | Generates Metadata + Article, Breadcrumb, Organization, WebSite schemas |
| generateCategoryMetadata(category, config) | Generates Metadata + CollectionPage, Breadcrumb, Organization, ItemList schemas |
| generateHomepageMetadata(input, config) | Generates Metadata + WebPage, Organization, WebSite, optional LocalBusiness schemas |
Metadata Functions
| Function | Description |
| :--- | :--- |
| toNextMetadata(data, config) | Converts SEO data to Next.js Metadata object (OG, Twitter, Canonical, etc.) |
| generatePaginationLinks(url, page, total) | Returns prev / next / canonical URLs |
| generatePaginatedTitle(title, page, suffix) | Appends page number to title |
Schema Generator Functions
| Function | Description |
| :--- | :--- |
| generateProductSchema(data) | Product with multi-image, reviews, return policy, shipping, variants |
| generateArticleSchema(data, config) | NewsArticle with author and publisher |
| generateFAQSchema(questions) | FAQPage |
| generateBreadcrumbSchema(items) | BreadcrumbList |
| generateVideoSchema(data) | VideoObject |
| generateEventSchema(data) | Event (online & offline) |
| generateLocalBusinessSchema(data) | LocalBusiness with geo and opening hours |
| generateOrganizationSchema(config) | Organization |
| generateWebSiteSchema(config) | WebSite with SearchAction |
| generateWebPageSchema(data, config) | WebPage |
| generateCollectionPageSchema(data, config) | CollectionPage (for categories/archives) |
| generateItemListSchema(data) | ItemList (for product listings) |
| generateHowToSchema(data) | HowTo with steps, tools, supplies |
| generateRecipeSchema(data) | Recipe with ingredients and instructions |
| generateJobPostingSchema(data) | JobPosting with salary and remote support |
| generateSoftwareSchema(data) | SoftwareApplication |
| generateBookSchema(data) | Book |
| generateMovieSchema(data) | Movie |
| generatePodcastSchema(data) | PodcastSeries |
| generatePodcastEpisodeSchema(data) | PodcastEpisode |
Utility Functions
| Function | Description |
| :--- | :--- |
| cleanSchema(obj) | Deep-removes undefined/null values from schema objects |
| validateSEO(type, data, fields) | Logs dev warnings for missing required fields |
Components
| Component | Description |
| :--- | :--- |
| <JsonLd schema={..} graph? /> | Server-safe JSON-LD renderer with optional @graph support |
Product Schema Features
Multi-Image Support
generateProductSchema({
name: "T-Shirt",
image: [ // Array of images
"https://store.com/front.jpg",
"https://store.com/back.jpg",
"https://store.com/detail.jpg",
],
// ...
});Individual Reviews
generateProductSchema({
// ...
rating: 4.5,
reviewCount: 128,
reviews: [
{ author: "Alice", ratingValue: 5, reviewBody: "Amazing quality!", datePublished: "2024-01-15" },
{ author: "Bob", ratingValue: 4, reviewBody: "Good value", datePublished: "2024-02-01" },
],
});Return Policy
generateProductSchema({
// ...
returnPolicy: {
returnPolicyCategory: 'MerchantReturnFiniteReturnWindow',
returnWithin: 30, // 30 days
returnMethod: 'ReturnByMail',
returnFees: 'FreeReturn',
},
});Shipping Details
generateProductSchema({
// ...
shipping: {
shippingRate: { value: 5.99, currency: "USD" },
shippingDestination: "US",
deliveryTime: { minDays: 3, maxDays: 7 },
freeShippingThreshold: 50, // Free shipping over $50
},
});Product Variants (AggregateOffer)
generateProductSchema({
name: "T-Shirt",
description: "...",
// When variants are provided, generates AggregateOffer with lowPrice/highPrice
variants: [
{ name: "Small", sku: "TS-S", price: 19.99 },
{ name: "Medium", sku: "TS-M", price: 22.99 },
{ name: "Large", sku: "TS-L", price: 24.99 },
],
});
// Result: AggregateOffer { lowPrice: 19.99, highPrice: 24.99, offerCount: 3 }Development Warnings
In development mode (NODE_ENV !== 'production'), the library logs warnings for missing required fields:
[react-seo] Warning: "image" is missing in Product schema. Google may not show rich results.
[react-seo] Warning: "price" is missing in Product schema. Google may not show rich results.⚠️ Important: SSR vs CSR
✅ Correct — Server-Side (Google sees everything on first crawl)
// page.tsx — NO 'use client'!
import { generateProductMetadata, JsonLd } from '@masters-ws/react-seo/core';
export async function generateMetadata() { /* ... */ }
export default function Page() {
return <JsonLd schema={schemas} graph />;
}❌ Incorrect — Client-Side (Google may not see metadata)
// page.tsx
'use client' // ⛔ Metadata is only injected after JavaScript loads!
export default function Page() {
return <SEO title="..." />; // Uses react-helmet → client-side only
}Rule of thumb: For Next.js App Router, always use
@masters-ws/react-seo/corefunctions in Server Components. Use'use client'only for interactive UI components, never for SEO logic.
Helmet Components Reference (Requires react-helmet-async)
These components are designed for React SPA or Next.js Pages Router:
| Component | Description |
| :--- | :--- |
| <SEOProvider> | Context provider — wraps your app with site config |
| <SEO /> | Main component for meta tags and inline schemas |
| <SeoArticle /> | Article/blog post SEO |
| <SeoProduct /> | E-commerce product SEO |
| <SeoFAQ /> | FAQ pages |
| <SeoVideo /> | Video content |
| <SeoEvent /> | Events and conferences |
| <SeoLocalBusiness /> | Physical business locations |
| <SeoCategory /> | Category pages with pagination |
| <SeoTag /> | Tag pages with pagination |
| <SeoAuthor /> | Author profile pages |
| <SeoHowTo /> | How-to guides |
| <SeoReview /> | Review pages |
| <SeoCourse /> | Online courses |
| <SeoRecipe /> | Recipes |
| <SeoJobPosting /> | Job listings |
| <Breadcrumb /> | Breadcrumb navigation with schema |
Note:
<SeoTag>,<SeoAuthor>, and<SeoCategory>acceptpageSuffixandtitlePrefixprops for localization (defaults to English). PasspageSuffix="صفحة"for Arabic, etc.
Configuration
SiteConfig
interface SiteConfig {
name: string; // Site name (used in title suffix)
url: string; // Base URL (no trailing slash)
description: string; // Default meta description
logo?: string; // Logo URL (used in Organization schema)
language?: string; // Locale, e.g. 'en_US' (default: 'ar_SA')
twitterHandle?: string; // Twitter @username
facebookAppId?: string; // Facebook App ID
themeColor?: string; // Mobile browser theme color
manifest?: string; // Web app manifest URL
socialLinks?: string[]; // Social media profile URLs
publisher?: string; // Publisher name
}SEOData
interface SEOData {
title?: string;
description?: string;
image?: string;
canonical?: string;
type?: 'website' | 'article' | 'product' | 'profile' | 'video' | 'faq';
robots?: string;
noindex?: boolean;
keywords?: string[];
prev?: string;
next?: string;
// Open Graph
ogTitle?: string;
ogDescription?: string;
ogImage?: string;
ogImageWidth?: number; // Default: 1200
ogImageHeight?: number; // Default: 630
ogImageAlt?: string;
ogType?: string;
ogLocale?: string;
// Twitter Cards
twitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player';
twitterTitle?: string;
twitterDescription?: string;
twitterImage?: string;
// Article-specific
publishedTime?: string;
modifiedTime?: string;
author?: { name: string; url?: string; image?: string };
tags?: string[];
section?: string;
readingTime?: number;
// Product-specific
product?: {
sku?: string;
brand?: string;
price?: number;
currency?: string;
availability?: string;
rating?: number;
reviewCount?: number;
};
// Multilingual
alternates?: Array<{ hreflang: string; href: string }>;
// Performance
dnsPrefetch?: string[];
preconnect?: string[];
prefetch?: string[];
preload?: Array<{ href: string; as: string; type?: string }>;
// Extras
whatsappImage?: string;
schema?: any;
}Changelog
v1.4.0
- ✨ generateHomepageMetadata() — One-call helper for homepage / landing pages
- ✨ cleanSchema() — Automatically strips
undefined/nullfrom all JSON-LD output - ✨ @graph pattern —
<JsonLd graph />combines schemas into single@context/@graphblock - ✨ Multi-image — Product and Article schemas accept
string | string[]for images - ✨ Individual reviews — Product schema supports
reviews[]array with author, rating, body - ✨ MerchantReturnPolicy — Product schema supports return policy (category, days, method, fees)
- ✨ ShippingDetails — Product schema supports shipping rate, destination, delivery time, free threshold
- ✨ Product variants — Automatically generates
AggregateOfferwithlowPrice/highPrice - ✨ generateCategoryMetadata() — One-call helper for category pages with pagination + ItemList
- ✨ WebPage schema —
generateWebPageSchema()for general pages - ✨ CollectionPage schema —
generateCollectionPageSchema()for category/archive pages - ✨ ItemList schema —
generateItemListSchema()for product listing pages - ✨ HowTo schema —
generateHowToSchema()with steps, tools, supplies (moved to core) - ✨ Recipe schema —
generateRecipeSchema()(moved to core) - ✨ JobPosting schema —
generateJobPostingSchema()with remote support (moved to core) - ✨ validateSEO() — Dev-only console warnings for missing required fields
- 🔧 i18n — Removed all hardcoded Arabic strings, all labels now configurable via props
- 🔧 Product schema — Added
gtin,mpn,condition,sellerfields
v1.3.0
- ✨ Added
<JsonLd>server-safe component for Next.js App Router - ✨ Added
generateProductMetadata()— one-call helper for product pages - ✨ Added
generateArticleMetadata()— one-call helper for article pages - 🔧 Enhanced
toNextMetadata()with article OG tags, product OG tags, pagination links - 📝 Updated README with SSR best practices
v1.2.1
- Initial stable release with core functions and Helmet components
License
MIT
