Corps des cartes
Le corps d'une carte est un document en blocs — validé à l'écriture, incapable de transporter du HTML, et toujours lisible aussi en texte brut.
Chaque carte a un corps, et c'est un document fait de blocs — pas une chaîne Markdown, pas du HTML.
Deux colonnes, une règle
Une carte porte son corps deux fois :
blocks— le document : une liste ordonnée de blocs typés ;body— la projection en texte brut de ce document, sans aucun marqueur.
La règle d'écriture est : jamais les deux.
Écrire blocks pose le document et en recalcule body. Écrire body pose le
texte et ramène le document à l'état dérivé de ce texte.
La lecture est plus simple : l'API renvoie toujours les deux. Une carte écrite avant l'arrivée des corps en blocs n'a pas de document stocké, et on en dérive un de son texte à la lecture — aucun consommateur n'a donc à savoir sous quel régime se trouve une carte, et aucun n'a à analyser du Markdown pour afficher du contenu.
Types de bloc
paragraph, heading, bulleted, numbered, todo, quote, callout,
code, divider, image, embed, bookmark, toggle.
En réponse, type est une chaîne ouverte. La liste ci-dessus est ce qu'on
peut écrire, et l'API refuse tout le reste à l'écriture — mais un corps que vous
lisez pourra un jour porter un type qui n'existait pas quand vous avez écrit
votre rendu. Rendez un type inconnu en dégradé (son text, s'il en a un) plutôt
que d'échouer, et renvoyez-le tel quel si vous réenregistrez le document.
Chaque bloc a un id stable (un UUIDv7, conservé d'une écriture à l'autre) et un
indent compris entre 0 et 5. Puis, selon le type : text pour les blocs
textuels, level pour un titre, checked pour une case à cocher, code et
language pour du code, fileId et alt pour une image, url (plus embedUrl et provider) pour un
embed, url plus un title et une description facultatifs pour un signet.
Le texte est fait de segments, pas de balisage
Le text d'un bloc textuel est une liste de segments (spans) : une chaîne
et ses marques.
{
"type": "paragraph",
"text": [
{ "text": "See the " },
{ "text": "reference", "link": "https://cms.spunto.net/api/docs" },
{ "text": " for details. " },
{ "text": "Required.", "bold": true }
]
}Les marques sont bold, italic, code, strike et link. Un lien est une
URL absolue http, https ou mailto.
Un segment peut aussi être une mention d'une autre carte — { "text": "Bug de connexion", "mention": "<id de la carte>" }. Voir
les mentions.
Il n'y a rien à assainir
Le format ne peut pas exprimer de HTML. Pas « le HTML est retiré » — il n'y a aucun endroit où le mettre. Un segment est une chaîne et un ensemble de marques booléennes, et la liste fermée des types de bloc est imposée à l'écriture.
C'est toute la raison d'être de cette forme. Un corps en Markdown ou en HTML aurait rendu chaque consommateur responsable d'assainir le même contenu, correctement, pour toujours.
Les images sont des identifiants
Un bloc image porte un fileId, jamais une URL.
Dans ce produit, les fichiers n'ont aucune adresse partageable — les octets sont servis par l'API depuis une route scopée, avec la même autorisation que tout le reste. Chaque consommateur compose l'URL qu'il sait servir. Voir fichiers et pièces jointes.
alt sert aussi de légende, et c'est lui qui atterrit dans la projection texte.
Les embeds sont une liste blanche
Un bloc embed montre une vidéo, une maquette ou un bac à sable de code dans la
carte. On écrit l'adresse de la page — { "type": "embed", "url": "https://youtu.be/…" } —
et l'API ne l'accepte que de YouTube, Vimeo, Loom, Figma, Google Docs, Google
Sheets, Google Slides, CodeSandbox ou CodePen, en https. Le reste est un 400
qui les liste — pour toute autre page, écrivez un
signet.
Un document Google est montré par sa page /preview, jamais /edit : le cadre
montre ce que le lecteur a le droit de voir chez Google. Un document partagé
à « tous ceux qui ont le lien » s'affiche pour tout le monde ; un document
restreint ne s'affiche qu'aux lecteurs connectés à un compte Google qui y a
accès, et les autres voient la page « accès refusé » de Google. Rien du partage
Google ne passe par Spunto. Un document publié sur le Web (/d/e/2PACX-…/pub)
est montré par son lecteur de publication.
L'API ajoute ensuite deux champs qu'on n'écrit jamais : provider, et
embedUrl, l'adresse du lecteur du fournisseur. C'est elle qui va dans une
<iframe> ; l'API la calcule à chaque écriture, donc aucun consommateur ne
recompose l'adresse d'un lecteur, et ce qu'un client envoie pour ces deux champs
est ignoré. Pour vérifier un lien avant de l'écrire,
GET /workspaces/{workspaceId}/embeds/resolve?url=… répond avec la même règle.
La projection texte d'un embed est son url.
Le document ne porte toujours aucun balisage : un embed est une adresse prise dans une liste fermée, pas une iframe.
Les signets : toute autre page
Un bloc bookmark est une carte de lien — titre, description, domaine — pour une
page qui n'est pas dans la liste des embeds : { "type": "bookmark", "url": "https://…", "title": "…" }. Rien n'est chargé dans la carte ; c'est un lien qui
s'ouvre dans un nouvel onglet. title et description sont du texte brut,
bornés à 300 et 1 000 caractères, et facultatifs (sans titre, on montre le
domaine).
POST /workspaces/{workspaceId}/link-previews { "url" } dit ce que deviendrait
une adresse collée : un embed (calculé sans rien charger) ou un signet, avec le
titre et la description de la page quand l'API a pu les lire. La lecture d'une
page par le serveur est gardée : adresses publiques seulement, vérifiées après
la résolution DNS et de nouveau à chaque redirection (donc ni localhost, ni
réseau privé, ni serveur de métadonnées du cloud), ports 80 et 443 seulement,
4 secondes et 512 Kio au plus, du HTML seulement. Une page illisible — ou qu'elle
refuse de lire — donne quand même un signet, sans titre. Ça n'a lieu qu'une fois,
au moment du coller : ouvrir une carte ne fait jamais rien charger au serveur.
La projection texte d'un signet est son titre et son adresse.
Les dépliables
Un bloc toggle est une ligne qu'on clique pour montrer ce qu'il y a dessous.
Son propre text est cette ligne ; son contenu, ce sont les blocs qui le
suivent avec un indent plus grand — la même règle que celle qui imbrique un
élément de liste, donc pas de champ children.
Ouvert ou fermé n'est pas dans le document : c'est un état de lecture, propre
à chacun, et tout dépliable s'ouvre fermé. Le stocker ferait de chaque clic une
écriture. Le contenu d'un dépliable fermé reste dans blocks et dans la
projection texte — il est caché, pas retiré.
Les mentions : citer une carte par son id
Dans l'éditeur, @ ou [[ ouvre une recherche parmi les cartes de l'espace ;
en choisir une écrit un segment qui porte l'id de la carte :
{ "text": "Bug de connexion", "mention": "0190a1b2-…" }L'id est vérifié à l'écriture — une carte de cet espace, ou un 400. Ce qui
couvre aussi une carte d'un autre espace : d'ici, elle est simplement
introuvable. Une mention déjà présente continue de s'enregistrer après la
suppression de sa carte.
text est un instantané du titre au moment de l'écriture. C'est ce qui atterrit
dans la projection texte et ce qu'affiche un client qui ignore les mentions —
mais pas ce qu'il faut rendre. ItemDetail.mentions liste chaque carte citée
avec sa clé (SPR-12), son title, son icon et sa homeCollectionId
actuels : la mention suit un renommage sans que la carte qui cite soit
réécrite. Une carte absente de cette liste a été supprimée : rendez-la de façon
neutre, pas avec l'instantané. En ?version=published, seules les cartes
publiées sont résolues, avec leur titre publié — un site ne lit jamais le
titre d'un brouillon par une mention.
Les corps ne sont pas dans les résultats de liste
Item — ce que renvoie une page de vue — n'a pas de corps.
Seul ItemDetail porte body et blocks, et seules les routes qui renvoient
une carte unique l'utilisent. Une vue de 50 lignes ne va pas chercher 50
documents.
Ni body ni blocks ne peuvent servir au tri ou au filtre. Il n'existe aucun
index de recherche sur les corps, donc rien d'honnête à proposer : la
recherche de l'espace de travail porte sur les titres
des cartes et les noms des listes, et le dit.
Sprints, points et burndown
Un sprint est une liste qui porte des dates. Ses cartes restent dans leur backlog ; les points d'effort, le burndown et la vélocité se lisent dans le journal de l'espace et dans ce que chaque sprint contient.
Utiliser l'API
URL de base, authentification, erreurs et client généré — ce qui vaut pour toutes les routes de l'API REST du CMS headless Spunto.