Pular para o conteúdo

Headless

No modo headless, o conteúdo continua sendo escrito e publicado no Canverly, mas quem desenha as páginas é um front seu. Esse front lê os posts pela API pública de leitura, que já está em produção, e monta o HTML do jeito que quiser.

O preço é que tudo o que a plataforma faz na hora de renderizar a página passa a ser responsabilidade do seu front. A lista está em O que o front próprio deixa de ter.

Use uma chave secreta (ck_…) criada só com o escopo posts:read.

  • Crie a chave no admin, em Configurações → Integrações API. Ela aparece uma única vez; o Canverly guarda apenas o hash.
  • Os escopos são opcionais por chave: a chave recebe só o que foi pedido na criação. Para ler posts basta posts:read. Não peça posts:write para o front: com ele, a chave consegue publicar e editar.
  • Cada chave vale para um site. O site é deduzido da chave, nunca de um parâmetro da requisição.

A chave vai no cabeçalho, em toda chamada:

Janela do terminal
curl -s "https://api.canverly.com/v1/posts?limit=10" \
-H "Authorization: Bearer ck_SUA_CHAVE"

A ck_ é segredo. Ela vive numa variável de ambiente ou num cofre de segredos do servidor que renderiza o seu front, e nunca vai para o navegador.

  • Não coloque a chave em variável com prefixo público (PUBLIC_*, NEXT_PUBLIC_*, VITE_*). Esses prefixos existem justamente para embutir o valor no JavaScript que o navegador baixa.
  • As rotas de leitura não devolvem cabeçalho de CORS para uma ck_. Chamar a API com ela a partir do navegador não funciona, de propósito.
  • Mandar uma ck_ na query (?key=ck_…) é recusado com 400, porque ela apareceria em log de acesso, no Referer e no histórico do navegador.

A chave pública de leitura (pk_…) foi feita para o widget: ela fica no HTML da página e só funciona nas origens cadastradas na própria chave, conferidas pelo cabeçalho Origin. Para um front que busca o conteúdo no servidor, a ck_ só com posts:read é o caminho. Detalhes em Chaves, escopos e restrições.

  • A chave pode ter restrições por IP/CIDR, região e janela de horário. Para um front com IP de saída fixo, a restrição de IP é uma boa camada extra. Requisição barrada recebe 403 com a dimensão que barrou.
  • As rotas de leitura aceitam 600 requisições por minuto por chave. Acima disso, 429 com Retry-After. Respeite o cabeçalho e use cache no seu lado (ver Cache e atualização).

Dois endpoints cobrem o front inteiro.

Endpoint Para quê
GET /v1/posts Listagem paginada do acervo público, com filtros. Sem o corpo do post.
GET /v1/posts/{reference} Um post, por slug ou id, com o corpo já em HTML (content_html).

Os dois só enxergam o que está publicado. Rascunho, post agendado para o futuro e post restrito nunca são selecionados.

A listagem devolve items (cada um no formato PublicPost) e next_cursor.

Campo Observação
id ULID de 26 caracteres.
slug Use na URL do seu front.
title, excerpt Texto puro.
published_at, updated_at RFC-3339, em UTC.
cover_url URL absoluta da capa, ou null quando não há imagem.
author Autor do post, ou null. Trate o null.
categories, tags Listas de termos.
url Endereço canônico do post no site da plataforma, não no seu front.
reading_time Minutos, no mínimo 1.
language, post_type Ex.: pt-BR, post.

O detalhe (GET /v1/posts/{reference}) traz os mesmos campos e mais o content_html, o corpo já renderizado e sanitizado.

Post que não existe, rascunho, agendado, restrito e post de outro site respondem o mesmo 404. É proposital: um 403 confirmaria que o rascunho existe. No seu front, trate o 404 da API como 404 da página.

A paginação é por cursor, do mais novo para o mais antigo. Não existe número de página, total nem has_more.

  1. Peça a primeira página: GET /v1/posts?limit=10.
  2. Se a resposta trouxer next_cursor, a próxima página é a mesma chamada com &cursor=<next_cursor>.
  3. next_cursor ausente ou null significa fim do acervo.
Janela do terminal
curl -s "https://api.canverly.com/v1/posts?limit=10&cursor=eyJwIjoi..." \
-H "Authorization: Bearer ck_SUA_CHAVE"
  • limit vai de 1 a 50 (padrão 20). Fora da faixa é 400: o valor nunca é cortado em silêncio.
  • O cursor é opaco. Não o decodifique nem o monte; cursor inválido é 400.
  • Publicar um post no meio da navegação não faz ninguém pular ou repetir item.
  • Na prática, o seu front oferece “mais antigos” (e “voltar ao início”). Página N direta não é possível com cursor.

Os filtros de GET /v1/posts combinam com a paginação:

Parâmetro Efeito
category Slug de categoria.
tag Slug de tag.
q Busca em título e resumo.
since RFC-3339. Só posts com updated_at >= since. Serve para sincronização incremental.
language Tag BCP-47, ex. pt-BR.

A página de categoria do starter é GET /v1/posts?category=<slug>. Os detalhes de cada parâmetro estão em Listar e ler posts.

No Canverly, o corpo de um post é um documento de blocos, não HTML. Para o front, a API renderiza esse documento no servidor e entrega o resultado pronto em content_html. O seu front não precisa conhecer o formato de blocos.

Bloco HTML em content_html
paragraph <p>
heading <h1> a <h6> (padrão <h2>)
list <ul class="cv-list cv-list-bullet"> ou <ol class="cv-list cv-list-ordered">
quote <blockquote class="cv-quote">
callout <aside class="cv-callout …">
code <pre class="cv-code">
image <figure class="cv-img"><img …><figcaption class="cv-caption">
gallery <div class="cv-gallery">
table <table class="cv-table">
divider <hr class="cv-divider">
columns Uma <section class="cv-column"> por coluna, em sequência (sem grade)
embed, video Um link: <p class="cv-embed-link"><a href="…"> (sem player)
html <div class="cv-html"> com o HTML do bloco, sanitizado

As classes cv-* são as mesmas do site da plataforma. Estilize-as no seu CSS; a API não manda folha de estilo.

  • Blocos de anúncio (ad), cartão de produto (product_card) e página de story (story_page) saem vazios, por decisão documentada na OpenAPI.
  • Qualquer outro tipo de bloco que o renderizador não conhece também sai vazio, sem aviso. É o caso, hoje, do bloco de posts relacionados (related_posts): se quiser a seção, monte-a com GET /v1/posts?category=, usando a primeira categoria do post, e tire o próprio post da lista.
  • Vídeo e embed não viram <iframe>: viram link. Se quiser o player, monte-o no seu front a partir do link.

A API sanitiza o HTML na saída, com uma allowlist fechada: sem <script>, <iframe> nem <form>, e embed de vídeo vira link. Mesmo assim, trate content_html como HTML vindo de outro sistema e sanitize de novo no seu servidor, com a mesma allowlist, antes de injetar. O starter faz isso. Se um dia algo mudar do lado de lá, o seu front continua sem XSS.

  • Capa: cover_url é uma URL absoluta da CDN, ou null. A API ainda não informa as dimensões da capa: reserve o espaço pelo CSS (proporção fixa) para evitar deslocamento de layout.
  • Imagens do corpo: chegam como <img src="…" alt="…" loading="lazy"> dentro de <figure class="cv-img">, sem width e height. Reserve o espaço pelo CSS. Bloco de imagem sem endereço sai vazio.
  • Política de conteúdo: as imagens vêm de um domínio de CDN, não do seu. Libere esse domínio no img-src da sua CSP.
Rota Cache-Control da API
GET /v1/posts public, max-age=60, stale-while-revalidate=300
GET /v1/posts/{reference} public, max-age=300, stale-while-revalidate=600
  • As respostas trazem ETag. Mande de volta em If-None-Match e, se nada mudou, a resposta é 304 sem corpo.
  • Não existe, hoje, aviso da plataforma para o seu front quando um post é publicado, editado ou despublicado. Um post novo aparece no seu front quando o cache (o da API e o seu) expira. Se o seu front guarda cópia local, use GET /v1/posts?since=<última sincronização> para buscar só o que mudou.

O corpo de erro e a lista completa de códigos estão em Erros e limites. No front:

Status Quando O que o front faz
304 If-None-Match casou Reusa a cópia que já tem.
400 limit fora de 1..50, cursor inválido, ck_ na query Corrija a chamada.
401 Chave ausente, inválida ou revogada Confira a variável de ambiente.
403 Falta o escopo posts:read, ou uma restrição da chave barrou Confira a chave no admin.
404 Post inexistente ou não público Responda 404 na sua página.
429 Mais de 600 req/min na chave Espere o Retry-After; aumente o seu cache.

Estas rotas também aceitam ck_, mas cada uma pede um escopo além de posts:read. Só adicione o escopo se o seu front precisar do dado.

Endpoint Escopo O que traz
GET /v1/sites/me sites:read Identidade do site: id, slug, primary_domain, default_language.
GET /v1/sites/me/seo seo:read SEO do site.
GET /v1/posts/{reference}/seo seo:read SEO do post.
GET /v1/sites/me/settings site:read O objeto settings do site, inteiro.

O site da plataforma faz uma série de coisas na hora de renderizar a página. No modo headless, nada disso chega ao seu front. Cada item abaixo passa a ser trabalho seu, ou deixa de existir.

O site da plataforma guarda a página HTML inteira em cache e a invalida quando o conteúdo muda. O seu front recebe só o cache HTTP da API, descrito acima, e não é avisado de publicação. Montar e invalidar o cache das suas páginas é com você.

  • Sitemaps. A plataforma gera os sitemaps do site. O seu front precisa gerar os dele, a partir de GET /v1/posts (percorrendo o cursor) ou de uma cópia local sincronizada com since.
  • Dados estruturados (JSON-LD). A API não entrega JSON-LD; o seu front monta o seu, fiel ao conteúdo.
  • Canonical. O campo url de cada post aponta para o site da plataforma. O canonical das suas páginas é decisão sua. Se o site da plataforma continuar no ar com o mesmo conteúdo, os dois endereços concorrem entre si nos buscadores: decida qual é o canônico antes de publicar o front.
  • hreflang e idiomas. A plataforma monta os hreflang e o prefixo de idioma nas URLs. O seu front recebe o language de cada post; a API não informa, hoje, quais posts são traduções uns dos outros.
  • Também saem de cena: robots.txt, llms.txt, feeds RSS/Atom e os metadados Open Graph/Twitter das páginas.

As Web Stories da plataforma são páginas AMP. Elas não existem no modo headless: o bloco story_page sai vazio em content_html.

No site da plataforma, o controle de consentimento decide se o código de terceiros colado pelo dono é emitido. No seu front não há banner nem esse controle: a gestão de consentimento, e a conformidade com a LGPD, passam a ser sua.

Os blocos de anúncio (ad) e os espaços de anúncio do layout não chegam ao seu front. Se o site vive de anúncio, a monetização precisa ser montada de novo.

Também ficam só no site da plataforma: páginas de autor, busca, comentários, formulários e newsletter, e o tema. Menu, logo e cores do tema não têm leitura própria na API: só existem dentro do objeto settings inteiro (ver o aviso em Outras leituras úteis). Também não há rota pública que liste as categorias do site: o front só conhece uma categoria pelos categories dos posts.