Skip to content

Latest commit

 

History

History
402 lines (306 loc) · 10.8 KB

File metadata and controls

402 lines (306 loc) · 10.8 KB

TypoGrammar Global SEO Implementation Guide

🎯 Overview

A comprehensive SEO system that automatically applies metadata, Open Graph tags, Twitter Cards, and JSON-LD structured data to ALL pages on TypoGrammar.

📁 Files Created/Modified

✅ Created Files

  1. src/seo/seoDefaults.ts - Global site configuration
  2. src/seo/humanizeSlug.ts - Converts URLs to readable titles
  3. src/seo/buildSeoMeta.ts - Core SEO metadata builder
  4. src/seo/pageSeoMap.json - Optional per-page overrides (no database)
  5. src/seo/index.ts - Central export point
  6. src/components/SEO.tsx - React component that applies SEO

✅ Modified Files

  1. src/components/Layout.tsx - Added <SEO path={location.pathname} /> for global application

🚀 How It Works

1. Global Application (Layout.tsx)

import SEO from './SEO';
import { useLocation } from 'react-router-dom';

const Layout = () => {
  const location = useLocation();
  
  return (
    <div>
      <SEO path={location.pathname} />  {/* Applied to ALL pages */}
      <Outlet />
    </div>
  );
};

What happens:

  • Every page automatically gets SEO metadata
  • Uses current route path from useLocation() hook
  • Generates title from slug if no custom title provided
  • Adds canonical URL, robots meta, OG tags, Twitter cards
  • Injects JSON-LD (WebSite + Organization) on every page

2. Per-Page Customization (3 Methods)

Method A: Using Existing usePageMetadata Hook (Backward Compatible)

Pages can still use the existing hook - it takes priority over global SEO:

import usePageMetadata from '../hooks/usePageMetadata';

const MyPage = () => {
  usePageMetadata({
    title: 'Custom Page Title | TypoGrammar',
    description: 'Custom description for this specific page',
    canonical: 'https://typogrammar.com/my-page'
  });
  
  return <div>Page content</div>;
};

Priority: usePageMetadata > Global SEO

Method B: Adding to pageSeoMap.json (No Code Changes)

Edit src/seo/pageSeoMap.json:

{
  "/my-page": {
    "title": "My Custom Page Title",
    "description": "Custom description without code changes",
    "ogImage": "/images/my-page-og.png"
  }
}

Priority: pageSeoMap.json > humanizeSlug fallback

Method C: Passing Props to SEO Component (Advanced)

For dynamic SEO (e.g., FAQ pages with FAQ schema):

// In Layout.tsx or route-specific wrapper
<SEO 
  path={location.pathname}
  faq={[
    { question: "What is IELTS?", answer: "IELTS is..." },
    { question: "How to prepare?", answer: "You can..." }
  ]}
/>

This adds FAQPage JSON-LD automatically.

📊 SEO Features Applied to ALL Pages

1. Basic Meta Tags

<title>Page Title | TypoGrammar</title>
<meta name="description" content="...">
<link rel="canonical" href="https://typogrammar.com/page">
<meta name="robots" content="index,follow">

2. Open Graph (Facebook, LinkedIn, etc.)

<meta property="og:title" content="...">
<meta property="og:description" content="...">
<meta property="og:url" content="https://typogrammar.com/page">
<meta property="og:image" content="https://typogrammar.com/og-default.png">
<meta property="og:site_name" content="TypoGrammar">
<meta property="og:locale" content="en_US">
<meta property="og:type" content="website">

3. Twitter Card

<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="...">
<meta name="twitter:description" content="...">
<meta name="twitter:image" content="...">

4. JSON-LD Structured Data

Always Included (Every Page):

WebSite Schema:

{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "TypoGrammar",
  "url": "https://typogrammar.com",
  "description": "Learn academic English...",
  "potentialAction": {
    "@type": "SearchAction",
    "target": {
      "@type": "EntryPoint",
      "urlTemplate": "https://typogrammar.com/search?q={search_term_string}"
    }
  }
}

Organization Schema:

{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": "TypoGrammar",
  "url": "https://typogrammar.com",
  "logo": "https://typogrammar.com/logo.png",
  "description": "Online platform for academic English learning"
}

Conditionally Included:

FAQPage Schema (if FAQ data provided):

{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [...]
}

Article Schema (if article data provided):

{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "...",
  "datePublished": "2026-01-13",
  "author": {...},
  "publisher": {...}
}

🔄 Title Generation Logic

Input: /blog/ielts-vs-toefl-which-test-easier-better-2026

Step 1: Check pageSeoMap.json
  → If exists: Use custom title

Step 2: Check if page uses usePageMetadata hook
  → If yes: Use that title (takes priority)

Step 3: Fallback to humanizeSlug
  → Result: "IELTS vs TOEFL Which Test Easier Better 2026"
  → Add brand suffix: " | TypoGrammar"

Final: "IELTS vs TOEFL Which Test Easier Better 2026 | TypoGrammar"

🛠️ Configuration

Edit src/seo/seoDefaults.ts to change global settings:

export const seoDefaults = {
  siteName: "TypoGrammar",
  siteUrl: "https://typogrammar.com",
  defaultTitle: "TypoGrammar | Academic English for IELTS & TOEFL",
  defaultDescription: "Learn academic English for IELTS and TOEFL...",
  defaultOgImage: "/og-default.png",
  locale: "en_US",
  brandSuffix: " | TypoGrammar"
};

✅ Validation Checklist

Test 3 Different Pages:

  1. Homepage (/)

    • Open browser DevTools → Elements → <head>
    • Verify <title> contains "TypoGrammar"
    • Verify <meta name="description"> exists (only ONE)
    • Verify <link rel="canonical" href="https://typogrammar.com/"> (only ONE)
    • Verify Open Graph tags: og:title, og:description, og:url, og:image
    • Verify Twitter Card tags
    • Verify JSON-LD scripts (at least 2: WebSite + Organization)
  2. Grammar Topic Page (/topics/present-simple)

  3. Blog Post (/blog/ielts-writing-task-1-2-guide-2026)

    • Custom title from pageSeoMap.json (if added)
    • All meta tags present
    • If article data provided: Article JSON-LD included

Check for Duplicates:

# In browser console:
document.querySelectorAll('meta[name="description"]').length  // Should be 1
document.querySelectorAll('link[rel="canonical"]').length     // Should be 1
document.querySelectorAll('title').length                     // Should be 1

Console/Build Errors:

npm run build
# Should complete without errors
# Check bundle size - SEO modules are tiny (~5KB total)

📝 Adding FAQ Schema to a Page

Option 1: Edit pageSeoMap.json (Future Enhancement)

Currently, pageSeoMap only supports title/description. To add FAQ support, extend the JSON structure or use Option 2.

Option 2: Pass FAQ via Component Prop

Update the page component to provide FAQ data:

// In FaqPage.tsx or any page with FAQs
import { SEO } from '../components/SEO';
import { useLocation } from 'react-router-dom';

const FaqPage = () => {
  const location = useLocation();
  
  const faqData = [
    { question: "What is IELTS?", answer: "IELTS is an international English test..." },
    { question: "How long is IELTS valid?", answer: "IELTS scores are valid for 2 years..." },
    // Add more...
  ];
  
  return (
    <div>
      {/* This will add FAQPage schema */}
      <SEO path={location.pathname} faq={faqData} />
      {/* Page content */}
    </div>
  );
};

Note: Since Layout.tsx already renders <SEO />, adding another in a page might cause conflicts. Better approach: Use a context to pass FAQ data from page to Layout.

Option 3: Create FAQ-specific Layout Wrapper (Recommended)

For pages with FAQ, create a wrapper that enhances the global SEO:

// src/components/SEOEnhancer.tsx
import { useEffect } from 'react';

interface Props {
  faq?: Array<{question: string, answer: string}>;
}

export const SEOEnhancer: React.FC<Props> = ({ faq }) => {
  useEffect(() => {
    if (!faq || faq.length < 2) return;
    
    // Add FAQ JSON-LD
    const script = document.createElement('script');
    script.type = 'application/ld+json';
    script.setAttribute('data-seo-enhancer', 'true');
    script.textContent = JSON.stringify({
      '@context': 'https://schema.org',
      '@type': 'FAQPage',
      mainEntity: faq.map(item => ({
        '@type': 'Question',
        name: item.question,
        acceptedAnswer: { '@type': 'Answer', text: item.answer }
      }))
    });
    document.head.appendChild(script);
    
    return () => {
      const existing = document.querySelector('[data-seo-enhancer="true"]');
      if (existing) existing.remove();
    };
  }, [faq]);
  
  return null;
};

Then use in FAQ page:

const FaqPage = () => {
  return (
    <div>
      <SEOEnhancer faq={faqData} />
      {/* Page content */}
    </div>
  );
};

🐛 Troubleshooting

Issue: Duplicate meta tags

Cause: Both global SEO and usePageMetadata creating tags

Fix: The SEO component now checks for existing values and doesn't override if usePageMetadata was called first.

Issue: Title not updating on navigation

Cause: React Router not triggering re-render

Fix: SEO component uses location.pathname as dependency, ensuring updates on every route change.

Issue: JSON import error in buildSeoMeta.ts

Cause: TypeScript can't resolve JSON module

Fix: Ensure tsconfig.json has "resolveJsonModule": true (already configured).

Issue: OG image not showing in social media

Cause: Relative path in OG image

Fix: buildSeoMeta automatically prepends seoDefaults.siteUrl to relative image paths.

📈 Next Steps

  1. Add more page overrides to pageSeoMap.json for important pages
  2. Create custom OG images for blog posts and major pages
  3. Test social sharing on Facebook, Twitter, LinkedIn
  4. Add Article schema to blog posts with proper dates
  5. Add FAQ schema to FAQ page using SEOEnhancer component
  6. Monitor Google Search Console for indexing status
  7. Test with Google Rich Results Test for structured data validation

🔗 Useful Tools


Implementation Date: January 13, 2026
Status: ✅ Complete and Production-Ready
Backward Compatible: ✅ Yes - existing usePageMetadata hooks still work