@getsitelift/nextjs-blog
v0.2.1
Published
The official Next.js integration for SiteLift. Fetch and render your SiteLift blog articles and competitor comparison pages in your App Router project.
Maintainers
Readme
@getsitelift/nextjs-blog
The official Next.js integration for SiteLift. Connect your Next.js App Router project to your SiteLift dashboard and render SEO-optimized blog articles and competitor comparison pages ("Brand vs X", "X alternatives") directly from the server.
This package ships a lightweight, zero-dependency client and a CLI that scaffolds a ready-to-use blog and comparison section into your codebase within seconds.
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.localfile:
SITELIFT_BLOG_API_KEY=your_api_key_here[!CAUTION] Keep your API key strictly on the server-side. Do not expose it to the client by prefixing it with
NEXT_PUBLIC_.
Scaffold (Recommended)
Use the built-in CLI to generate a clean, unstyled blog and comparison section inside your Next.js application. Run it 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 → Settings in 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.
Flags for scripts and CI:
| Flag | Effect |
| ----------------------- | ---------------------------------------------------------- |
| --yes, -y | Accept every default without prompting (also the behaviour when stdin is not a terminal). |
| --compare-path <path> | Use this prefix for comparison pages instead of asking. |
| --no-compare | Skip the comparison pages. |
| --no-blog | Skip the blog. |
| --force | Overwrite folders that already exist. |
Need a specific folder? Pass the path: npx @getsitelift/nextjs-blog init src/app.
Post-Scaffold Checklist
- Verify Installation: Make sure
@getsitelift/nextjs-blogis in yourpackage.json. - Environment Variables: Double-check that
SITELIFT_BLOG_API_KEYis present in your.env.local. AddNEXT_PUBLIC_SITE_URL=https://your-domain.comso canonical URLs, JSON-LD and sitemaps are absolute, and optionallySITELIFT_BRAND_NAME=Your Brandfor the comparisons hub heading. - Image Domains: Allow external images by updating your
next.config.ts(ornext.config.js):
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [{ protocol: "https", hostname: "*" }],
},
};
export default nextConfig;- Start Developing: Run
npm run devand open/blogfor articles and/compare(or the prefix you chose) for comparison pages.
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>Tags: {article.tags.join(", ")}</p>}
</main>
);
}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.
Data Caching & Next.js ISR
This package is designed to work harmoniously with Next.js Incremental Static Regeneration (ISR).
- The blog starter defaults to
revalidate: 86400(24 hours). The comparison starter uses 1 hour, because comparison pages are published manually from the dashboard and you usually want them live soon after. - Next.js serves its cached version until the revalidation window expires. For instant updates, every fetch is tagged so you can call
revalidateTag()from a Route Handler or Server Action:
import { revalidateTag } from "next/cache";
revalidateTag("sitelift-comparisons"); // all comparison pages and the hub
revalidateTag("sitelift-articles"); // all articles
revalidateTag("sitelift"); // everything fetched through this packagePer-page tags are sitelift-article:{slug} and sitelift-comparison:{slug}.
- Want to always fetch fresh data? Pass
revalidate: falseto theBlogClientoptions to trigger acache: 'no-store'behavior.
BlogClient API Reference
Constructor
| Property | Type | Default | Details |
| ------------ | ----------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------- |
| apiKey | string | process.env.SITELIFT_BLOG_API_KEY | Required. Your secure SiteLift API token. |
| baseUrl | string | process.env.SITELIFT_BLOG_API_URL | Optional override for testing environments. Defaults to https://api.sitelift.io/v1/delivery |
| revalidate | number \| false | 86400 | Controls the caching behavior of the underlying fetch calls. |
| fetch | function | globalThis.fetch | Inject a custom fetch implementation if needed. |
| retries | number | 3 | Retries on 429 and 502/503/504 responses with exponential backoff (Retry-After is honoured). 0 disables. |
| retryDelayMs | number | 1000 | Base delay between retries, doubled on each attempt and capped at 30 seconds. |
Available Methods
| Method | Returns | Description |
| ---------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------- |
| listArticles(options?) | Promise<BlogArticleSummary[]> | Retrieves a paginated list of published articles (metadata only, no bodies). |
| getAllArticles() | Promise<BlogArticleSummary[]> | Automatically paginates and returns all published articles (metadata only, no bodies). |
| getArticleBySlug(slug) | Promise<BlogArticle \| null> | Retrieves the complete content and metadata for a specific article slug. |
| getArticleById(id) | Promise<BlogArticle \| null> | Retrieves an article using its unique identifier. |
| getArticleBySlugAndMarkPublished(slug) | Promise<BlogArticle \| null> | Fetches the article and triggers a status update in SiteLift to mark it as published. |
| getSitemapEntries() | Promise<BlogSitemapEntry[]> | Returns lightweight data (slugs and update times) 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 unpublished or unknown. |
| getComparePath() | Promise<string> | The dashboard prefix (default compare). |
| getComparisonSitemapEntries() | Promise<ComparisonSitemapEntries> | Slugs and update times of every published comparison page. |
Non-2xx responses throw a SiteLiftApiError with a status field. Rate-limited (429) and transient upstream (502/503/504) responses are retried first; see the retries option.
The delivery API allows 100 requests per minute per API token. A next build prerendering a few hundred articles stays well inside that because each article costs one request, and lists are lightweight.
Available Article Fields
List methods return BlogArticleSummary (everything below except contentHtml, contentMarkdown, schemaMarkup and images). getArticleBySlug() and friends return the full BlogArticle:
title,slug,language,tags,statuscontentHtml,contentMarkdownfeaturedImageUrl,images(Contains all article media and placement hints)seoMeta(Custom title and description for<head>)schemaMarkup(JSON-LD structured data)createdAt,updatedAt
Available Comparison Fields
kind("vs"or"alternatives"),title,slug,competitor({ name, domain })contentHtml,contentMarkdown(the HTML starts with the H1 and featured image and ends with a link to the hub)featuredImageUrl,seoMeta,schemaMarkup(JSON-LD generated by SiteLift)comparisonData(structured verdict, pros and cons, comparison tables, alternatives and FAQ)verifiedAt(when the competitor facts were last verified,nullwhen unverified),publishedAt,updatedAt
Building Your Own Hub
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>
);
}Utilities
slugify(text): Transforms any string into a clean, URL-safe slug.readingTimeMinutes(html): Calculates a rough reading time based on the HTML content word count.stripToText(html): Strips out HTML tags to provide clean, plain text.serializeJsonLd(value):JSON.stringifythat is safe to place inside a<script type="application/ld+json">tag.
License
MIT © SiteLift
