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_hereYour 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.