Structured Data and Schema.org
Structured Data and Schema.org
JSON-LD for Articles and Blog Posts
// components/ArticleSchema.ts
interface ArticleProps {
title: string;
description: string;
author: string;
publishedAt: string;
updatedAt: string;
image: string;
url: string;
}
export function generateArticleSchema(article: ArticleProps) {
return {
'@context': 'https://schema.org',
'@type': 'Article',
headline: article.title,
description: article.description,
image: article.image,
datePublished: article.publishedAt,
dateModified: article.updatedAt,
author: {
'@type': 'Person',
name: article.author,
url: 'https://mydevblog.com/authors/jane',
},
publisher: {
'@type': 'Organization',
name: 'MyDevBlog',
logo: {
'@type': 'ImageObject',
url: 'https://mydevblog.com/logo.png',
},
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': article.url,
},
};
}
FAQ Schema for Rich Snippets
// lib/faq-schema.ts
interface FAQItem {
question: string;
answer: string;
}
export function generateFAQSchema(faqs: FAQItem[]) {
return {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqs.map(faq => ({
'@type': 'Question',
name: faq.question,
acceptedAnswer: {
'@type': 'Answer',
text: faq.answer,
},
})),
};
}
// Usage in Astro page
---
const faqs = [
{ question: 'What is React?', answer: 'A JavaScript library for building user interfaces.' },
{ question: 'Why use hooks?', answer: 'Hooks let you use state and lifecycle in functional components.' },
];
const faqSchema = generateFAQSchema(faqs);
---
<script type="application/ld+json" set:html={JSON.stringify(faqSchema)} />
```\n
### Product Schema for E-commerce
```javascript
// schemas/product.ts
export function generateProductSchema(product) {
return {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
image: product.images,
description: product.description,
sku: product.sku,
brand: { '@type': 'Brand', name: product.brand },
offers: {
'@type': 'Offer',
url: `https://mystore.com/products/${product.slug}`,
priceCurrency: 'USD',
price: product.price,
availability: product.inStock
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
itemCondition: 'https://schema.org/NewCondition',
},
aggregateRating: product.rating
? {
'@type': 'AggregateRating',
ratingValue: product.rating.average,
reviewCount: product.rating.count,
}
: undefined,
};
}
BreadcrumbList Schema
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Home", "item": "https://mydevblog.com" },
{ "@type": "ListItem", "position": 2, "name": "React", "item": "https://mydevblog.com/category/react" },
{ "@type": "ListItem", "position": 3, "name": "Performance Guide", "item": "https://mydevblog.com/posts/react-performance" }
]
}
</script>
Validate Structured Data
# Test with Google Rich Results Test
# https://search.google.com/test/rich-results
# Or use schema.org validator
# https://validator.schema.org/
# Programmatic validation
npx schema-dts-gen --validate src/schemas/
Sitemaps and Robots.txt
Sitemaps and Robots.txt
robots.txt Configuration
# robots.txt - Control crawler access
User-agent: *
Allow: /
Disallow: /api/
Disallow: /admin/
Disallow: /dashboard/
Disallow: /private/
Disallow: /search?*
Disallow: /*.json$
# Block AI crawlers if desired
User-agent: GPTBot
Disallow: /
User-agent: ChatGPT-User
Disallow: /
# Sitemap location
Sitemap: https://mydevblog.com/sitemap.xml
Sitemap: https://mydevblog.com/sitemap-index.xml
Dynamic Sitemap Generation
// scripts/generate-sitemap.ts
import { writeFileSync } from 'fs';
import { db } from '../src/lib/database.js';
const BASE_URL = 'https://mydevblog.com';
interface SitemapURL {
loc: string;
lastmod?: string;
changefreq: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';
priority: string;
}
async function generateSitemap(): Promise<void> {
const posts = await db.query(
`SELECT slug, updated_at FROM posts WHERE published = true ORDER BY updated_at DESC`
);
const categories = await db.query(
`SELECT slug, updated_at FROM categories ORDER BY updated_at DESC`
);
const staticPages: SitemapURL[] = [
{ loc: BASE_URL, changefreq: 'daily', priority: '1.0', lastmod: new Date().toISOString() },
{ loc: `${BASE_URL}/about`, changefreq: 'monthly', priority: '0.8' },
{ loc: `${BASE_URL}/blog`, changefreq: 'daily', priority: '0.9' },
{ loc: `${BASE_URL}/contact`, changefreq: 'yearly', priority: '0.5' },
];
const postUrls: SitemapURL[] = posts.rows.map(post => ({
loc: `${BASE_URL}/posts/${post.slug}`,
lastmod: post.updated_at.toISOString(),
changefreq: 'monthly',
priority: '0.7',
}));
const categoryUrls: SitemapURL[] = categories.rows.map(cat => ({
loc: `${BASE_URL}/category/${cat.slug}`,
lastmod: cat.updated_at.toISOString(),
changefreq: 'weekly',
priority: '0.6',
}));
const allUrls = [...staticPages, ...postUrls, ...categoryUrls];
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${allUrls.map(url => ` <url>
<loc>${url.loc}</loc>
${url.lastmod ? `<lastmod>${url.lastmod}</lastmod>` : ''}
<changefreq>${url.changefreq}</changefreq>
<priority>${url.priority}</priority>
</url>`).join('\n')}
</urlset>`;
writeFileSync('public/sitemap.xml', xml);
console.log(`Sitemap generated with ${allUrls.length} URLs`);
}
generateSitemap();
Sitemap Index for Large Sites
<!-- sitemap-index.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>https://mydevblog.com/sitemap-posts.xml</loc>
<lastmod>2025-01-15T00:00:00Z</lastmod>
</sitemap>
<sitemap>
<loc>https://mydevblog.com/sitemap-categories.xml</loc>
<lastmod>2025-01-14T00:00:00Z</lastmod>
</sitemap>
<sitemap>
<loc>https://mydevblog.com/sitemap-pages.xml</loc>
<lastmod>2025-01-10T00:00:00Z</lastmod>
</sitemap>
</sitemapindex>
Canonical URLs and Pagination
<!-- Canonical URL to prevent duplicate content -->
<link rel="canonical" href="https://mydevblog.com/posts/react-performance">
<!-- Pagination signals -->
<link rel="prev" href="https://mydevblog.com/blog?page=2">
<link rel="next" href="https://mydevblog.com/blog?page=4">
<!-- Hreflang for multi-language -->
<link rel="alternate" hreflang="en" href="https://mydevblog.com/en/posts/react-performance">
<link rel="alternate" hreflang="es" href="https://mydevblog.com/es/posts/react-performance">
<link rel="alternate" hreflang="x-default" href="https://mydevblog.com/posts/react-performance">
Submit Sitemap to Search Engines
# Google Search Console
# Visit: https://search.google.com/search-console
# Add sitemap URL: https://mydevblog.com/sitemap.xml
# Bing Webmaster Tools
# Visit: https://www.bing.com/webmasters
# Submit: https://mydevblog.com/sitemap.xml
# Programmatic ping
curl "https://www.google.com/ping?sitemap=https://mydevblog.com/sitemap.xml"
curl "https://www.bing.com/indexnow" -d '{"host": "mydevblog.com", "urlList": ["https://mydevblog.com/posts/react-performance"]}'
Accessibility and WCAG Standards
Accessibility and WCAG Standards
Semantic HTML for Screen Readers and SEO
<!-- BAD: non-semantic -->
<div class="header">
<div class="nav">
<div class="nav-item" onclick="goHome()">Home</div>
</div>
</div>
<div class="main">
<div class="post-title">My Article</div>
<div class="post-content">Content here</div>
</div>
<!-- GOOD: semantic HTML -->
<header>
<nav aria-label="Main navigation">
<a href="/">Home</a>
</nav>
</header>
<main>
<article>
<h1>My Article</h1>
<p>Content here</p>
</article>
</main>
ARIA Labels and Landmarks
// Accessible navigation component
function Navigation() {
return (
<nav aria-label="Main navigation" role="navigation">
<ul role="menubar">
<li role="none">
<a role="menuitem" href="/" aria-current="page">Home</a>
</li>
<li role="none">
<a role="menuitem" href="/blog">Blog</a>
</li>
<li role="none">
<a role="menuitem" href="/about">About</a>
</li>
</ul>
</nav>
);
}
// Accessible form with error handling
function ContactForm() {
const [error, setError] = useState(null);
return (
<form aria-labelledby="form-title" noValidate>
<h2 id="form-title">Contact Us</h2>
{error && (
<div role="alert" aria-live="assertive" className="error">
{error}
</div>
)}
<div>
<label htmlFor="email">Email address</label>
<input
id="email"
type="email"
aria-required="true"
aria-invalid={!!error}
aria-describedby={error ? 'email-error' : undefined}
/>
{error && <span id="email-error" className="error-text">{error}</span>}
</div>
<button type="submit">Send</button>
</form>
);
}
Image Accessibility
<!-- Descriptive alt text for meaningful images -->
<img src="chart.png" alt="Bar chart showing 40% increase in React adoption from 2023 to 2025">
<!-- Empty alt for decorative images -->
<img src="decorative-border.svg" alt="" role="presentation">
<!-- Complex images with long description -->
<figure>
<img src="architecture.png" alt="System architecture diagram" aria-describedby="arch-desc">
<figcaption id="arch-desc">
The system uses a three-tier architecture: React frontend communicates with
Express API which connects to PostgreSQL database and Redis cache.
</figcaption>
</figure>
Keyboard Navigation
// Focus management for modal dialogs
function Modal({ isOpen, onClose, title, children }) {
const closeButtonRef = useRef(null);
const previousFocusRef = useRef(null);
useEffect(() => {
if (isOpen) {
previousFocusRef.current = document.activeElement;
closeButtonRef.current?.focus();
}
return () => {
previousFocusRef.current?.focus();
};
}, [isOpen]);
useEffect(() => {
function handleEscape(e) {
if (e.key === 'Escape') onClose();
}
if (isOpen) {
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div className="overlay" onClick={onClose} />
<div className="modal-content">
<h2 id="modal-title">{title}</h2>
{children}
<button ref={closeButtonRef} onClick={onClose} aria-label="Close dialog">
×
</button>
</div>
</div>
);
}
Color Contrast and WCAG Compliance
/* WCAG AA requires 4.5:1 contrast ratio for normal text */
/* WCAG AAA requires 7:1 contrast ratio for normal text */
/* Pass: good contrast */
.text-primary {
color: #1a1a2e; /* contrast 15.4:1 on white */
}
.text-secondary {
color: #555555; /* contrast 7.5:1 on white */
}
/* Fail: poor contrast */
.text-bad {
color: #aaaaaa; /* contrast 2.3:1 on white - FAILS */
}
/* Focus indicators - required for keyboard navigation */
:focus-visible {
outline: 2px solid #005fcc;
outline-offset: 2px;
}
/* Skip to main content link */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #005fcc;
color: white;
padding: 8px;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
Automated Accessibility Testing
# Install axe-core for automated testing
npm install axe-core @axe-core/react @axe-core/playwright
# React integration
import axe from '@axe-core/react';
if (process.env.NODE_ENV !== 'production') {
axe(React, ReactDOM, 1000);
}
# Playwright integration
test('page has no accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
# CLI audit
npx pa11y https://mydevblog.com
npx lighthouse https://mydevblog.com --only-categories=accessibility
Quiz
1. What is the recommended length for a meta description tag?
2. Which structured data format does Google recommend for SEO?
3. What does the canonical URL tag help prevent?
Flashcards
Question
What are the three main social media meta tag sets for SEO?
Click to reveal answer
Answer
Open Graph (Facebook/LinkedIn) uses og:title, og:description, og:image, og:url. Twitter Card uses twitter:card, twitter:title, twitter:description, twitter:image. Standard meta uses name="description", name="robots", and canonical link tags. Each platform reads its own tags to generate rich previews when content is shared.
Question
What is the purpose of JSON-LD structured data in SEO?
Click to reveal answer
Answer
JSON-LD (JavaScript Object Notation for Linked Data) provides search engines with explicit information about page content in a machine-readable format. It enables rich snippets in search results like star ratings, FAQ dropdowns, product prices, and event details. Schema.org vocabulary defines types like Article, Product, FAQPage, and BreadcrumbList that help Google understand and display content appropriately.
Question
How does WCAG accessibility compliance improve SEO rankings?
Click to reveal answer
Answer
WCAG standards improve SEO because search engines use similar criteria to evaluate pages. Semantic HTML (proper heading hierarchy, alt text, ARIA labels) helps crawlers understand content structure. Keyboard navigation ensures all content is accessible to crawlers. Color contrast and readable fonts improve user engagement metrics. Captioned videos and descriptive link text provide additional context signals that search engines use for ranking.
Revision Notes
Key Takeaways
- 1. Always include unique title tags (50-60 chars) and meta descriptions (120-160 chars) on every page
- 2. Use JSON-LD for structured data as it is Google's recommended format and easier to maintain
- 3. Generate XML sitemaps dynamically and submit them to Google Search Console and Bing Webmaster Tools
- 4. Implement Open Graph and Twitter Card tags to control how content appears when shared on social media
- 5. Semantic HTML and WCAG accessibility standards directly impact both user experience and search rankings
- 6. Use canonical URLs to prevent duplicate content issues across paginated, filtered, or multi-language pages
Interview Tips
- • Explain the difference between on-page SEO (meta tags, content, structure) and off-page SEO (backlinks, social signals)
- • Describe how you would audit a site's SEO: check meta tags, structured data validation, sitemap submission, Core Web Vitals, and accessibility
- • Know the Google Search Console features: URL Inspection, Performance reports, Coverage errors, Sitemap submission
- • Discuss why server-side rendering (SSR) or static site generation (SSG) improves SEO compared to client-side rendering
- • Explain structured data types: Article, Product, FAQ, BreadcrumbList, and how they produce rich snippets
Cheat Sheet
SEO Quick Reference: Title 50-60 chars | Meta description 120-160 chars | OG image 1200x630px | Twitter card summary_large_image | JSON-LD preferred over Microdata | Canonical URL prevents duplicates | Sitemap: max 50,000 URLs / 50MB | robots.txt: User-agent, Allow, Disallow, Sitemap | WCAG AA: 4.5:1 contrast ratio | Alt text required for all meaningful images