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.

SEO Sniper