1. Writing <html> and <head> in page components

Page components should never include document-level tags. These belong in the Document component only.

WRONG — page writes <html> tagsBAD
// pages/AboutPage.tsx — DO NOT DO THIS
export function AboutPage() {
  const seo = mergeSEOConfig(siteConfig, { title: "About" });
  return (
    <html>          {/* WRONG */}
      <head>        {/* WRONG */}
        <SEOHead {...seo} />
      </head>
      <body>        {/* WRONG */}
        <h1>About</h1>
      </body>
    </html>
  );
}
CORRECT — page returns content inside DocumentGOOD
// pages/AboutPage.tsx — CORRECT
export function AboutPage() {
  const seo = mergeSEOConfig(siteConfig, { title: "About" });
  return (
    <Document seo={seo}>
      <h1>About</h1>
    </Document>
  );
}
TIP

Rule: If you are writing <html> or <head> inside a pages/ file, it is wrong. Only the Document file should have these.

2. Using <SEOHead> in Next.js App Router pages

Next.js App Router manages <head> automatically. Use generateMetadata instead.

WRONG — SEOHead in App RouterBAD
// app/about/page.tsx — DO NOT DO THIS
export default function AboutPage() {
  const seo = mergeSEOConfig(siteConfig, { title: "About" });
  return (
    <div>
      <SEOHead {...seo} />  {/* WRONG in App Router */}
      <h1>About</h1>
    </div>
  );
}
CORRECT — use generateMetadataGOOD
// app/about/page.tsx — CORRECT
import { mergeSEOConfig, buildTitle } from "react-ssr-seo-toolkit";
import type { Metadata } from "next";

const seo = mergeSEOConfig(siteConfig, { title: "About" });

export const metadata: Metadata = {
  title: buildTitle(seo.title!, seo.titleTemplate),
  description: seo.description,
};

export default function AboutPage() {
  return <div><h1>About</h1></div>;
}

3. Forgetting to merge with site config

WRONG — creating config from scratchBAD
// Loses all site defaults!
const seo = createSEOConfig({
  title: "About Us",
  description: "About page.",
});
// Missing: titleTemplate, openGraph.siteName, twitter.site, etc.
CORRECT — merge with site configGOOD
const seo = mergeSEOConfig(siteConfig, {
  title: "About Us",
  description: "About page.",
});
// Inherits titleTemplate, openGraph.siteName, twitter.site, etc.

4. Not setting canonical URLs

Without canonical URLs, search engines may index duplicate content.

Always set canonicalGOOD
const seo = mergeSEOConfig(siteConfig, {
  title: "About",
  description: "About page.",
  canonical: buildCanonicalUrl(SITE_URL, "/about"),
});

5. Using JSON.stringify for JSON-LD

JSON.stringify can create XSS vulnerabilities with characters like <, >, &.

WRONG — unsafe serializationBAD
<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
CORRECT — use safe methodsGOOD
// Option 1: JsonLd component (recommended)
<JsonLd data={schema} />

// Option 2: safeJsonLdSerialize for manual rendering
<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{ __html: safeJsonLdSerialize(schema) }}
/>

6. Duplicate SEO tags

Do not render <SEOHead> in both the Document and the page.

WRONG — SEOHead in both placesBAD
// Document already renders <SEOHead />
export function AboutPage() {
  return (
    <Document seo={seo}>
      <SEOHead {...seo} />  {/* DUPLICATE! */}
      <h1>About</h1>
    </Document>
  );
}

7. Hardcoding URLs

Hardcoded URLs cause trailing slash and protocol inconsistencies.

WRONG — hardcoded URLsBAD
canonical: "https://mysite.com/about/"  // Trailing slash
openGraph: { url: "http://mysite.com/about" }  // Wrong protocol
CORRECT — use buildCanonicalUrlGOOD
const url = buildCanonicalUrl(SITE_URL, "/about");
// Consistent: no trailing slash, correct protocol
const seo = mergeSEOConfig(siteConfig, {
  canonical: url,
  openGraph: { url },
});

8. SSR/CSR confusion

!

SEO tags must be server-rendered. In a client-side only app (Create React App, Vite SPA without SSR), search engines won't see your meta tags. You need SSR (Express, Next.js, React Router SSR, etc.) for this to work.

9. Missing Open Graph images

Without OG images, shared links look plain on social media.

Always include OG imagesGOOD
openGraph: {
  images: [{
    url: "https://mysite.com/og-image.jpg",
    width: 1200,   // Recommended: 1200x630
    height: 630,
    alt: "Description of the image",
  }],
}

10. Not testing your SEO output

  • View Page Source — Right-click → "View Page Source" to check the raw HTML
  • Google Rich Results Test — Validate JSON-LD structured data
  • Facebook Sharing Debugger — Test social sharing preview
  • Twitter Card Validator — Preview Twitter Card appearance
  • Google Search Console — Monitor indexing and errors

Quick reference: What goes where

FileShould containShould NOT contain
config/seo.tscreateSEOConfig, SITE_URLPage-specific data
Document.tsx<html>, <head>, <SEOHead>, <JsonLd>Page-specific content
pages/*.tsxmergeSEOConfig, schemas, content in <Document><html>, <head>, <SEOHead>
server.tsxrenderToString, route matchingSEO config, meta tags
VIEW

Need more help? Check the FAQ or the API Reference.