{
  "openapi": "3.1.0",
  "info": {
    "title": "nivel.fr — surface de contenu HTTP",
    "version": "1.0.0",
    "summary": "Surface HTTP lisible par les agents pour le site vitrine nivel.fr.",
    "description": "nivel.fr est un site vitrine statique. Il n'expose pas d'API applicative : cette spécification décrit sa surface HTTP réelle pour les agents — négociation de contenu (HTML ou Markdown via l'en-tête Accept), versions Markdown des pages, fichiers de découverte, et le format des réponses d'erreur.\n\nVersionnement : la surface est en version majeure 1. Les agents peuvent l'épingler via l'en-tête de requête `X-API-Version: 1` (optionnel ; seule la v1 existe aujourd'hui). Chaque réponse porte l'en-tête `X-Content-Version` indiquant la version servie. Les changements incompatibles sont annoncés à l'avance (voir x-deprecation-policy).\n\nLimites de débit : le contenu statique est servi sans quota bloquant. À titre indicatif, chaque réponse porte les en-têtes standard `RateLimit-Limit`, `RateLimit-Remaining` et `RateLimit-Reset` décrivant une politique « soft » généreuse ; un dépassement éventuel renverrait 429 avec `Retry-After`.",
    "x-deprecation-policy": "Les chemins publiés dans cette spécification sont stables. Tout retrait ou changement incompatible est annoncé au moins 90 jours à l'avance via l'en-tête de réponse `Deprecation` (RFC 9745) et une date de retrait dans l'en-tête `Sunset` (RFC 8594), et documenté dans /llms.txt. Aucune rupture sans préavis.",
    "contact": {
      "name": "Jocelyn Joubert",
      "email": "jocelyn.joubert@nivel.fr",
      "url": "https://nivel.fr/contact"
    },
    "license": {
      "name": "© Nivel — tous droits réservés",
      "url": "https://nivel.fr/mentions-legales"
    }
  },
  "servers": [
    { "url": "https://nivel.fr" }
  ],
  "externalDocs": {
    "description": "Documentation lisible pour agents et développeurs",
    "url": "https://nivel.fr/docs"
  },
  "tags": [
    { "name": "content", "description": "Pages du site, négociées en HTML ou Markdown." },
    { "name": "discovery", "description": "Fichiers de découverte lisibles par les agents." }
  ],
  "paths": {
    "/": {
      "get": {
        "tags": ["content"],
        "operationId": "getHomepage",
        "summary": "Page d'accueil",
        "description": "Renvoie la page d'accueil en HTML (défaut) ou en Markdown lorsque l'en-tête `Accept: text/markdown` est fourni. La réponse porte toujours `Vary: Accept`.",
        "parameters": [
          { "$ref": "#/components/parameters/AcceptContentType" },
          { "$ref": "#/components/parameters/ApiVersion" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/Page" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "5XX": { "$ref": "#/components/responses/Error" },
          "default": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/{page}": {
      "get": {
        "tags": ["content"],
        "operationId": "getPage",
        "summary": "Page de contenu par slug",
        "description": "Renvoie une page de contenu en HTML (défaut) ou en Markdown si `Accept: text/markdown`. Les URLs sont « propres » (sans extension `.html`). Répond 404 si le slug est inconnu.",
        "parameters": [
          { "$ref": "#/components/parameters/PageSlug" },
          { "$ref": "#/components/parameters/AcceptContentType" },
          { "$ref": "#/components/parameters/ApiVersion" }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/Page" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "5XX": { "$ref": "#/components/responses/Error" },
          "default": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/{page}.md": {
      "get": {
        "tags": ["content"],
        "operationId": "getPageMarkdown",
        "summary": "Version Markdown directe d'une page",
        "description": "Sert directement la version Markdown d'une page, sans négociation de contenu. Utile pour récupérer le texte brut d'une page connue.",
        "parameters": [
          { "$ref": "#/components/parameters/PageSlug" },
          { "$ref": "#/components/parameters/ApiVersion" }
        ],
        "responses": {
          "200": {
            "description": "Contenu Markdown de la page.",
            "headers": {
              "Vary": { "$ref": "#/components/headers/Vary" },
              "X-Content-Version": { "$ref": "#/components/headers/ContentVersion" },
              "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
              "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
              "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": {
              "text/markdown": { "schema": { "type": "string" } }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "5XX": { "$ref": "#/components/responses/Error" },
          "default": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "tags": ["discovery"],
        "operationId": "getLlmsTxt",
        "summary": "Guide pour agents (llms.txt)",
        "description": "Index structuré du site pour les modèles de langage et agents, avec une section « quand solliciter Nivel » décrivant les cas d'usage adaptés.",
        "parameters": [
          { "$ref": "#/components/parameters/ApiVersion" }
        ],
        "responses": {
          "200": {
            "description": "Guide au format texte.",
            "content": { "text/plain": { "schema": { "type": "string" } } }
          },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "5XX": { "$ref": "#/components/responses/Error" },
          "default": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/sitemap.xml": {
      "get": {
        "tags": ["discovery"],
        "operationId": "getSitemap",
        "summary": "Plan du site",
        "description": "Liste complète des URLs publiques du site au format sitemap XML.",
        "parameters": [
          { "$ref": "#/components/parameters/ApiVersion" }
        ],
        "responses": {
          "200": {
            "description": "Plan du site au format XML.",
            "content": { "application/xml": { "schema": { "type": "string" } } }
          },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "5XX": { "$ref": "#/components/responses/Error" },
          "default": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/robots.txt": {
      "get": {
        "tags": ["discovery"],
        "operationId": "getRobotsTxt",
        "summary": "Directives robots",
        "description": "Directives d'exploration pour les robots, incluant l'emplacement du sitemap.",
        "parameters": [
          { "$ref": "#/components/parameters/ApiVersion" }
        ],
        "responses": {
          "200": {
            "description": "Directives d'exploration au format texte.",
            "content": { "text/plain": { "schema": { "type": "string" } } }
          },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "5XX": { "$ref": "#/components/responses/Error" },
          "default": { "$ref": "#/components/responses/Error" }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "tags": ["discovery"],
        "operationId": "getOpenApi",
        "summary": "Cette spécification",
        "description": "Renvoie cette spécification OpenAPI décrivant la surface HTTP du site.",
        "parameters": [
          { "$ref": "#/components/parameters/ApiVersion" }
        ],
        "responses": {
          "200": {
            "description": "La spécification OpenAPI de la surface HTTP.",
            "content": {
              "application/json": { "schema": { "type": "object", "additionalProperties": true } }
            }
          },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "5XX": { "$ref": "#/components/responses/Error" },
          "default": { "$ref": "#/components/responses/Error" }
        }
      }
    }
  },
  "components": {
    "headers": {
      "Vary": {
        "description": "Toujours `Accept` pour signaler la négociation de contenu.",
        "schema": { "type": "string" }
      },
      "ContentVersion": {
        "description": "Version majeure de la surface de contenu servie (actuellement « 1 »).",
        "schema": { "type": "string", "example": "1" }
      },
      "RateLimitLimit": {
        "description": "Nombre de requêtes autorisées sur la fenêtre courante (politique soft, indicative).",
        "schema": { "type": "integer", "example": 600 }
      },
      "RateLimitRemaining": {
        "description": "Nombre de requêtes restantes sur la fenêtre courante.",
        "schema": { "type": "integer", "example": 599 }
      },
      "RateLimitReset": {
        "description": "Secondes avant la réinitialisation de la fenêtre.",
        "schema": { "type": "integer", "example": 60 }
      },
      "Deprecation": {
        "description": "Présent uniquement si la ressource est dépréciée (RFC 9745). Valeur : date/horodatage de dépréciation. Absent tant que la ressource est stable.",
        "schema": { "type": "string", "example": "Sun, 01 Nov 2026 00:00:00 GMT" }
      },
      "Sunset": {
        "description": "Présent uniquement si un retrait est planifié (RFC 8594). Date à partir de laquelle la ressource ne sera plus disponible ; annoncée au moins 90 jours à l'avance.",
        "schema": { "type": "string", "format": "date-time", "example": "Mon, 01 Feb 2027 00:00:00 GMT" }
      }
    },
    "parameters": {
      "AcceptContentType": {
        "name": "Accept",
        "in": "header",
        "required": false,
        "description": "Négociation de contenu. `text/markdown` renvoie la version Markdown ; toute autre valeur (ex. `text/html`) renvoie le HTML. La réponse porte `Vary: Accept`.",
        "schema": {
          "type": "string",
          "enum": ["text/html", "text/markdown"],
          "default": "text/html"
        }
      },
      "ApiVersion": {
        "name": "X-API-Version",
        "in": "header",
        "required": false,
        "description": "Épingle la version majeure de la surface. Seule la version « 1 » existe aujourd'hui ; la valeur par défaut est « 1 ». Les changements incompatibles introduiront une nouvelle version majeure, annoncée via les en-têtes `Deprecation`/`Sunset` (voir info.x-deprecation-policy).",
        "schema": {
          "type": "string",
          "enum": ["1"],
          "default": "1"
        }
      },
      "PageSlug": {
        "name": "page",
        "in": "path",
        "required": true,
        "description": "Slug de la page (sans extension).",
        "schema": {
          "type": "string",
          "enum": [
            "docs",
            "a-propos",
            "contact",
            "ressources",
            "cahier-pratique",
            "faq",
            "barometre",
            "realisations",
            "atelier-collectif",
            "atelier-ia",
            "optimisation-processus",
            "automatisation-et-ia",
            "automatisation-independants",
            "automatisation-zapier",
            "automatisation-make",
            "automatisation-n8n",
            "mentions-legales",
            "politique-de-confidentialite",
            "cgu-cgs"
          ]
        }
      }
    },
    "responses": {
      "Page": {
        "description": "Contenu de la page, en HTML ou Markdown selon l'en-tête `Accept`.",
        "headers": {
          "Vary": { "$ref": "#/components/headers/Vary" },
          "X-Content-Version": { "$ref": "#/components/headers/ContentVersion" },
          "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
          "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
          "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" },
          "Deprecation": { "$ref": "#/components/headers/Deprecation" },
          "Sunset": { "$ref": "#/components/headers/Sunset" }
        },
        "content": {
          "text/html": { "schema": { "type": "string" } },
          "text/markdown": { "schema": { "type": "string" } }
        }
      },
      "NotFound": {
        "description": "Ressource introuvable. Le corps est négocié selon `Accept` : JSON structuré RFC 9457 (`application/problem+json` ou `application/json`), Markdown (`text/markdown`) ou HTML (défaut). Le statut HTTP reste 404 dans tous les cas.",
        "headers": {
          "Vary": { "$ref": "#/components/headers/Vary" },
          "X-Content-Version": { "$ref": "#/components/headers/ContentVersion" },
          "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
          "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
          "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
        },
        "content": {
          "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } },
          "application/json": { "schema": { "$ref": "#/components/schemas/Problem" } },
          "text/markdown": { "schema": { "type": "string" } },
          "text/html": { "schema": { "type": "string" } }
        }
      },
      "Error": {
        "description": "Réponse d'erreur générique (4xx/5xx), corps structuré RFC 9457 sur `application/problem+json` ou `application/json`. Le statut HTTP réel reflète l'erreur.",
        "headers": {
          "X-Content-Version": { "$ref": "#/components/headers/ContentVersion" },
          "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
          "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
          "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" },
          "Deprecation": { "$ref": "#/components/headers/Deprecation" },
          "Sunset": { "$ref": "#/components/headers/Sunset" }
        },
        "content": {
          "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } },
          "application/json": { "schema": { "$ref": "#/components/schemas/Problem" } }
        }
      },
      "TooManyRequests": {
        "description": "Trop de requêtes (429). Corps structuré RFC 9457 ; l'en-tête `Retry-After` indique le délai avant de réessayer.",
        "headers": {
          "Retry-After": {
            "description": "Secondes à attendre avant de réessayer.",
            "schema": { "type": "integer", "example": 60 }
          },
          "RateLimit-Limit": { "$ref": "#/components/headers/RateLimitLimit" },
          "RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
          "RateLimit-Reset": { "$ref": "#/components/headers/RateLimitReset" }
        },
        "content": {
          "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } },
          "application/json": { "schema": { "$ref": "#/components/schemas/Problem" } }
        }
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "Réponse d'erreur structurée pour les agents, alignée sur RFC 9457 (Problem Details) et étendue de champs de résolution.",
        "required": ["type", "title", "status", "code"],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "URI identifiant le type d'erreur.",
            "example": "https://nivel.fr/openapi.json#not-found"
          },
          "title": {
            "type": "string",
            "description": "Résumé court et lisible du type d'erreur.",
            "example": "Not Found"
          },
          "status": {
            "type": "integer",
            "description": "Statut HTTP.",
            "example": 404
          },
          "detail": {
            "type": "string",
            "description": "Explication lisible par un humain, spécifique à cette occurrence."
          },
          "code": {
            "type": "string",
            "description": "Code d'erreur stable et lisible par machine.",
            "example": "not_found"
          },
          "hint": {
            "type": "string",
            "description": "Piste de résolution concrète."
          },
          "links": {
            "type": "object",
            "description": "Ressources utiles pour se réorienter.",
            "properties": {
              "sitemap": { "type": "string", "format": "uri" },
              "llms": { "type": "string", "format": "uri" },
              "openapi": { "type": "string", "format": "uri" }
            }
          }
        }
      }
    }
  }
}
