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

@stack201/cms-sdk

v0.7.0

Published

SDK do Stack CMS: o cliente da Delivery API e o que os sites precisam para SEO, GEO e AEO

Readme

stack-cms-sdk

SDK do Stack CMS (@stack201/cms-sdk): o cliente da Delivery API e o que os sites precisam para SEO, GEO e AEO, com entradas para React, Next, Astro, Vue e Svelte.

  • API: mchlima/stack-cms-api (https://api.rasku.app; até 01/11/2026, https://cms-api.stack201.com.br, que sai do ar).
  • Como desenvolver e as regras do repositório: AGENTS.md. As regras do SDK: docs/spec.md.

Uso

import { createCmsClient, CmsNotFoundError } from '@stack201/cms-sdk';

const cms = createCmsClient({ apiKey: process.env.CMS_DELIVERY_KEY!, locale: 'pt-BR' });

const { items, next } = await cms.posts.list({ category: 'seo' });
try {
  const post = await cms.posts.get('core-web-vitals');
  // post.html, post.hast, post.seo, post.jsonLd, post.alternates…
} catch (error) {
  if (error instanceof CmsNotFoundError) {
    // 404 do site
  }
}

A chave fica no servidor, numa variável de ambiente.

Os avisos do CMS (webhooks) são conferidos assim:

import { verifyCmsWebhook } from '@stack201/cms-sdk/webhooks';

export async function POST(request: Request) {
  const event = await verifyCmsWebhook(request, process.env.CMS_WEBHOOK_SECRET!);
  // event.type, event.paths: as páginas a revalidar
  return new Response(null, { status: 204 });
}

O conteúdo, com os componentes do site

O CMS entrega cada post como html e hast. O <CmsContent> desenha o hast trocando cada bloco (os de todo space, como note e answer, e os do seu space, como cta) e cada elemento (a, img) por um componente do site. A interface é a mesma em todos os frameworks: hast, blocks, components e, opcional, customBlocks (o space.customBlocks, que tipa os atributos e avisa em desenvolvimento dos blocos sem componente). Um bloco do space sem componente nunca quebra a página: o conteúdo dele fica e o bloco sem conteúdo some.

React e Next (também em Server Components):

import { CmsContent, CmsHead, type BlockProps } from '@stack201/cms-sdk/react';

const Cta = ({ attributes }: BlockProps) => <a href={attributes.href}>{attributes.label}</a>;

<CmsHead seo={post.seo} alternates={post.alternates} jsonLd={post.jsonLd} type="article" />
<CmsContent hast={post.hast} components={{ a: Link, img: Image }} blocks={{ note: Callout, cta: Cta }} />

Vue e Nuxt (o bloco recebe name, attributes, id e node como props e o conteúdo no slot):

<script setup lang="ts">
import { CmsContent } from '@stack201/cms-sdk/vue';
import Cta from '~/components/Cta.vue';
import { NuxtLink } from '#components';
</script>

<template>
  <CmsContent :hast="post.hast" :blocks="{ cta: Cta }" :components="{ a: NuxtLink }" />
</template>

Astro (componentes do Astro ou de qualquer framework do site; o conteúdo chega no <slot />):

---
import CmsContent from '@stack201/cms-sdk/astro/CmsContent.astro';
import Cta from '../components/Cta.astro';
import Callout from '../components/Callout.tsx';
---

<CmsContent hast={post.hast} blocks={{ cta: Cta, note: Callout }} components={{ a: Link }} />

Svelte 5 e SvelteKit (o conteúdo chega em children):

<script lang="ts">
  import { CmsContent } from '@stack201/cms-sdk/svelte';
  import Cta from '$lib/Cta.svelte';
  let { data } = $props();
</script>

<CmsContent hast={data.post.hast} blocks={{ cta: Cta }} />

Outro framework ou HTML puro: cada bloco vira o HTML que a função devolve (escape o que vier de fora); o resto sai como o HTML do CMS.

import { renderHtml } from '@stack201/cms-sdk';

const html = renderHtml(post.hast, {
  customBlocks: space.customBlocks,
  blocks: {
    cta: ({ attributes }) =>
      `<a class="cta" href="${escape(attributes.href)}">${escape(attributes.label)}</a>`,
    destaque: (block, children) => `<section class="destaque">${children}</section>`,
  },
});

blocksOf(hast, customBlocks) lista os blocos de um post, com os atributos tipados (number vira número, boolean vira booleano), e missingBlocks(space.customBlocks, blocks) diz quais blocos do space ainda não têm componente no site.

Os tipos dos atributos de cada bloco do space saem da Delivery API:

CMS_DELIVERY_KEY=… npx cms-sdk blocks-types --out src/cms-blocks.ts
import type { CmsBlockAttributes } from './cms-blocks';

const Cta = ({ attributes }: BlockProps<CmsBlockAttributes['cta']>) => (
  <a href={attributes.href} data-variant={attributes.variant}>
    {attributes.label}
  </a>
);

blockTypes(customBlocks) dá o mesmo texto, e AttributesOf<typeof definicao> tipa uma definição as const.

O <head>

Com React, o <CmsHead> do exemplo acima. Sem React, headTags(page) e headHtml(page) dão as mesmas tags do <head>, e jsonLdString(data) dá o JSON-LD escapado.

Next e Astro

Com Next (App Router), sem depender do pacote next:

// middleware.ts: redirecionamentos, sitemaps, feeds, llms.txt, robots.txt e o .md de cada post
import { NextResponse, type NextRequest } from 'next/server';
import { createCmsMiddleware } from '@stack201/cms-sdk/next';

const cmsMiddleware = createCmsMiddleware(cms);
export async function middleware(request: NextRequest) {
  return (await cmsMiddleware(request)) ?? NextResponse.next();
}

// app/api/cms/route.ts: o webhook do CMS revalida só as páginas afetadas
import { revalidatePath } from 'next/cache';
import { createRevalidateRoute } from '@stack201/cms-sdk/next';

export const POST = createRevalidateRoute({
  secret: process.env.CMS_WEBHOOK_SECRET!,
  revalidatePath,
});

cmsMetadata(post) dá o Metadata do generateMetadata, cmsRedirects(cms) dá os redirecionamentos do next.config, e cmsFileRoute(cms) serve os arquivos como rotas, para quem não quer o middleware.

Com Astro (site estático), as coleções e os arquivos entram no build:

// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { cmsLoader } from '@stack201/cms-sdk/astro';
import type { Post } from '@stack201/cms-sdk';

const posts = defineCollection({
  loader: cmsLoader(cms, { collection: 'posts' }),
  schema: z.custom<Post>(),
});

// src/pages/sitemap.xml.ts (e llms.txt.ts, robots.txt.ts…)
export const GET = cmsFileEndpoint(cms);

// src/pages/sitemaps/[file].ts (e feeds/[file].ts)
export const getStaticPaths = () => cmsFilePaths(cms, 'sitemaps');
export const GET = cmsFileEndpoint(cms);

render(entry) dá o HTML do post e os títulos, cmsMarkdownEndpoint(cms) serve o .md de cada post, cmsPostSlugs(cms) alimenta o getStaticPaths e cmsAstroRedirects(cms) dá os redirects do astro.config. Para publicar no CMS e o site se refazer, crie um Deploy Hook no projeto da Vercel e cadastre o endereço dele como webhook do space.

Categorias em vários níveis

Com o modelo de endereço {path} (/guias/categoria/sites/desempenho), cada categoria tem slugPath (sites/desempenho), também nas mães, nas filhas e nas categorias de cada post. A página da categoria fica numa rota de vários níveis, e categories.get aceita o caminho inteiro ou as partes dele (cada / vai como %2F):

// Next: app/guias/categoria/[...path]/page.tsx
export default async function Page({ params }: { params: Promise<{ path: string[] }> }) {
  const category = await cms.categories.get((await params).path);
  // category.parents, category.children (cada um com slugPath e url), category.posts…
}
---
// Astro: src/pages/guias/categoria/[...path].astro
import { cmsCategoryPaths } from '@stack201/cms-sdk/astro';

export const getStaticPaths = () => cmsCategoryPaths(cms);
const category = await cms.categories.get(Astro.params.path!);
---

Com o modelo {slug}, o slug sozinho também serve, porque ali ele é único. Com {path}, use sempre o caminho: o slug sozinho não acha uma subcategoria. A mesma rota [...path] serve os dois modelos: cmsCategoryPaths dá o que o endereço tem depois do prefixo (o caminho com {path}, o slug com {slug}). No cmsLoader do Astro, a categoria tem o slugPath como id. Mudar o slug ou a mãe no CMS gera os 301 sozinho, e eles chegam pelos redirecionamentos de sempre.

O feed da categoria fica no endereço dela: /guias/categoria/sites/desempenho/rss.xml (e atom.xml, feed.json). No Next, o middleware já serve. No Astro:

// src/pages/guias/categoria/[...path]/rss.xml.ts (e atom.xml.ts, feed.json.ts)
export const getStaticPaths = () => cmsCategoryPaths(cms);
export const GET = cmsFileEndpoint(cms);

O redirecionamento de uma categoria leva junto os feeds dela (…/antiga/rss.xml → …/nova/rss.xml). Na API, o arquivo é feeds/category-sites.desempenho.rss, com . no lugar de /.

Menus

const menus = await cms.menus.list(); // [{ key, name, description }]
const menu = await cms.menus.get('principal', { locale: 'pt-BR' });
// menu.items: [{ label, kind, href, url, external, children }], já sem os itens ocultos

No Astro, cmsMenu(cms, 'principal', { locale }) lê o menu no build, ou cmsLoader(cms, { collection: 'menus' }) traz todos como coleção (getEntry('menus', 'principal')).

O aviso menu.updated chega quando o menu muda e também quando uma página dele muda de nome ou de endereço, sai do ar ou ganha ou perde uma subcategoria (menuChangeOf(event).reason é 'pages'). Ele não traz páginas. No Next, createRevalidateRoute revalida o site todo (revalidatePath('/', 'layout')) por padrão, porque o menu costuma estar no layout; onMenuUpdated troca isso pelo que o site disser:

import { revalidatePath, revalidateTag } from 'next/cache';

export const POST = createRevalidateRoute({
  secret: process.env.CMS_WEBHOOK_SECRET!,
  revalidatePath,
  // A busca do menu marcada com cms.menus.get(key, {}, { init: { next: { tags: [`menu:${key}`] } } })
  onMenuUpdated: (menu) => {
    revalidateTag(`menu:${menu.key}`, 'max');
    if (menu.previousKey) revalidateTag(`menu:${menu.previousKey}`, 'max');
  },
});

Fora do Next, menuChangeOf(event) (de @stack201/cms-sdk/webhooks) dá key, previousKey e deleted do aviso.