SDK TypeScript · Mavellium

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-sdk

Opçã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 install

O 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.

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-sdk

O 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ávelExemploDescrição
JANUS_URLhttps://cms.exemplo.com.brURL base do Janus (sem barra final)
JANUS_TENANT_IDminha-empresacompanySlug 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 }))
}

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/:pageSlug

Tipo 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çãoRetornoAção recomendada
Sem conexão / DNSnullnotFound()
HTTP 404nullnotFound()
HTTP 5xxlança JanusAPIErrorCapturado 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):

Azul
Roxo
Verde
Laranja
Rosa
Âmbar

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              ← gracioso

Comportamento por método

MétodoErro de redeHTTP 404HTTP 5xx
getPosts[][][]
getPostSlugs[][][]
getRelatedPosts[][][]
getCategories[][][]
getPostnullnullnull
getPagenullnullJanusAPIError ↑

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.