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

@sprintup-cms/sdk

v1.9.9

Published

Official SDK for SprintUp Forge CMS — typed API client, Next.js helpers, and React block renderer

Readme

@sprintup-cms/sdk

Official SDK for SprintUp Forge CMS — typed API client, Next.js App Router helpers, and a React block renderer.


Table of contents

  1. Install
  2. Environment variables
  3. Initial setup
  4. Verify your connection
  5. Adding new pages
  6. Custom page layouts
  7. Navigation and footer — automatically rendered by the SDK via CMS Globals
  8. Anchor links — scroll-to-section navigation (v1.9.0+)
  9. SEO settings — per-page meta tags and Open Graph
  10. Extending with custom blocks
  11. ISR and caching strategy
  12. API reference
  13. Block types reference
  14. Troubleshooting

Install

npm install @sprintup-cms/sdk@^1.9.4
# or
pnpm add @sprintup-cms/sdk@^1.9.4
# or
yarn add @sprintup-cms/sdk@^1.9.4

Environment variables

Add the following to your website's .env.local. Never commit these to git.

# ── Required ──────────────────────────────────────────────────────────────────
# Base URL of your SprintUp Forge CMS deployment
NEXT_PUBLIC_CMS_URL=https://your-cms.vercel.app

# API key from CMS Admin → Settings → API Keys → Create key
CMS_API_KEY=cmsk_xxxxxxxxxxxxxxxxxxxx

# App ID from CMS Admin → Settings → Apps
# (shown in the URL when you select a site: /admin/dashboard?app=school-website)
CMS_APP_ID=school-website

# ── Required for webhooks ──────────────────────────────────────────────────────
# Secret that the CMS will send with every webhook request — validate on your end
CMS_WEBHOOK_SECRET=your-random-secret

# ── Optional ──────────────────────────────────────────────────────────────────
# Public URL of this website — used by sitemap generation
NEXT_PUBLIC_SITE_URL=https://www.yourschool.edu

Generate a secure webhook secret:

openssl rand -hex 32

Where to find your values in the CMS:

| Variable | Location in CMS Admin | |---|---| | NEXT_PUBLIC_CMS_URL | The domain your CMS is deployed to | | CMS_API_KEY | Settings → API Keys → Create new key | | CMS_APP_ID | Settings → Apps → click your site → copy App ID | | CMS_WEBHOOK_SECRET | Settings → Webhooks → add secret when registering the URL |


Initial setup

Connect to the CMS

The SDK reads your environment variables automatically. No extra configuration is required for the default singleton client.

// lib/cms.ts
import { cmsClient } from '@sprintup-cms/sdk'
export default cmsClient

For multiple sites or custom configuration:

import { createCMSClient } from '@sprintup-cms/sdk'

export const schoolCms = createCMSClient({
  baseUrl: process.env.NEXT_PUBLIC_CMS_URL,
  apiKey:  process.env.CMS_API_KEY,
  appId:   process.env.CMS_APP_ID,
})

Catch-all page route

This single file automatically renders every page published in your CMS — no manual routing needed.

// app/[...slug]/page.tsx
export { CMSCatchAllPage as default, generateMetadata } from '@sprintup-cms/sdk/next'

How it works:

  • Fetches the page by slug from the CMS on each request (ISR, 60s revalidation).
  • Falls through to notFound() when the slug does not exist in the CMS.
  • Activates draft/preview mode automatically when draftMode() is enabled.
  • Renders <CMSBlocks> for standard pages and structured content for page-type pages.

On-demand revalidation webhook

When an editor publishes or updates a page in the CMS, this webhook fires to instantly clear the ISR cache for that page.

// app/api/cms-revalidate/route.ts
export { POST } from '@sprintup-cms/sdk/next'

Then register the webhook in your CMS Admin → Settings → Webhooks:

URL:    https://www.yourschool.edu/api/cms-revalidate
Secret: (same value as CMS_WEBHOOK_SECRET)
Events: published, deleted, archived

Custom hook with extra logic:

// app/api/cms-revalidate/route.ts
import { createRevalidateHandler } from '@sprintup-cms/sdk/next'

export const POST = createRevalidateHandler({
  secret: process.env.CMS_WEBHOOK_SECRET,
  onRevalidate: async ({ slug, event }) => {
    console.log(`CMS revalidated /${slug} — event: ${event}`)
    // e.g. update a search index, notify Slack, etc.
  },
})

Preview mode exit

// app/api/cms-preview/exit/route.ts
export { previewExitGET as GET } from '@sprintup-cms/sdk/next'

The CMS editor links to /api/cms-preview/exit?redirect=/your-slug to leave draft mode. This handler disables draftMode() and redirects the editor back to the live page.


Sitemap

// app/sitemap.ts
import type { MetadataRoute } from 'next'
import { cmsClient } from '@sprintup-cms/sdk'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const data = await cmsClient.getSitemap()
  if (!data?.enabled) return []

  return data.urls.map(url => ({
    url:             `${process.env.NEXT_PUBLIC_SITE_URL}${url.loc}`,
    lastModified:    url.lastmod,
    changeFrequency: url.changefreq as MetadataRoute.Sitemap[0]['changeFrequency'],
    priority:        url.priority,
  }))
}

Verify your connection

Add a temporary status check page to confirm your API key and App ID are working:

// app/cms-status/page.tsx  (remove before going to production)
import { cmsClient } from '@sprintup-cms/sdk'

export default async function CmsStatusPage() {
  const status = await cmsClient.getStatus()
  if (!status) return <p>CMS connection failed — check your environment variables.</p>

  return (
    <div style={{ padding: 32, fontFamily: 'monospace' }}>
      <h1>CMS Status</h1>
      <p>App ID: {status.appId}</p>
      <p>Total pages: {status.totalPages}</p>
      <p>Published: {status.publishedPages}</p>
      <pre>{JSON.stringify(status.pages.slice(0, 5), null, 2)}</pre>
    </div>
  )
}

Adding new pages

You never write code to add a new page. All content management happens in the CMS:

  1. Go to CMS Admin → Content (scoped to your site).
  2. Click New Page.
  3. Choose a page type (e.g. Standard Page, Blog Post, Event).
  4. Fill in the title, slug, and content blocks.
  5. Click Publish.
  6. The webhook fires, your Next.js site revalidates within seconds, and the page is live at https://yoursite.com/<slug>.

To add a custom page type (e.g. Programme, Staff Profile):

  1. Go to CMS Admin → Page Types.
  2. Click New Page Type.
  3. Define sections and fields.
  4. Create content using that type.
  5. In your website, customize the rendering — see Custom page layouts.

Custom page layouts

Override the default rendering for a specific page type by wrapping CMSCatchAllPage:

// app/[...slug]/page.tsx
import { notFound } from 'next/navigation'
import { cmsClient } from '@sprintup-cms/sdk'
import { CMSBlocks } from '@sprintup-cms/sdk/react'
import type { Metadata } from 'next'

export async function generateMetadata({ params }: { params: Promise<{ slug: string[] }> }): Promise<Metadata> {
  const { slug } = await params
  const page = await cmsClient.getPage(slug.join('/'))
  if (!page) return { title: 'Not Found' }
  return {
    title:       page.seo?.title       || page.title,
    description: page.seo?.description || '',
  }
}

export default async function Page({ params }: { params: Promise<{ slug: string[] }> }) {
  const { slug } = await params
  const page = await cmsClient.getPage(slug.join('/'))
  if (!page) notFound()

  // Render a blog post with a custom layout
  if (page.pageType === 'blog-post') {
    return (
      <article className="max-w-2xl mx-auto py-16 px-6">
        <h1 className="text-4xl font-bold">{page.title}</h1>
        <time className="text-sm text-gray-500">{page.publishedAt}</time>
        <CMSBlocks blocks={page.blocks} />
      </article>
    )
  }

  // Fall back to default layout for all other page types
  return (
    <main className="max-w-5xl mx-auto py-12 px-6">
      <h1 className="text-4xl font-bold mb-8">{page.title}</h1>
      <CMSBlocks blocks={page.blocks} />
    </main>
  )
}

Navigation and footer

As of v1.8.62, navigation and footer are managed as CMS Globals — configured entirely in the CMS Admin under Globals → Navigation and Globals → Footer. You do not need to write any header or footer code. The SDK renders them automatically.

How it works

CMSCatchAllPage (the SDK's catch-all page handler) automatically fetches the active globals for your app and renders:

  • <CMSHeader> — a sticky top nav bar with your logo, nav links, and CTA buttons
  • <CMSFooter> — a configurable section grid with optional footer bottom bar

You do not need to create components/layout/header.tsx or components/layout/footer.tsx.

Configuring navigation in the CMS

Go to CMS Admin → Globals → Navigation and add items:

| Item type | Rendered as | Options | |---|---|---| | link | Nav link in the centre area | Label, URL, open in new tab | | button | CTA button, right-aligned | Label, URL, variant: primary / outline / ghost | | dropdown | Link with children | Label, URL, child links |

Configuring footer in the CMS

Go to CMS Admin → Globals → Footer and add sections in any order:

| Section type | Description | |---|---| | brand | Logo image and tagline | | links | A column of links with a custom title — add as many as you need | | contact | Email, phone, address with a custom section title | | social | Facebook, X, Instagram, LinkedIn, YouTube icons |

Optionally enable Footer Bottom (a full-width legal bar):

  • Copyright text
  • Legal links (Privacy Policy, Terms, etc.)

Links: internal vs external

When adding links inside footer sections, the link input auto-searches your published CMS pages as you type. Select a page to create an internal link, or type a full URL (https://...) for an external link. External links open in a new tab automatically.

Custom layout: accessing globals manually

If you are building a custom page outside CMSCatchAllPage, you can access globals directly:

// Only needed for custom layouts — CMSCatchAllPage handles this automatically
import { cmsClient } from '@sprintup-cms/sdk'

const globals = await cmsClient.getGlobals()
const nav    = globals?.nav     // CMSPage with sectionData.items[]
const footer = globals?.footer  // CMSPage with sectionData.sections[]

Deprecated: cmsClient.getSiteStructure() and menus.header / menus.footer are the old approach and should not be used for navigation or footer.


Anchor links

Added in v1.9.0 — Navigation items can now scroll to specific sections on any page.

How it works

  1. Every block has an anchor ID — auto-generated from the block title (e.g. "Our Features" → #our-features), or set manually in the editor.

  2. Add anchor nav items — In CMS Admin → Globals → Navigation, add a nav item with type "Anchor" and pick a section from the dropdown.

  3. Cross-page support — Anchors can link to sections on other pages. The href is resolved as /about#team for cross-page or #features for same-page.

Using navItems from getGlobals()

const { navItems } = await cmsClient.getGlobals()

// navItems already have href resolved:
// navItems[0].href === "/about#team"     (cross-page)
// navItems[1].href === "#features"       (same-page)

Handling anchor clicks

For same-page anchors, use smooth scrolling instead of navigation:

'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'

function NavLink({ href, item, children, className }) {
  const pathname = usePathname()
  const hashIdx = href.indexOf('#')
  const isSamePage = hashIdx >= 0 && (href.slice(0, hashIdx) === '' || href.slice(0, hashIdx) === pathname)

  if (item?.type === 'anchor' && isSamePage) {
    return (
      <a href={href} className={className}
        onClick={(e) => {
          e.preventDefault()
          document.querySelector(href.slice(hashIdx))?.scrollIntoView({ behavior: 'smooth' })
          window.history.pushState(null, '', href)
        }}>
        {children}
      </a>
    )
  }
  return <Link href={href} className={className}>{children}</Link>
}

Block anchor IDs

Render id on block root elements so the browser can scroll to them:

<section id={block.content.anchorId || toAnchorId(block.content.title)}>

SEO settings

Every page includes SEO metadata configured in the CMS editor.

Data shape

interface CMSPageSeo {
  title?: string          // Meta title (overrides page.title)
  description?: string    // Meta description
  keywords?: string[]     // Meta keywords
  ogImage?: string        // Open Graph image URL
  noIndex?: boolean       // Exclude from search engines
}

Using with Next.js generateMetadata

export async function generateMetadata({ params }): Promise<Metadata> {
  const page = await cmsClient.getPage(slug)
  if (!page) return { title: 'Not Found' }

  return {
    title:       page.seo?.title || page.title,
    description: page.seo?.description,
    keywords:    page.seo?.keywords?.join(', '),
    robots:      page.seo?.noIndex ? 'noindex,nofollow' : undefined,
    openGraph: {
      title:       page.seo?.title || page.title,
      description: page.seo?.description,
      images:      page.seo?.ogImage ? [page.seo.ogImage] : undefined,
    },
  }
}

Global SEO defaults (site name, default OG image) are merged automatically by the CMS API — you always receive the resolved values.


Extending with custom blocks

Override any built-in block or add a completely new block type using the custom prop:

import { CMSBlocks } from '@sprintup-cms/sdk/react'
import { MyVideoPlayer } from '@/components/video-player'
import { ProgrammeCard } from '@/components/programme-card'

<CMSBlocks
  blocks={page.blocks}
  custom={{
    // Override built-in video block with your own player
    'video': (block) => <MyVideoPlayer src={block.data?.url} />,

    // Add a completely custom block type created in the CMS
    'programme-card': (block) => (
      <ProgrammeCard
        title={block.data?.title}
        duration={block.data?.duration}
        applyUrl={block.data?.applyUrl}
      />
    ),
  }}
/>

The custom prop is a Record<blockType, (block: CMSBlock) => React.ReactNode>. It takes priority over all built-in renderers.


ISR and caching strategy

| Data | Revalidation | Cache tag | |---|---|---| | Individual page | 60 seconds | cms-page-{slug} | | All pages list | 60 seconds | cms-pages-{appId} | | Page type schema | 3600 seconds | cms-page-type-{id} | | Globals (nav + footer) | 60 seconds | cms-globals-{appId} | | Sitemap | 3600 seconds | cms-sitemap-{appId} | | Status check | No cache | — | | ~~Site structure~~ | ~~300 seconds~~ | Deprecated — use Globals |

The revalidation webhook calls revalidateTag('cms-page-{slug}') and revalidatePath('/{slug}') when the CMS publishes a page, giving you instant updates without a full rebuild.


API reference

cmsClient

| Method | Description | |---|---| | getPage(slug) | Fetch a single published page by slug | | getPages(options?) | Fetch multiple pages — filterable by type, group, page, perPage | | getBlogPosts() | Shorthand for getPages({ type: 'blog-post' }) | | getEvents() | Shorthand for getPages({ type: 'event-page' }) | | getAnnouncements() | Shorthand for getPages({ type: 'announcement-page' }) | | getPageType(id) | Fetch a page type schema by ID | | getGlobals() | Fetch navigation and footer globals — returns { nav, footer }. CMSCatchAllPage calls this automatically. | | getPreviewPage(token) | Fetch a draft page for preview mode | | getPageWithPreview(slug, token?) | Fetch page — preview if token present, live otherwise | | getSitemap() | Fetch all published slugs with sitemap metadata | | getStatus() | Connectivity check — returns counts and page list | | ~~getSiteStructure()~~ | Deprecated. Used the old Site Structure editor. Navigation and footer are now CMS Globals — use getGlobals() or rely on CMSCatchAllPage. |


Block types reference

| Block type | Key fields in block.data | |---|---| | heading | text, level (1–4) | | text | text | | richtext | content (HTML string) | | image | src, alt, caption | | hero | title, subtitle, badge, primaryButton, primaryUrl, secondaryButton, secondaryUrl, alignment | | centered-hero | title, subtitle, badge, primaryButton, primaryUrl, secondaryButton, secondaryUrl, backgroundImage, backgroundColor, overlayOpacity | | product-hero | title, subtitle, badge, primaryButton, primaryUrl, image, imageAlt, browserChrome, urlBar, trustedBy[], alignment | | bento-hero | title, subtitle, badge, primaryButton, primaryUrl, cards[] (title, description, icon, featured), alignment | | minimal-hero | eyebrow, title, subtitle, primaryButton, primaryUrl, secondaryButton, secondaryUrl, alignment | | split-hero | title, subtitle, button, buttonUrl, image, imageAlt, imagePosition (left/right), alignment | | cta | title, subtitle, primaryButton, primaryUrl, secondaryButton, secondaryUrl | | faq | title, items[] — each { question, answer } | | stats | items[] — each { value, label, description } | | testimonial | quote, author, role, avatar | | quote | quote, author, source | | alert | message, type (info/success/warning/error) | | divider | — | | spacer | size (sm/md/lg/xl) | | video | url (YouTube or Vimeo), title, autoplay |


Troubleshooting

Pages return empty (getPage returns null)

  • Check CMS_APP_ID matches exactly what is shown in CMS Admin → Apps.
  • Verify the page status is Published (not Draft).
  • Confirm the API key has read permission for the app.

getStatus() returns null

  • All three env vars (NEXT_PUBLIC_CMS_URL, CMS_API_KEY, CMS_APP_ID) must be set.
  • The CMS URL must not have a trailing slash.
  • Test the API key directly: curl -H "X-CMS-API-Key: cmsk_xxx" https://your-cms.vercel.app/api/v1/school-website/status

Webhook not firing / pages stale

  • Confirm the webhook URL is registered in CMS Admin → Settings → Webhooks.
  • CMS_WEBHOOK_SECRET on both sides must match exactly.
  • Check your deployment logs for [sprintup-cms] revalidate error: messages.

Navigation or footer not showing

  • Go to CMS Admin → Globals → Navigation (or Footer) and check that content is published (status = Published).
  • Ensure at least one item/section is added. An empty globals document renders nothing.
  • Changes are cached for 60 seconds. Hard-refresh or wait a moment after publishing.
  • Do not use getSiteStructure() — it does not return globals data.

Preview mode not working

  • The /api/cms-preview/exit route must exist.
  • draftMode() requires a Next.js App Router project (not Pages Router).

Requirements

  • Node.js 18+
  • Next.js 14+ (for /next entry)
  • React 18+ (for /react entry)

License

MIT — SprintUp IO