@riqmi/cms-client
v0.1.0
Published
Typed, zero-dependency client for the Riqmi headless CMS API — list, filter, and fetch blog articles. Framework-agnostic with first-class Next.js caching and revalidation helpers.
Downloads
127
Maintainers
Readme
@riqmi/cms-client
New to Riqmi CMS? Start with the Riqmi CMS guide (
/docs/riqmi-cmsin the Riqmi app) — the self-contained walkthrough from turning the CMS on to wiring it into your site. This README is the package reference behind it.
Typed, zero-dependency client for the Riqmi headless CMS API. Fetch your published blog — list, filter by tag, and article detail — from any JS runtime, with first-class Next.js caching and on-demand revalidation.
- Zero runtime dependencies — just the platform
fetch. - Fully typed responses (
CmsArticleSummary,CmsArticle,CmsTag, …). - Next.js-aware — every fetch is tagged so a webhook can purge exactly what changed.
- Framework-agnostic — works in Next.js, Remix, NestJS, plain Node, edge runtimes.
Install
pnpm add @riqmi/cms-client
# or: npm i @riqmi/cms-client / yarn add @riqmi/cms-clientRequires Node 18+ (global
fetch). In older runtimes pass your ownfetchvia thefetchoption.
Configure
In Riqmi, set CMS type to Riqmi CMS (Settings → Content settings) and Publish each article you want exposed. Then set on the consuming site:
RIQMI_CMS_BASE_URL="https://<your-deployment>.convex.site"
RIQMI_CMS_SITE_KEY="example.com" # business normalized domain: lowercase, no www.Quick start
// lib/cms.ts
import { createRiqmiCmsClient } from '@riqmi/cms-client'
export const cms = createRiqmiCmsClient({
baseUrl: process.env.RIQMI_CMS_BASE_URL!,
siteKey: process.env.RIQMI_CMS_SITE_KEY!,
// applied to every request; per-call options override it
defaultRequestInit: { next: { revalidate: 300 } }
})const { items } = await cms.listArticles() // all published articles (summaries)
const { items } = await cms.listArticles({ tag: 'engineering' }) // filter by tag
const { items } = await cms.listArticles({ limit: 10 }) // latest 10
const detail = await cms.getArticle('my-slug') // CmsArticleDetail | null
const { items: tags } = await cms.listTags() // [{ name, count }]getArticle returns null when the slug (or site) is not found, so it maps cleanly onto notFound(). Other non-2xx responses throw a RiqmiCmsError ({ status, code }).
Next.js App Router
Blog list with tag filtering
// app/blog/page.tsx
import Link from 'next/link'
import { cms } from '@/lib/cms'
export default async function BlogPage({ searchParams }: { searchParams: Promise<{ tag?: string }> }) {
const { tag } = await searchParams
const [{ items: articles }, { items: tags }] = await Promise.all([cms.listArticles({ tag }), cms.listTags()])
return (
<main>
<nav>
<Link href='/blog'>All</Link>
{tags.map((t) => (
<Link key={t.name} href={`/blog?tag=${encodeURIComponent(t.name)}`}>
{t.name} ({t.count})
</Link>
))}
</nav>
{articles.map((a) => (
<article key={a.id}>
<Link href={`/blog/${a.slug}`}>{a.title}</Link>
{a.excerpt ? <p>{a.excerpt}</p> : null}
</article>
))}
</main>
)
}Article detail + metadata
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { cms } from '@/lib/cms'
export async function generateStaticParams() {
const { items } = await cms.listArticles()
return items.map((a) => ({ slug: a.slug }))
}
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params
const detail = await cms.getArticle(slug)
if (!detail) return {}
const { article } = detail
return {
title: article.metaTitle ?? article.title,
description: article.metaDescription ?? article.description ?? article.excerpt,
alternates: article.url ? { canonical: article.url } : undefined
}
}
export default async function ArticlePage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const detail = await cms.getArticle(slug)
if (!detail) notFound()
const { article } = detail
return (
<article>
<h1>{article.title}</h1>
{/* contentHtml is server-rendered from markdown; sanitize if your editors allow raw HTML */}
<div dangerouslySetInnerHTML={{ __html: article.contentHtml }} />
</article>
)
}On-demand revalidation (webhook)
The API is cached (max-age=60, stale-while-revalidate=300). To make a publish appear immediately, configure the Revalidation webhook URL on the Riqmi CMS integration card and add a route handler. No shared secret is required — the endpoint only ever triggers an ISR cache refresh, so it's unauthenticated by design.
Because every client fetch is already tagged (riqmi:list, riqmi:article:<slug>), one webhook purges exactly what changed:
// app/api/riqmi/revalidate/route.ts
import { revalidateTag } from 'next/cache'
import { parseRiqmiWebhook, riqmiTagsForPayload } from '@riqmi/cms-client'
export async function POST(request: Request) {
const payload = await parseRiqmiWebhook(request)
if (!payload) return new Response('Invalid payload', { status: 400 })
for (const tag of riqmiTagsForPayload(payload)) revalidateTag(tag)
return Response.json({ revalidated: true })
}This package never imports next/cache, so it stays zero-dep and runtime-agnostic — you call revalidateTag in your own handler.
Sitemap & RSS
Riqmi serves these directly. Proxy them from your site:
// app/blog/sitemap.xml/route.ts
import { cms } from '@/lib/cms'
export async function GET() {
const res = await fetch(cms.getSitemapUrl(), { next: { revalidate: 3600 } })
return new Response(await res.text(), { headers: { 'Content-Type': 'application/xml' } })
}cms.getFeedUrl() works the same way for feed.xml.
API
| Method | Returns | Notes |
| --- | --- | --- |
| createRiqmiCmsClient(config) | RiqmiCmsClient | config: { baseUrl, siteKey, defaultRequestInit?, fetch? } |
| client.listArticles({ tag?, limit?, requestInit? }) | Promise<CmsArticleList> | Summaries only |
| client.getArticle(slug, { requestInit? }) | Promise<CmsArticleDetail \| null> | null on 404 |
| client.listTags({ requestInit? }) | Promise<CmsTagList> | [{ name, count }] |
| client.getSitemapUrl() / getFeedUrl() | string | Absolute URLs |
| parseRiqmiWebhook(request) | Promise<RiqmiWebhookPayload \| null> | Parses + validates the body; null on invalid |
| riqmiTagsForPayload(payload) | string[] | Tags to revalidateTag |
| riqmiArticleTag(slug) / RIQMI_LIST_TAG | string | Tag conventions |
All response types (CmsArticleSummary, CmsArticle, CmsSite, CmsTag, …) and RiqmiCmsError are exported.
Notes
- The list endpoint returns summaries; use
getArticleforcontentHtml/contentMarkdown/images. - Tags filter case-insensitively. Server caps
limitat 200. contentHtmlis generated from trusted Riqmi markdown — sanitize in the app if third-party editors can inject raw HTML.
