janus-sdk
janus-sdk é o cliente TypeScript oficial para a API pública do Janus CMS. Agnóstico de framework — funciona em Next.js, Astro, Node.js puro e qualquer runtime com fetch nativa (Node ≥ 18).
O SDK encapsula toda a comunicação com o Janus: busca de posts de blog, categorias, slugs para geração estática e páginas headless via /api/v1/content/:tenant/:slug. Exporta os formatos ESM e CommonJS simultaneamente, com tipos TypeScript completos e erros tipados para tratamento granular.
- Framework-agnostic
- Next.js, Astro, Node.js puro, Bun, Deno
- ESM + CJS
- Dual output via tsup · tipos .d.ts inclusos
- Fetch nativa
- Sem dependências · Node ≥ 18
- Erros tipados
- JanusAPIError com status e url
Instalação
Escolha o método adequado ao seu cenário: projeto independente via registry npm, projeto dentro do monorepo Mavellium via workspace, ou desenvolvimento local com pnpm link.
Opção A — Projeto externo (npm / pnpm / yarn)
Instale o pacote publicado no registry:
npm i janus-sdk
# ou
pnpm add janus-sdk
# ou
yarn add janus-sdkOpção B — Monorepo Mavellium (pnpm workspace)
Se o seu projeto já está dentro do monorepo, o janus-sdk já está linkado via workspace. Declare a dependência no package.json do seu projeto e instale na raiz:
// package.json do seu projeto
{
"dependencies": {
"janus-sdk": "workspace:*"
}
}
// Instale na raiz do monorepo
pnpm installO pnpm cria um symlink em node_modules/janus-sdk → pasta janus-sdk/dist/. Rode pnpm build no SDK antes de usar, ou mantenha pnpm dev em watch mode em outro terminal.
Opção C — Desenvolvimento local (pnpm link)
Para testar o SDK em um projeto externo sem publicar no npm:
# 1. No diretório do SDK — registra o link global
cd janus-sdk
pnpm build
pnpm link --global
# 2. No seu projeto externo — conecta ao link
cd /caminho/do/seu-projeto
pnpm link --global janus-sdk
# A partir daqui, importe normalmente:
# import { JanusClient } from "janus-sdk"
# Quando terminar os testes:
pnpm unlink janus-sdkO link aponta diretamente para janus-sdk/dist/. Qualquer rebuild do SDK com pnpm build reflete imediatamente no projeto linkado, sem precisar re-executar o link.
Variáveis de ambiente necessárias
| Variável | Exemplo | Descrição |
|---|---|---|
| JANUS_URL | https://cms.exemplo.com.br | URL base do Janus (sem barra final) |
| JANUS_TENANT_ID | minha-empresa | companySlug cadastrado no Janus |
Inicialização
Instancie o JanusClient passando a URL base do Janus e o identificador do seu tenant. O construtor aceita um terceiro campo opcional, defaultInit, mesclado em todos os fetches — use para opções de cache específicas do framework.
Interface de configuração
interface JanusClientConfig {
baseUrl: string; // URL base do Janus
tenantId: string; // companySlug registrado no Janus
defaultInit?: RequestInit; // opções de fetch mescladas em todas as chamadas
}Exemplo básico (Node.js / runtime genérico)
import { JanusClient } from "janus-sdk"
const client = new JanusClient({
baseUrl: "https://cms.exemplo.com.br",
tenantId: "minha-empresa",
})
const posts = await client.getPosts()
console.log(posts)Padrão recomendado — singleton para Next.js App Router
Instancie fora dos componentes para reutilizar entre requests. Use defaultInit para configurar ISR globalmente — cada método pode sobrescrever se precisar de uma política diferente.
// src/lib/janus.ts
import { JanusClient } from "janus-sdk"
export const janus = new JanusClient({
baseUrl: process.env.JANUS_URL!,
tenantId: process.env.JANUS_TENANT_ID!,
defaultInit: {
next: { revalidate: 60 }, // ISR: revalida todas as rotas a cada 60 s
} as RequestInit,
})O campo next é uma extensão da API fetch do Next.js e não existe no tipo padrão RequestInit do DOM — daí o cast as RequestInit. Em produção, o Next.js resolve o tipo corretamente via augmentation global.
Uso em Server Component
// app/blog/page.tsx
import { janus } from "@/lib/janus"
export default async function BlogPage() {
const posts = await janus.getPosts()
return (
<ul>
{posts.map((post) => (
<li key={post.slug}>{post.title}</li>
))}
</ul>
)
}Métodos de blog
Todos os métodos de blog comunicam com GET /api/{tenantId}/blog/... e sempre retornam [] ou null em caso de erro — nunca lançam exceção. Seguros para uso em generateStaticParams e builds estáticos.
getPosts(opts?)
Retorna posts publicados. Aceita filtros opcionais por categoria, destaque e limite. Sempre retorna Post[].
// Todos os posts publicados (limite padrão: 50)
const posts = await janus.getPosts()
// Com filtros
const destaques = await janus.getPosts({ featured: true })
const categoria = await janus.getPosts({ categoryId: "abc123" })
const recentes = await janus.getPosts({ limit: 6 })
// Tipagem de retorno:
// Post[] (array vazio em caso de erro)
interface GetPostsOptions {
categoryId?: string
featured?: boolean
limit?: number // padrão: 50
}getPost(slug)
Retorna um post pelo slug ou null se não encontrado. Chame notFound() após verificar o retorno.
// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation"
import { janus } from "@/lib/janus"
import { processHtmlBody } from "janus-sdk"
export default async function PostPage({
params,
}: {
params: { slug: string }
}) {
const post = await janus.getPost(params.slug)
if (!post) notFound()
const { processedHtml, headings } = processHtmlBody(post.htmlBody)
return (
<article>
<h1>{post.title}</h1>
<p>{post.description}</p>
<div dangerouslySetInnerHTML={{ __html: processedHtml }} />
</article>
)
}getPostSlugs()
Retorna todos os slugs publicados. Ideal para generateStaticParams — sempre retorna string[], nunca lança.
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const slugs = await janus.getPostSlugs()
return slugs.map((slug) => ({ slug }))
}getRelatedPosts(categoryId, excludeSlug)
Retorna até 3 posts da mesma categoria, excluindo o slug informado. Sempre retorna Post[].
const related = await janus.getRelatedPosts(
post.category.id,
post.slug,
)
// Post[] (máximo 3 itens da mesma categoria)getCategories()
Retorna todas as categorias do tenant. Sempre retorna CmsCategory[].
const categories = await janus.getCategories()
// CmsCategory[]
interface CmsCategory {
id: string
name: string
slug?: string
description?: string
image?: string
}Página headless
O método getPage busca uma página publicada via endpoint público do Janus:
GET /api/v1/content/:tenantId/:pageSlugTipo de retorno
interface JanusPage {
slug: string
name: string
schema: unknown // schema de campos definido no builder
content: unknown // dados preenchidos pelo cliente
updatedAt: string
}schema e content são unknown intencionalmente — o schema é dinâmico por projeto. Faça o cast no consumidor para o tipo esperado.
Uso em Server Component
// app/[slug]/page.tsx
import { notFound } from "next/navigation"
import { janus } from "@/lib/janus"
// Defina o tipo do seu content
interface HomeContent {
hero_title: string
hero_subtitle: string
cta_label: string
}
export default async function DynamicPage({
params,
}: {
params: { slug: string }
}) {
// getPage retorna null em 404 e re-lança JanusAPIError em 5xx
const page = await janus.getPage(params.slug)
if (!page) notFound()
const content = page.content as HomeContent
return (
<main>
<h1>{content.hero_title}</h1>
<p>{content.hero_subtitle}</p>
</main>
)
}Comportamento de erros
| Situação | Retorno | Ação recomendada |
|---|---|---|
| Sem conexão / DNS | null | notFound() |
| HTTP 404 | null | notFound() |
| HTTP 5xx | lança JanusAPIError | Capturado pelo error.tsx |
Erros 5xx são re-lançados para que o Next.js renderize o error.tsx correspondente, evitando que uma falha do servidor entregue uma página em branco silenciosamente.
Funções utilitárias
Exportadas como funções puras — não dependem de uma instância do JanusClient. Importe diretamente do pacote.
processHtmlBody(html)
Injeta atributos id únicos em todas as tags <h2> e <h3> do HTML e retorna a lista de headings para construção de Sumário (ToC).
import { processHtmlBody } from "janus-sdk"
const { processedHtml, headings } = processHtmlBody(post.htmlBody)
// headings: TocHeading[]
// [
// { id: "introducao", text: "Introdução", level: 2 },
// { id: "configuracao", text: "Configuração", level: 3 },
// ]
// Exemplo de ToC com React:
function TableOfContents({ headings }: { headings: TocHeading[] }) {
return (
<nav>
<ul>
{headings.map((h) => (
<li key={h.id} style={{ paddingLeft: h.level === 3 ? "1rem" : 0 }}>
<a href={`#${h.id}`}>{h.text}</a>
</li>
))}
</ul>
</nav>
)
}getCategoryColor(name)
Retorna um bundle de classes Tailwind CSS para uma categoria, determinístico pelo nome — o mesmo nome sempre produz a mesma cor.
import { getCategoryColor } from "janus-sdk"
const { bg, text, border, dot } = getCategoryColor("Inteligência Artificial")
// { bg: "bg-blue-50", text: "text-blue-700", border: "border-blue-200", dot: "bg-blue-500" }
function CategoryBadge({ name }: { name: string }) {
const { bg, text, border, dot } = getCategoryColor(name)
return (
<span className={`${bg} ${text} ${border} border rounded-full px-2 py-0.5 text-xs flex items-center gap-1`}>
<span className={`${dot} w-1.5 h-1.5 rounded-full`} />
{name}
</span>
)
}Paleta disponível (6 cores, seleção por hash do nome):
formatDate(isoDate)
Formata uma string ISO 8601 para português brasileiro usando Intl.DateTimeFormat.
import { formatDate } from "janus-sdk"
formatDate("2026-05-17T00:00:00.000Z") // "17 de maio de 2026"
formatDate("2026-01-01T00:00:00.000Z") // "1 de janeiro de 2026"Tratamento de erros
O SDK separa erros de rede (sem conexão, DNS, timeout) de erros HTTP (respostas 4xx/5xx). Erros de rede sempre resultam em retorno gracioso. Erros HTTP geram JanusAPIError internamente — cada método decide se o captura ou propaga.
Como o fetch interno funciona
fetchJson(path)
│
├── fetch() lança exceção (sem rede, DNS, timeout)
│ └── retorna null ← gracioso, não quebra o build
│
├── res.ok === false (HTTP 4xx / 5xx)
│ └── throw new JanusAPIError ← tipado, capturado pelo método público
│
└── res.json() lança exceção (JSON malformado)
└── retorna null ← graciosoComportamento por método
| Método | Erro de rede | HTTP 404 | HTTP 5xx |
|---|---|---|---|
| getPosts | [] | [] | [] |
| getPostSlugs | [] | [] | [] |
| getRelatedPosts | [] | [] | [] |
| getCategories | [] | [] | [] |
| getPost | null | null | null |
| getPage | null | null | JanusAPIError ↑ |
Métodos de lista ( getPosts, getCategories etc.) nunca lançam — seguros para builds estáticos. getPage propaga erros 5xx para que o error.tsx do Next.js os trate.
JanusAPIError
class JanusAPIError extends Error {
readonly status: number // código HTTP (ex: 404, 500, 503)
readonly url: string // URL completa que falhou
}Capturando manualmente
Se precisar de controle granular sobre erros em getPage, capture JanusAPIError e decida com base no status:
import { JanusAPIError } from "janus-sdk"
import { notFound } from "next/navigation"
export default async function Page({ params }) {
let page
try {
page = await janus.getPage(params.slug)
} catch (err) {
if (err instanceof JanusAPIError) {
console.error(`Janus ${err.status} em ${err.url}`)
// 503 → Janus indisponível, mostrar fallback
// 500 → erro interno, deixar error.tsx agir
}
throw err // propaga para o error.tsx do Next.js
}
if (!page) notFound() // 404 ou não publicado
}Crie um error.tsx para capturar erros propagados pelo SDK:
// app/error.tsx
"use client"
export default function Error({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div>
<h2>Erro ao carregar o conteúdo</h2>
<button onClick={reset}>Tentar novamente</button>
</div>
)
}Quer o Janus CMS no seu projeto?
O janus-sdk é desenvolvido e mantido pela Mavellium como parte do ecossistema Janus. Entre em contato para avaliar a adoção no seu stack.