npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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/null values 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" and rel="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 hreflang management
  • Performance Optimized — DNS Prefetch, Preconnect, Preload support

Installation

For Next.js App Router (Zero Dependencies):

npm install @masters-ws/react-seo

For 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=1 for 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:


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 schemas

Usage: 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/core functions 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> accept pageSuffix and titlePrefix props for localization (defaults to English). Pass pageSuffix="صفحة" 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/null from all JSON-LD output
  • @graph pattern<JsonLd graph /> combines schemas into single @context/@graph block
  • 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 AggregateOffer with lowPrice/highPrice
  • generateCategoryMetadata() — One-call helper for category pages with pagination + ItemList
  • WebPage schemagenerateWebPageSchema() for general pages
  • CollectionPage schemagenerateCollectionPageSchema() for category/archive pages
  • ItemList schemagenerateItemListSchema() for product listing pages
  • HowTo schemagenerateHowToSchema() with steps, tools, supplies (moved to core)
  • Recipe schemagenerateRecipeSchema() (moved to core)
  • JobPosting schemagenerateJobPostingSchema() 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, seller fields

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