Back to Dynamic Webapps
Next.js Integration

Next.js Integration Guide

The recommended framework for SEO Sniper. Full SSR/SSG support with automatic meta tag generation and optimal SEO performance.

Server-Side Rendering

Full HTML delivered to crawlers for perfect SEO indexing

Static Generation

Pre-build articles at deploy time for blazing fast loads

Automatic Meta Tags

Built-in Metadata API for SEO-friendly head management

API Helper Function

typescript
// lib/seosniper.ts
const API_URL = process.env.SEOSNIPER_API_URL!; // Your Convex deployment URL
const TOKEN = process.env.SEOSNIPER_TOKEN!;     // Your site API token (sst_...)

export interface Article {
  id: string;
  slug: string;
  title: string;
  metaTitle: string;
  metaDescription: string;
  contentHtml: string;        // Raw HTML
  styledContentHtml: string;  // HTML with styles pre-applied
  contentMarkdown: string;    // Markdown source
  publishedAt: string | null;
  featuredImageUrl: string | null;
  featuredImageAlt: string | null;
  keywordsUsed: string[];
  wordCount: number;
  blogStyle: {
    template: string;
    customCss: string | null;
    stylingMethod: string;
  } | null;
}

export async function getArticles(): Promise<Article[]> {
  const response = await fetch(`${API_URL}/api/v1/articles`, {
    headers: { 'Authorization': `Bearer ${TOKEN}` },
    next: { revalidate: 60 } // Revalidate every minute
  });

  if (!response.ok) {
    throw new Error('Failed to fetch articles');
  }

  const data = await response.json();
  return data.articles;
}

export async function getArticleBySlug(slug: string): Promise<Article | null> {
  const response = await fetch(`${API_URL}/api/v1/articles/${slug}`, {
    headers: { 'Authorization': `Bearer ${TOKEN}` },
    next: { revalidate: 60 }
  });

  if (!response.ok) {
    if (response.status === 404) return null;
    throw new Error('Failed to fetch article');
  }

  const data = await response.json();
  return data.article;
}

export async function getArticleSlugs(): Promise<string[]> {
  const articles = await getArticles();
  return articles.map((a) => a.slug);
}

Blog List Page

tsx
// app/blog/page.tsx
import Link from 'next/link';
import Image from 'next/image';
import { getArticles } from '@/lib/seosniper';

export const metadata = {
  title: 'Blog | Your Site',
  description: 'Read our latest articles and insights.',
};

export default async function BlogPage() {
  const articles = await getArticles();

  return (
    <div className="max-w-4xl mx-auto py-12 px-4">
      <h1 className="text-4xl font-bold mb-8">Blog</h1>

      <div className="grid gap-8">
        {articles.map((article) => (
          <article
            key={article.id}
            className="border rounded-lg overflow-hidden"
          >
            {article.featuredImageUrl && (
              <Image
                src={article.featuredImageUrl}
                alt={article.featuredImageAlt || article.title}
                width={800}
                height={400}
                className="w-full h-48 object-cover"
              />
            )}
            <div className="p-6">
              <Link href={`/blog/${article.slug}`}>
                <h2 className="text-2xl 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('en-US', {
                  year: 'numeric',
                  month: 'long',
                  day: 'numeric'
                })}
              </time>
            </div>
          </article>
        ))}
      </div>
    </div>
  );
}

Dynamic Article Page with Metadata

tsx
// app/blog/[slug]/page.tsx
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { getArticleBySlug, getArticleSlugs } from '@/lib/seosniper';

interface Props {
  params: Promise<{ slug: string }>;
}

// Generate static params for SSG
export async function generateStaticParams() {
  const slugs = await getArticleSlugs();
  return slugs.map((slug) => ({ slug }));
}

// Generate dynamic metadata for SEO
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const article = await getArticleBySlug(slug);

  if (!article) {
    return { title: 'Article Not Found' };
  }

  return {
    title: article.metaTitle,
    description: article.metaDescription,
    keywords: article.keywordsUsed.join(', '),
    openGraph: {
      title: article.metaTitle,
      description: article.metaDescription,
      type: 'article',
      publishedTime: article.publishedAt,
      ...(article.featuredImageUrl && {
        images: [{
          url: article.featuredImageUrl,
          alt: article.featuredImageAlt || article.title
        }]
      })
    },
    twitter: {
      card: 'summary_large_image',
      title: article.metaTitle,
      description: article.metaDescription,
      ...(article.featuredImageUrl && {
        images: [article.featuredImageUrl]
      })
    }
  };
}

export default async function ArticlePage({ params }: Props) {
  const { slug } = await params;
  const article = await getArticleBySlug(slug);

  if (!article) {
    notFound();
  }

  return (
    <article className="max-w-3xl mx-auto py-12 px-4">
      <header className="mb-8">
        <h1 className="text-4xl font-bold mb-4">{article.title}</h1>
        <time className="text-gray-500">
          {new Date(article.publishedAt).toLocaleDateString('en-US', {
            year: 'numeric',
            month: 'long',
            day: 'numeric'
          })}
        </time>
      </header>

      {/* Render the pre-styled HTML content */}
      <div
        className="article"
        dangerouslySetInnerHTML={{ __html: article.styledContentHtml }}
      />
    </article>
  );
}

Environment Variables

Add these to your .env.local file. You can find these values in Settings → Integrations.

bash
# .env.local
SEOSNIPER_API_URL=https://your-deployment.convex.site
SEOSNIPER_TOKEN=sst_your_token_here

Your token automatically scopes requests to your site - no site ID needed.

Auto-Rebuild on Publish (Optional)

Set up a webhook to automatically rebuild your site when articles are published. Go to Settings → Webhooks and add your deployment hook URL.

Vercel: Use a Deploy Hook URL from Project Settings → Git → Deploy Hooks
Netlify: Use a Build Hook URL from Site Settings → Build & Deploy → Build Hooks

SEO Sniper will POST to your webhook when articles are published, updated, or deleted.

SEO Sniper