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

@einblick/sdk

v0.7.12

Published

Typed client and code generator for the Einblick public API.

Readme

@einblick/sdk

Typed client and code generator for the Einblick public API.

Framework support:

  • @einblick/sdk: framework-agnostic typed read client, asset URL helpers, and code generator
  • @einblick/sdk/react: React-based in-page editing provider, the <EinblickImage> asset component, and login runtime
  • @einblick/sdk/next: Next.js App Router helpers — cache-tag builder, revalidation route handler, and the <EinblickNextEditBoot> provider

The raw HTTP API is documented as OpenAPI 3.1 at /api/v1/schema and returns JSON:API documents for resource requests. The SDK normalizes that into ergonomic typed objects for application code.

Install

npm install @einblick/sdk

Generate types

Set your API key in the consuming app:

EINBLICK_API_KEY=api_...

Then generate the typed schema file:

npx @einblick/sdk generate --output app/lib/einblick.generated.ts

The SDK defaults to the hosted Einblick API URL.

Use it

import { createGeneratedEinblickClient } from '@/app/lib/einblick.generated'

const einblick = createGeneratedEinblickClient()

const posts = await einblick.request('posts', { limit: 10 })
const post = await einblick.request('posts', { slug: 'das-atelier' })

Regenerate the file whenever the allowed resource fields change.

Modeling editable CMS content

Prefer first-class CMS field types for anything an editor is expected to change from the external site. Short labels belong in string, longer prose in text or markdown, media in image/images/file/files, and repeatable items in collections connected by relations. Avoid putting editor-facing copy, CTA labels, or prose segments inside json; the hosted drawer can edit JSON, but it is hard for non-technical editors and field-level inline editing cannot target individual JSON properties.

When importing or seeding CMS media, store the file in the folder for the CMS record that owns the image/file field, or in a collection folder allowed by that field's existing-file-selection policy. Public API asset resolution is scoped to the resource, field, and allowed folder roots. Reusing a file ID from another record can make the SDK/public API return null for that media field even though the file exists.

React in-page editing

The current in-page editing runtime ships from the React subpath:

import { EinblickEditProvider, EinblickLoginButton } from '@einblick/sdk/react'

This layer is designed for React-based hosts such as Next.js, Remix, or Astro projects that render React islands. The core @einblick/sdk client itself does not depend on React.

The in-page editing UI auto-detects en vs de from the page language and falls back to English. You can override it explicitly:

<EinblickEditProvider siteKey='site_...' locale='de'>
  ...
</EinblickEditProvider>

Authenticated edit mode renders the fixed Einblick control bar by default. The bar is SDK-owned chrome; heavier CMS collection and record editing panels stay hosted on the Einblick app origin. If a site needs the previous launcher or custom chrome, configure it explicitly:

<EinblickEditProvider siteKey='site_...' chrome='badge'>
  ...
</EinblickEditProvider>

The SDK chrome uses Einblick orange by default. For host sites where that color does not stand out enough, pass any valid CSS color as accentColor; it replaces the orange hover outlines, quick edit buttons, badge chrome, and bottom bar. null, an empty string, or an omitted value keeps the default.

<EinblickEditProvider siteKey='site_...' accentColor='#2563eb'>
  ...
</EinblickEditProvider>

EinblickNextEditBoot forwards the same prop and also reads NEXT_PUBLIC_EINBLICK_ACCENT_COLOR when the prop is omitted.

The bar sets --einblick-editor-bottom-offset and data-einblick-editor-bar-active on <html>. By default it also reserves body scroll space so page content is not hidden behind the bar. Host sites with their own fixed bottom UI can opt in to moving that UI with CSS:

.fixed-bottom-widget {
  bottom: calc(
    var(--existing-bottom, 0px) + var(--einblick-editor-bottom-offset, 0px)
  );
}

Set reserveBottomSpace={false} if the host site wants full control over bottom spacing.

The default bar shows a single Collections button. It opens a bottom sheet with all resources available to the external site session: CMS collections are grouped under an expandable CMS item, and supported native resources appear next to that group. Prefer this general entry point over duplicating resource buttons in the host site.

Next.js best practices

This section covers the recommended setup for Next.js App Router projects that read from the Einblick CMS and use the in-page editor. The pattern is the same one used in production by schaum, lucameusburger, janmeusburger, z57, and klara-puermayer.

1. Server-side data fetching with cache tags

Fetch CMS data in Server Components (or Server Actions) and wrap the readers in cache() so that multiple components in the same request share the same work. Pass next.tags on the underlying fetch so they can be revalidated on demand. Use the same tag helper in the revalidation route; Einblick posts the changed resourceSlug for in-page saves and for direct CMS edits when the external site has a revalidation endpoint configured.

// app/lib/einblick-cache.ts
export const EINBLICK_CMS_TAGS = {
  all: 'einblick-cms',
  projects: 'einblick-cms:projects',
  siteSettings: 'einblick-cms:site-settings',
} as const

export function getEinblickCmsTags(resourceSlug?: string | null): string[] {
  switch (resourceSlug) {
    case 'projects':
      return [EINBLICK_CMS_TAGS.all, EINBLICK_CMS_TAGS.projects]
    case 'site-settings':
      return [EINBLICK_CMS_TAGS.all, EINBLICK_CMS_TAGS.siteSettings]
    default:
      return [EINBLICK_CMS_TAGS.all]
  }
}
// app/lib/cms.ts
import 'server-only'
import { cache } from 'react'
import { createGeneratedEinblickClient } from './einblick.generated'
import { getEinblickCmsTags } from './einblick-cache'

const REVALIDATE_SECONDS = 60

const getRevalidatedFetch = (tags: string[]) => ({
  next: { revalidate: REVALIDATE_SECONDS, tags },
})

const client = createGeneratedEinblickClient()

export const getProjects = cache(async () =>
  client.request('projects', {
    limit: 100,
    fetch: getRevalidatedFetch(getEinblickCmsTags('projects')),
  }),
)

Using 'server-only' prevents the module from being imported by client components by accident and leaking your API key. Using the narrowest tag that still covers the resource keeps the invalidation scope small.

2. The revalidate route

Expose a POST route that the edit provider pings after each save. Return fast — the route's only job is to punch a hole in the Next.js cache.

// app/api/einblick/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextResponse } from 'next/server'
import { getEinblickCmsTags } from '@/lib/einblick-cache'

export async function POST(request: Request) {
  let resourceSlug: string | undefined
  try {
    const body = (await request.json()) as { resourceSlug?: string }
    resourceSlug = body.resourceSlug
  } catch {}

  for (const tag of getEinblickCmsTags(resourceSlug)) {
    revalidateTag(tag, { expire: 0 })
  }
  revalidatePath('/', 'layout')

  return NextResponse.json({ ok: true })
}

Use both layers for statically rendered App Router sites: revalidateTag expires the CMS fetch/data cache, and revalidatePath expires the prerendered route cache that Vercel may still serve. For route handlers and webhooks, use { expire: 0 } so the next request blocks for fresh data. profile: "max" is stale-while-revalidate and can make router.refresh() show the old data once after a save.

3. The edit provider with soft refresh

Mount <EinblickEditProvider> once at the root of your app. On save, revalidate the relevant tag then call router.refresh() — this is a Next.js-specific soft re-render that re-runs server components with the fresh data while keeping client state and scroll position. It is not a browser reload.

// app/components/EinblickEditBoot.tsx
'use client'

import { EinblickEditProvider } from '@einblick/sdk/react'
import { useRouter } from 'next/navigation'

export default function EinblickEditBoot({
  children,
}: {
  children: React.ReactNode
}) {
  const siteKey = process.env.NEXT_PUBLIC_EINBLICK_SITE_KEY
  const router = useRouter()

  const handleSave = async (event: { binding: { resourceSlug: string } }) => {
    try {
      await fetch('/api/einblick/revalidate', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ resourceSlug: event.binding.resourceSlug }),
        cache: 'no-store',
      })
    } catch {
      // Keep the optimistic edit state even if revalidation fails.
    }
    router.refresh()
  }

  if (!siteKey) {
    return children
  }

  return (
    <EinblickEditProvider siteKey={siteKey} onSave={handleSave}>
      {children}
    </EinblickEditProvider>
  )
}

Then in the root layout:

// app/layout.tsx
import EinblickEditBoot from '@/components/EinblickEditBoot'

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

Don't skip router.refresh(). Without it, the server cache is clean but the page the user is looking at is still showing the old server-rendered payload. In-page edits to image/images/file/files fields will appear stale (or partially updated) until the next navigation — a full reload is not required, router.refresh() is the correct primitive.

4. Editable bindings on rendered content

Use createEditableBinding in your server-side data mappers to produce record and field bindings that travel alongside the content, and render them with EditableRegion, EditableText, or EditableImage:

// app/projects/[slug]/page.tsx
import { EditableRegion, EditableText } from '@einblick/sdk/react'
import { getProjectBySlug } from '@/lib/data'

export default async function ProjectPage({
  params,
}: {
  params: { slug: string }
}) {
  const project = await getProjectBySlug(params.slug)
  if (!project) return null

  return (
    <article>
      <EditableText as='h1' binding={project.bindings.title}>
        {project.title}
      </EditableText>
      <EditableRegion binding={project.bindings.description}>
        <p>{project.description}</p>
      </EditableRegion>
    </article>
  )
}

For file/image bindings rendered as galleries (arrays), wrap the whole gallery in an EditableRegion bound to the field. Do not rely on the SDK patching individual <img> elements after save — the server returns file IDs, not URLs, and a single DOM selector can't represent an array of assets. The router.refresh() step above is what re-renders the gallery correctly.

For list UIs, wrap the repeated surface with EditableCollection and keep record-level EditableRegion bindings on each item. This gives editors one clear list outline, while each rendered item keeps its normal hover-to-edit state:

import {
  EditableCollection,
  EditableRegion,
  createEditableCollectionBinding,
} from '@einblick/sdk/react'

const projectListBinding = createEditableCollectionBinding({
  resourceSlug: 'projects',
  label: 'Projects',
})

<EditableCollection as='ul' binding={projectListBinding}>
  {projects.map((project) => (
    <EditableRegion as='li' key={project.id} binding={project.bindings.region}>
      ...
    </EditableRegion>
  ))}
</EditableCollection>

If the list should expose an add action in edit mode, set showCreateButton. The SDK renders a hidden button at the end of the collection and reveals it only for authenticated editors; for ul and ol collections the button is wrapped in a final li.

<EditableCollection as='ul' binding={projectListBinding} showCreateButton>
  {projects.map((project) => (
    <EditableRegion as='li' key={project.id} binding={project.bindings.region}>
      ...
    </EditableRegion>
  ))}
</EditableCollection>

Do not attach the list create action to the first item with projects[0]?.bindings.collection. That makes the add button look like it belongs to one record and fails for empty lists. EditableCollection only needs the resource slug, so empty lists can still expose the create action.

5. Environment variables

| Variable | Where | Notes | | ------------------------------- | ----------- | ---------------------------------------------------------------------- | | EINBLICK_API_KEY | server-only | Never expose to the client. Used by createGeneratedEinblickClient(). | | NEXT_PUBLIC_EINBLICK_SITE_KEY | public | Site key for the in-page editor. Public by design; scoped to the site. |

6. Common mistakes

  • Calling the SDK client from a client component. Import it only from server code. 'use server' or 'server-only' at the top of the module prevents accidents.
  • Skipping the cache() wrapper. Without it, multiple components in the same render will each make their own HTTP call.
  • Only invalidating tags on static App Router pages. The CMS fetch may be fresh while Vercel still serves prerendered route output. Pair tags with a route invalidation such as revalidatePath("/", "layout"), or a narrower affected-path map for larger apps.
  • Omitting router.refresh() in onSave. The server cache will be correct, but the user will stare at stale DOM until they navigate.

@einblick/sdk/next — App Router helpers

For Next.js App Router projects the next subpath ships drop-ins that replace the cache-tag, revalidate-route, and edit-boot boilerplate every einblick consumer used to copy by hand:

// lib/einblick-cache.ts
import { createEinblickCmsTags } from '@einblick/sdk/next'

export const einblickTags = createEinblickCmsTags({
  fanOut: { 'site-settings': ['gallery-slides'] },
})
// app/api/einblick/revalidate/route.ts
import { createEinblickRevalidateHandler } from '@einblick/sdk/next'
import { einblickTags } from '@/lib/einblick-cache'

export const POST = createEinblickRevalidateHandler({
  tags: einblickTags,
  secret: process.env.EINBLICK_REVALIDATE_SECRET,
})
// app/layout.tsx
import { EinblickNextEditBoot } from '@einblick/sdk/next'

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

The boot reads NEXT_PUBLIC_EINBLICK_SITE_KEY automatically and calls router.refresh() after saves. Cache invalidation is delivered separately by Einblick's server-to-server webhook. The revalidation route fails closed unless EINBLICK_REVALIDATE_SECRET is set; keep this value server-only and configure the same secret on the external site in Einblick. Never expose it through a NEXT_PUBLIC_* variable. The handler defaults to revalidateTag(tag, { expire: 0 }) and revalidatePath("/", "layout"). For larger apps, pass paths: [] to disable route invalidation, or pass a resource-aware paths function to invalidate only affected pages.

Direct edits made inside Einblick use the same route. In System Settings → API, register the external site's allowed origins, keep the default /api/einblick/revalidate endpoint unless the host uses another path, and set the same revalidation secret there if the route requires one.

Asset helpers

The core package ships ergonomic image-CDN helpers so consumers don't hand-roll URL composition. Common cases:

import {
  getEinblickAssetCdnUrl,
  getEinblickAssetOgUrl,
  getEinblickAssetPreviewUrl,
  getEinblickAssetAlt,
  isEinblickVideoAsset,
} from '@einblick/sdk'

const heroSrc = getEinblickAssetCdnUrl(asset, {
  width: 1800,
  format: 'webp',
  quality: 84,
})
const ogSrc = getEinblickAssetOgUrl(asset) // 1200×630, cover, jpeg
const preview = getEinblickAssetPreviewUrl(asset) // long-edge 1800, webp
const alt = getEinblickAssetAlt(asset, 'Fallback alt text')

For React consumers, drop-in <EinblickImage> from @einblick/sdk/react renders the asset with sensible defaults:

import { EinblickImage } from '@einblick/sdk/react'
;<EinblickImage
  asset={tour.image_banner}
  alt={getEinblickAssetAlt(tour.image_banner, tour.name)}
  transform={{ width: 1600, format: 'webp', quality: 82 }}
/>

The component returns null when the asset is missing — no broken-image placeholder. Videos are passed through untransformed.

Raw API

  • GET /api/v1/schema returns the key-scoped OpenAPI 3.1 document
  • GET /api/v1 lists accessible resources
  • GET /api/v1/{resource} lists records for a resource
  • GET /api/v1/{resource}/{recordSlug} fetches a single record

Authenticate with:

Authorization: Bearer api_...