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

next-ssp

v0.1.1

Published

Server-side props pipeline for the Next.js pages router: a step engine with lifecycle hooks, deterministic error-to-status mapping, a React Query dehydration companion and a MetaHead component.

Readme

next-ssp

A server-side props pipeline for the Next.js pages router: a step engine with lifecycle hooks, deterministic error-to-status mapping, an optional React Query dehydration companion and a MetaHead component.

Pages router only. The package builds on getServerSideProps / getStaticProps, which do not exist in the App Router — there are no plans for an App Router counterpart (RSC loaders solve this differently). Peer next >= 14 covers the types only.

  • next-ssp — the core: step engine, errors[]/statusCode props contract, error serialization & sanitization, status strategy, meta head utilities, deep serialization pair. Knows nothing about sessions or React Query.
  • next-ssp/react-query — optional companion (peer @tanstack/react-query@^5): creates the QueryClient before the pipeline, dehydrates the cache into props, collects per-query prefetch errors, re-hydrates on the client.
  • next-ssp/react — optional client subpath (peer react): the MetaHead component.

Installation

npm install next-ssp
# with the React Query companion:
npm install next-ssp @tanstack/react-query

Peer dependencies: next >= 14 (types only), optionally @tanstack/react-query@^5 and react@^18 || ^19.

Requires Node.js 20+. Ships ESM and CommonJS builds; the CommonJS build works without require(esm) support, so Jest and CommonJS custom servers need no extra transform settings.

Core factory

import { createGetServerSideProps } from 'next-ssp'

const getServerSideProps = createGetServerSideProps({
  // The single pipeline slot — session, SEO redirects, prefetch; the order is yours.
  // Plain-function steps are error-caught automatically.
  steps: [
    mySessionStep,        // return { redirect } / { notFound } to short-circuit
    mySeoRedirectStep,
  ],
  // One variadic sync option; async work belongs in steps.
  metaHead: (context) => ({ title: 'Page' }),
  metaHeadOptions: { resolveOrigin: () => 'https://example.com' },
  // Injectable status strategy (default: deterministic 500 > 503 > 502 > 404 > 400 > min(4xx)).
  determineStatusFromErrors: (errors) => 500,
  // Project base props, merged under the core keys.
  composeProps: (context) => ({ session: context.sessionData }),
  hooks: {
    onBegin: (context) => {},
    onCaughtError: (error) => logger.error(error),
    onDone: (context, sspTime) => logger.info({ sspTime }),
  },
})

Resulting props contract (core keys cannot be overridden by composeProps):

interface Props {
  metaHead: IMetaHead
  error?: IErrorObject      // a single sanitized, JSON-safe step error
  statusCode?: number       // duplicated from the response status
  // ...everything returned by composeProps
}

One step error lands in props.error (sanitized in production). Projects that accumulate several errors may fold them into a single AggregateError — toErrorObject serializes it as { name: 'AggregateError', message, errors: [...] }, so a dev UI can unfold the list back.

Step engine

A step is a function or { name, stepFunction, atNextData?, catchError? }. stepFunction may be an array — the functions run in parallel and the break results are prioritized deterministically: error > redirect > notFound. atNextData: false skips the step on /_next/data requests. catchError: true converts a thrown error into the break result { error }.

Use the lower-level engine directly when the factory does not fit:

import { abstractGetServerSideProps, expandServerSideSteps } from 'next-ssp'

Request path utilities

import { checkIsNextDataPath, checkIsStaticResourcePath, nextDataToRoutePath } from 'next-ssp'

checkIsNextDataPath('/_next/data/<buildId>/lakes.json')     // true (type predicate)
checkIsStaticResourcePath('/_next/static/chunks/app.js')    // true (type predicate)
nextDataToRoutePath('/_next/data/<buildId>/lakes.json?p=2') // '/lakes?p=2'

nextDataToRoutePath maps a data request back to its page route path — handy for logging and metrics in custom servers.

React Query companion

import { createGetServerSideProps, prefetchQuery } from 'next-ssp/react-query'
import { QueryClient } from '@tanstack/react-query'

const getServerSideProps = createGetServerSideProps({
  createQueryClient: () => new QueryClient({ defaultOptions: myDefaults }),
  configureQueryClient: (queryClient) => queryClient.setQueryData(['settings'], settings),
  steps: async (context) => {
    // Soft prefetch: the failure is recorded per-query and the page keeps rendering.
    await prefetchQuery(context, { queryKey: ['news'], queryFn: fetchNews })
    // Critical query — let the error break the pipeline instead.
    await context.queryClient.fetchQuery({ queryKey: ['session'], queryFn: fetchSession })
  },
  metaHead: (context) => ({
    title: context.queryClient.getQueryData(['news'])?.title,
  }),
})

The companion mixes into props: reactQueryDehydratedState (serialized cache) and, when soft prefetches failed, reactQueryDehydratedErrors ({ queryKey, error }[]). Those errors also feed the status strategy.

Client re-hydration

import { SspQueryProvider } from 'next-ssp/react-query'

function QueryProvider({ dehydratedState, children }) {
  return (
    <SspQueryProvider
      dehydratedState={dehydratedState}
      isExcludedFromWarnQueryKey={queryKey => queryKey[0] === 'settings'}
      devtoolsProps={{ initialIsOpen: false }}
    >
      {children}
    </SspQueryProvider>
  )
}

SspQueryProvider creates the client QueryClient (createQueryClient is injectable), re-hydrates the state, and in development warns about prefetched queries no client observer consumed (isExcludedFromWarnQueryKey suppresses known-false positives). The React Query devtools panel is rendered in development only; pass devtoolsProps: false to disable it entirely (@tanstack/react-query-devtools is an optional peer — the panel is lazy-loaded and never reaches a production bundle).

The lower-level hydrateDehydratedState is still exported for custom providers:

import { hydrateDehydratedState } from 'next-ssp/react-query'

MetaHead

import { MetaHead } from 'next-ssp/react'

<MetaHead {...pageProps.metaHead} />

prepareMetaHead (used by the factory) joins keyword arrays, truncates long descriptions on sentence boundaries and resolves relative canonical/image through metaHeadOptions.resolveOrigin. The title is passed through untouched — compose your own format ("Page | Project" and similar) at the call site.

For proxy infrastructures getRequestOrigin(req) is a ready-made strict resolver (x-forwarded-proto/x-forwarded-host headers, memoized per request); getRequestProtocol/getRequestHost are exported separately.

Deep serialization

undefined cannot cross the JSON boundary of getServerSideProps props. serializeDeep replaces it with a non-printable sentinel and deserializeDeep revives it (plus ISO date strings). Use the pair — never one of them alone.

Both are built on the exported deepTransform — a structure-preserving deep map over arrays and plain objects (returns the original reference when nothing changed), useful on its own for generic deep value mapping.

Status strategy

determineStatusFromErrors classifies errors structurally: responseInfo.status marks an API error, name: 'ValidationError' marks a validation error, anything else is unexpected (→ 500). Deterministic API priority: 500 > honest 503 > other 5xx as 502 > 404 > min(4xx). API 5xx/404 win over validation errors.

Error pipeline: toErrorObject (full JSON-safe object) → status strategy → sanitizeErrorObject → props. An injected strategy receives the full objects (the step error first, then collected errors such as per-query prefetch failures) and the context, so headers like Retry-After can be derived from responseInfo.headers and set on context.res:

createGetServerSideProps({
  determineStatusFromErrors: (errors, context) => {
    const status = defaultDetermineStatus(errors)
    if (status === 503) context.res.setHeader('Retry-After', retryAfterFrom(errors))
    return status
  },
})

Those full objects are server-only: in production props keep just the client contract (name, message, code, params, responseInfo.status, nested inner/errors). The onCaughtError(error, context, stepName) hook receives the raw thrown value for logging.

Custom strategies usually only tweak the classification and keep the priority ladder as is — compose it from the exported building block instead of copying:

import { determineStatusFromApiStatuses } from 'next-ssp'

// An empty list means "API errors without any known status" (e.g. a network
// failure) — 502, not 500.
determineStatusFromApiStatuses([503, 500]) // 500
determineStatusFromApiStatuses([]) // 502

On /_next/data requests 5xx statuses are remapped to 203: Next.js fails to render 5xx inside data JSON.

Error page helper

import { createGetStaticPropsForErrorPage } from 'next-ssp/react-query'

// The title format is the project's identity — compose it at the call site.
const { getStaticProps } = createGetStaticPropsForErrorPage({
  title: '404 | My Project',
})

License

MIT. Errors are serialized with serialize-error (MIT): the ESM build depends on it, the CommonJS build inlines it together with non-error because both are ESM-only — see THIRD-PARTY-NOTICES.md.