Le schéma dans le code. Le contenu par HTTP.

Définis tes collections en TypeScript, modifie-les dans l'admin, lis-les via REST, GraphQL ou le loader Astro. Hébergé dans l'UE, ou sur tes propres serveurs.

L'admin Osprey en train de modifier une page publiée directement sur place : la page en direct au centre avec quatorze champs modifiables encadrés, la liste des champs à gauche, le sélecteur de langue et le bouton Publier dans la barre. L'admin Osprey en train de modifier une page publiée directement sur place : la page en direct au centre avec quatorze champs modifiables encadrés, la liste des champs à gauche, le sélecteur de langue et le bouton Publier dans la barre.
L'admin, en train de modifier une page en direct. Une capture, pas une maquette. L'admin existe en anglais et en italien, donc ces captures sont la série anglaise.

Quatre choses que tu peux vérifier.

  • Le schéma Quatorze types de champ Texte, textarea, texte riche, nombre, booléen, date et heure, slug, select, relation, média, groupe, tableau, JSON, blocs. Tous exportés par le paquet core.
  • La lecture publique Sans clé, 404 par défaut Une collection se lit sans clé seulement si sa règle d'accès admet un appelant anonyme. Tout le reste répond 404, jamais 403.
  • Les langues Dix langues de contenu Champs localisés, une matrice de couverture par page et par langue, et les manques en CSV.
  • Les clés Hachées, montrées une fois Les clés API sont stockées sous forme de hachage HMAC avec pepper. Tu vois la clé à la création ; l'admin garde les quatre derniers caractères.
Chaque fonction, en détail →

D'un fichier TypeScript à une page sur le web.

Fais défiler. Le même contenu, à chacune de ses quatre étapes.

  1. 01 · Schéma

    Tu définis la collection en TypeScript.

    Slug, champs, règles d'accès. Elle vit dans ton dépôt et passe en revue comme n'importe quel autre code.

    content/blog-post.tsTS
    import { defineCollection, fields } from '@osprey/core'
    
    export const blogPost = defineCollection({
      slug: 'blog-post',
      access: { read: () => true },
      fields: {
        slug:        fields.slug({ from: 'title' }),
        locale:      fields.text({ required: true }),
        title:       fields.text({ required: true }),
        excerpt:     fields.textarea(),
        body:        fields.richText({ required: true }),
        publishedAt: fields.datetime(),
      },
    })
  2. 02 · Admin

    L'admin lit le schéma et dessine le formulaire.

    Champs obligatoires, sélecteur de langue, corps en texte riche. Rien n'est configuré deux fois.

  3. 03 · API

    Les entrées publiées sortent en JSON.

    Un GET, sans clé, avec SEO, canonical et hreflang déjà fusionnés dans chaque entrée.

    entriesHTTP
    GET /api/v1/collections/blog-post/entries?locale=en
    Host: cms.altovar.net
    
    200 OK
    { "data": [ {
        "id": "fdoc_16d9i3ef",
        "data":       { "title": "EU-first content infrastructure", … },
        "seo":        { "metaTitle": "EU-first content infrastructure", … },
        "canonical":  "https://…/en/eu-first-content-infrastructure",
        "alternates": { "en": …, "x-default": … }
      } ],
      "pagination": { "page": 1, "limit": 20, "total": 3 } }
  4. 04 · Site

    Ton frontend les affiche.

    Le loader Astro récupère les entrées au build. Le blog ci-dessous est construit exactement ainsi, depuis ce CMS.

    Ouvrir le blog en direct →
    src/content.config.tsASTRO
    import { defineCollection } from 'astro:content'
    import { ospreyLoader } from '@osprey/astro'
    
    const blog = defineCollection({
      loader: ospreyLoader({
        collection: 'blog-post',
        baseUrl:    'https://cms.altovar.net',
        tenantId:   'altovar',
      }),
    })

L'admin, dans ton thème et ta langue.

Clair ou sombre, anglais ou italien. Chaque capture de ce site suit le thème que tu choisis dans l'en-tête : c'est le même admin, photographié deux fois.

Ton contenu. Ton serveur. Aujourd'hui.

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