⚙️ 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.
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.).
Clicar em "Add custom integration"
Apenas Owners e Admins conseguem criar integrações. Editors/Authors não enxergam o botão.
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
Identidade de aplicação para chamar a Admin API.
Cada integração tem chaves próprias e revogáveis.
Nome aparece no histórico de cada ação automatizada.
Só Owner/Admin cria; Editor/Author não enxerga.
🔑 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
24 hex chars. Vai na claim kid do JWT.
64 hex chars. Chave HMAC para assinar o JWT.
Content = leitura pública; Admin = leitura+escrita.
Trocar a chave periodicamente reduz janela de exposição.
📦 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
Mantido pela Ghost Foundation; segue a API.
Nunca no browser — o secret expõe sua conta.
posts, pages, tags, members, images.
Trave em v5.0; evite canary.
🎫 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
idda 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.nonee 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
HMAC-SHA256: assina com segredo compartilhado.
Token de vida curta; gere um por request.
Claim que liga o token à Admin Integration.
Prefixo do header é Ghost, não Bearer.
📝 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
Sinaliza que o corpo veio em HTML, não Mobiledoc.
draft, published, scheduled.
Array de strings; criadas automaticamente se não existirem.
Resumo curto exibido em listagens e feeds.
🖼️ 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
HEADantes de enviar. - ✓Deixar
og_*herdar demeta_*quando o copy é o mesmo.
✗ O que NÃO fazer
- ✗Apontar para imagens em
localhostou 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
Capa do post; URL pública obrigatória.
Base de SEO; alimenta <title> e description.
Override para Open Graph (Facebook/LinkedIn).
Override para Twitter Card.
🎯 Resumo do Módulo
id:secret, guardada em env var, com plano de rotação.@tryghost/admin-api inicializada com URL, key e versão travada em v5.0.kid, aud=/admin/ e secret convertido de hex.source: 'html' e status correto.feature_image, meta_*, og_* e twitter_* preenchidos.Próximo Módulo:
4.3 — WordPress REST API e Application Passwords