# Widgets Capturia — guide d'intégration et API publique

Ce document décrit la totalité de la surface publique du système de widgets
embeddables Capturia : les 4 widgets disponibles, les deux modes
d'intégration (iframe et web component), l'API JSON sous-jacente, la
licence applicable, le tracking anonyme côté serveur et la politique de
rate-limit.

> **Public visé** : journalistes, écoles, agences SEO, intégrateurs
> partenaires souhaitant embarquer les scores Capturia sur leurs pages.
>
> **Licence des données** : [Creative Commons CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.fr).
> Réutilisation libre, y compris commerciale, sous réserve de citer la
> source « Capturia » et de garder visible le lien `capturia.ecole-futee.com`
> injecté par le widget.

---

## 1. Les 4 widgets disponibles

| Widget         | Description courte                                                                 | URL iframe                                          | Web component                                                       |
| -------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------- |
| `badge`        | Carte 320×240 — score d'exposition IA d'un métier + tag + lien fiche.              | `/embed/badge/<slug>`                               | `<capturia-badge data-metier="<slug>">`                           |
| `sector`       | Carte 600×500 — top 5 métiers d'un secteur les moins exposés à l'IA.               | `/embed/sector/<slug>`                              | `<capturia-sector data-secteur="<slug>" data-limit="5">`          |
| `compare`      | Carte 800×400 — comparateur 2 métiers côte à côte (score, niveau, salaire médian). | `/embed/compare?a=<slugA>&b=<slugB>`                | `<capturia-compare data-a="<slugA>" data-b="<slugB>">`            |
| `score-global` | Carte 280×120 — indice synthétique d'exposition IA + delta vs trimestre précédent. | `/embed/score-global`                               | `<capturia-score-global>`                                         |

### 1.1 Mode iframe (zéro JS, indexable, immédiat)

Tous les widgets exposent une route HTML autonome, ~< 50 KB, charte
Capturia figée, headers permissifs `X-Frame-Options: ALLOWALL` et
`Content-Security-Policy: frame-ancestors *`. Aucune dépendance externe,
aucun cookie posé.

```html
<iframe
  src="https://capturia.ecole-futee.com/embed/badge/kinesitherapeute"
  width="320" height="240"
  style="border:0;border-radius:14px;overflow:hidden"
  loading="lazy"
  title="Capturia — Badge métier"
  referrerpolicy="no-referrer-when-downgrade"
></iframe>
```

### 1.2 Mode web component (responsive, hydraté)

Un seul script vanilla, pas de Shadow DOM (pour rester indexable par
Googlebot). Les composants sont rendus en Light DOM et stylés via une
feuille préfixée `.ssw-`. Les liens sortants `dofollow` sont injectés
au runtime, jamais en HTML statique.

```html
<script src="https://capturia.ecole-futee.com/js/capturia-widget.js" async></script>
<capturia-badge data-metier="kinesitherapeute"></capturia-badge>
```

Le composant détecte les masquages CSS basiques (`display:none`,
`visibility:hidden`, `opacity:0`, hauteur nulle) et **désactive son
rendu** en affichant le message « Widget Capturia désactivé : la
mention de la source doit rester visible. » C'est volontairement
contournable (ce n'est pas une protection blindée — juste un signal
honnête).

---

## 2. API publique `/api/v1/*`

Quatre endpoints JSON publics, lecture seule, accessibles sans clé d'API
en v1 (cf. § 5 — politique de rate-limit).

### 2.1 Endpoints

| Méthode | Chemin                                  | Description                                                       |
| ------- | --------------------------------------- | ----------------------------------------------------------------- |
| `GET`   | `/api/v1/metier/:slug`                  | Fiche métier publique (score, intervalle de confiance, narratif). |
| `GET`   | `/api/v1/secteur/:slug`                 | Métadonnées secteur + top 10 métiers (asc, moins exposés).        |
| `GET`   | `/api/v1/score-global`                  | Indice synthétique pondéré + décomposition par catégorie.         |
| `GET`   | `/api/v1/metiers?page=&limit=`          | Liste paginée des métiers scorés (limit max 100).                 |
| `GET`   | `/api/v1/compare?a=&b=`                 | Comparateur 2 métiers (utilisé par le widget compare).            |

### 2.2 Headers communs

Toutes les réponses 2xx exposent :

- `Content-Type: application/json`
- `X-License: CC-BY-SA-4.0`
- `X-Capturia-Updated: <ISO8601>` — date du dernier `computed_at`
  agrégé sur le payload (pour invalider intelligemment les caches CDN).
- `Cache-Control: public, max-age=3600`

Aucun cookie n'est posé. CORS ouvert (`Access-Control-Allow-Origin: *`)
en lecture seule, pas de `credentials: include`.

### 2.3 Codes d'erreur

| Statut | Cas                                                              | Payload                                                       |
| ------ | ---------------------------------------------------------------- | ------------------------------------------------------------- |
| `400`  | Paramètres manquants (`/compare` sans `a` ou `b`)                | `{ "error": "params_required", "required": ["a","b"] }`       |
| `404`  | Ressource inconnue (`metier/inexistant`, `secteur/inexistant`)   | `{ "error": "not_found" }`                                    |
| `429`  | Rate-limit dépassé (cf. § 5)                                     | `{ "error": "rate_limit_exceeded", "retry_after": <int> }`    |
| `500`  | Erreur interne                                                   | `{ "error": "Erreur interne", "code": "INTERNAL_ERROR" }`     |

### 2.4 Exemples cURL

```bash
# Fiche métier
curl https://capturia.ecole-futee.com/api/v1/metier/kinesitherapeute

# Top secteur
curl https://capturia.ecole-futee.com/api/v1/secteur/btp-construction

# Comparateur
curl 'https://capturia.ecole-futee.com/api/v1/compare?a=comptable&b=expert-comptable'

# Indice global
curl https://capturia.ecole-futee.com/api/v1/score-global

# Liste paginée
curl 'https://capturia.ecole-futee.com/api/v1/metiers?page=1&limit=20'
```

### 2.5 Schémas de réponse (forme abrégée)

```jsonc
// /api/v1/metier/:slug
{
  "slug": "kinesitherapeute",
  "rome_code": "J1404",
  "title_fr": "Kinésithérapeute",
  "score": 26,
  "score_intervalle": [22, 30],
  "sources_count": 5,
  "sector": "Santé",
  "education_level": "bac+5",
  "tag": "Métier-refuge",
  "narrative": { "short": "...", "tasks": [...] },
  "last_computed": "2026-04-01T12:00:00.000Z",
  "fiche_url": "https://capturia.ecole-futee.com/metier/kinesitherapeute",
  "license": "Creative Commons CC-BY-SA 4.0",
  "_links": {
    "self":        { "href": "https://capturia.ecole-futee.com/api/v1/metier/kinesitherapeute" },
    "embed_badge": { "href": "https://capturia.ecole-futee.com/embed/badge/kinesitherapeute" }
  }
}

// /api/v1/secteur/:slug
{
  "slug": "btp-construction",
  "name": "BTP & Construction",
  "jobs_count": 25,
  "avg_score": 28,
  "top": [
    { "rank": 1, "slug": "couvreur", "title_fr": "Couvreur", "score": 18, "tag": "Métier-refuge", "fiche_url": "..." }
  ],
  "last_computed": "2026-04-01T12:00:00.000Z",
  "license": "Creative Commons CC-BY-SA 4.0"
}

// /api/v1/score-global
{
  "score": 40,
  "tag": "En transformation",
  "sample_size": 235,
  "period": "T2 2026",
  "delta_vs_previous": -1.2,
  "previous_period": "T1 2026",
  "decomposition": {
    "empirical_share": 0.58,
    "theoretical_share": 0.39,
    "institutional_share": 0.03
  },
  "license": "Creative Commons CC-BY-SA 4.0"
}
```

---

## 3. Licence Creative Commons CC-BY-SA 4.0 — obligations

Les données scorées et métadonnées exposées via les widgets et l'API
sont publiées sous **Creative Commons CC-BY-SA 4.0**. Vous pouvez les
réutiliser librement, y compris à titre commercial, sous réserve des
obligations suivantes :

1. **Citer la source** « Capturia » à proximité de toute donnée
   réutilisée (titre, légende, paragraphe d'introduction, etc.).
2. **Garder visible** la mention « Source : Capturia » et le lien
   vers `capturia.ecole-futee.com` (ou la fiche concernée) injectés par les
   widgets. Toute suppression / masquage CSS de cette mention est
   interdite (cf. anti-masquage § 1.2).
3. Indiquer la **date de la version réutilisée** quand elle est
   importante (ex. comparaison historique). Le header
   `X-Capturia-Updated` est l'autorité.
4. Ne pas laisser entendre que vous êtes affilié à Capturia, ni que
   vous fournissez vous-même un service Capturia.

---

## 4. Tracking anonyme — table `widget_views`

Chaque chargement d'iframe `/embed/...` et chaque fetch initial
déclenché par un web component incrémente une ligne dans la table
PostgreSQL `widget_views`. Volontairement minimaliste pour rester
RGPD-friendly :

| Colonne           | Type                          | Description                                                                                                |
| ----------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `id`              | `serial PRIMARY KEY`          | Identifiant auto-incrémenté.                                                                               |
| `widget_type`     | `varchar(16) NOT NULL`        | `'badge'` \| `'sector'` \| `'compare'` \| `'global'`.                                                      |
| `resource_slug`   | `text` (nullable)             | Slug de la ressource ciblée. `NULL` pour `widget_type='global'`.                                           |
| `viewed_at`       | `timestamptz NOT NULL`        | Horodatage UTC.                                                                                            |
| `country_code`    | `varchar(2)` (nullable)       | Code ISO 3166-1 alpha-2 extrait des headers de proxy (CF, Vercel) si présents. `NULL` sinon (documenté).    |
| `referrer_domain` | `text` (nullable)             | **Domaine seul** du `Referer` (ex. `lemonde.fr`). **Jamais l'URL complète.** `NULL` si absent ou localhost. |

Index : `(widget_type, viewed_at DESC)` et `(referrer_domain, viewed_at DESC)`
pour permettre les agrégations par type / par intégrateur sur fenêtre temporelle.

**Ce que nous NE stockons PAS** :

- Aucune adresse IP, en clair ou hashée.
- Aucun chemin d'URL, paramètre de requête ou identifiant de session
  en provenance du `Referer`.
- Aucun `User-Agent` brut.
- Aucun cookie, aucun fingerprint.

L'insertion est **strictement fire-and-forget** : une panne d'écriture
ne bloque jamais le rendu d'un widget ni une réponse API. Une perte
ponctuelle de tracking est préférable à une dégradation d'expérience
intégrateur.

> **Note v1** : le double-comptage iframe + appel API web component est
> volontairement accepté. La déduplication sera évaluée en v2 quand on
> aura les volumes réels.
>
> **Quoi est compté** : seuls les rendus réussis (HTTP 200) sont
> trackés. Les 404 (slug inconnu) et les 400 (paramètres manquants)
> ne sont pas comptés — ce sont des erreurs d'intégration, pas des
> vues utilisateur. Si vous remarquez un trou dans vos stats,
> vérifiez d'abord le slug.

### 4.1 Stratégie de rétention recommandée

Aucun job de purge / agrégation n'est implémenté en v1, mais la
politique cible (à mettre en place quand la table grossit) est la
suivante :

- **0–90 jours** : conservation brute (lignes individuelles).
- **90–365 jours** : agrégation mensuelle par couple
  `(widget_type, referrer_domain, country_code, mois)` puis suppression
  des lignes brutes.
- **>365 jours** : suppression complète des agrégats détaillés ; on ne
  garde qu'un total annuel par `(widget_type, country_code, année)`.

Cette stratégie est documentée ici plutôt qu'implémentée pour rester
honnête sur l'état v1.

---

## 5. Politique de rate-limit

L'API publique `/api/v1/*` est limitée à **60 requêtes par minute par
IP**. Au-delà :

- Statut `429 Too Many Requests`.
- Header `Retry-After: <secondes>` indiquant le délai avant nouvelle
  tentative.
- Headers `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`
  sur toutes les réponses.
- Payload : `{ "error": "rate_limit_exceeded", "message": "...", "retry_after": <int> }`.

Le rate-limit s'applique **par IP normalisée** (IPv4 et IPv6-mapped IPv4
sont fusionnés dans le même bucket).

> **Bypass interne (server-to-server)** : les routes Next.js iframe
> (`/embed/sector`, `/embed/compare`, `/embed/score-global`) appellent
> l'API en interne via la boucle locale (`API_INTERNAL_URL`, par
> défaut `http://localhost:8080`). Le rate-limiter exempte les IPs
> loopback (`127.0.0.1`, `::1`, `::ffff:127.0.0.1`) — sinon tout le
> trafic iframe légitime serait agrégé sur une seule IP backend et
> déclencherait des 429 dès ~30 req/min (compare = 2 appels). L'IP
> loopback n'étant jamais routable depuis l'extérieur, l'exemption
> est intrinsèquement sûre — pas de secret partagé à gérer.
>
> **Limite connue v1 — dette technique assumée** : l'implémentation est
> **in-memory process-local**. Si nous passons à plusieurs instances
> derrière un load-balancer, chaque instance aura son propre compteur,
> ce qui multiplie effectivement la limite par le nombre d'instances.
> À migrer vers un store partagé (Redis, en commençant par un simple
> `INCR` + `EXPIRE`) si la pression réelle le justifie. Pour l'instant
> (mono-instance Replit deployment), cette implémentation est
> suffisante.

### 5.1 Comment éviter le rate-limit

- **Côté CDN** : nos réponses sont `Cache-Control: public, max-age=3600`,
  un CDN devant votre serveur résout la majorité du trafic sans nous
  toucher.
- **Côté client** : factorisez les appels (pas un fetch par badge si
  vous affichez 50 badges sur une page — utilisez `/api/v1/metiers`).
- **Au-delà de 10 000 vues/mois ou en cas de besoin spécifique** : voir
  § 6 — contactez-nous, on peut whitelister ou prévoir un canal dédié.

---

## 6. Contact pour usages massifs

Pour toute intégration au-delà de **10 000 vues / mois** ou pour
qualifier un cas d'usage particulier (presse nationale, école avec
intranet, agrégateur orientation…), écrivez-nous à :

**[contact@capturia.ecole-futee.com](mailto:contact@capturia.ecole-futee.com?subject=Int%C3%A9gration%20widgets%20Capturia)**

Précisez :

- Le ou les widgets envisagés.
- Le volume estimé (vues/mois).
- Le ou les domaines hôtes.
- Si vous avez besoin d'une variante (couleurs, langue, branding
  école — possible à la marge en v2).

Nous répondons sous 5 jours ouvrés.

---

## 7. Changelog & versioning

L'API est versionnée par préfixe URL (`/api/v1/`). Les changements
non rétrocompatibles passeront par `/api/v2/` ; `/api/v1/` restera
servi pendant au moins 12 mois après la sortie d'une v2.

| Date       | Version | Changement                                                                  |
| ---------- | ------- | --------------------------------------------------------------------------- |
| 2026-05-04 | v1      | Tracking `widget_views`, rate-limit 60 req/min/IP, doc complète (Task #71). |
| 2026-04    | v1      | 4 web components (badge, sector, compare, score-global) — Task #70.         |
| 2026-04    | v1      | 3 widgets iframe additionnels + endpoints API — Task #69.                   |
| 2026-04    | v1      | Widget badge iframe + endpoint `/api/v1/metier/:slug` — Task #68.           |
