Browse by type
Elevate your Next.js website by integrating SEObot's Blog API. This setup allows you to fetch and render real-time, SEO-optimized blog content directly into your website.
Visit the DevHunt Blog to check out an example of SEObot Blog API integration.
First, set up your SEObot API Key environment variable. Create a .env.local file for local development:
SEOBOT_API_KEY=<your_seobot_key>
⚠️ You can use following DEMO SEOBOT_API_KEY for local development:
SEOBOT_API_KEY=a8c58738-7b98-4597-b20a-0bb1c2fe5772
npm install
# or
yarn install
# or
pnpm install
npm run dev
# or
yarn dev
# or
pnpm dev
Once the server is running, navigate to http://localhost:3000/blog in your browser to view the application.
This project incorporates the SEObot Blog API Client Library. For more details and to install the npm package, visit seobot on npmjs.
You can begin customizing your blog design by editing the files under the app/blog/ route. Your changes will be automatically reflected in the app thanks to Next.js's hot reloading feature.
This project comes with a separate blog/sitemap.xml generated dynamically. If you have an existing dynamic sitemap, you can programmatically merge the blog sitemap into it. The exact steps depend on how you're generating your main sitemap, but the goal is to combine them seamlessly.
If you prefer not to merge, it's crucial to submit the standalone blog/sitemap.xml to your Google Search Console for SEO.
blog/sitemap.xml.⚠️ Google Limits: keep this in mind that Google restricts sitemaps to 50,000 URLs and a file size of up to 50MB.
If you find any bugs or have feature suggestions, please open an issue or submit a pull request.
This project is licensed under the MIT License.
For additional assistance or information, feel free to reach out.
Revolutionize your website's content strategy with real-time, automated, SEO-optimized blog posts. Get started with SEObot AI Blog Autopilot integration today!
````markdown
This document provides complete, copy-paste-ready instructions for integrating SEObot's Blog API into a Next.js project using the App Router with full Server-Side Rendering (SSR). Follow every section in order. All code is production-ready TypeScript.
SEObot is an AI blog autopilot. It generates and manages SEO-optimized blog posts. You integrate it into your Next.js app using the seobot npm package, which wraps the SEObot REST API with built-in caching. Blog content is fetched server-side at request time (SSR) using Next.js App Router async Server Components.
Key facts:
- Package: seobot on npm
- API client: BlogClient class
- Articles contain a pre-rendered .html field — render it with dangerouslySetInnerHTML
- All data fetching MUST happen in Server Components (no 'use client')
- API key is stored in SEOBOT_API_KEY environment variable (server-side only, never exposed to client)
- Demo API key for testing: a8c58738-7b98-4597-b20a-0bb1c2fe5772
npm install seobot
Create or update .env.local:
SEOBOT_API_KEY=your_seobot_api_key_here
For testing, use the demo key:
SEOBOT_API_KEY=a8c58738-7b98-4597-b20a-0bb1c2fe5772
Important: Never prefix this variable with NEXT_PUBLIC_. It must remain server-side only.
Create types/blog.ts:
// types/blog.ts
export interface ITag {
id: string;
title: string;
slug: string;
}
export interface ICategory {
id: string;
title: string;
slug: string;
}
export interface IRelatedPost {
id: string;
headline: string;
slug: string;
}
export interface IArticle {
id: string;
slug: string;
headline: string;
metaDescription: string;
metaKeywords: string;
tags: ITag[];
category: ICategory;
readingTime: number;
html: string; // Pre-rendered HTML — use dangerouslySetInnerHTML
markdown: string;
outline: string;
deleted: boolean;
published: boolean;
publishedAt: string; // ISO date string
createdAt: string;
updatedAt: string;
relatedPosts: IRelatedPost[];
image: string; // Featured image URL
isTool?: boolean;
isVideo?: boolean;
isNews?: boolean;
}
export interface IArticlesResponse {
articles: IArticle[];
total: number;
}
Create utils/blog.ts:
// utils/blog.ts
import { BlogClient } from 'seobot';
import type { IArticle, IArticlesResponse } from '@/types/blog';
const getClient = () => {
const apiKey = process.env.SEOBOT_API_KEY;
if (!apiKey) throw new Error('SEOBOT_API_KEY is not set');
return new BlogClient(apiKey);
};
/**
* Fetch a paginated list of articles.
* @param page - Zero-based page index (0 = first page, 1 = second page, etc.)
* @param limit - Number of articles per page (default: 10)
*/
export async function getArticles(
page: number = 0,
limit: number = 10
): Promise<IArticlesResponse> {
const client = getClient();
return client.getArticles(page, limit);
}
/**
* Fetch articles filtered by category slug.
* @param page - Zero-based page index
*/
export async function getCategoryArticles(
categorySlug: string,
page: number = 0,
limit: number = 10
): Promise<IArticlesResponse> {
const client = getClient();
return client.getCategoryArticles(categorySlug, page, limit);
}
/**
* Fetch articles filtered by tag slug.
* @param page - Zero-based page index
*/
export async function getTagArticles(
tagSlug: string,
page: number = 0,
limit: number = 10
): Promise<IArticlesResponse> {
const client = getClient();
return client.getTagArticles(tagSlug, page, limit);
}
/**
* Fetch a single article by its slug.
* Returns null if not found.
*/
export async function getArticle(slug: string): Promise<IArticle | null> {
try {
const client = getClient();
return await client.getArticle(slug);
} catch {
return null;
}
}
Create app/blog/page.tsx:
// app/blog/page.tsx
import Link from 'next/link';
import Image from 'next/image';
import { Metadata } from 'next';
import { getArticles } from '@/utils/blog';
export const dynamic = 'force-dynamic';
export const metadata: Metadata = {
title: 'Blog',
description: 'Read our latest articles',
};
interface PageProps {
searchParams: { page?: string };
}
export default async function BlogPage({ searchParams }: PageProps) {
const ARTICLES_PER_PAGE = 12;
const currentPage = Math.max(1, parseInt(searchParams.page ?? '1', 10));
const apiPage = currentPage - 1; // API is zero-based
const { articles, total } = await getArticles(apiPage, ARTICLES_PER_PAGE);
const totalPages = Math.ceil(total / ARTICLES_PER_PAGE);
return (
<main className="max-w-6xl mx-auto px-4 py-12">
<h1 className="text-4xl font-bold mb-8">Blog</h1>
{articles.length === 0 && (
No articles found.
)}
{articles.map((article) => (
<Link key={article.id} href={`/blog/${article.slug}`} className="group block">
<article className="border rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
{article.image && (
<Image src={article.image} alt={article.headline} fill className="object-cover" />
)}
{article.category && (
<span className="text-sm text-blue-600 font-medium uppercase tracking-wide">
{article.category.title}
</span>
)}
<h2 className="text-lg font-semibold mt-1 mb-2 group-hover:text-blue-600 transition-colors">
{article.headline}
</h2>
{article.metaDescription}
<time dateTime={article.publishedAt}>
{new Date(article.publishedAt).toLocaleDateString('en-US', {
year: 'numeric', month: 'long', day: 'numeric',
})}
</time>
<span>{article.readingTime} min read</span>
</article>
</Link>
))}
{totalPages > 1 && (
<nav className="mt-12 flex justify-center gap-2" aria-label="Pagination">
{currentPage > 1 && (
<Link href={`/blog?page=${currentPage - 1}`} className="px-4 py-2 border rounded hover:bg-gray-100">
Previous
</Link>
)}
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<Link
key={page}
href={`/blog?page=${page}`}
className={`px-4 py-2 border rounded ${page === currentPage ? 'bg-blue-600 text-white border-blue-600' : 'hover:bg-gray-100'}`}
>
{page}
</Link>
))}
{currentPage < totalPages && (
<Link href={`/blog?page=${currentPage + 1}`} className="px-4 py-2 border rounded hover:bg-gray-100">
Next
</Link>
)}
</nav>
)}
</main>
);
}
Create app/blog/[slug]/page.tsx:
```tsx
// app/blog/[slug]/page.tsx
import { Metadata } from 'next';
import Image from 'next/image';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { getArticle } from '@/utils/blog';
export const dynamic = 'force-dynamic';
interface PageProps { params: { slug: string }; }
export async function generateMetadata({ params }: PageProps): Promise
return { title: article.headline, description: article.metaDescription, keywords: article.metaKeywords, openGraph: { title: article.headline, description: article.metaDescription, images: article.image ? [{ url: article.image }] : [], type: 'article', publishedTime: article.publishedAt, }, twitter: { card: 'summary_large_image', title: article.headline, description: article.metaDescription, images: article.image ? [article.image] : [], }, }; }
export default async function ArticlePage({ params }: PageProps) { const article = await getArticle(params.slug);
if (!article || !article.published || article.deleted) { notFound(); }
return (
{/ Breadcrumb /} {/* Header */}
<header className="mb-8">
<h1 className="text-4xl font-bold leading-tight mb-4">{article.headline}</h1>
<time dateTime={article.publishedAt}>
{new Date(article.publishedAt).toLocaleDateString('en-US', {
year: 'numeric', month: 'long', day: 'numeric',
})}
</time>
<span>{article.readingTime} min read</span>
{article.tags?.length > 0 && (
{article.tags.map((tag) => (
<Link key={tag.id} href={`/blog/tag/${tag.slug}`} className="text-xs bg-gray-100 hover:bg-gray-200 px-2 py-1 rounded">
#{tag.title}
</Link>
))}
)}
</header>
{/* Featured Image */}
{article.image && (
<Image src={article.image} alt={article.headline} fill className="object-cover" priority />
)}
{/* Article HTML Content — SEObot provides pre-rendered HTML */}
{/* Related Posts */}
{article.relatedPosts?.length > 0 && (
<section className="mt-12 border-t pt-8">
<h2 className="text-2xl font-semibold mb-4">Related Articles</
$ claude mcp add seobot-nextjs-blog \
-- python -m otcore.mcp_server <graph>