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

devigo

v0.6.1

Published

Official JavaScript SDK for Devigo Studio. The whitelabel headless CMS built for every site.

Readme

Devigo JavaScript SDK

Official JavaScript SDK for the Devigo CMS. TypeScript-first, zero dependencies, ESM only.

Installation

npm install devigo

Requires Node.js 18 or later (uses the native fetch API).

Quick Start

import { createClient } from 'devigo'

const devigo = createClient({
  token: process.env.DEVIGO_API_TOKEN,
  baseUrl: process.env.DEVIGO_API_DOMAIN,   // https://api.devigo.studio
  siteId: process.env.DEVIGO_SITE_ID,
  environment: process.env.ENVIRONMENT,     // 'local' | 'staging' | 'live'
})

const home = await devigo.pages.get('homepage')
const posts = await devigo.posts('blog').list({ limit: 10 })
const header = await devigo.menus.get('header')

Environments

environment is required — there is no server-side default, and the API rejects values it does not recognise. Devigo sites use local, staging and live.

Caching

GET responses are cached per client for its lifetime, so a static build that renders 50 pages fetches each menu once rather than 50 times. Pass cache: false for a long-lived SSR client that must always read fresh content, or call devigo.clearCache() to drop what's cached.

Languages

A site can be written in more than one language. Each is a code — en, pl, pt-BR — and a translated page is a page of its own, with its own slug.

await devigo.locales()                        // → { default_locale: 'en', locales: ['en', 'pl'] }

const pl = devigo.locale('pl')                // the same client, reading Polish
await pl.pages.get('zajezdnie')               // → Page, with `locale` and `translations`
await pl.menus.items('header')                // links already under /pl/

page.translations and entry.translations list every language the page is in, itself included, as { locale, slug } — what hreflang links and a language switcher need. A language with no menu of its own gets the default language's, with its links pointed at translated pages. Settings in a language fall back, field by field, to the default language's.

The convention everywhere is the default language at the root and every other under its code; localePath('/depots', 'pl', 'en') gives /pl/depots.

Astro

// astro.config.mjs
import { defineConfig } from 'astro/config'
import { devigoIntegration } from 'devigo/astro'

export default defineConfig({
  integrations: [devigoIntegration({ postTypes: ['blog'] })],
})

Credentials are read from DEVIGO_API_TOKEN, DEVIGO_API_DOMAIN, DEVIGO_SITE_ID and ENVIRONMENT in your .env, so no loadEnv wiring is needed. Pass them explicitly to override. In dev the integration polls for content changes and restarts the server so a CMS edit shows up on refresh.

Languages in Astro

The integration reads the site's languages when the dev server starts and at build, and routes every one: pages at /pl/{slug}, posts at /pl/{type}/{slug}, listings at /pl/{type}. Only what has been translated is routed — an untranslated page has no /pl/ address. Layouts receive the page's locale alongside page or entry, and devigo:options exports defaultLocale and locales. A site with one language gets exactly the routes it always had. Adding a language needs a dev server restart, as it changes which routes exist. Pass locales: false to route the default only.

Favicon

The Favicon set in Devigo Studio is downloaded at dev start and at build, and written into your project's public/:

| Setting | Written | Linked in <head> | | --- | --- | --- | | an .svg | favicon.svg, favicon.ico | both | | an .ico | favicon.ico | favicon.ico | | any other image | favicon.ico (converted) | favicon.ico | | nothing set | — | whatever your project ships |

Real files rather than a CDN link, so /favicon.ico is your icon for everything that asks for it by name — browsers request it whether or not a <link> says to — and so the last good icon survives a build with no network. A raster setting never writes favicon.svg, because a file that promises vector would then hold something else and browsers prefer it.

In dev the icon follows the setting without a restart — whether it is changed in Studio or in the on-page editor's Styles tab.

Render the tags with the component — it is handed what the integration wrote, so nothing has to re-derive which files exist:

---
import Favicon from 'devigo/astro/components/Favicon.astro';
---
<head>
  <Favicon />
</head>

The files replaced are set aside under .astro/, and put back if the setting is later cleared. Converting to .ico uses sharp, which Astro's image pipeline already installs; without it the .ico is left alone and a warning says so. Pass favicon: false to manage the files yourself.

On-page editing (experimental)

devigoIntegration({ editor: true })

Puts an editing bar on the site in astro dev. Turn editing on and every CMS block outlines as you pass over it; click one and its text becomes editable where it sits — click into a heading and type, exactly as you would in a document. Everything that isn't free text — toggles, radios, selects, images — opens in a panel alongside, because "does this button open in a new tab" is a choice, not something you type. Save writes to the CMS and the page reloads with the change. Menu items are editable too, by label.

Text is matched to the page by its value, so a component that renders a field verbatim gets inline editing for free, with no changes to it. One that transforms, truncates or puts the value in an attribute simply keeps that field in the panel; the bar says how many of each.

Repeaters list their items collapsed, one line each, and clicking a line opens that item on its own screen with a breadcrumb back. While a repeater is open the page shows a dashed + below where the next item would render. Clicking it adds the item and saves, so the empty one appears on the page immediately and you land in it ready to type — an item that only existed in the panel would leave you wondering whether the click had done anything.

Settings is a button on the bar rather than something you click on the page, since a logo or a set of social links has no single element to point at. It opens the same panel over the site's own settings fields.

Rich text fields get the same toolbar Devigo Studio gives them — headings, bold, italic, lists, links, images from your media library — over a </> button for editing the HTML directly. Pasted content is reduced to the same tags the server keeps, so formatting from Google Docs or Word survives and everything else is dropped before it can be saved.

The panel is built from each component's own field definitions, so it works on your components with no changes to them. Needs a second credential in your .env —

DEVIGO_EDIT_TOKEN=dv_edit_…

— generated per site under Integrations → On-Page Editing in Devigo Studio. The build token will not work: it cannot write, and it is deliberately rejected, so a token that already sits in every checkout and CI job never becomes the one that can rewrite a site.

The token stays on the dev server. The browser talks to /__devigo, the dev server talks to Studio, and nothing that can change content is ever readable from the page. Requests from another origin are refused, and the site and environment are pinned from your own config rather than taken from the request — so edits land in whatever ENVIRONMENT this project reads, and editing what you are looking at changes exactly that and nothing else.

None of it exists in a build. astro build never registers the proxy, and the edit token is not read.

curl http://localhost:4321/__devigo/status
# {"editing":true,"siteId":"…","environment":"local","baseUrl":"…"}

Being experimental, it has edges worth knowing:

| | | |---|---| | Blocks are found by URL | The page is looked up by the path it is served at, which is what the injected page route does. A project routing CMS pages itself may not line up, and the bar says so rather than guessing. | | The panel covers the right of the page | Close it (Esc) to reach a block underneath. | | Some field types stay in Studio | Flexible content, conditional fields and form pickers carry structure the panel cannot present honestly. | | Tables are preserved, not built | A rich text field keeps any table already in it, but building one is a six-tool job that wants a full-width screen. | | Menu items edit their label | Where a link points is not recoverable from the rendered page — only the resolved href is — so that stays in Studio. |

API Reference

Pages

| Method | Returns | |---|---| | pages.list() | PageSummary[] — no sections or SEO fields | | pages.get(slug) | Page — full page with sections | | pages.all(options?) | Page[] — every page, fully hydrated | | pages.homepage() | Page \| null — the page flagged is_homepage |

pages.all() is what getStaticPaths usually wants. The detail endpoint does not return name, so all() carries it over from the list response — without it, a page with no title_tag has nothing to fall back on for its title.

const pages = await devigo.pages.all({ exclude: ['404-page-not-found'] })

SEO fields are flat on the page: title_tag and meta_description.

Posts

devigo.posts('blog').list({ limit: 10, offset: 0 })  // → { data, total }
devigo.posts('blog').get('my-post')                  // → PostEntry
devigo.posts('blog').all()                           // → PostEntry[], all pages

Entry fields are on field_values; title_tag, meta_description and schema sit alongside it.

Menus

devigo.menus.list()          // → MenuSummary[]
devigo.menus.get('header')   // → Menu, with a nested `items` tree

Settings

const settings = await devigo.settings.get()   // → { header_logo, company_name, ... }

Forms

const form = await devigo.forms.get('basic-enquiry-form')  // uuid or slug
await devigo.forms.submit(form, { email: '[email protected]', message: 'Hello!' })

forms.list() returns every form's uuid, name and slug.

Submission has three rules the SDK handles for you: values are wrapped in a fields object, select fields are coerced to arrays, and untouched honeypot fields are dropped. Use buildSubmitPayload(form, values) if you are posting by hand.

The submit endpoint is public and CORS-open, so passing a Form you already fetched lets you submit from the browser with no credentials. Passing a uuid or slug instead costs an authenticated round-trip to read the definition first, which means exposing your API token to the client.

Analytics

devigo.analytics.consent(true)   // persists to localStorage
devigo.analytics.autoTrack()     // tracks load + client-side navigation

Consent is stored per site under devigo_consent_{siteId}. Tracking no-ops until consent is granted, and on the server.

The track endpoint is authenticated, so browser tracking ships your API token to the client — where it also grants read access to draft content. Weigh that deliberately.

Helpers

import { internalHref, componentHandle, resolveComponent, flattenComponents } from 'devigo'

| Helper | Purpose | |---|---| | internalHref(url) | Normalise a CMS URL to its canonical trailing-slash form. CMS links arrive without one and would otherwise 301 on click. | | componentHandle(name) | "Content & Image" → "ContentAndImage" | | resolveComponent(registry, name) | Look a CMS component up in a registry of your own components | | flattenComponents(page) | Flatten the section → row → column → component tree in document order |

Components are identified by the display name typed into the CMS, which is editable content rather than a stable id. Match through resolveComponent() rather than on the raw string, so renames behave consistently everywhere.

import Hero from './Hero.astro'
import ContentAndImage from './ContentAndImage.astro'

const Renderer = resolveComponent({ Hero, ContentAndImage }, block.component)

Error Handling

import { createClient, DevigoNotFoundError, DevigoAuthError } from 'devigo'

try {
  const page = await devigo.pages.get('missing-page')
} catch (err) {
  if (err instanceof DevigoNotFoundError) { /* 404 */ }
  if (err instanceof DevigoAuthError) { /* 401/403 */ }
}

| Error Class | Status | Description | |---|---|---| | DevigoApiError | any | Base class for all API errors | | DevigoNotFoundError | 404 | Resource not found | | DevigoAuthError | 401/403 | Invalid or expired token |

Migrating from 0.1.x

0.2.0 corrects the types to match what the API actually returns. Most sites need no code changes — the runtime shapes were always these — but TypeScript will now flag code written against the old declarations.

| Change | Action | |---|---| | environment is required | Set it; production was never valid | | posts(type).get() returned undefined fields | Now returns the entry — previously broken, so any workaround can go | | forms.submit() posted to a 404 | Now works; takes a Form or an id | | Page.meta.title / .description | Use page.title_tag / page.meta_description | | Component.type / .field_values | Use component.component / .props | | Section.id, Row.id, Column.id, Page.id | Removed — the API returns no ids | | Form.id: number | Form.uuid: string | | media resource | Removed — the endpoint does not exist | | globals resource | Replaced by settings |

pages.list() now returns PageSummary[] rather than Page[], which is what it always returned at runtime. Use pages.all() where you need full pages.

License

UNLICENSED