SEO Best Practices Skill
Complete SEO implementation for SaaS applications and modern web apps.
When to Use This Skill
-
Creating any public page (landing, pricing, blog, about)
-
Improving Google search rankings
-
Implementing rich snippets and social cards
-
Optimizing Core Web Vitals
-
Setting up structured data markup
-
Configuring sitemaps and robots.txt
Required Meta Tags
Every public page MUST include these meta tags for optimal SEO:
SEO Head Component (React/TypeScript)
// src/components/common/SEOHead.tsx import { Helmet } from 'react-helmet-async';
interface SEOProps { title: string; description: string; canonical?: string; image?: string; type?: 'website' | 'article' | 'product'; publishedTime?: string; modifiedTime?: string; author?: string; keywords?: string[]; noindex?: boolean; }
export default function SEOHead({
title,
description,
canonical,
image = '/og-image.jpg',
type = 'website',
publishedTime,
modifiedTime,
author,
keywords = [],
noindex = false
}: SEOProps) {
const siteName = 'YourApp';
const fullTitle = ${title} | ${siteName};
const url = canonical || (typeof window !== 'undefined' ? window.location.href : '');
const fullImageUrl = image.startsWith('http')
? image
: ${typeof window !== 'undefined' ? window.location.origin : ''}${image};
return ( <Helmet> {/* Basic Meta Tags */} <title>{fullTitle}</title> <meta name="description" content={description} /> {keywords.length > 0 && ( <meta name="keywords" content={keywords.join(', ')} /> )} <link rel="canonical" href={url} />
{/* Robots */}
{noindex && <meta name="robots" content="noindex, nofollow" />}
{/* Open Graph (Facebook, LinkedIn) */}
<meta property="og:type" content={type} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={url} />
<meta property="og:image" content={fullImageUrl} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:locale" content="en_US" />
<meta property="og:site_name" content={siteName} />
{/* Twitter Cards */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={fullImageUrl} />
<meta name="twitter:site" content="@yourapp" />
<meta name="twitter:creator" content="@yourapp" />
{/* Article Meta Tags (for blog posts) */}
{type === 'article' && publishedTime && (
<>
<meta property="article:published_time" content={publishedTime} />
{modifiedTime && (
<meta property="article:modified_time" content={modifiedTime} />
)}
{author && <meta property="article:author" content={author} />}
</>
)}
{/* Mobile Optimization */}
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5" />
<meta name="theme-color" content="#000000" />
</Helmet>
); }
Usage Example:
// In any public page component <SEOHead title="Pricing Plans" description="Choose the perfect plan for your needs. Starting at $19/month." canonical="https://yourapp.com/pricing" keywords={['pricing', 'plans', 'subscription', 'saas pricing']} />
Schema.org Structured Data
Structured data helps search engines understand your content and display rich results.
- Landing Page (Organization + Website Schema)
// src/pages/Home.tsx export default function Home() { const organizationSchema = { "@context": "https://schema.org", "@type": "Organization", "name": "YourApp", "url": "https://www.yourapp.com", "logo": "https://www.yourapp.com/logo.png", "description": "Brief description of your SaaS product", "foundingDate": "2024", "founders": [ { "@type": "Person", "name": "Founder Name" } ], "contactPoint": { "@type": "ContactPoint", "email": "contact@yourapp.com", "contactType": "Customer Service", "availableLanguage": ["English", "Spanish"] }, "sameAs": [ "https://twitter.com/yourapp", "https://www.linkedin.com/company/yourapp", "https://github.com/yourapp" ] };
const websiteSchema = { "@context": "https://schema.org", "@type": "WebSite", "name": "YourApp", "url": "https://www.yourapp.com", "potentialAction": { "@type": "SearchAction", "target": { "@type": "EntryPoint", "urlTemplate": "https://www.yourapp.com/search?q={search_term_string}" }, "query-input": "required name=search_term_string" } };
return ( <> <SEOHead title="Home" description="Your SEO-optimized description here. Keep it under 160 characters." keywords={['saas', 'productivity', 'automation']} />
{/* JSON-LD Structured Data */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteSchema) }}
/>
{/* Page content */}
</>
); }
- Blog Post (Article Schema)
// src/pages/BlogPost.tsx interface BlogPostProps { post: { title: string; excerpt: string; slug: string; featuredImage: string; publishedAt: string; updatedAt: string; author: { name: string; slug: string; image: string; }; content: string; readingTime: number; }; }
export default function BlogPost({ post }: BlogPostProps) {
const articleSchema = {
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": post.title,
"description": post.excerpt,
"image": post.featuredImage,
"datePublished": post.publishedAt,
"dateModified": post.updatedAt,
"author": {
"@type": "Person",
"name": post.author.name,
"url": https://www.yourapp.com/author/${post.author.slug},
"image": post.author.image
},
"publisher": {
"@type": "Organization",
"name": "YourApp",
"logo": {
"@type": "ImageObject",
"url": "https://www.yourapp.com/logo.png"
}
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": https://www.yourapp.com/blog/${post.slug}
},
"wordCount": post.content.split(' ').length,
"timeRequired": PT${post.readingTime}M
};
const breadcrumbSchema = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Home",
"item": "https://www.yourapp.com"
},
{
"@type": "ListItem",
"position": 2,
"name": "Blog",
"item": "https://www.yourapp.com/blog"
},
{
"@type": "ListItem",
"position": 3,
"name": post.title,
"item": https://www.yourapp.com/blog/${post.slug}
}
]
};
return (
<>
<SEOHead
title={post.title}
description={post.excerpt}
canonical={https://www.yourapp.com/blog/${post.slug}}
image={post.featuredImage}
type="article"
publishedTime={post.publishedAt}
modifiedTime={post.updatedAt}
author={post.author.name}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleSchema) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
/>
{/* Article content */}
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
</>
); }
- Pricing Page (Product/Offer Schema)
// src/pages/Pricing.tsx export default function Pricing() { const plans = [ { name: 'Starter', price: 9.99, currency: 'USD', description: 'Perfect for individuals getting started' }, { name: 'Pro', price: 29.99, currency: 'USD', description: 'For professionals and small teams' }, { name: 'Enterprise', price: 99.99, currency: 'USD', description: 'Advanced features for large organizations' } ];
const productSchema = plans.map(plan => ({
"@context": "https://schema.org",
"@type": "Product",
"name": YourApp ${plan.name},
"description": plan.description,
"brand": {
"@type": "Brand",
"name": "YourApp"
},
"offers": {
"@type": "Offer",
"url": "https://www.yourapp.com/pricing",
"priceCurrency": plan.currency,
"price": plan.price.toString(),
"priceValidUntil": "2026-12-31",
"itemCondition": "https://schema.org/NewCondition",
"availability": "https://schema.org/InStock",
"seller": {
"@type": "Organization",
"name": "YourApp"
}
}
}));
const faqSchema = { "@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [ { "@type": "Question", "name": "Can I cancel my subscription anytime?", "acceptedAnswer": { "@type": "Answer", "text": "Yes, you can cancel your subscription at any time with no penalties." } }, { "@type": "Question", "name": "Do you offer a free trial?", "acceptedAnswer": { "@type": "Answer", "text": "Yes, we offer a 14-day free trial for all plans. No credit card required." } } ] };
return ( <> <SEOHead title="Pricing Plans" description="Choose the perfect plan for your needs. Starting at $9.99/month. 14-day free trial available." canonical="https://www.yourapp.com/pricing" keywords={['pricing', 'plans', 'subscription', 'cost', 'free trial']} />
{productSchema.map((schema, index) => (
<script
key={index}
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
))}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqSchema) }}
/>
{/* Pricing content */}
</>
); }
- SaaS Software Application Schema
// For your main landing page const softwareAppSchema = { "@context": "https://schema.org", "@type": "SoftwareApplication", "name": "YourApp", "applicationCategory": "BusinessApplication", "operatingSystem": "Web, iOS, Android", "offers": { "@type": "Offer", "price": "9.99", "priceCurrency": "USD" }, "aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.8", "ratingCount": "256" }, "screenshot": "https://www.yourapp.com/screenshots/dashboard.png" };
Dynamic Sitemap Generation
Sitemap Utility (TypeScript)
// src/utils/generateSitemap.ts import { collection, getDocs, query, where } from 'firebase/firestore'; import { db } from '@/lib/firebase';
interface SitemapEntry { loc: string; lastmod?: string; changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never'; priority?: number; }
export async function generateSitemap(): Promise<string> { const baseUrl = 'https://www.yourapp.com';
// Static pages const staticPages: SitemapEntry[] = [ { loc: '/', changefreq: 'weekly', priority: 1.0 }, { loc: '/pricing', changefreq: 'monthly', priority: 0.9 }, { loc: '/features', changefreq: 'monthly', priority: 0.8 }, { loc: '/blog', changefreq: 'daily', priority: 0.9 }, { loc: '/about', changefreq: 'monthly', priority: 0.6 }, { loc: '/contact', changefreq: 'monthly', priority: 0.6 }, { loc: '/privacy-policy', changefreq: 'yearly', priority: 0.3 }, { loc: '/terms-of-service', changefreq: 'yearly', priority: 0.3 } ];
// Dynamic blog posts const postsQuery = query( collection(db, 'blog-posts'), where('published', '==', true) ); const postsSnapshot = await getDocs(postsQuery);
const blogPosts: SitemapEntry[] = postsSnapshot.docs.map(doc => {
const data = doc.data();
return {
loc: /blog/${data.slug},
lastmod: data.updatedAt?.toDate().toISOString() || new Date().toISOString(),
changefreq: 'monthly',
priority: 0.7
};
});
// Dynamic category pages (if applicable)
const categoriesSnapshot = await getDocs(collection(db, 'categories'));
const categoryPages: SitemapEntry[] = categoriesSnapshot.docs.map(doc => ({
loc: /category/${doc.data().slug},
changefreq: 'weekly',
priority: 0.6
}));
const allPages = [...staticPages, ...blogPosts, ...categoryPages];
// Generate XML
const sitemap = <?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"> ${allPages.map(page => <url>
<loc>${baseUrl}${page.loc}</loc>
${page.lastmod ? <lastmod>${page.lastmod}</lastmod> : ''}
${page.changefreq ? <changefreq>${page.changefreq}</changefreq> : ''}
${page.priority !== undefined ? <priority>${page.priority.toFixed(1)}</priority> : ''}
</url>).join('\n')} </urlset>;
return sitemap; }
Server-side Sitemap Endpoint (Express/Node.js)
// api/sitemap.ts (Vercel/Netlify Functions) import { generateSitemap } from '../utils/generateSitemap';
export default async function handler(req: any, res: any) { try { const xml = await generateSitemap();
res.setHeader('Content-Type', 'application/xml');
res.setHeader('Cache-Control', 'public, max-age=3600, s-maxage=3600');
res.status(200).send(xml);
} catch (error) { console.error('Error generating sitemap:', error); res.status(500).json({ error: 'Failed to generate sitemap' }); } }
Firebase Cloud Function Sitemap
// functions/src/sitemap.ts import * as functions from 'firebase-functions'; import { generateSitemap } from './utils/generateSitemap';
export const sitemap = functions.https.onRequest(async (req, res) => { try { const xml = await generateSitemap();
res.set('Content-Type', 'application/xml');
res.set('Cache-Control', 'public, max-age=3600');
res.status(200).send(xml);
} catch (error) { console.error('Error generating sitemap:', error); res.status(500).send('Error generating sitemap'); } });
robots.txt Configuration
public/robots.txt
Allow all crawlers
User-agent: * Allow: /
Block private pages
Disallow: /dashboard Disallow: /dashboard/* Disallow: /settings Disallow: /settings/* Disallow: /admin Disallow: /admin/* Disallow: /api/*
Block sensitive paths
Disallow: /auth/ Disallow: /checkout/ Disallow: /*.json$
Sitemap location
Sitemap: https://www.yourapp.com/sitemap.xml
Crawl delay (optional, use if server load is an issue)
Crawl-delay: 10
Core Web Vitals Optimization
- Image Optimization
// Always use optimized images with proper attributes
// ✅ CORRECT: Modern image with lazy loading <img src="/images/hero.webp" alt="Descriptive alt text for SEO" loading="lazy" width={1200} height={630} decoding="async" />
// ✅ BETTER: Responsive images with srcset <img srcSet="/images/hero-320w.webp 320w, /images/hero-640w.webp 640w, /images/hero-1280w.webp 1280w" sizes="(max-width: 640px) 320px, (max-width: 1280px) 640px, 1280px" src="/images/hero-1280w.webp" alt="Descriptive alt text" loading="lazy" width={1280} height={720} />
// ✅ BEST: Next.js Image component (automatic optimization) import Image from 'next/image';
<Image src="/images/hero.jpg" alt="Descriptive alt text" width={1200} height={630} priority={false} // true for above-the-fold images placeholder="blur" blurDataURL="data:image/jpeg;base64,..." />
- Code Splitting by Routes
// src/router.tsx import { lazy, Suspense } from 'react'; import { BrowserRouter, Routes, Route } from 'react-router-dom'; import LoadingSpinner from '@/components/common/LoadingSpinner';
// ✅ Lazy load routes for better initial load performance const Home = lazy(() => import('@/pages/public/Home')); const Pricing = lazy(() => import('@/pages/public/Pricing')); const Blog = lazy(() => import('@/pages/public/Blog')); const BlogPost = lazy(() => import('@/pages/public/BlogPost')); const Dashboard = lazy(() => import('@/pages/dashboard/Dashboard')); const Features = lazy(() => import('@/pages/public/Features'));
export default function Router() { return ( <BrowserRouter> <Suspense fallback={<LoadingSpinner />}> <Routes> <Route path="/" element={<Home />} /> <Route path="/pricing" element={<Pricing />} /> <Route path="/features" element={<Features />} /> <Route path="/blog" element={<Blog />} /> <Route path="/blog/:slug" element={<BlogPost />} /> <Route path="/dashboard/*" element={<Dashboard />} /> </Routes> </Suspense> </BrowserRouter> ); }
- Preload Critical Resources
// src/main.tsx or index.html import { createRoot } from 'react-dom/client';
// Preload critical fonts const preloadFont = (href: string) => { const link = document.createElement('link'); link.rel = 'preload'; link.as = 'font'; link.type = 'font/woff2'; link.href = href; link.crossOrigin = 'anonymous'; document.head.appendChild(link); };
preloadFont('/fonts/Inter-var.woff2'); preloadFont('/fonts/Inter-Bold.woff2');
// Preconnect to external domains const preconnect = (href: string) => { const link = document.createElement('link'); link.rel = 'preconnect'; link.href = href; document.head.appendChild(link); };
preconnect('https://fonts.googleapis.com'); preconnect('https://cdn.yourcdn.com');
createRoot(document.getElementById('root')!).render(<App />);
- Vite Build Optimization
// vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({ plugins: [ react(), visualizer({ open: true }) // Analyze bundle size ], build: { rollupOptions: { output: { // Manual chunk splitting for optimal caching manualChunks: { 'react-vendor': ['react', 'react-dom', 'react-router-dom'], 'firebase-vendor': ['firebase/app', 'firebase/auth', 'firebase/firestore', 'firebase/storage'], 'ui-vendor': ['lucide-react', 'sonner', '@radix-ui/react-dialog'], 'form-vendor': ['react-hook-form', 'zod', '@hookform/resolvers'] } } }, chunkSizeWarningLimit: 1000, minify: 'terser', terserOptions: { compress: { drop_console: true, // Remove console.log in production drop_debugger: true } } }, optimizeDeps: { include: ['react', 'react-dom', 'react-router-dom'] } });
- Next.js Configuration for Performance
// next.config.js /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, swcMinify: true,
// Image optimization images: { formats: ['image/webp', 'image/avif'], deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], },
// Compression compress: true,
// Headers for caching async headers() { return [ { source: '/:all*(svg|jpg|png|webp|avif)', headers: [ { key: 'Cache-Control', value: 'public, max-age=31536000, immutable', }, ], }, ]; }, };
module.exports = nextConfig;
- Performance Monitoring Component
// src/components/PerformanceMonitor.tsx import { useEffect } from 'react';
export default function PerformanceMonitor() { useEffect(() => { // Measure Core Web Vitals if (typeof window !== 'undefined' && 'web-vitals' in window) { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { getCLS(console.log); getFID(console.log); getFCP(console.log); getLCP(console.log); getTTFB(console.log); }); }
// Log performance metrics
if (window.performance) {
const perfData = window.performance.timing;
const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart;
console.log(`Page load time: ${pageLoadTime}ms`);
}
}, []);
return null; }
// Add to your App component function App() { return ( <> <PerformanceMonitor /> {/* Rest of your app */} </> ); }
SEO Pre-Deploy Checklist
Meta Tags & Structured Data
-
All pages have unique <title> tags (50-60 characters)
-
All pages have unique meta descriptions (150-160 characters)
-
Canonical URLs set on all pages
-
Open Graph tags implemented (title, description, image, url)
-
Twitter Card tags implemented
-
Schema.org markup on homepage (Organization/WebSite)
-
Schema.org markup on blog posts (Article)
-
Schema.org markup on pricing (Product/Offer)
-
Breadcrumb schema on inner pages
-
FAQ schema where applicable
Technical SEO
-
sitemap.xml generated and accessible
-
robots.txt configured correctly
-
All images have descriptive alt text
-
Images optimized (WebP/AVIF format)
-
Lazy loading implemented for below-fold images
-
HTTPS enabled site-wide
-
404 page exists and is helpful
-
Redirects (301) set up for changed URLs
-
No broken internal links
-
Mobile-responsive design verified
Performance
-
Lighthouse score > 90 on mobile
-
Lighthouse score > 95 on desktop
-
LCP (Largest Contentful Paint) < 2.5s
-
FID (First Input Delay) < 100ms
-
CLS (Cumulative Layout Shift) < 0.1
-
Code splitting implemented
-
Critical CSS inlined
-
Fonts preloaded
-
External resources preconnected
Analytics & Monitoring
-
Google Search Console configured
-
Google Analytics 4 installed
-
Core Web Vitals tracking enabled
-
Conversion tracking set up
-
Error tracking configured (Sentry, etc.)
Content
-
H1 tag on every page (only one per page)
-
Proper heading hierarchy (H1 → H2 → H3)
-
Internal linking strategy implemented
-
Blog content plan created
-
Target keywords researched
-
Content length adequate (1000+ words for pillar content)
Tools & Testing
Testing Your SEO Implementation
1. Test with Lighthouse (Chrome DevTools)
Open DevTools → Lighthouse → Run audit
2. Check structured data
Visit: https://search.google.com/test/rich-results
Enter your URL
3. Validate sitemap
Visit: https://www.xml-sitemaps.com/validate-xml-sitemap.html
4. Check mobile-friendliness
Visit: https://search.google.com/test/mobile-friendly
5. Analyze Core Web Vitals
Visit: https://pagespeed.web.dev/
Recommended Tools
SEO Analysis:
-
Ahrefs - Comprehensive SEO toolset
-
SEMrush - Keyword research & competitor analysis
-
Moz - Domain authority tracking
-
Screaming Frog - Technical SEO crawler
Performance:
-
Lighthouse - Built into Chrome DevTools
-
WebPageTest - Detailed performance analysis
-
GTmetrix - Performance & optimization recommendations
-
Google PageSpeed Insights - Core Web Vitals
Schema Testing:
-
Google Rich Results Test
-
Schema Markup Validator
-
JSON-LD Playground
Common SEO Mistakes to Avoid
❌ Don't Do This:
-
Duplicate title tags across multiple pages
-
Missing alt text on images
-
Blocking CSS/JS in robots.txt
-
Slow page load times (> 3 seconds)
-
Non-responsive design on mobile
-
Broken links (404 errors)
-
Thin content (< 300 words on important pages)
-
Keyword stuffing in meta tags or content
-
Using only h1 tags or skipping heading levels
-
Not setting canonical URLs (causes duplicate content issues)
✅ Best Practices:
-
Write unique, descriptive titles for each page
-
Create compelling meta descriptions that encourage clicks
-
Optimize images (compress, use modern formats, add alt text)
-
Implement proper URL structure (readable, hierarchical)
-
Create high-quality content that answers user questions
-
Build internal links to important pages
-
Update content regularly to keep it fresh
-
Monitor and fix errors in Google Search Console
-
Use HTTPS everywhere
-
Focus on user experience first, SEO second
Resources
-
Google Search Central: https://developers.google.com/search
-
Schema.org Documentation: https://schema.org/
-
Web.dev SEO Guide: https://web.dev/learn/seo/
-
Moz Beginner's Guide to SEO: https://moz.com/beginners-guide-to-seo
-
Core Web Vitals: https://web.dev/vitals/
Summary
This SEO skill provides everything needed to implement comprehensive SEO for modern SaaS applications:
-
Meta tags for social sharing and search engines
-
Structured data (Schema.org) for rich results
-
Dynamic sitemap generation
-
Performance optimization for Core Web Vitals
-
Complete checklist for pre-deploy verification
Apply these practices consistently across all public pages to maximize organic search visibility.