🔑 Pegando a API key do Dev.to
O Dev.to roda sobre o Forem, plataforma open-source da própria Dev.to. A API é REST, autenticada por uma DEV API Key pessoal — não OAuth, não scopes, é uma chave única que dá acesso de escrita à sua conta.
Para gerar a chave: Settings → Extensions → DEV Community API Keys. Dê um nome descritivo (ex.: postiz-prod) e copie o valor — ele só aparece uma vez.
# Caminho na UI
https://dev.to/settings/extensions
# Bloco "DEV Community API Keys"
# 1. Project name: postiz-prod
# 2. Clique em "Generate API Key"
# 3. Copie o valor (formato hex de ~40 caracteres)
# Teste rápido — listar seus artigos (200 = chave válida)
curl -H "api-key: SEU_DEV_API_KEY" \
https://dev.to/api/articles/me
💡 Dica prática
Crie uma chave por integração (uma para Postiz, outra para script local, outra para CI). Se uma vazar, você revoga só ela — sem derrubar as outras automações.
Conceitos-chave
Engine open-source que roda o Dev.to e comunidades irmãs.
Credencial pessoal e revogável — sem scopes, acesso total à conta.
Nome literal: api-key (não Authorization).
~30 req/30s — gentil mas existe.
📝 POST /api/articles do Forem
Para publicar um post, basta um POST em https://dev.to/api/articles com o header api-key e um JSON aninhado em {"article": {...}}. Os campos essenciais são title, body_markdown e published.
Com published: false, o post vira rascunho — útil para revisar antes de soltar. Com true, ele aparece no feed na hora.
# Publicar artigo no Dev.to
curl -X POST https://dev.to/api/articles \
-H "api-key: $DEV_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"article": {
"title": "Subindo Postiz com Docker Compose",
"body_markdown": "# Introdução\n\nNeste post mostro como...",
"published": true,
"tags": ["docker", "selfhosted", "devops"],
"series": "MkBlogs"
}
}'
# Resposta esperada (201 Created)
# {
# "id": 1234567,
# "url": "https://dev.to/seu-user/subindo-postiz-...",
# "slug": "subindo-postiz-com-docker-compose-XXXX",
# "published_at": "2026-05-24T12:00:00Z"
# }
⚡ Dica prática
O Forem aceita até quatro tags por artigo e só permite tags que já existem. Confira em https://dev.to/tags antes de criar a request — tag inexistente derruba o POST com 422.
Conceitos-chave
Endpoints HTTP convencionais com verbos GET/POST/PUT.
Corpo em Markdown — o Forem renderiza no servidor.
Boolean que controla rascunho vs publicado.
Máx. 4, todas devem existir no catálogo do Dev.to.
🎨 Frontmatter & cover_image
Em vez de mandar título, tags e cover em campos JSON separados, o Forem aceita um frontmatter YAML embutido no body_markdown. Vantagem: o mesmo Markdown que você escreveria localmente publica direto, sem transformar nada.
O cover_image precisa ser uma URL pública (HTTPS) — o Dev.to não aceita upload binário pelo endpoint de articles. Use Cloudinary, R2, ou as próprias imagens hospedadas em outro post.
# body_markdown com frontmatter
---
title: Subindo Postiz com Docker Compose
published: true
description: Stack completo em 50 minutos.
tags: docker, selfhosted, devops
cover_image: https://cdn.example.com/covers/postiz-cover.png
canonical_url: https://meublog.dev/postiz-docker
series: MkBlogs
---
# Introdução
Neste post mostro como subir o Postiz...
## Setup
```yaml
services:
postiz:
image: ghcr.io/gitroomhq/postiz-app:latest
```
Quando há frontmatter, ele sobrescreve os campos do JSON externo. Padronize: ou tudo no frontmatter, ou tudo nos campos top-level — misturar gera confusão.
# Request com frontmatter (mais limpo)
curl -X POST https://dev.to/api/articles \
-H "api-key: $DEV_API_KEY" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"article": {
"body_markdown": "---\ntitle: Meu post\npublished: true\ntags: docker, devops\ncover_image: https://cdn.example.com/cover.png\n---\n\n# Conteúdo aqui"
}
}
JSON
Conceitos-chave
Bloco YAML entre --- no topo do markdown.
URL pública HTTPS — 1000×420px é o ideal.
Agrupa posts numa sequência navegável no perfil.
Frontmatter ganha do JSON top-level.
🌐 Hashnode via GraphQL
O Hashnode escolheu outro caminho: GraphQL em um único endpoint, https://gql.hashnode.com/. Em vez de muitos endpoints REST, você manda uma query/mutation descrevendo exatamente o que quer.
A autenticação é via Personal Access Token, gerado em Account Settings → Developer → Generate New Token. Vai no header Authorization sem prefixo Bearer — sutileza fácil de errar.
# Smoke test — query "me" valida o token
curl -X POST https://gql.hashnode.com/ \
-H "Authorization: $HASHNODE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "query { me { id username publications(first: 5) { edges { node { id title } } } } }"
}'
# Resposta esperada
# {
# "data": {
# "me": {
# "id": "654...",
# "username": "seu-user",
# "publications": {
# "edges": [
# { "node": { "id": "abc123", "title": "Meu Blog" } }
# ]
# }
# }
# }
# }
📊 Por que GraphQL aqui?
- Uma request, dados certos: peça só os campos que precisa, evita N+1 e overfetch.
- Schema introspectivo: use o playground para descobrir tipos sem ler docs externas.
- Tipagem forte: mutations rejeitam campos inválidos com mensagens claras, no formato do schema.
- Variáveis separadas: a query é fixa; só as variables mudam — bom para template/cache.
Conceitos-chave
Query language com endpoint único e schema tipado.
Personal Access Token — substitui senha em automações.
ID do seu blog Hashnode — necessário em toda mutation de post.
UI interativa que documenta o schema vivo.
✏️ Mutation publishPost
A mutation publishPost recebe um PublishPostInput com title, contentMarkdown, publicationId e opcionais como tags, cover e canonical. Use sempre variáveis — nunca interpole strings na query.
# Mutation completa
mutation PublishPost($input: PublishPostInput!) {
publishPost(input: $input) {
post {
id
slug
url
publishedAt
}
}
}
# Variáveis (variables)
{
"input": {
"title": "Subindo Postiz com Docker Compose",
"contentMarkdown": "# Introdução\n\nNeste post...",
"publicationId": "abc123_id_da_sua_publication",
"tags": [
{ "slug": "docker", "name": "Docker" },
{ "slug": "devops", "name": "DevOps" }
],
"coverImageOptions": {
"coverImageURL": "https://cdn.example.com/cover.png"
},
"originalArticleURL": "https://meublog.dev/postiz-docker"
}
}
Mandar via curl é direto: corpo JSON com query e variables separados.
# Disparar via curl
curl -X POST https://gql.hashnode.com/ \
-H "Authorization: $HASHNODE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "mutation PublishPost($input: PublishPostInput!) { publishPost(input: $input) { post { id slug url } } }",
"variables": {
"input": {
"title": "Subindo Postiz com Docker Compose",
"contentMarkdown": "# Introdução\n\nNeste post...",
"publicationId": "SUA_PUBLICATION_ID",
"tags": [{ "slug": "docker", "name": "Docker" }],
"originalArticleURL": "https://meublog.dev/postiz-docker"
}
}
}'
🔧 Dica prática
Erros do GraphQL voltam com HTTP 200 OK e um campo errors no JSON. Não confie só no status — parseie response.errors antes de declarar sucesso.
Conceitos-chave
Operação GraphQL que altera estado (create/update/delete).
Parâmetros tipados injetados na query — evita SQL-injection-like.
Objeto tipado (ex.: PublishPostInput) com campos validados.
Mesmo com HTTP 200, parseie esse array antes de seguir.
🔗 Cross-posting com canonical_url
Republicar o mesmo post em Dev.to + Hashnode + seu blog próprio multiplica audiência — mas sem cuidado isso vira duplicate content e o Google penaliza todos os três. A solução é a canonical_url: você declara qual versão é a "original" e as cópias dizem ao Google "indexe aquela ali".
# Dev.to — campo "canonical_url"
{
"article": {
"title": "Subindo Postiz com Docker Compose",
"body_markdown": "...",
"canonical_url": "https://meublog.dev/postiz-docker",
"published": true
}
}
# Hashnode — campo "originalArticleURL"
{
"input": {
"title": "Subindo Postiz com Docker Compose",
"contentMarkdown": "...",
"publicationId": "SUA_PUB_ID",
"originalArticleURL": "https://meublog.dev/postiz-docker"
}
}
✓ O que FAZER
- ✓Publicar primeiro no seu domínio próprio; só depois replicar.
- ✓Apontar
canonical_urleoriginalArticleURLsempre para o seu domínio. - ✓Esperar o Google indexar o original (1-3 dias) antes do cross-post.
- ✓Manter o mesmo título — facilita reconhecimento da plataforma.
✗ O que NÃO fazer
- ✗Publicar em Dev.to e Hashnode sem canonical — Google escolhe um aleatório.
- ✗Apontar canonical da Dev.to para o Hashnode (ou vice-versa) — confunde rank.
- ✗Esquecer da meta
rel="canonical"no seu próprio blog. - ✗Cross-postar conteúdo "thin" — penaliza a marca em todas as três.
🧭 Dica prática
Dev.to e Hashnode mostram um aviso visível na interface ("originalmente publicado em…") quando você usa canonical. Isso é bom: mais cliques voltam para o seu domínio, o ranking principal fica com você e a comunidade ainda lê na plataforma preferida.
Conceitos-chave
URL "oficial" que o Google deve indexar entre cópias.
Mesmo HTML em múltiplas URLs — confunde indexação.
Sinal de relevância concentrado no canonical.
Republicar conteúdo em múltiplas plataformas com referência ao original.
🎯 Resumo do Módulo
api-key, body {"article": {...}} com published e tags.gql.hashnode.com, PAT no header Authorization sem prefixo.errors[] mesmo com 200.canonical_url / originalArticleURL apontando para o domínio próprio.Próximo Módulo:
4.4 — Medium, Substack e plataformas sem API aberta