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

@rankfirst/next

v0.1.0

Published

Next.js adapter for RankFirst content delivery.

Readme

@rankfirst/next

Next.js adapter for pulling RankFirst articles into your site, rendering them with sane MDX defaults, and revalidating them seconds after publish.

Quickstart

If you want the setup applied for you inside an existing Next.js App Router repo:

npx rankfirst

That CLI will:

  • optionally open a browser link flow against your logged-in RankFirst account
  • fetch the API key + webhook secret directly into .env.local
  • create the server-only RankFirst client helper
  • add the blog list/detail routes
  • add the signed revalidation endpoint
  • add sitemap support
  • patch next.config.* for RankFirst images
  • append the required env vars to .env.example

If you want to do it manually or customize every file yourself, keep reading.

What you get

  • pull published RankFirst articles into your Next.js app at build time
  • render them with a ready-made article component
  • survive RankFirst outages with a last-known-good snapshot cache
  • revalidate the cache within seconds after publish via a signed webhook

Setup flow

  1. In RankFirst, open your site's Integration page and generate:
    • an API key
    • a webhook secret
    • the webhook URL pointing at your Next.js app
  2. Install @rankfirst/next in your Next.js repo.
  3. Add the env vars below.
  4. Add a server-only RankFirst client helper.
  5. Add blog list/detail routes plus the revalidation route.
  6. Add sitemap entries and next/image remote patterns.

1. Install

npm install @rankfirst/next

2. Add env vars

RANKFIRST_API_KEY=rf_live_...
RANKFIRST_WEBHOOK_SECRET=...
RANKFIRST_BASE_URL=https://rankfirst.co
NEXT_PUBLIC_SITE_URL=https://your-site.com

Security note: RANKFIRST_API_KEY is a bearer secret. Only read it in server components, route handlers, and server actions. Never prefix it with NEXT_PUBLIC_, never pass it into a client component, and rotate it immediately if it ever appears in browser devtools.

RANKFIRST_BASE_URL is optional. Leave it alone for production. Point it at your local RankFirst app when you want to test against an unshipped API, for example http://localhost:3000.

3. Create a server-only client

Put this in lib/rankfirst.server.ts:

import "server-only";
import { cache } from "react";
import { createResilientClient } from "@rankfirst/next";

export const getRankFirstClient = cache(() =>
  createResilientClient({
    apiKey: process.env.RANKFIRST_API_KEY!,
    baseUrl: process.env.RANKFIRST_BASE_URL,
  }),
);

That gives you one shared server-only helper instead of repeating the client config in every route.

4. Add blog routes

app/blog/page.tsx

import Link from "next/link";
import { getRankFirstClient } from "@/lib/rankfirst.server";

export default async function BlogPage() {
  const client = getRankFirstClient();
  const snapshot = await client.getSnapshot();

  return (
    <main className="mx-auto max-w-3xl space-y-8 px-6 py-12">
      <h1 className="text-4xl font-semibold">Blog</h1>
      <ul className="space-y-6">
        {snapshot.articles.map((article) => (
          <li key={article.id}>
            <Link href={`/blog/${article.slug}`} className="block space-y-2 hover:underline">
              <h2 className="text-2xl font-semibold">{article.headline}</h2>
              <p className="text-sm text-neutral-600">{article.excerpt}</p>
            </Link>
          </li>
        ))}
      </ul>
    </main>
  );
}

app/blog/[slug]/page.tsx

import { notFound } from "next/navigation";
import {
  generateRankFirstMetadata,
  generateRankFirstStaticParams,
  RankFirstArticle,
} from "@rankfirst/next";
import { getRankFirstClient } from "@/lib/rankfirst.server";

export const dynamicParams = true;

export async function generateStaticParams() {
  const client = getRankFirstClient();
  return generateRankFirstStaticParams(client);
}

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const client = getRankFirstClient();
  const { slug } = await params;
  const snapshot = await client.getSnapshot();
  const article = snapshot.articles.find((entry) => entry.slug === slug);

  if (!article) {
    return {};
  }

  return generateRankFirstMetadata(article, {
    baseUrl: process.env.NEXT_PUBLIC_SITE_URL!,
  });
}

export default async function BlogArticlePage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const client = getRankFirstClient();
  const { slug } = await params;
  const snapshot = await client.getSnapshot();
  const article = snapshot.articles.find((entry) => entry.slug === slug);

  if (!article) {
    notFound();
  }

  return (
    <main className="mx-auto max-w-3xl px-6 py-12">
      <RankFirstArticle article={article} tokens={snapshot.site?.designTokens ?? null} />
    </main>
  );
}

If you want a path other than /blog, that's fine. Just keep the same route shape and pass basePath into the sitemap / metadata helpers if needed.

5. Add the revalidation webhook route

app/api/rankfirst/revalidate/route.ts

import { createRevalidationHandler } from "@rankfirst/next";

export const { POST } = createRevalidationHandler({
  secret: process.env.RANKFIRST_WEBHOOK_SECRET!,
});

Then go back to RankFirst and set your webhook URL to:

https://your-site.com/api/rankfirst/revalidate

6. Add sitemap support

app/sitemap.ts

import type { MetadataRoute } from "next";
import { getRankFirstSitemapEntries } from "@rankfirst/next";
import { getRankFirstClient } from "@/lib/rankfirst.server";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const client = getRankFirstClient();
  const baseUrl = process.env.NEXT_PUBLIC_SITE_URL!;
  const articles = await getRankFirstSitemapEntries(client, { baseUrl });

  return [{ url: baseUrl }, ...articles];
}

7. Allow RankFirst images

Add RankFirst storage to next.config.ts so next/image can optimize article images:

import type { NextConfig } from "next";

const config: NextConfig = {
  images: {
    remotePatterns: [{ protocol: "https", hostname: "r2.rankfirst.co" }],
  },
};

export default config;

8. Optional styling inheritance

If the site owner fills in the RankFirst integration form, article typography and colors inherit those values. If not, RankFirstArticle falls back to normal Tailwind prose styling.

9. Build-time resilience

Every successful fetch writes .rankfirst-snapshot.json at the project root. On the next build, if RankFirst is down or returns an error, the adapter serves that last-known-good snapshot instead of dropping your blog routes.

You can:

  • add .rankfirst-snapshot.json to .gitignore and keep it local, or
  • commit it if you want new infra to keep serving the previous article set during an outage.

10. Verify locally

Once your env vars are in place:

npm run dev

Then check:

  • /blog shows your published RankFirst posts
  • /blog/[slug] renders the full article
  • POST /api/rankfirst/revalidate is reachable from the public internet in your preview / prod environment
  • sitemap.xml includes the RankFirst URLs

Troubleshooting

  • First-ever build with RankFirst down and no snapshot yet: the resilient client returns an empty article list and logs a warning. Set onCompleteFailure: "throw" if you prefer a hard failure.
  • Secret rotation: rotate the API key or webhook secret in the RankFirst integration page, then update your env vars and redeploy.
  • Verify webhook signatures manually:
curl -X POST https://your-site.com/api/rankfirst/revalidate \
  -H "content-type: application/json" \
  -H "x-rankfirst-signature: sha256=..." \
  -H "x-rankfirst-timestamp: 1712345678901" \
  -H "x-rankfirst-delivery-id: 00000000-0000-0000-0000-000000000000" \
  -d '{"event":"article.published","articleId":"123","slug":"hello","publishedAt":"2026-04-24T00:00:00.000Z"}'