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:

bash
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:

.env.local
SITELIFT_BLOG_API_KEY=your_api_key_here

Warning

Keep your API key strictly on the server-side. Do not expose it to the client by prefixing it with 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:

bash
npx @getsitelift/nextjs-blog init

The command is interactive and asks:

  1. App directory: auto-detects src/app or app.
  2. Scaffold the blog? Default yes. Creates app/blog.
  3. Include competitor comparison pages? Default yes.
  4. 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:

PathPurpose
/blogThe 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.xmlAn auto-updating sitemap to keep search engines informed.
/compareThe 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.xmlSitemap 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):

next.config.ts
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

lib/sitelift.ts
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

app/blog/page.tsx
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

app/blog/[slug]/page.tsx
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

MethodReturnsDescription
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:

titlesluglanguagetagsstatuscontentHtmlcontentMarkdownfeaturedImageUrlimagesseoMetaschemaMarkupcreatedAtupdatedAt

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

kindtitleslugcompetitorcontentHtmlcontentMarkdownfeaturedImageUrlseoMetaschemaMarkupcomparisonDataverifiedAtpublishedAtupdatedAt

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.

app/compare/page.tsx
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.