Next.js App Router
Fetch and render SiteLift articles and competitor comparison pages as static pages in your Next.js 13+ App Router application using our official SDK.
The official Next.js integration for SiteLift. Seamlessly connect your Next.js App Router project to your SiteLift dashboard and render lightning-fast, SEO-optimized blog articles and competitor pages("Brand vs X" and "X alternatives") directly from the server.
This package comes equipped with a lightweight, zero-dependency client and an intuitive CLI tool to scaffold a ready-to-use blog structure into your codebase within seconds. View on npm
Quick Start
Get up and running with the Next.js App Router integration in minutes.
Installation
Add the package to your project using npm, pnpm, or yarn:
npm install @getsitelift/nextjs-blog(Note: Requires Node.js 18.17+ and a Next.js App Router setup)
Configuration
Locate your API key in the SiteLift dashboard (Settings → Integrations → Next.js Blog). Store the key securely in your .env.local file:
SITELIFT_BLOG_API_KEY=your_api_key_hereWarning
NEXT_PUBLIC_.Scaffold (Recommended)
Want to skip the manual setup? Use our built-in CLI to generate a clean, unstyled blog and competitor comparison section right inside your Next.js application.
Run the following command at the root of your project:
npx @getsitelift/nextjs-blog initThe command is interactive and asks:
- App directory: auto-detects
src/apporapp. - Scaffold the blog? Default yes. Creates
app/blog. - Include competitor comparison pages? Default yes.
- URL prefix for comparison pages: default
compare. This must match Competitor Pages → Settingsin your SiteLift dashboard, because canonical URLs and the "See all comparisons" link inside each page are built from the dashboard value. Set the prefix in the dashboard first, then scaffold.
Folders that already exist are left untouched, so re-running init on a project that already has a blog only adds the comparison pages. For scripts and CI use --yes, --compare-path <path>, --no-compare, --no-blog and --force.
Need it in a specific folder? Pass the path: npx @getsitelift/nextjs-blog init src/app.
Built-in Routes from the CLI
When you use the init command, you get the following structure out of the box:
| Path | Purpose |
|---|---|
| /blog | The main archive displaying all your published articles. |
| /blog/[slug] | The dedicated article page, complete with server-side rendering, JSON-LD schema injection, and metadata tags for optimal SEO. |
| /blog/sitemap.xml | An auto-updating sitemap to keep search engines informed. |
| /compare | The comparisons hub: every published "Brand vs X" and "X alternatives" page grouped by competitor, with CollectionPage JSON-LD. |
| /compare/[slug] | A single comparison page with canonical and Open Graph tags and the JSON-LD generated by SiteLift. |
| /compare/sitemap.xml | Sitemap for the hub and every published comparison page. Submit it alongside /blog/sitemap.xml. |
/compare is the default prefix. If you chose another one during init, the folder and the routes use that name instead. Set NEXT_PUBLIC_SITE_URLto your site's origin so canonical URLs, JSON-LD and sitemaps are absolute, and optionally SITELIFT_BRAND_NAME for the hub heading.
Post-Scaffold Checklist
Verify Installation
Make sure @getsitelift/nextjs-blog is in your package.json.
Environment Variable
Double-check that SITELIFT_BLOG_API_KEY is present in your .env.local.
Image Domains
Allow external images by updating your next.config.ts (or next.config.js):
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [{ protocol: "https", hostname: "*" }],
},
};
export default nextConfig;Start Developing: Run npm run dev and navigate to /blog to see your articles in action!
Manual Integration
If you prefer to build the pages yourself, it's just as simple. Instantiate the BlogClient in a utility file and fetch data directly in your Server Components.
1. Create the Client
import { BlogClient } from "@getsitelift/nextjs-blog";
export const siteLiftBlog = new BlogClient({
apiKey: process.env.SITELIFT_BLOG_API_KEY,
revalidate: 86400, // Caches responses for 24 hours
});2. List Articles
import { siteLiftBlog } from "@/lib/sitelift";
import Link from "next/link";
export default async function BlogArchive() {
const articles = await siteLiftBlog.listArticles();
return (
<section>
<h1>Our Latest Articles</h1>
{articles.map((article) => (
<article key={article.id}>
<Link href={`/blog/${article.slug}`}>
<h2>{article.title}</h2>
</Link>
</article>
))}
</section>
);
}3. Render a Single Article
import { siteLiftBlog } from "@/lib/sitelift";
import { notFound } from "next/navigation";
export default async function ArticleView({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const article = await siteLiftBlog.getArticleBySlug(slug);
if (!article) return notFound();
return (
<main>
{/* Article content includes Title and Featured Image */}
<div dangerouslySetInnerHTML={{ __html: article.contentHtml || "" }} />
{/* Display Tags */}
{article.tags && <p className="leading-loose">Tags: {article.tags.join(", ")}</p>}
</main>
);
}Data Caching & Next.js ISR
This package is designed to work harmoniously with Next.js Incremental Static Regeneration (ISR).
Default Caching
The blog starter defaults to revalidate: 86400 (24 hours). The comparison starter uses 1 hour, since those pages are published manually from the dashboard.
Cache Invalidation
Next.js serves its cached version until the revalidation window expires. This applies to new articles and to edits saved in the SiteLift editor after publishing. Every fetch is tagged, so revalidateTag('sitelift-comparisons'), revalidateTag('sitelift-articles') or revalidateTag('sitelift') refreshes content on demand.
Always Fresh Data
Want real-time data? Pass revalidate: false to the BlogClient options to trigger a cache: 'no-store' behavior.
List calls return metadata only (no article bodies), so a hub of hundreds of posts stays small enough for the Next.js data cache. The delivery API allows 100 requests per minute per token; if a large build trips it, the client waits for Retry-After and retries (3 attempts by default, configurable with the retries and retryDelayMs options).
BlogClient API Reference
Available Methods
| Method | Returns | Description |
|---|---|---|
| listArticles() | Promise<BlogArticleSummary[]> | Retrieves a paginated list of published articles. |
| getAllArticles() | Promise<BlogArticleSummary[]> | Automatically paginates and returns all published articles. |
| getArticleBySlug(slug) | Promise<BlogArticle | null> | Retrieves the complete content and metadata for a specific article slug. |
| getArticleById(id) | Promise<BlogArticle | null> | Retrieves the complete content and metadata for a specific article by its UUID. |
| getArticleBySlugAndMarkPublished(slug) | Promise<BlogArticle | null> | Retrieves an article by slug and simultaneously marks its status as published. |
| getSitemapEntries() | Promise<BlogSitemapEntry[]> | Returns lightweight data perfect for building sitemap.xml. |
| listComparisons() | Promise<ComparisonList> | Every published comparison page plus the comparePath prefix set in the dashboard. |
| getComparisonBySlug(slug) | Promise<ComparisonPage | null> | Full content of one comparison page, or null when it is unpublished or unknown. |
| getComparePath() | Promise<string> | The comparison URL prefix set in the dashboard (default compare). |
| getComparisonSitemapEntries() | Promise<ComparisonSitemapEntries> | Slugs and update times of every published comparison page. |
Available Article Fields
When you fetch an article, you have access to a rich set of data:
Available Comparison Fields
A comparison page carries the same content fields as an article plus the competitor it covers. kind is "vs" or "alternatives", competitor holds the name and domain, contentHtml starts with the H1 and featured image and ends with a link to the hub, comparisonData is the structured verdict, tables, alternatives and FAQ, and verifiedAt is when the competitor facts were last verified (null while unverified).
Building Your Own Hub
The scaffolded hub is a starting point. To build your own, group the pages by competitor with groupComparisons (the "vs" page comes first) and add the CollectionPage JSON-LD with comparisonHubJsonLd. Links use thecomparePath returned by the client so they always match the prefix in your dashboard.
import { groupComparisons, comparisonHubJsonLd, serializeJsonLd } from "@getsitelift/nextjs-blog";
import { siteLiftBlog } from "@/lib/sitelift";
export default async function Hub() {
const { comparePath, data } = await siteLiftBlog.listComparisons();
const groups = groupComparisons(data); // one entry per competitor, vs page first
const jsonLd = comparisonHubJsonLd({
siteUrl: "https://your-domain.com",
comparePath,
brandName: "Acme",
pages: data,
});
return (
<section>
{jsonLd && (
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: serializeJsonLd(jsonLd) }} />
)}
{groups.map((g) => (
<article key={g.competitor.domain}>
<h2>{g.competitor.name}</h2>
{g.pages.map((p) => (
<a key={p.id} href={`/${comparePath}/${p.slug}`}>{p.title}</a>
))}
</article>
))}
</section>
);
}Non-2xx responses from the API throw a SiteLiftApiError with a status field, and serializeJsonLd is a JSON.stringify that is safe inside a <script> tag. How the pages are generated, verified and published is covered in Competitor Pages.