Skip to content

Pays

Catalogue ISO 3166 borné (~250 documents) hébergé dans l’index Meilisearch MEILISEARCH_COUNTRIES_INDEX (défaut countries). Lecture seule depuis Meili — aucun domaine Country côté MySQL, l’index est alimenté par un processus externe.

Identité côté API : le id JSON:API d’une ressource countries est le code ISO 3166-2 en minuscules (fr, us, ad, …). Les documents Meili stockent le code en majuscules dans le champ id (FR, US) — Hydrogen normalise à la sérialisation. Les URLs sont case-insensitive end-to-end (/api/countries/fr/api/countries/FR).

Descriptions Markdown : la ressource détail (GET /api/countries/{code}) embarque un attribut description dont la valeur est le contenu brut Markdown du fichier resources/lang/<locale>/countries/<code>/description.md. Le layout est un dossier par pays (<code>/) contenant description.md — cela laisse la place pour ajouter d’autres ressources éditoriales par pays plus tard (faq.md, highlights.md, dossier images/, …) sans casser les URLs de description. La locale est résolue par LocaleResolverMiddleware (header Accept-Language ou param). Si le fichier n’existe pas pour la locale courante, fallback sur la locale SupportedLocales::DEFAULT ; si aucune des deux n’existe, description = null (le pays sert quand même, juste sans blurb). Le Markdown n’est pas rendu côté serveur — chaque surface (web, mobile, BO preview) applique son propre sanitizer.

Garde anti-path-traversal : le code passe par une regex ^[a-z]{2,3}$ avant toute lecture disque. Toute tentative d’injection (../, \0, etc.) court-circuite la résolution → description = null.

Retourne tout le catalogue en un seul appel — pas de pagination, pas de limit/offset exposés au client.

  • Auth : aucune.

  • Action : ListCountriesAction.

  • Pipeline : un seul appel Meili avec limit = COUNTRIES_MAX_RESULTS (défaut 1000), q = "" (ordre naturel de l’index).

  • Pas de description dans le listing : charger 250 fichiers Markdown sur un endpoint de catalogue est gaspilleur. Le blurb n’est disponible que sur le détail.

  • Réponse 200 OK :

{
"data": [
{
"type": "countries",
"id": "fr",
"attributes": {
"name": "France",
"slug": "france",
"codes": { "iso_3166_2": "FR", "iso_3166_3": "FRA", "wikipedia": null },
"region": "Western Europe",
"continent": "Europe",
"continent_id": "EU",
"status": "officially-assigned",
"stats": { "surface": "549393.44", "stats": null },
"timezones": { "utc": ["+01:00", "+02:00"] },
"official_name": { "fr-FR": "République française", "en-US": "French Republic" },
"latitude": 46,
"longitude": 2
}
}
],
"links": { "self": "https://api.example/api/countries" },
"meta": { "totalHits": 249, "count": 249 }
}
  • Garde-fou catalogue : si meta.totalHits > COUNTRIES_MAX_RESULTS, la réponse ajoute meta.truncated = true + meta.maxResults = <cap>.
  • Réponses d’erreur :
    • 503 Search backend unavailable — Meilisearch injoignable.

Même schéma que /api/establishments/search : trois modes.

  1. Full-text : ?q=<term>.
  2. Geo-rayon : ?lat=&lng=&distance= (mètres) — les trois ensemble, sinon 422.
  3. Combiné : q + lat+lng+distance.
  • Auth : aucune.

  • Action : SearchCountriesAction.

  • Query :

    • q (string, optionnel) — recherche full-text.
    • lat (float [-90, 90]), lng (float [-180, 180]), distance (int positif, mètres) — tous trois requis pour le mode géo, sinon 422 Incomplete geo parameters.
    • limit (int, défaut 20, plafond 50).
    • offset (int ≥ 0, défaut 0).
  • Plafond distance : COUNTRY_NEARBY_MAX_DISTANCE_METERS (défaut 5 000 000 m = 5 000 km). Les coordonnées pays sont des centroïdes à l’échelle continentale, un cap serré n’aurait pas de sens. Dépassement → 422 Distance too large.

  • Tri géo : en mode géo, tri par _geoPoint(lat, lng):asc (distance croissante depuis le centre). En mode full-text pur, tri par pertinence Meili.

  • Réponse 200 OK :

{
"data": [
{ "type": "countries", "id": "fr", "attributes": { "name": "France", "...": "..." } }
],
"links": {
"self": "https://api.example/api/countries/search?q=france",
"first": "https://api.example/api/countries/search?q=france&limit=20",
"prev": null,
"next": null,
"last": null
},
"meta": {
"totalHits": 1,
"limit": 20,
"offset": 0,
"query": "france",
"center": { "lat": 48.85, "lng": 2.35 },
"distance": 1000000
}
}

Les attributs center et distance ne sont présents qu’en mode géo.

  • Réponses d’erreur :
    • 422 Incomplete geo parameters — un des lat/lng/distance fourni mais pas les autres.
    • 422 Invalid latitude / Invalid longitude / Invalid distance — valeurs hors bornes ou non numériques.
    • 422 Distance too largedistance > COUNTRY_NEARBY_MAX_DISTANCE_METERS.
    • 503 Search backend unavailable.

Détail d’un pays par son code ISO 3166-2 (2 ou 3 caractères, case-insensitive). Filtre Meili exact sur id = "<UPPER>" — pas de recherche full-text (le scorer pourrait surfacer le mauvais continent sur des codes de bord).

  • Auth : aucune.

  • Action : GetCountryAction.

  • Path :

    • {code} — regex [a-zA-Z]{2,3}. Casse normalisée à la résolution (majuscules pour Meili, minuscules pour le id JSON:API). Format invalide → 404.
  • Différence clé avec le listing : ajoute un attribut description chargé depuis resources/lang/<locale>/countries/<code>.md (voir intro de section).

  • Réponse 200 OK (locale fr-FR) :

{
"data": {
"type": "countries",
"id": "fr",
"attributes": {
"name": "France",
"slug": "france",
"codes": { "iso_3166_2": "FR", "iso_3166_3": "FRA", "wikipedia": null },
"region": "Western Europe",
"continent": "Europe",
"continent_id": "EU",
"status": "officially-assigned",
"stats": { "surface": "549393.44", "stats": null },
"timezones": { "utc": ["+01:00", "+02:00"] },
"official_name": { "fr-FR": "République française", "en-US": "French Republic" },
"latitude": 46,
"longitude": 2,
"description": "# France\n\nLa **France**, officiellement la *République française*, …"
}
}
}
  • Réponses d’erreur :
    • 404 Country not found — code invalide (regex non matchée) OU code valide mais aucun document Meili correspondant.
    • 503 Search backend unavailable.

Pré-requis index Meilisearch (one-shot ops) :

  • id dans filterableAttributes (pour le lookup exact du détail)
  • _geo dans filterableAttributes ET sortableAttributes (pour ?lat=&lng=&distance=)
  • les champs texte recherchables (name, slug, …) dans searchableAttributes (* couvre tout sur le déploiement courant)