React Integration Guide
Integrate SEO Sniper articles into your React application using hooks and components. Works with Create React App, Vite, and custom setups.
Installation
No additional packages required - just use the native fetch API or your preferred HTTP client.
Custom Hook for Articles
// hooks/useArticles.ts
import { useState, useEffect } from 'react';
interface Article {
id: string;
slug: string;
title: string;
metaTitle: string;
metaDescription: string;
contentHtml: string;
publishedAt: string;
featuredImageUrl?: string;
}
const API_URL = import.meta.env.VITE_SEOSNIPER_API_URL;
const API_TOKEN = import.meta.env.VITE_SEOSNIPER_API_TOKEN;
export function useArticles() {
const [articles, setArticles] = useState<Article[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
async function fetchArticles() {
try {
const response = await fetch(
`${API_URL}/api/v1/articles`,
{
headers: {
'Authorization': `Bearer ${API_TOKEN}`
}
}
);
if (!response.ok) {
throw new Error('Failed to fetch articles');
}
const data = await response.json();
setArticles(data.articles);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
}
fetchArticles();
}, []);
return { articles, loading, error };
}
export function useArticle(slug: string) {
const [article, setArticle] = useState<Article | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
async function fetchArticle() {
try {
const response = await fetch(
`${API_URL}/api/v1/articles/${slug}`,
{
headers: {
'Authorization': `Bearer ${API_TOKEN}`
}
}
);
if (!response.ok) {
throw new Error('Article not found');
}
const data = await response.json();
setArticle(data.article);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
}
fetchArticle();
}, [slug]);
return { article, loading, error };
}Blog List Component
// components/BlogList.tsx
import { Link } from 'react-router-dom';
import { useArticles } from '../hooks/useArticles';
export function BlogList() {
const { articles, loading, error } = useArticles();
if (loading) {
return <div className="text-center py-8">Loading articles...</div>;
}
if (error) {
return <div className="text-red-500">Error: {error.message}</div>;
}
return (
<div className="max-w-4xl mx-auto py-8">
<h1 className="text-3xl font-bold mb-8">Blog</h1>
<div className="space-y-6">
{articles.map((article) => (
<article key={article.id} className="border rounded-lg p-6">
{article.featuredImageUrl && (
<img
src={article.featuredImageUrl}
alt={article.title}
className="w-full h-48 object-cover rounded-lg mb-4"
/>
)}
<Link to={`/blog/${article.slug}`}>
<h2 className="text-xl font-semibold hover:text-blue-600">
{article.title}
</h2>
</Link>
<p className="text-gray-600 mt-2">
{article.metaDescription}
</p>
<time className="text-sm text-gray-500 mt-4 block">
{new Date(article.publishedAt).toLocaleDateString()}
</time>
</article>
))}
</div>
</div>
);
}Article Page Component
// components/ArticlePage.tsx
import { useParams } from 'react-router-dom';
import { Helmet } from 'react-helmet-async';
import { useArticle } from '../hooks/useArticles';
export function ArticlePage() {
const { slug } = useParams<{ slug: string }>();
const { article, loading, error } = useArticle(slug!);
if (loading) {
return <div className="text-center py-8">Loading...</div>;
}
if (error || !article) {
return <div className="text-center py-8">Article not found</div>;
}
return (
<>
<Helmet>
<title>{article.metaTitle}</title>
<meta name="description" content={article.metaDescription} />
<meta property="og:title" content={article.metaTitle} />
<meta property="og:description" content={article.metaDescription} />
{article.featuredImageUrl && (
<meta property="og:image" content={article.featuredImageUrl} />
)}
</Helmet>
<article className="max-w-3xl mx-auto py-8 px-4">
<h1 className="text-4xl font-bold mb-4">{article.title}</h1>
<time className="text-gray-500 block mb-8">
{new Date(article.publishedAt).toLocaleDateString()}
</time>
{/* Render HTML content safely */}
<div
className="prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: article.contentHtml }}
/>
</article>
</>
);
}Router Configuration
// App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { HelmetProvider } from 'react-helmet-async';
import { ArticlesProvider } from './context/ArticlesContext';
import { BlogList } from './components/BlogList';
import { ArticlePage } from './components/ArticlePage';
function App() {
return (
<HelmetProvider>
<ArticlesProvider>
<BrowserRouter>
<Routes>
<Route path="/blog" element={<BlogList />} />
<Route path="/blog/:slug" element={<ArticlePage />} />
</Routes>
</BrowserRouter>
</ArticlesProvider>
</HelmetProvider>
);
}SEO Consideration
Client-side rendered React apps may have limited SEO. For better search engine visibility, consider using Next.js with SSR/SSG or implementing prerendering with tools like react-snap.
Frequently Asked Questions
How do I make sure search engines see my content?
A client-only React app renders an empty shell for crawlers. Use server-side rendering, static generation, or a prerendering step so each article ships as real HTML with its title and meta description already in the markup.
Where should I store my API token?
On the server or in a build-time environment variable — never in the client bundle. Fetch articles in a server component, API route, or build script, then send only the rendered content to the browser.
How do I set per-article meta tags?
Use your head manager (React Helmet, or your framework's document-head API) to set the metaTitle and metaDescription returned by the API for each article's route.
How do I refresh content when I publish?
Add a webhook that triggers your host's rebuild or revalidation, so new and updated articles appear automatically without a manual redeploy.
Does each article need its own URL?
Yes — give every article a unique, crawlable route (e.g. /blog/:slug) and add it to your sitemap so search engines can discover and index each page.