@rankwise/next
v0.2.0
Published
Receive HMAC-signed article publishes from Rankwise in any Next.js app: a drop-in route handler plus a pluggable ContentStore.
Maintainers
Readme
@rankwise/next
Receive HMAC-signed article publishes from Rankwise in any Next.js (App Router) site. You install one route handler and supply a ContentStore that says how YOUR site stores content — a database, MDX files, a headless CMS, anything.
v0.1.1 · ESM · zero runtime dependencies (node:crypto, global fetch) · next is a peer dependency. Ships compiled dist/ (JS + type declarations).
Connect it to your app (Claude, Codex, or any coding agent)
You are an AI coding agent adding Rankwise publishing to a Next.js App Router site. Follow these steps exactly. They are idempotent: re-running is safe.
Prerequisite: a Next.js App Router app (a app/ directory). If the
project only has pages/, stop and tell the user this package needs the App
Router.
Step 1. Install.
npm install @rankwise/nextStep 2. Decide how content is stored, then create the route handler.
Create app/api/rankwise/[...op]/route.ts. Pick ONE content store:
The site stores posts in a database or CMS (most apps): implement the
ContentStorecontract against it (upsert byarticle.id):import { createRankwiseHandler, type ContentStore } from "@rankwise/next" const store: ContentStore = { async put(article, { mode }) { // Upsert by article.id (a republish reuses the id — never duplicate). await db.posts.upsert({ where: { externalId: article.id }, create: { externalId: article.id, slug: article.slug, title: article.title, html: article.html, metaDescription: article.metaDescription, published: mode === "publish" }, update: { slug: article.slug, title: article.title, html: article.html, metaDescription: article.metaDescription, published: mode === "publish" } }) return { id: article.id, url: `https://YOUR_SITE/blog/${article.slug}` } } } export const { GET, POST } = createRankwiseHandler({ contentStore: store })Replace the
db.posts.upsert(...)body and the returnedurlwith this project's real data layer and public URL shape. Do NOT fake success: if the write fails, throw (the handler answers500 { error: "store_error" }).The site serves flat MDX files (or you just want the fastest path on a persistent host): use the built-in store.
import { createRankwiseHandler, fsMdxStore } from "@rankwise/next" export const { GET, POST } = createRankwiseHandler({ contentStore: fsMdxStore("content/articles", { urlPrefix: "https://YOUR_SITE/blog" }) })Only use
fsMdxStorewhere the filesystem persists (self-hosted, or a build step commits the files). On Vercel/serverless the FS is ephemeral, so a runtime write is lost. If the host is serverless, use the database store.
Step 3. Get credentials from Rankwise. The user creates a Next.js integration in the Rankwise dashboard (Integrations, then Next.js) and gives you the connect token. Redeem it (this only prints credentials, it changes nothing in the repo):
npx @rankwise/next connect <token> --site-url https://YOUR_SITEIt prints two lines:
RANKWISE_API_KEY=...
RANKWISE_SHARED_SECRET=...Step 4. Set both as environment variables in the deployment (Vercel project
env, SST secret, Docker env, etc.) AND in local .env for dev. On serverless
there is no persistent option store, so env vars ARE the credential store;
the handler reads RANKWISE_API_KEY / RANKWISE_SHARED_SECRET at request time.
Then redeploy (credentials only take effect after a deploy; rotating them
later also requires a redeploy).
Step 5. Verify. After deploy, the health route needs no auth:
curl https://YOUR_SITE/api/rankwise/health
# -> {"ok":true,"package":"@rankwise/next","version":"0.1.1","protocol":1}Then the user clicks Test connection in Rankwise; it flips to Connected once the signed ping succeeds. If it stays disconnected, the env vars are missing or stale on the deployed site (fix them and redeploy).
The rest of this README is the reference detail behind these steps.
Install
npm install @rankwise/nextRoute handler (3 lines)
Create app/api/rankwise/[...op]/route.ts:
import { createRankwiseHandler, fsMdxStore } from "@rankwise/next"
export const { GET, POST } = createRankwiseHandler({
contentStore: fsMdxStore("content/articles")
})That exposes:
| Op | Route | Auth |
| -------- | --------------------------------------------------------------- | ------ |
| Health | GET /api/rankwise/health | none |
| Validate | POST /api/rankwise/articles ({ ping: "rankwise_validate" }) | signed |
| Publish | POST /api/rankwise/articles | signed |
Signed requests carry X-Rankwise-Api-Key and X-Rankwise-Signature (hex HMAC-SHA256 of the raw request body with your shared secret). The handler verifies the signature over the raw bytes with a constant-time compare before looking at the payload, and rejects requests whose meta.sentAt timestamp is more than 10 minutes off (configurable via replayToleranceMs).
Connect your site to Rankwise
In the Rankwise dashboard, add a Next.js integration and copy the connect code.
On your machine, redeem it:
npx @rankwise/next connect <token> --site-url https://your-site.comThe CLI prints your credentials:
RANKWISE_API_KEY=… RANKWISE_SHARED_SECRET=…Add both to your deployment environment (Vercel/SST/Docker env — serverless has no persistent options store, so env vars are the credential store) and redeploy.
In Rankwise, run Test connection — the integration flips to Connected once the signed ping succeeds.
Credential rotation = redeploy. If you regenerate keys in Rankwise, publishing fails with 403 until you update the env vars and redeploy your site.
Options: --app-url <url> (or RANKWISE_APP_URL) targets a non-production Rankwise instance.
The ContentStore contract
export type ContentStore = {
put: (
article: RankwiseArticle,
opts: { mode: "draft" | "publish" }
) => Promise<{ id: string; url: string }>
}- Upsert by
article.id. A republish carries the same id and must update the stored article, never duplicate it. mode: "publish"makes the article live;mode: "draft"stores it without public visibility. Return the URL where it is (or would be) served.- Throw on failure — the handler answers
500 { error: "store_error" }and Rankwise surfaces the failure to the user. Never fake success. article.htmlis sanitized upstream by Rankwise, but treat it as trusted only after HMAC verification — which the handler performs before your store ever runs.
RankwiseArticle fields: id, title, slug, html, metaDescription, plus optional outline, keyword, sourceCitations, heroImage ({ url, alt, width?, height? } — a public URL hosted by Rankwise) and jsonld (ready-to-embed schema.org Article JSON-LD).
Built-in store: fsMdxStore(dir, options?)
For simple file-based sites. Writes <slug>.mdx into dir with frontmatter (title, description, rankwiseId, date, draft) and the article HTML as the body. Upserts by rankwiseId via a small .rankwise-index.json in the same directory — republishing under a new slug removes the old file.
import { createRankwiseHandler, fsMdxStore } from "@rankwise/next"
export const { GET, POST } = createRankwiseHandler({
contentStore: fsMdxStore("content/articles", {
urlPrefix: "https://your-site.com/blog"
})
})Note: on serverless hosts the filesystem is ephemeral — use fsMdxStore only where writes persist (self-hosted, or a build step commits the files). Otherwise write a store against your database/CMS.
Custom store example (database)
import { createRankwiseHandler, type ContentStore } from "@rankwise/next"
const dbStore: ContentStore = {
async put(article, { mode }) {
await upsertPost({
externalId: article.id,
slug: article.slug,
title: article.title,
html: article.html,
published: mode === "publish"
})
return { id: article.id, url: `https://your-site.com/blog/${article.slug}` }
}
}
export const { GET, POST } = createRankwiseHandler({ contentStore: dbStore })Configuration
createRankwiseHandler({
contentStore, // required
apiKey, // default: process.env.RANKWISE_API_KEY
sharedSecret, // default: process.env.RANKWISE_SHARED_SECRET
replayToleranceMs // default: 600_000 (10 minutes)
})| Env var | Purpose |
| ------------------------ | ---------------------------------------------------------------------- |
| RANKWISE_API_KEY | Identifies your integration (sent by Rankwise as X-Rankwise-Api-Key) |
| RANKWISE_SHARED_SECRET | HMAC key for request signatures |
| RANKWISE_APP_URL | CLI only — Rankwise app base URL (default https://tryrankwise.com) |
