⚡ AutomationsAI|Portal de Cursos →

Verificando acesso...

MÓDULO 4.3

✍️ Dev.to & Hashnode

Publicar via API nas duas maiores plataformas de blog técnico: REST do Dev.to (Forem), GraphQL do Hashnode e cross-posting com canonical_url sem penalidade de SEO.

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

🔑 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

Forem

Engine open-source que roda o Dev.to e comunidades irmãs.

API key

Credencial pessoal e revogável — sem scopes, acesso total à conta.

Header

Nome literal: api-key (não Authorization).

Rate limit

~30 req/30s — gentil mas existe.

2

📝 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

REST

Endpoints HTTP convencionais com verbos GET/POST/PUT.

body_markdown

Corpo em Markdown — o Forem renderiza no servidor.

published

Boolean que controla rascunho vs publicado.

tags

Máx. 4, todas devem existir no catálogo do Dev.to.

3

🎨 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

Frontmatter

Bloco YAML entre --- no topo do markdown.

cover_image

URL pública HTTPS — 1000×420px é o ideal.

series

Agrupa posts numa sequência navegável no perfil.

Precedência

Frontmatter ganha do JSON top-level.

4

🌐 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

GraphQL

Query language com endpoint único e schema tipado.

PAT

Personal Access Token — substitui senha em automações.

publicationId

ID do seu blog Hashnode — necessário em toda mutation de post.

Playground

UI interativa que documenta o schema vivo.

5

✏️ 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

Mutation

Operação GraphQL que altera estado (create/update/delete).

Variables

Parâmetros tipados injetados na query — evita SQL-injection-like.

Input type

Objeto tipado (ex.: PublishPostInput) com campos validados.

errors[]

Mesmo com HTTP 200, parseie esse array antes de seguir.

6

🔗 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_url e originalArticleURL sempre 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

Canonical

URL "oficial" que o Google deve indexar entre cópias.

Duplicate content

Mesmo HTML em múltiplas URLs — confunde indexação.

SEO juice

Sinal de relevância concentrado no canonical.

Cross-posting

Republicar conteúdo em múltiplas plataformas com referência ao original.

🎯 Resumo do Módulo

DEV API Key gerada — uma chave por integração, copiada no momento da criação.
POST /api/articles dominado — header api-key, body {"article": {...}} com published e tags.
Frontmatter + cover_image — YAML embutido no markdown, cover via URL HTTPS pública.
Hashnode GraphQL conectado — endpoint gql.hashnode.com, PAT no header Authorization sem prefixo.
Mutation publishPost executada — query fixa, variables tipadas, atenção a errors[] mesmo com 200.
Cross-posting sem penalidadecanonical_url / originalArticleURL apontando para o domínio próprio.

Próximo Módulo:

4.4 — Medium, Substack e plataformas sem API aberta