Back to Dynamic Webapps
React Integration

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

typescript
// 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

tsx
// 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

tsx
// 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

tsx
// 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.