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

@estokad/next

v0.1.6

Published

Next.js adapter: draft mode helpers, EstokadProvider, auto-tagging Estokad.* components.

Readme

@estokad/next

Next.js App Router adapter for Estokad: typed content fetching (re-exports @estokad/sdk), draft-mode route handlers, the visual-edit overlay, and auto-tagging server components.

Install

pnpm add @estokad/next

@estokad/sdk is a dependency — you do not install it separately; createClient and the client types are re-exported.

Compatibility

Peer range: Next ^14.2 || ^15, React ^18.2 || ^19.

| Surface | Next 14.2 / React 18 | Next 15 / React 19 | | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ------------------ | | createClient / .gql, renderRichText, estokadDraftMode, estokadOverlayRoute, signDraftToken / verifyDraftToken | runs + typechecks | runs + typechecks | | async server components EstokadProvider, <Estokad.*> | runs, but does not typecheck under @types/react@18 | runs + typechecks |

draftMode() / cookies() are async on Next 15 and sync on Next 14.2; the package awaits them, which is a no-op passthrough on 14 and required on 15 — so the runtime is correct on both.

The async-component caveat is the well-known @types/react@18 limitation: an async function component types as (props) => Promise<Element>, which React 18's JSX types reject (Promise<Element> is not a valid ReactNode). React 19's types fixed this. On a React-18-pinned project you have three options, in order of preference:

  1. Skip the React components. Visual edit is delivered by the injected overlay script, not the components. Mount estokadOverlayRoute and inject <script type="module" src="/api/estokad/overlay"> yourself in your layout when (await draftMode()).isEnabled — about six lines, fully typed on React 18. EstokadProvider is only a convenience wrapper around exactly that.
  2. Render <Estokad.*> and suppress the call-site type error (// @ts-expect-error async server component — runs on Next 14, typed on React 19). Runtime is correct.
  3. Move that project's @types/react to v19 (types only; React runtime can stay 18) if your toolchain allows it.

The data, draft-mode, overlay, rich-text, and token surfaces — the bulk of the adapter — are fully typed and run on Next 14.2 / React 18 with no workaround.

Type your content

The client is keyed by content type. Declare the types your workspace exposes by augmenting ContentTypes (the interface lives in @estokad/sdk; estokad push will generate this file for you once codegen is wired, until then declare it by hand):

// estokad.d.ts
import type {} from '@estokad/next'

declare module '@estokad/sdk' {
  interface ContentTypes {
    article: {
      title: string
      slug: string
      body: import('@estokad/next').TiptapDoc
      hero: string | null
    }
    siteSettings: { siteName: string }
  }
}

Each accessor cms.<type> is then typed against the matching interface.

Create the client

import { createClient } from '@estokad/next'

const cms = createClient({
  workspace: 'your-workspace-slug',
  apiUrl: 'https://api.estokad.com',
  apiKey: process.env.ESTOKAD_PUBLIC_KEY!, // a delivery/read key
})

Pass draft: true (with a draft-scoped key) to read unpublished content — typically only inside the draft-mode branch.

Fetch + render

// app/articles/[slug]/page.tsx
import { createClient, Estokad } from '@estokad/next'

const cms = createClient({
  workspace: 'your-workspace-slug',
  apiUrl: 'https://api.estokad.com',
  apiKey: process.env.ESTOKAD_PUBLIC_KEY!,
})

export default async function ArticlePage({ params }: { params: { slug: string } }) {
  const article = await cms.article.get({ slug: params.slug })
  if (!article) return null

  // EntryEnvelope<T> flattens the content fields onto the object, plus
  // `id` and an optional `_meta` ({ status, publishedAt, updatedAt, createdAt }).
  return (
    <article>
      <Estokad.Text
        value={article.title}
        entryId={article.id}
        contentType="article"
        fieldName="title"
        as="h1"
      />
      <Estokad.RichText
        value={article.body}
        entryId={article.id}
        contentType="article"
        fieldName="body"
      />
      <Estokad.Image
        src={article.hero}
        alt={article.title}
        entryId={article.id}
        contentType="article"
        fieldName="hero"
      />
    </article>
  )
}

cms.<type> exposes get({ id } | { slug }), list({ first?, offset? }), create(data), update(id, data), publish(id), unpublish(id), delete(id). Raw GraphQL is cms.gql<T>(query, variables?).

The <Estokad.*> components (Text, RichText, Number, Date, Image, Field) emit plain HTML in production. In draft mode they add data-estokad-* attributes the overlay reads for click-to-edit. Every one takes entryId, contentType, fieldName plus its value/src. They're async Server Components — see Compatibility on React 18.

Images with next/image

Estokad serves assets through imgproxy (Bunny.net-fronted): the URL you get is already resized, format-converted, and signed. Don't put Next's optimizer on top of it — it would re-process the image, and it forces you to list the (deployment-specific) CDN host in images.remotePatterns.

Use the supplied loader instead. It returns the Estokad URL untouched, which bypasses remotePatterns entirely (Next only enforces that for its built-in optimizer) and avoids the double image pipeline, while keeping <Image>'s layout / priority / lazy-loading / CLS handling:

import Image from 'next/image'
import { estokadImageLoader } from '@estokad/next'
;<Image loader={estokadImageLoader} src={asset.url} width={1200} height={630} alt="" />

Project-wide, set it as the default loader so you never touch remotePatterns for Estokad assets:

// estokad-image-loader.ts
export { estokadImageLoader as default } from '@estokad/next'
// next.config.js
module.exports = {
  images: { loader: 'custom', loaderFile: './estokad-image-loader.ts' },
}

Choose the size you need when you resolve the asset URL server-side (the variant-url endpoint takes width/quality); the loader does not resize — resizing is Estokad's job, done before the URL exists.

Draft mode

The Studio preview link calls a route on your site that toggles Next.js draft mode:

// app/api/draft/route.ts
import { estokadDraftMode } from '@estokad/next/draft'

export const GET = estokadDraftMode.enable({
  workspace: 'your-workspace-slug',
  previewSecret: process.env.ESTOKAD_PREVIEW_SECRET!,
  // or, recommended in production: jwksUrl: 'https://api.estokad.com/v1/<ws>/.well-known/jwks.json'
})

// disable is itself the handler — do not call it
export const POST = estokadDraftMode.disable

estokadDraftMode.enable(config) returns the route handler; estokadDraftMode.disable is the handler. Token verification is HMAC (previewSecret) or RS256/JWKS (jwksUrl, takes precedence). Low-level codecs signDraftToken / verifyDraftToken / verifyDraftTokenJwks are exported from the package root.

Visual edit overlay

Mount the overlay route once:

// app/api/estokad/overlay/route.ts
export { estokadOverlayRoute as GET } from '@estokad/next/overlay-route'

Then either wrap your layout with EstokadProvider (it injects <script type="module" src="/api/estokad/overlay"> when draft mode is on, and is a zero-overhead pass-through otherwise):

// app/layout.tsx
import { EstokadProvider } from '@estokad/next'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <EstokadProvider>{children}</EstokadProvider>
      </body>
    </html>
  )
}

…or, on React 18 (to avoid the async-component type caveat), inline the same six lines yourself:

// app/layout.tsx
import { draftMode } from 'next/headers'

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const { isEnabled } = await draftMode()
  return (
    <html>
      <body>
        {children}
        {isEnabled ? <script type="module" src="/api/estokad/overlay" async /> : null}
      </body>
    </html>
  )
}

EstokadProvider accepts an optional overlaySrc to override the default /api/estokad/overlay path.

Documentation

Full reference at docs.estokad.com/docs/visual-edit and docs.estokad.com/docs/getting-started.

License

Apache-2.0. Estokad is a trademark of Samarkand Industries OÜ; this license grants no trademark rights (Apache-2.0 §6).