Protocolo de publicação do Lume — v0.1
Especificação pública. Estado: rascunho v0.1 (2026-10-27). Primeira implementação: o site da Shore (
www.shore.pt). Pode ser implementada por qualquer site, agência ou CMS. Mudanças incompatíveis só numa versão nova (/v1).
Para que serve
O Lume escreve, revê e aprova notícias e artigos. Este protocolo é a porta pela qual o Lume os entrega a um site. O site continua a mandar:
- decide que campos, idiomas e categorias existem;
- escolhe quem pode publicar, através do âmbito do token;
- guarda tudo no seu próprio backoffice, com a sua auditoria.
O Lume nunca cria autores. Só escolhe entre os que o site expõe.
Princípios
- O site declara, o Lume adapta-se.
GET /capabilitiesdiz que tipos, campos, limites e blocos existem. O Lume não envia o que o site não aceita. - Conteúdo estruturado, não HTML. O corpo é uma lista de blocos portáveis (§6). O site desenha-os com o seu design.
- Nada se publica por engano. Criar e publicar são pedidos separados, com âmbitos separados. Um token só com
entries:draftnunca torna nada público. - Nada se duplica.
POST /entriesexigeIdempotency-Key. Repetir o mesmo pedido devolve a mesma entrada. - Erros ambíguos não se repetem às cegas. Depois de um erro 5xx ou de uma resposta que não chegou, o cliente consulta
GET /entries/{id}ou repete com a mesmaIdempotency-Key.
1. Descoberta
GET https://<site>/.well-known/lume.json (público, sem autenticação)
{
"protocol": "lume-publishing",
"version": "0.1",
"api": "https://www.shore.pt/api/lume/v0",
"auth": { "type": "bearer" },
"site": { "name": "Shore", "url": "https://www.shore.pt", "locales": ["pt-PT", "en"], "defaultLocale": "pt-PT" }
}
Os caminhos abaixo são relativos a api.
2. Autenticação e âmbitos
- Token: cada pedido leva
Authorization: Bearer <token>.- O token é criado no backoffice do site por um administrador, como uma «ligação», e mostrado uma única vez.
- O site guarda só um hash e pode revogá-lo a qualquer momento.
- Âmbitos:
| Âmbito | Permite |
|---|---|
authors:read |
GET /authors |
entries:draft |
GET /entries, GET /entries/{id}, POST /entries, PATCH /entries/{id} |
media:write |
POST /media |
entries:publish |
POST /entries/{id}/publish e alterar entradas já publicadas |
- Ações em nome da ligação: cada ação fica registada na auditoria do site em nome da ligação, por exemplo um utilizador técnico «Lume».
- Limite de edição: o cliente só pode alterar entradas que criou com essa ligação.
3. Erros
Todas as respostas de erro têm a mesma forma:
{ "error": { "code": "validation_failed", "message": "O título SEO tem mais de 220 caracteres.", "field": "locales.pt-PT.seo.title" } }
| HTTP | code |
Quando |
|---|---|---|
| 400 | bad_request |
JSON inválido, cabeçalho em falta |
| 401 | unauthorized |
Token em falta, inválido ou revogado |
| 403 | forbidden |
O token não tem o âmbito necessário |
| 404 | not_found |
A entrada não existe ou não pertence à ligação |
| 409 | slug_taken, entry_published, conflict |
Slug ocupado; entrada publicada sem entries:publish; outro conflito |
| 413 | payload_too_large |
Ficheiro acima do limite |
| 415 | unsupported_media_type |
Tipo de ficheiro não aceite |
| 422 | validation_failed |
Campo inválido; field aponta o caminho |
| 5xx | internal_error |
Erro do site. Ambíguo: o pedido pode ou não ter sido aplicado |
message: é para pessoas, no idioma por omissão do site.field: usa caminhos com pontos, por exemplolocales.en.body.3.text.
4. GET /capabilities
{
"protocol": "lume-publishing",
"version": "0.1",
"site": { "name": "Shore", "url": "https://www.shore.pt", "locales": ["pt-PT", "en"], "defaultLocale": "pt-PT" },
"scopes": ["authors:read", "entries:draft", "media:write"],
"types": [
{
"type": "news",
"name": { "pt-PT": "Notícia", "en": "News" },
"locales": { "required": ["pt-PT", "en"], "aligned": true },
"slug": { "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", "max": 160, "perLocale": false },
"categories": [{ "key": "cybersecurity", "name": { "pt-PT": "Cibersegurança", "en": "Cybersecurity" } }],
"newCategories": false,
"fields": {
"title": { "max": 220 },
"excerpt": { "max": 700 },
"intro": { "max": 6000 },
"seo.title": { "max": 220, "recommended": 60 },
"seo.description": { "max": 500, "recommended": 155 }
},
"body": { "blocks": ["heading", "paragraph", "list", "quote", "image"], "headingLevels": [2, 3], "startsWithHeading": true, "maxBlocks": 400, "textMax": 6000 },
"image": { "requiredToPublish": false, "minWidth": 1200, "types": ["image/jpeg", "image/png", "image/webp"], "maxBytes": 10485760 },
"author": { "requiredToPublish": true, "mustBePublished": true },
"reviewer": { "supported": true, "distinctFromAuthor": true },
"sources": { "stored": true, "displayed": false },
"preview": false,
"scheduling": false
}
]
}
locales.aligned: true: o corpo de todos os idiomas tem a mesma sequência de blocos (mesmo tipo e, nas listas, o mesmo número de itens). Sites que guardam cada idioma lado a lado precisam disto.body.startsWithHeading: o corpo começa num título de nível 2. O texto antes do primeiro título vai emintro.scopes: são os da ligação que fez o pedido.
5. Autores e entradas existentes
GET /authors
{ "authors": [ { "id": "2c2e…", "name": "Maria Silva", "slug": "maria-silva", "status": "published",
"jobTitle": { "pt-PT": "Diretora de Tecnologia", "en": "Technology Director" },
"url": "https://www.shore.pt/autores/maria-silva", "image": "https://…" } ] }
- Estados:
statusépublished,draftouarchived. Só um autorpublishedserve para publicar.
GET /entries?type=news
{ "entries": [ { "id": "…", "type": "news", "status": "published", "slug": "nis2-portugal",
"title": { "pt-PT": "…", "en": "…" }, "url": { "pt-PT": "https://…", "en": "https://…" },
"publishedAt": "2026-07-25T10:30:00Z", "updatedAt": "…", "managed": false } ] }
- Para que serve: saber que slugs estão ocupados, sugerir ligações internas e evitar repetir temas.
managed: étruenas entradas criadas por esta ligação.
GET /entries/{id}
Devolve { "entry": Entry } (§7). Só funciona em entradas criadas por esta ligação. Inclui alterações feitas no backoffice, por exemplo se alguém publicou à mão.
6. Criar e atualizar
POST /entries
- Cabeçalho obrigatório:
Idempotency-Key: <até 200 caracteres>. - Resposta:
201 { "entry": Entry }quando cria.200, com a mesma entrada, quando a chave já foi usada por esta ligação; nesse caso o corpo é ignorado.
{
"type": "news",
"slug": "nis2-o-que-muda-em-2027",
"category": "cybersecurity",
"authorId": "2c2e…",
"reviewerId": null,
"image": { "mediaId": "news/2026/9f1c….webp", "alt": { "pt-PT": "…", "en": "…" }, "position": "center" },
"locales": {
"pt-PT": {
"title": "…", "excerpt": "…", "intro": "…",
"body": [
{ "type": "heading", "level": 2, "text": "O que muda" },
{ "type": "paragraph", "text": "…" },
{ "type": "list", "ordered": false, "items": ["…", "…"] },
{ "type": "heading", "level": 3, "text": "…" },
{ "type": "quote", "text": "…", "cite": "…" },
{ "type": "image", "mediaId": "news/2026/…", "alt": "…", "caption": "…" }
],
"seo": { "title": "…", "description": "…" }
},
"en": { "…": "…" }
},
"sources": [ { "title": "Diretiva (UE) 2022/2555", "url": "https://eur-lex.europa.eu/…" } ],
"lume": { "publicationId": "…", "approvedBy": "Ana Reis", "aiAssisted": true }
}
- Texto: é texto simples, sem HTML nem Markdown. Ligações dentro do texto ficam para uma versão futura.
- Estado: a entrada nasce em rascunho.
PATCH /entries/{id}
- Corpo: o mesmo de
POST, semtype. Substitui o conteúdo inteiro. - Estado: não muda (rascunho continua rascunho).
- Entrada já publicada: atualizá-la muda a página pública. Exige
entries:publish; sem ele dá409 entry_published.
POST /entries/{id}/publish (entries:publish)
- Corpo: vazio.
- Resposta:
{ "entry": Entry }, comstatus: "published",publishedAteurl. - Validação: o site aplica as suas regras de publicação, por exemplo autor publicado; se falharem, devolve
422com o motivo. - Confirmação: o cliente só considera publicado depois de abrir o
urle encontrar a página.
7. Entrada (Entry)
{ "id": "…", "type": "news", "status": "draft", "slug": "…",
"url": { "pt-PT": "https://www.shore.pt/noticias/…", "en": "https://www.shore.pt/en/noticias/…" },
"publishedAt": null, "updatedAt": "2026-10-27T10:00:00Z", "editUrl": "https://www.shore.pt/backoffice" }
status:draft,publishedouarchived.url: é o endereço que a página terá ou tem. Só responde quandostatusépublished.
8. Ficheiros: POST /media (media:write)
- Pedido: o corpo é o próprio ficheiro, com
Content-Typeigual ao tipo real, por exemploimage/webp. O nome original vai, opcionalmente, emX-Filename. - Validação: o site confirma a assinatura do ficheiro, não só o cabeçalho.
- Resposta:
{ "media": { "id": "news/2026/9f1c….webp", "url": "https://…/9f1c….webp", "type": "image/webp", "bytes": 182311 } }
- Utilização: o
idé o que vai emimage.mediaIde nos blocosimage. - Texto alternativo: vai na entrada, em cada idioma, não no ficheiro.
9. Segurança
- Ligação: só HTTPS.
- Token: é um segredo. O site guarda só um hash e mostra o prefixo para o identificar.
- Acesso: o token só vê e altera as suas entradas. Autores e entradas publicadas são de leitura.
- Registos: o site regista cada escrita na auditoria. O cliente nunca regista o token.
10. Versões futuras (não implementado)
- Webhook
site → Lume(entry.published,entry.updated,entry.unpublished), assinado com HMAC. - Ligações e ênfase dentro do texto.
- Pré-visualização (
previewUrl) e agendamento. - Slug por idioma e outros tipos: caso de cliente, evento, vaga.