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

@elgavio/sdk

v0.5.0

Published

The Elgavio delivery API client, with generated content types and the elgavio CLI

Readme

@elgavio/sdk

The client for Elgavio's delivery API: a project's published content, read with a delivery token. It runs anywhere with fetch (Node 20+, browsers, Bun, Deno, edge runtimes, React Native, Electron) and has no dependencies. It also ships the elgavio CLI, which generates your project's content types and fails CI when they drift from the live schema.

npm install @elgavio/sdk

Reading content

import { createClient } from '@elgavio/sdk';

const elgavio = createClient({ token: process.env.ELGAVIO_TOKEN });

const { data, meta } = await elgavio.entries('posts', {
  filter: { tags: { contains: 'news' }, publishedAt: { gte: '2026-01-01' } },
  sort: ['-publishedAt', 'title'],
  fields: ['excerpt', 'cover'],
  limit: 3,
});

A collection's Try it page in Elgavio builds these calls for you.

| method | reads | | ----------------------- | --------------------------------------------------------------------------- | | entries(key, query) | one page of a collection: { data, meta }, at most 100 a page | | all(key, query) | every entry the query matches, page after page | | entry(id, query) | one entry by id, in whichever collection it is | | single(key, query) | a single-entry collection's entry | | tree(key, query) | a hierarchical collection, nested, for menus | | resolve(path, query) | what a public path serves: an entry, a redirect, or null | | paths(query) | one page of every published path, for static generation and sitemaps | | redirects(query) | one page of the redirects | | changes(since) | the change feed: what changed since your last next | | verifyPreview(code) | a preview code from Elgavio's Preview button: see Preview | | strings(query) | the project's strings in one language: see Strings | | project(), schema() | the project's languages and collections; its fields, for the type generator |

  • filter: { key: value } is equality; { key: { op: value } } takes eq, ne, in (an array), lt, lte, gt, gte, contains and exists, as the field allows. Several filters must all match.
  • sort: keys, each descending with a leading -.
  • fields: the top-level keys to serve, blocks among them. id, collection, locale, title, slug, path, publishedAt and alternates always come.
  • locale: per call, or for every call with createClient({ locale }). The project's default language otherwise.
  • richText: 'html' (the default), 'json' (the document, for native and component renderers) or 'text'.

Caching

The client revalidates every response with its ETag, so unchanged content costs a 304 and no body. Responses are kept in memory by default; pass your own adapter to keep them across launches (AsyncStorage, a file), or cache: false to turn it off.

const elgavio = createClient({
  token,
  cache: {
    get: async (key) => JSON.parse((await AsyncStorage.getItem(key)) ?? 'null') ?? undefined,
    set: (key, value) => AsyncStorage.setItem(key, JSON.stringify(value)),
  },
});

Errors

A refused request throws an ElgavioError with the API's status, code, reason and fieldErrors. A filter on a field that isn't filterable, for instance, is a FIELD_ERRORS reason whose field error names filter.<key>.

import { isElgavioError } from '@elgavio/sdk';

try {
  await elgavio.single('settings');
} catch (error) {
  if (isElgavioError(error, 'DELIVERY_TOKEN_INVALID')) {
    // the token is unknown, revoked or expired
  }
  throw error;
}

Strings

The texts an app keeps outside its content (button labels, messages, the words of a screen) are the project's strings: keys in folders, a text per language. strings() reads them in one language.

const strings = await elgavio.strings({ locale: 'de' });
strings.Settings.Profile.Title; // 'Profil'
  • format: 'nested' (the default) is an object per folder, in the order the catalog has; 'flat' is one object of dotted keys ('Settings.Profile.Title').
  • missing: what a string with no text in the language becomes. 'empty' (the default) is "", so every key is always there. 'omit' leaves the key out. 'default' serves the project's default language's text, and "" where that is empty too.
  • prefix: a folder's key, or an array of them, to serve only what is in them. Keys stay whole (Settings.Profile.Title is that whatever you asked for), so two responses merge and a key reads the same wherever it was loaded. A key that is not a folder is refused.
  • locale: per call, or for every call with createClient({ locale }).

Pick missing for the library that reads the catalog. react-intl treats "" as a missing message and falls back to the default locale's, logging an error each time: 'omit' lets its own fallback run quietly, or 'default' gives it the text. i18next renders an empty string as it is, unless returnEmptyString: false, so 'omit' or 'default' there too. 'empty' suits an app that must see every key.

An integer-like key ("404") sorts first in any JavaScript object, whatever order the API sent. That is how JavaScript objects work, and no response can keep such a key's place in a nested answer.

A folder is never left out of a nested answer: one with no strings, or whose strings 'omit' left out, is {}.

Strings in the app

For an app that bundles its strings, elgavio strings writes a file per language. Say where in elgavio.config.json:

{
  "strings": {
    "output": "src/locales/{locale}.json",
    "format": "nested",
    "missing": "omit"
  }
}

{locale} is each language's code, and format and missing are strings()'s (optional). The files are written as the API serves them, and not checked by elgavio check: a translator's edit should not fail a build. Run the command before the build, or on a schedule, to pick it up.

Typed content

npx elgavio types    # writes elgavio-types.ts from the live schema
npx elgavio check    # exits 1, with a diff, when the committed file no longer matches
import { createClient } from '@elgavio/sdk';
import type { ElgavioContent, PostsEntry } from './elgavio-types';

const elgavio = createClient<ElgavioContent>({ token: process.env.ELGAVIO_TOKEN });

Collection keys, fields, filters and sort keys are then checked against your schema: single() takes only single-entry collections, tree() only hierarchical ones, fields narrows the result, and richText: 'json' types every rich-text field as the document. Commit the file and run elgavio check in CI, so a renamed field fails the build before it breaks your app.

The strings are typed too, by their keys and never their texts, so a translator's edit changes nothing here. strings() answers with the nested shape or the flat keys, narrowed by prefix and made partial by missing: 'omit', and prefix takes only folders. The file also exports the keys for your own code:

import type { StringKey, StringFolder } from './elgavio-types';

const t = (key: StringKey) => messages[key];

With react-intl or i18next, use them to check your calls against the catalog. A key renamed or deleted in Elgavio then fails elgavio check, and a call to it fails your build.

// i18next: the keys of the nested catalog
import type { StringTree } from './elgavio-types';

declare module 'i18next' {
  interface CustomTypeOptions {
    resources: { translation: StringTree };
  }
}

// react-intl: message ids
import type { StringKey } from './elgavio-types';
declare global {
  namespace FormatjsIntl {
    interface Message {
      ids: StringKey;
    }
  }
}

A types file written by an earlier version has no strings: strings() then takes any key and answers untyped, until you run elgavio types again.

The CLI reads ELGAVIO_TOKEN (and ELGAVIO_API_URL, if you aren't on https://api.elgavio.com) from the environment, then .env.local, then .env. In CI, set it as a secret. A --token flag exists, but it stays in your shell history. elgavio.config.json can set where the file goes:

{ "output": "src/elgavio-types.ts" }

The check compares a hash of what the file says, so your formatter may reformat it.

Next.js

@elgavio/sdk/next turns every published path into generateStaticParams for a catch-all route.

// app/blog/[...slug]/page.tsx
import { staticParams } from '@elgavio/sdk/next';

export const generateStaticParams = () =>
  staticParams(elgavio, { collection: 'posts', basePath: '/blog' });

localizedStaticParams does the same in every language, as { locale, slug }, for a route under [locale]. An entry at the route's own path has an empty slug, which only an optional catch-all, [[...slug]], takes.

Webhooks

A project's webhooks POST what changed, signed in Elgavio-Signature with the webhook's signing secret. verifyWebhook checks the signature against the raw body and returns the payload: the changes as /changes lists them, at most 100, with truncated when there were more. It refuses a signature older than five minutes (tolerance, in seconds).

import { verifyWebhook } from '@elgavio/sdk';

const payload = await verifyWebhook(rawBody, request.headers.get('elgavio-signature'), {
  secret: process.env.ELGAVIO_WEBHOOK_SECRET!,
});
for (const change of payload.changes) {
  // Refetch change.id, purge change.path, reindex…
}

A refused call throws ElgavioWebhookError, whose reason is signatureMissing, signatureInvalid or signatureExpired. A build hook from Vercel, Netlify or Cloudflare Pages needs none of this: it ignores the body.

In Next.js, revalidateFromWebhook is the whole route handler. It revalidates each changed path (and a moved entry's old one), or the whole app for a change to the content types, media or strings, or a truncated call, and answers 401 to an unsigned one.

// app/api/elgavio/route.ts
import { revalidatePath } from 'next/cache';
import { revalidateFromWebhook } from '@elgavio/sdk/next';

export const POST = revalidateFromWebhook({
  secret: process.env.ELGAVIO_WEBHOOK_SECRET!,
  revalidatePath,
  // Optional: where a change shows in your routes.
  pathsOf: (change) => (change.path === null ? [] : [`/${change.locale}${change.path}`]),
});

Preview

A collection's preview URL, set in its settings, is where Elgavio's Preview button opens a draft: a route of your app (https://your.app/api/preview) or a link into it (yourapp://preview?id={id}). {id}, {collection}, {path} and {locale} are filled in, and a code valid five minutes is added as elgavioPreview.

Your app verifies the code, then reads in draft mode: each entry as its draft where that draft would pass publishing, else as published. Draft responses are never cached.

  • With a server: a secret token with the preview scope reads drafts. createClient({ token, draft: true }).
  • Without one: a public token never reads drafts on its own. verifyPreview(code) returns a previewSession, valid an hour, that lets it: createClient({ token, draft: true, previewSession: previewSession.value }).

In Next.js, draftModeRoute is the whole preview route: it verifies the code, turns on draft mode and redirects to the entry.

// app/api/preview/route.ts
import { draftMode } from 'next/headers';
import { createClient } from '@elgavio/sdk';
import { draftModeRoute } from '@elgavio/sdk/next';

const elgavio = createClient({ token: process.env.ELGAVIO_PREVIEW_TOKEN });

export const GET = draftModeRoute({ client: elgavio, draftMode });

Then read with draft: (await draftMode()).isEnabled, and turn it off with (await draftMode()).disable() in a route of your own.

Versions

Until its first stable release the SDK is 0.x. From then on its major version follows the delivery API's: /delivery/v1 is 1.x.

License

MIT