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

@intelex-ia/sdk

v0.2.0

Published

SDK pra consumir a Content API da IntelexIA em sites headless (Next.js, Astro, etc.). Multiplataforma Fase 2b.

Readme

@intelex-ia/sdk

SDK pra consumir a Content API IntelexIA em sites headless (Next.js, Astro etc.). Multiplataforma Fase 2b.

O que você ganha: getArticles / getArticleBySlug tipados, mergePosts() pra unir conteúdo antigo + novo do painel, <ArticleJsonLd> pra injetar schema JSON-LD, e articleToMetadata pra plugar no generateMetadata do Next.js App Router. Zero dependências (usa fetch global).

Instalação rápida (recomendado)

Se você está começando agora, use o CLI — ele detecta a estrutura do seu projeto e configura tudo automaticamente:

npx @intelex-ia/cli init

Cria os arquivos no lugar certo (App Router ou Pages Router, com ou sem src/), configura .env.local, gera um INTELEXIA.md completo na raiz e instala o SDK pra você.

Instalação manual

npm install @intelex-ia/sdk
# ou pnpm add @intelex-ia/sdk / yarn add @intelex-ia/sdk

Configuração

  1. No painel da IntelexIA, cadastre o site como "Headless (código)" (vai pedir domain + URL e secret de revalidate).
  2. Abra o site na lista → seção "Tokens de API (Content API)"Gerar token. Você recebe itx_… (mostrado uma única vez).
  3. Coloque em .env.local:
INTELEXIA_API_TOKEN=itx_seu_token_aqui
INTELEXIA_API_URL=https://api.innovarticles.intelexia.com.br  # opcional, é o default
INTELEXIA_REVALIDATE_SECRET=cole-o-mesmo-segredo-do-painel

Três modos de integração

Cada cliente tem uma situação diferente. Escolha o modo que faz sentido:

| Modo | Quando usar | O que muda no /blog | |---|---|---| | 🔀 Additive (recomendado) | Cliente já tem posts antigos e quer adicionar os do painel sem perder os antigos | mergePosts() une os dois feeds, dedupa por slug, ordena por data | | 🔁 Replace | Cliente migrou tudo pro painel — só os novos contam | Substitui getAllPosts() por client.getArticles() | | 🆕 Isolated | Cliente quer manter /blog antigo intacto e criar rota nova só pra IA | Cria /blog/ia separado, blog antigo zero alterado |


Modo 1: Additive (cliente existente — caso mais comum)

// app/blog/page.tsx
import { intelexia } from "@/lib/intelexia";
import { mergePosts } from "@intelex-ia/sdk";
import { posts as localPosts } from "@/lib/posts"; // seu array atual
import Link from "next/link";

export default async function BlogIndex() {
  // 1) Busca os artigos do painel
  const { items: remote } = await intelexia.getArticles({ limit: 50 });

  // 2) Mistura com os seus posts antigos
  const all = mergePosts({
    local: localPosts,
    remote,
    mapLocal: (p) => ({
      slug: p.slug,
      title: p.title,
      excerpt: p.excerpt,
      publishedAt: p.date,
      featuredImageUrl: null,
      category: p.category,
    }),
    // Opcional — defaults sensatos:
    // dedupe: "remote-wins"  (painel substitui local se slug colide)
    // sortBy: "date-desc"    (mais recente primeiro)
  });

  return (
    <ul>
      {all.map((p) => (
        <li key={p.slug}>
          <Link href={`/blog/${p.slug}`}>{p.title}</Link>
          <time>{new Date(p.publishedAt).toLocaleDateString("pt-BR")}</time>
          {p.source === "remote" && <span className="badge-ia">✨ IA</span>}
        </li>
      ))}
    </ul>
  );
}

Pro post individual, tenta o painel primeiro, cai pro local se 404:

// app/blog/[slug]/page.tsx
import { intelexia } from "@/lib/intelexia";
import { IntelexiaApiError } from "@intelex-ia/sdk";
import { ArticleJsonLd, articleToMetadata } from "@intelex-ia/sdk/react";
import { getPostBySlug } from "@/lib/posts";
import { notFound } from "next/navigation";

export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;

  // 1) Tenta painel
  try {
    const { article } = await intelexia.getArticleBySlug(slug);
    return (
      <article>
        <ArticleJsonLd article={article} />
        <h1>{article.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: article.contentHtml ?? "" }} />
      </article>
    );
  } catch (err) {
    if (!(err instanceof IntelexiaApiError) || err.status !== 404) throw err;
  }

  // 2) Fallback: post antigo local
  const local = getPostBySlug(slug);
  if (!local) notFound();
  return (
    <article>
      <h1>{local.title}</h1>
      {/* renderiza o local do jeito que você já fazia */}
    </article>
  );
}

Modo 2: Replace (migração completa)

// app/blog/page.tsx
import { intelexia } from "@/lib/intelexia";

export default async function BlogIndex() {
  const { items } = await intelexia.getArticles({ limit: 20 });
  return (
    <ul>
      {items.map((a) => (
        <li key={a.id}>
          <Link href={`/blog/${a.slug}`}>{a.title}</Link>
        </li>
      ))}
    </ul>
  );
}

Pro post individual, sem fallback:

import { intelexia } from "@/lib/intelexia";
import { IntelexiaApiError } from "@intelex-ia/sdk";
import { ArticleJsonLd, articleToMetadata } from "@intelex-ia/sdk/react";
import { notFound } from "next/navigation";

async function fetchArticle(slug: string) {
  try {
    return await intelexia.getArticleBySlug(slug);
  } catch (err) {
    if (err instanceof IntelexiaApiError && err.status === 404) notFound();
    throw err;
  }
}

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const { article } = await fetchArticle(slug);
  return articleToMetadata(article);
}

export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const { article } = await fetchArticle(slug);
  return (
    <article>
      <ArticleJsonLd article={article} />
      <h1>{article.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: article.contentHtml ?? "" }} />
    </article>
  );
}

Modo 3: Isolated (rota nova, blog antigo intacto)

Cria app/blog/ia/page.tsx e app/blog/ia/[slug]/page.tsx com o mesmo código do modo Replace, mas em rota separada (/blog/ia em vez de /blog). Zero alteração no blog existente.


Cliente — lib/intelexia.ts

import { createClient } from "@intelex-ia/sdk";

export const intelexia = createClient({
  token: process.env.INTELEXIA_API_TOKEN!,
  // ISR: revalida a cada 60s; o webhook revalidate invalida sob demanda
  init: { next: { revalidate: 60, tags: ["intelexia-articles"] } },
});

Webhook de revalidate — app/api/revalidate/route.ts

Quando um artigo publica/edita no painel, nosso worker faz POST no seu endpoint. Implemente assim:

import { NextResponse } from "next/server";
import { revalidatePath, revalidateTag } from "next/cache";

export async function POST(req: Request) {
  const headerSecret = req.headers.get("x-revalidate-secret");
  const body = (await req.json().catch(() => ({}))) as {
    secret?: string;
    slug?: string | null;
    domain?: string;
  };
  const got = headerSecret ?? body.secret ?? "";
  if (!process.env.INTELEXIA_REVALIDATE_SECRET || got !== process.env.INTELEXIA_REVALIDATE_SECRET) {
    return NextResponse.json({ ok: false, error: "unauthorized" }, { status: 401 });
  }

  if (body.slug) revalidatePath(`/blog/${body.slug}`);
  revalidatePath("/blog");
  revalidateTag("intelexia-articles");

  return NextResponse.json({ ok: true, revalidated: { slug: body.slug ?? null } });
}

O worker envia o secret tanto no header x-revalidate-secret quanto no body, tem timeout de 15s e é fire-and-forget (se seu endpoint falhar, o artigo continua publicado e a ISR atualiza pelo revalidate: 60).

API

createClient(config)

| Opção | Tipo | Default | | -------- | -------------------- | ------------------------------------------------- | | token | string (obrig.) | — | | apiUrl | string | https://api.innovarticles.intelexia.com.br | | fetch | typeof fetch | globalThis.fetch | | init | RequestInit | undefined (passe { next: { revalidate: N } }) |

client.getArticles({ limit?, offset?, init? })

Resposta: { ok, mode, lifecycle, count, items: ArticleListItem[] }.

client.getArticleBySlug(slug, { init? })

Resposta: { ok, mode, lifecycle, article: Article }. 404 → joga IntelexiaApiError (use try/catch + notFound()).

mergePosts({ local, remote, mapLocal, dedupe?, sortBy? }) 🆕 0.2.0

| Opção | Tipo | Default | | ---------- | ------------------------------------------------- | ---------------- | | local | readonly TLocal[] (qualquer shape) | — | | remote | readonly ArticleListItem[] | — | | mapLocal | (post: TLocal) => Omit<UnifiedPost,"source"> \| null | — | | dedupe | "remote-wins" \| "local-wins" \| "throw" | "remote-wins" | | sortBy | "date-desc" \| "date-asc" \| "none" | "date-desc" |

Retorna UnifiedPost[] com source: "local" | "remote". mapLocal retornando null filtra o post (útil pra esconder rascunhos). Slugs duplicados resolvidos pela estratégia dedupe. Veja exemplo no Modo Additive acima.

mode: "full" | "plain"

  • full (sites active/paused): inclui schema JSON-LD, metaTitle/Description, keyword(s), interlinking interno no contentHtml.
  • plain (site cancelled): só conteúdo cru — schema/meta/keywords vêm null, interlinking removido. O ArticleJsonLd e o articleToMetadata lidam graciosamente (omitem o que não tem).

A decisão é server-side e não-negociável — o cliente não escolhe o modo.

Helpers /react

  • <ArticleJsonLd article={article} /> — renderiza <script type="application/ld+json"> com o schemaJson. No-op se ausente.
  • articleToMetadata(article) → objeto compatível com o Metadata do Next.js App Router (title, description, keywords, openGraph, twitter, customMetaTags como other).

Erros

import { IntelexiaApiError, MergeConflictError } from "@intelex-ia/sdk";

try {
  await intelexia.getArticleBySlug("post-inexistente");
} catch (err) {
  if (err instanceof IntelexiaApiError) {
    console.error(err.status, err.message); // 404, "Artigo não encontrado."
  }
}

try {
  mergePosts({ local, remote, mapLocal, dedupe: "throw" });
} catch (err) {
  if (err instanceof MergeConflictError) {
    console.error(`Conflito de slug: ${err.slug}`);
  }
}

Compatibilidade

  • Node 18+ (fetch nativo), Edge Runtime, Browser.
  • React 18+ (peer dep opcional — só pros helpers /react).
  • TypeScript: tipos publicados no dist.
  • Frameworks: Next.js (App Router e Pages Router), Astro, Remix, SvelteKit, Vite, qualquer JS.

Changelog

  • 0.2.0 — adiciona mergePosts() + tipo UnifiedPost + MergeConflictError (3 modos de integração).
  • 0.1.0 — release inicial: createClient, getArticles, getArticleBySlug, <ArticleJsonLd>, articleToMetadata.