⚡ AutomationsAI|Portal de Cursos →

Verificando acesso...

MÓDULO 4.2

👻 Ghost Admin API

Conectar no Ghost via Admin API: Custom Integration, chaves id:secret, JWT HS256, publicação de posts em HTML/Markdown e configuração de feature image + SEO.

6
Tópicos
35
Minutos
Básico
Nível
Prático
Tipo
1

⚙️ Criar Custom Integration

A Admin API do Ghost só responde a quem se identifica como uma Custom Integration. Não dá pra usar a senha de admin nem o token do dashboard — toda automação precisa de uma integração própria, com escopo isolado e revogável em um clique.

No painel do Ghost vá em Settings → Integrations → Add custom integration. Dê um nome descritivo (ex.: MkBlogs Publisher) — esse nome aparece no histórico de cada post publicado pela API.

1

Abrir Settings → Integrations

No menu lateral do admin do Ghost, role até Integrations. A seção "Custom integrations" fica abaixo das integrações nativas (Zapier, Slack, etc.).

2

Clicar em "Add custom integration"

Apenas Owners e Admins conseguem criar integrações. Editors/Authors não enxergam o botão.

3

Nomear e salvar

Use nomes que identifiquem origem e propósito: MkBlogs Publisher, CI Auto-publish, n8n Pipeline. Facilita auditoria depois.

💡 Dica prática

Crie uma integração por automação. Se um script vazar a chave, você revoga só aquela integração sem derrubar o resto. Uma chave única compartilhada vira ponto único de falha.

Conceitos-chave

Custom Integration

Identidade de aplicação para chamar a Admin API.

Escopo

Cada integração tem chaves próprias e revogáveis.

Auditoria

Nome aparece no histórico de cada ação automatizada.

Permissões

Só Owner/Admin cria; Editor/Author não enxerga.

2

🔑 Admin API Key (id:secret)

Cada integração gera duas chaves: a Content API Key (read-only, para apps públicos) e a Admin API Key (read/write, para publicar). Para escrever posts você precisa da segunda.

A Admin Key vem no formato id:secret — duas strings hex separadas por dois-pontos. O id identifica a integração (24 chars), o secret é o segredo HMAC de 64 chars hex usado para assinar JWTs.

# Formato da Admin API Key
# <id 24 hex>:<secret 64 hex>
6582f1c4a3e9b7d8f201e3a4:0f2b9c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b

# Estrutura
# ├─ id     → cola na claim "kid" do JWT
# └─ secret → assina o JWT com HS256 (precisa ser convertido de hex p/ bytes)

✓ O que FAZER

  • Guardar a chave em variável de ambiente (GHOST_ADMIN_KEY).
  • Regenerar imediatamente se cair em log/git/Slack.
  • Usar uma integração diferente por ambiente (dev/staging/prod).
  • Rotacionar via botão Regenerate no painel a cada 90 dias.

✗ O que NÃO fazer

  • Commitar a chave no repositório — nem em .env.example.
  • Compartilhar uma chave entre múltiplas aplicações.
  • Confundir Content Key (32 hex) com Admin Key (id:secret).
  • Embutir a chave no frontend (browser/mobile) — é segredo de servidor.

🔄 Regenerar chave comprometida

No painel da integração existe um botão Regenerate ao lado da Admin API Key. Clicar invalida a chave antiga na hora — qualquer pipeline rodando com ela passa a retornar 401 Unauthorized. Tenha o pipeline pronto para receber a nova antes de clicar.

Conceitos-chave

id

24 hex chars. Vai na claim kid do JWT.

secret

64 hex chars. Chave HMAC para assinar o JWT.

Content vs Admin

Content = leitura pública; Admin = leitura+escrita.

Rotação

Trocar a chave periodicamente reduz janela de exposição.

3

📦 Lib @tryghost/admin-api

Dá pra falar com a Admin API "na mão" (montar JWT + fetch), mas o jeito recomendado pela própria Ghost é a lib oficial @tryghost/admin-api. Ela cuida da geração do JWT, do versionamento de endpoint e do parsing das respostas.

Instale via npm/pnpm e importe em Node ≥ 18. A lib é puramente server-side — não funciona no browser porque expõe o secret.

# Instalação
npm install @tryghost/admin-api
# ou
pnpm add @tryghost/admin-api

# Versão mínima recomendada: ^1.13.0 (suporta Ghost 5.x)
// init.js — inicializar o client
import GhostAdminAPI from '@tryghost/admin-api';

const api = new GhostAdminAPI({
  url: process.env.GHOST_URL,           // ex: https://meublog.com
  key: process.env.GHOST_ADMIN_KEY,     // formato id:secret
  version: 'v5.0'                       // versão da API
});

// Teste rápido — listar últimos 3 posts
const posts = await api.posts.browse({ limit: 3 });
console.log(posts.map(p => p.title));

Dica prática

A lib aceita o campo version com valores tipo 'v5.0' ou 'canary'. Trave a versão (não use canary) em produção — endpoints canary podem mudar formato sem aviso.

Conceitos-chave

SDK oficial

Mantido pela Ghost Foundation; segue a API.

Server-only

Nunca no browser — o secret expõe sua conta.

Recursos

posts, pages, tags, members, images.

Versionamento

Trave em v5.0; evite canary.

4

🎫 JWT auth (HS256)

Por baixo dos panos, cada chamada à Admin API vai com um header Authorization: Ghost <token>. O token é um JWT assinado com HS256, usando o secret da Admin Key como chave HMAC.

O JWT tem expiração curtíssima — 5 minutos. A ideia é que cada request gere um token novo. Tokens "eternos" são anti-padrão aqui; se vazarem, comprometem por muito tempo.

// Gerar token JWT manualmente (a lib oficial já faz isso por você)
import jwt from 'jsonwebtoken';

function ghostToken(adminKey) {
  const [id, secret] = adminKey.split(':');

  // O secret vem em hex — precisa virar Buffer
  const secretBuffer = Buffer.from(secret, 'hex');

  return jwt.sign({}, secretBuffer, {
    keyid: id,             // claim "kid" — identifica a integração
    algorithm: 'HS256',
    expiresIn: '5m',       // expiração curta (recomendado)
    audience: '/admin/'    // audiência fixa exigida pelo Ghost
  });
}

// Uso
const token = ghostToken(process.env.GHOST_ADMIN_KEY);
const res = await fetch('https://meublog.com/ghost/api/admin/posts/', {
  headers: { Authorization: `Ghost ${token}` }
});

📊 Claims obrigatórias do JWT

  • kid: o id da Admin Key. Liga o token à integração certa no banco do Ghost.
  • iat / exp: emissão e expiração. exp - iat ≤ 5min — JWTs longos são recusados.
  • aud: sempre /admin/. Outras audiências dão 401.
  • alg: sempre HS256. none e RS* são rejeitados.

⏱️ Dica prática

Erro mais comum: esquecer de converter o secret de hex para Buffer antes de passar pro jwt.sign. Sem Buffer.from(secret, 'hex'), a assinatura sai usando os caracteres ASCII e o Ghost devolve 401 em todas as chamadas.

Conceitos-chave

HS256

HMAC-SHA256: assina com segredo compartilhado.

expiresIn 5m

Token de vida curta; gere um por request.

kid

Claim que liga o token à Admin Integration.

Bearer Ghost

Prefixo do header é Ghost, não Bearer.

5

📝 Publicar post em Markdown/HTML

O endpoint posts.add aceita conteúdo em HTML (padrão) ou em Mobiledoc/Lexical (formato interno do Ghost). Para automação a partir de Markdown, o caminho mais simples é converter MD → HTML no seu lado e mandar como HTML, sinalizando source: 'html'.

O campo status controla o destino: draft (rascunho, default), published (publica na hora) ou scheduled (combinado com published_at futuro).

// publish.js — publicar post a partir de HTML
import GhostAdminAPI from '@tryghost/admin-api';

const api = new GhostAdminAPI({
  url: process.env.GHOST_URL,
  key: process.env.GHOST_ADMIN_KEY,
  version: 'v5.0'
});

const html = `
  <h2>Primeira automação no Ghost</h2>
  <p>Esse post foi criado via Admin API.</p>
  <ul>
    <li>JWT gerado pela lib</li>
    <li>Markdown convertido para HTML</li>
    <li>Status: published</li>
  </ul>
`;

const post = await api.posts.add(
  {
    title: 'Olá Ghost via API',
    html,
    status: 'published',
    tags: ['automação', 'api'],
    excerpt: 'Primeiro post automatizado no blog.'
  },
  { source: 'html' }  // 👈 diz ao Ghost para parsear o campo `html`
);

console.log('Publicado:', post.url);
// Markdown → HTML antes de mandar (usando `marked`)
import { marked } from 'marked';

const md = `
## Título

Texto com **negrito** e [link](https://exemplo.com).

- item 1
- item 2
`;

const html = marked.parse(md);
await api.posts.add({ title: 'Post MD', html, status: 'draft' }, { source: 'html' });

💡 Dica prática

Comece sempre com status: 'draft' nos primeiros testes. O post aparece no admin como rascunho e você confere se a formatação ficou certa antes de publicar pra audiência. Trocar para 'published' é um simples posts.edit.

Conceitos-chave

source: 'html'

Sinaliza que o corpo veio em HTML, não Mobiledoc.

status

draft, published, scheduled.

tags

Array de strings; criadas automaticamente se não existirem.

excerpt

Resumo curto exibido em listagens e feeds.

6

🖼️ Feature image e SEO meta

Um post "completo" no Ghost não é só título + corpo. Ele tem feature image (capa exibida no topo e em cards sociais), meta_title / meta_description (SEO e Open Graph) e og_image / twitter_image (override por canal).

A feature image precisa ser uma URL pública — ou você faz upload via api.images.upload() e usa a URL retornada, ou aponta para um CDN externo (Unsplash, S3, R2).

// Post completo com capa + SEO
const post = await api.posts.add(
  {
    title: 'Como configurar seu blog em 2026',
    html: '<p>Conteúdo do post...</p>',
    status: 'published',

    // Capa
    feature_image: 'https://cdn.meublog.com/capas/setup-blog-2026.jpg',
    feature_image_alt: 'Tela de configuração do Ghost',
    feature_image_caption: 'Painel de Settings do Ghost 5.x',

    // SEO base (usado em <title> e meta description)
    meta_title: 'Configurar Blog em 2026 — Guia Prático',
    meta_description: 'Passo a passo de setup de blog com Ghost, Admin API e CI.',

    // Open Graph (Facebook/LinkedIn) — opcional, herda dos meta_*
    og_title: 'Guia: configurar seu blog em 2026',
    og_description: 'Setup completo de blog moderno em menos de uma hora.',
    og_image: 'https://cdn.meublog.com/og/setup-blog-2026.jpg',

    // Twitter Card — opcional, herda dos og_*
    twitter_title: 'Configurar Blog em 2026',
    twitter_description: 'Setup prático de blog em 2026.',
    twitter_image: 'https://cdn.meublog.com/twitter/setup-blog-2026.jpg'
  },
  { source: 'html' }
);

📐 Tamanhos recomendados

  • feature_image: 1920×1080 (16:9), JPEG/WebP, < 500 KB.
  • og_image: 1200×630 px — formato canônico do Facebook/LinkedIn.
  • twitter_image: 1200×675 (16:9) para Twitter Card Large.
  • meta_title: ≤ 60 chars para não truncar no Google.
  • meta_description: 140–160 chars; única por post.

✓ O que FAZER

  • Sempre preencher feature_image_alt (acessibilidade + SEO).
  • Servir capas por CDN (cache-friendly, baixa latência).
  • Validar URL das imagens com HEAD antes de enviar.
  • Deixar og_* herdar de meta_* quando o copy é o mesmo.

✗ O que NÃO fazer

  • Apontar para imagens em localhost ou rede privada.
  • Reutilizar a mesma capa em todos os posts da série.
  • Subir PNGs gigantes (> 2 MB) — derruba o LCP.
  • Esquecer meta_description — o Google inventa um trecho ruim.

Conceitos-chave

feature_image

Capa do post; URL pública obrigatória.

meta_*

Base de SEO; alimenta <title> e description.

og_*

Override para Open Graph (Facebook/LinkedIn).

twitter_*

Override para Twitter Card.

🎯 Resumo do Módulo

Custom Integration criada — uma integração nomeada por automação em Settings → Integrations.
Admin API Key entendida — formato id:secret, guardada em env var, com plano de rotação.
Lib oficial instalada@tryghost/admin-api inicializada com URL, key e versão travada em v5.0.
JWT HS256 dominado — token curto (5min) com claims kid, aud=/admin/ e secret convertido de hex.
Post publicado via API — HTML (ou Markdown convertido) enviado com source: 'html' e status correto.
SEO e capa configuradosfeature_image, meta_*, og_* e twitter_* preenchidos.

Próximo Módulo:

4.3 — WordPress REST API e Application Passwords