@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.
Maintainers
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/getArticleBySlugtipados,mergePosts()pra unir conteúdo antigo + novo do painel,<ArticleJsonLd>pra injetar schema JSON-LD, earticleToMetadatapra plugar nogenerateMetadatado Next.js App Router. Zero dependências (usafetchglobal).
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 initCria 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/sdkConfiguração
- No painel da IntelexIA, cadastre o site como "Headless (código)" (vai pedir domain + URL e secret de revalidate).
- Abra o site na lista → seção "Tokens de API (Content API)" → Gerar token. Você recebe
itx_…(mostrado uma única vez). - 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-painelTrê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-secretquanto no body, tem timeout de 15s e é fire-and-forget (se seu endpoint falhar, o artigo continua publicado e a ISR atualiza pelorevalidate: 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(sitesactive/paused): inclui schema JSON-LD, metaTitle/Description, keyword(s), interlinking interno nocontentHtml.plain(sitecancelled): só conteúdo cru — schema/meta/keywords vêmnull, interlinking removido. OArticleJsonLde oarticleToMetadatalidam 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 oschemaJson. No-op se ausente.articleToMetadata(article)→ objeto compatível com oMetadatado Next.js App Router (title, description, keywords, openGraph, twitter, customMetaTags comoother).
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()+ tipoUnifiedPost+MergeConflictError(3 modos de integração). - 0.1.0 — release inicial:
createClient,getArticles,getArticleBySlug,<ArticleJsonLd>,articleToMetadata.
