A comprehensive SEO system that automatically applies metadata, Open Graph tags, Twitter Cards, and JSON-LD structured data to ALL pages on TypoGrammar.
- src/seo/seoDefaults.ts - Global site configuration
- src/seo/humanizeSlug.ts - Converts URLs to readable titles
- src/seo/buildSeoMeta.ts - Core SEO metadata builder
- src/seo/pageSeoMap.json - Optional per-page overrides (no database)
- src/seo/index.ts - Central export point
- src/components/SEO.tsx - React component that applies SEO
- src/components/Layout.tsx - Added
<SEO path={location.pathname} />for global application
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
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
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
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.
<title>Page Title | TypoGrammar</title>
<meta name="description" content="...">
<link rel="canonical" href="https://typogrammar.com/page">
<meta name="robots" content="index,follow"><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"><meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="...">
<meta name="twitter:description" content="...">
<meta name="twitter:image" content="...">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"
}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": {...}
}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"
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"
};-
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)
- Open browser DevTools → Elements →
-
Grammar Topic Page (/topics/present-simple)
- Title should be "Present Simple | TypoGrammar"
- Description auto-generated or from pageSeoMap
- Canonical: https://typogrammar.com/topics/present-simple
- All meta tags present
-
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
# 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 1npm run build
# Should complete without errors
# Check bundle size - SEO modules are tiny (~5KB total)Currently, pageSeoMap only supports title/description. To add FAQ support, extend the JSON structure or use Option 2.
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.
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>
);
};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.
Cause: React Router not triggering re-render
Fix: SEO component uses location.pathname as dependency, ensuring updates on every route change.
Cause: TypeScript can't resolve JSON module
Fix: Ensure tsconfig.json has "resolveJsonModule": true (already configured).
Cause: Relative path in OG image
Fix: buildSeoMeta automatically prepends seoDefaults.siteUrl to relative image paths.
- Add more page overrides to
pageSeoMap.jsonfor important pages - Create custom OG images for blog posts and major pages
- Test social sharing on Facebook, Twitter, LinkedIn
- Add Article schema to blog posts with proper dates
- Add FAQ schema to FAQ page using SEOEnhancer component
- Monitor Google Search Console for indexing status
- Test with Google Rich Results Test for structured data validation
- Google Rich Results Test: https://search.google.com/test/rich-results
- Facebook Sharing Debugger: https://developers.facebook.com/tools/debug/
- Twitter Card Validator: https://cards-dev.twitter.com/validator
- Schema.org Validator: https://validator.schema.org/
Implementation Date: January 13, 2026
Status: ✅ Complete and Production-Ready
Backward Compatible: ✅ Yes - existing usePageMetadata hooks still work