Tout ce qu'il fait, sur une page.

Vérifié dans le code qui le fait tourner, le 2 septembre 2026. Rien ici n'est prévu ou à venir.

L'admin existe en anglais et en italien, donc ces captures sont la série anglaise.

Le schéma, c'est le formulaire.

Ce que tu écris à gauche est ce que les rédacteurs obtiennent à droite.

  • fields.text()
  • fields.textarea()
  • fields.richText()
  • fields.number()
  • fields.boolean()
  • fields.datetime()
  • fields.slug()
  • fields.select()
  • fields.relation()
  • fields.media()
  • fields.group()
  • fields.array()
  • fields.json()
  • fields.blocks()
Des drapeaux, pas des écrans
required et localized sont deux booléens sur le champ.
Slug depuis le titre
fields.slug({ from: 'title' }) se remplit seul ; select porte des options fixes.
Aussi depuis l'admin
Les collections personnalisées se créent dans l'admin et vivent en base de données.

Un contenu, quatre façons de le lire.

Prends celle que ta stack parle déjà.

Les entrées publiques ne demandent pas de clé. Les documents de back-office demandent une clé API ou un bearer token.

shellHTTP
curl 'https://cms.altovar.net/api/v1/collections/blog-post/entries?locale=en'

{ "data": [ { "id": "fdoc_16d9i3ef", "locale": "en",
              "data": { "title": "EU-first content infrastructure", … },
              "seo":  { "metaTitle": …, "metaDescription": …, "robots": "index,follow" } } ],
  "pagination": { "page": 1, "limit": 20, "total": 3, "totalPages": 1 } }

# The back-office route is the same shape, with a key:
curl -H 'x-api-key: sk_…' \
     'https://cms.altovar.net/api/v1/collections/blog-post/documents?limit=10'

Un client typé avec la même enveloppe : data et pagination.

blog.tsTS
import { OspreyClient } from '@osprey/client'

const osprey = new OspreyClient({
  baseUrl:  'https://cms.altovar.net',
  tenantId: 'altovar',
})

const { data, pagination } = await osprey
  .collection('blog-post')
  .listEntries({ locale: 'en', limit: 10 })

Requêtes seulement, en POST. GraphiQL est disponible en développement.

graphqlGQL
POST /api/v1/graphql
x-api-key: sk_…

query Posts($limit: Int) {
  documents(collectionSlug: "blog-post", status: "published", limit: $limit) {
    data { id status data }
    pagination { total }
  }
}

Les entrées arrivent dans les content collections d'Astro au build. Aucune clé sur l'edge.

src/pages/index.astroASTRO
---
import { getCollection } from 'astro:content'

const posts = (await getCollection('blog'))
  .filter((p) => p.data.locale === 'en')
  .sort((a, b) => b.data.publishedAt.localeCompare(a.data.publishedAt))
---
{posts.map((p) => (
  <article>
    <a href={'/' + p.data.slug}>{p.data.title}</a>
    <p>{p.data.excerpt}</p>
  </article>
))}
Lire le document OpenAPI →

Tu publies ici, ça apparaît là.

Les brouillons restent privés. Une entrée publiée atteint l'endpoint public en moins d'une minute, et un webhook peut reconstruire le site.

Trois statuts
draft, published, archived. Les pages ajoutent relecture, approbation et planification.
Webhook signé
HMAC-SHA256 sur le corps dans x-osprey-signature. Neuf événements, secret chiffré au repos.
En-têtes de cache
Les lectures publiques portent max-age=60 avec stale-while-revalidate, prêtes pour n'importe quel cache en amont.

Dix langues, une matrice.

Chaque champ localisé, chaque langue, comptés.

Langues de contenu
en, it, es, de, fr, nl, pl, sv, pt, ro, définies par espace de travail.
Admin
Anglais et italien, à changer depuis la barre du haut.
Couverture
Une matrice par page et par langue, et les chaînes manquantes en CSV.
Diffusion
Un paramètre de requête choisit la langue ; les alternates hreflang voyagent avec chaque entrée.

Ton domaine, en quatre étapes.

Par l'API aujourd'hui. La page de réglages arrive, et cette page le dit.

  1. Réclamer Envoie l'hôte en POST à l'endpoint des domaines. Il reste en attente.
  2. Prouver Ajoute l'enregistrement TXT _osprey-verify.<host> avec le jeton reçu.
  3. Vérifier Renvoie un POST avec verify: true. L'hôte passe sur l'espace de travail.
  4. Pointer CNAME vers la plateforme. Le certificat est émis à la première requête.
domainsHTTP
POST /api/v1/admin/domains
Authorization: Bearer <jwt>
{ "host": "www.example.com" }

{ "status": "pending",
  "verification": { "type": "TXT",
                    "name": "_osprey-verify.www.example.com",
                    "value": "osprey-verify=…" } }

Les domaines personnalisés dépendent de l'offre ou de l'add-on d'hébergement.

Un contrôle qui montre son travail.

Chaque ligne ci-dessous est une route et un écran, pas une feuille de route.

Rôles
admin, editor, author, contributor, viewer
Clés API
hachées, montrées une fois, drapeau lecture seule, expiration
Journal d'audit
qui, quoi, quand et d'où, pour chaque changement d'état
Révisions
historique des pages avec diff et restauration
Corbeille
les pages supprimées attendent ; tu les restaures ou les supprimes pour de bon
Ensemble
présence et édition partagée des pages, champ par champ
Consentement
bandeau cookies et journal des consentements avec IP hachée
Effacement et export
un appel caviarde une personne concernée ; export depuis l'admin ou la CLI
Connexion
OpenID Connect par espace de travail, ou un mot de passe local
Limites de débit
un seau par famille de routes, limites dans les en-têtes de réponse

Les pièces autour.

Deux paquets publiés, une CLI, un serveur MCP et une pile auto-hébergée.

@osprey/client
SDK typé, rendu de blocs React, gestionnaire de consentement
@osprey/astro
loader de contenu, head SEO, synchronisation des traductions
CLI
setup, init, dev, build, generate-types, link, export
Serveur MCP
seize outils via stdio, pour l'agent IA que tu utilises déjà
Auto-hébergement
un fichier compose et un assistant de premier lancement sur /setup
Assistant IA
traduit les manques, complète le SEO, écrit les alt ; éteint tant que tu ne l'allumes pas

Ton contenu. Ton serveur. Aujourd'hui.

Offre gratuite, sans carte. Hébergé dans l'UE, ou sur tes propres machines.