npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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

  1. Locate your API key in the SiteLift dashboard (Settings → Integrations → Next.js Blog).
  2. Store the key securely in your .env.local file:
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 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 → 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

  1. Verify Installation: Make sure @getsitelift/nextjs-blog is in your package.json.
  2. Environment Variables: Double-check that SITELIFT_BLOG_API_KEY is present in your .env.local. Add NEXT_PUBLIC_SITE_URL=https://your-domain.com so canonical URLs, JSON-LD and sitemaps are absolute, and optionally SITELIFT_BRAND_NAME=Your Brand for the comparisons hub heading.
  3. 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;
  1. Start Developing: Run npm run dev and open /blog for 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 package

Per-page tags are sitelift-article:{slug} and sitelift-comparison:{slug}.

  • Want to always fetch fresh data? Pass revalidate: false to the BlogClient options to trigger a cache: '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, status
  • contentHtml, contentMarkdown
  • featuredImageUrl, 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, null when 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.stringify that is safe to place inside a <script type="application/ld+json"> tag.

License

MIT © SiteLift