# Nexxis Rich Snippets — v4.0.0

> **Module Schema.org JSON-LD pour WordPress / Enfold**, spécialisé agences digitales à implantation **multi-sites physiques**. Produit un graphe JSON-LD complet, propre, validé Rich Results Test, à partir d'un registre d'agences centralisé et d'une configuration ACF par page.

**Statut v4.0.0** : stable en production — refonte complète du monoagence v3.2.5 vers un modèle multi-agences avec Organization mère + LocalBusiness filiales + patterns hub/pilier.

---

## Table des matières

1. [Philosophie & Architecture](#1-philosophie--architecture)
2. [Fichiers du module](#2-fichiers-du-module)
3. [Modèle de données](#3-modèle-de-données)
4. [Fonctionnalités v4.0.0](#4-fonctionnalités-v400)
5. [Schema Types supportés](#5-schema-types-supportés)
6. [Hooks & Filtres exposés](#6-hooks--filtres-exposés)
7. [Configuration wp-config.php](#7-configuration-wp-configphp)
8. [Cron jobs](#8-cron-jobs)
9. [Cache & invalidation](#9-cache--invalidation)
10. [Historique détaillé](#10-historique-détaillé)

---

## 1. Philosophie & Architecture

### Problème résolu

Les modules Schema.org du marché émettent un JSON-LD monoagence (une adresse, un téléphone, un horaire). Pour une agence digitale qui gère des clients avec **plusieurs établissements physiques**, ils produisent des signaux ambigus voire faux (NAP d'une agence poussée sur toutes les pages).

### Solution v4

- **Registre d'agences central** : une source de vérité dans l'admin Toolkit, éditable visuellement, enrichissable depuis Google Places ou le Registre FR.
- **Liaison explicite page ↔ agence** (ACF `nx_agency_id`) : chaque page d'agence physique est rattachée à l'entrée du registre correspondante → le JSON-LD émis est exact.
- **Organization mère + subOrganization** : en mode multi, la HP émet une `Organization` parente et chaque agence émet un `LocalBusiness` (sous-type précis) avec `branchOf → #organization`.
- **Cascade fallback** : mode Solo → l'agence principale est utilisée partout ; mode Multi → chaque page choisit ; sans choix → fallback documenté.

### Stack technique

- **WordPress 6.x** + **PHP 7.4+**
- **ACF Pro** (requis) pour l'édition par page
- **Enfold theme** (compat profonde, extraction FAQ des shortcodes `[av_faq_single_em2]`, `[av_toggle]`)
- **API externes** :
  - OpenAI Chat Completions (suggestion @type, description, knowsAbout, FAQ)
  - Google Places Details (NAP auto-fill + reviews sync hebdo)
  - API gouv.fr Recherche Entreprises (identifiant juridique FR, gratuite sans clé)

---

## 2. Fichiers du module

| Fichier | Rôle |
|---|---|
| `nexxis-rich-snippets.php` | Header du plugin MU — bootstrap |
| `class-rich-snippets.php` | Init : chargement des includes, cron, constantes |
| `helpers.php` | Options DB, registre d'agences, getters cascade (logo, phone, email), dashboard summary, QA warnings, cron callbacks, sanitize, API gouv.fr |
| `helpers-inject.php` | Construction du graphe JSON-LD complet (`nexxis_rs_build_page_graph`) + injection front via `wp_head` |
| `admin-ui.php` | Page admin principale : Profil, Identité juridique, Zones, Catalogue, Agences (repeater), Stratégie, IA, Config API. ~4000 lignes, UX v4 avec layout 2 colonnes, sticky save, Ctrl/Cmd+S, info icons, dashboard graphe |
| `admin-ia-helpers.php` | AJAX handlers `nrs_generate_faq` + `nrs_generate_desc` + injection JS côté éditeur post |
| `acf-nexxis-schema.php` | Définition du field group ACF (`nx_schema_type`, `nx_agency_id`, `nx_service_description`, `nx_has_faq`, `nx_faqs`, etc.) — conditional logic v4 |
| `metabox-helper.php` | Metabox `Nexxis Snippet Helper (Prévisualisation)` + warnings cohérence + diagnostic reviews (debug) |
| `admin-pages-list.php` | Colonne `Schema` dans la liste des pages WP (badges @type + indicateurs 📝 💬 🏙️ 🏢 ✓ ⚠️) + row action « 🧪 Tester Schema » |
| `qa-lint.php` | Notices admin globales (logo, phone, stratégie, OpenAI, profil, SIRET…) — dismissible |
| `reviews-trustindex-sync.php` | Legacy Trustindex sync (conservée pour rétrocompat) |
| `page-geo-autofill.php` | Auto-fill lat/lng des city pages via Google Geocoding API |

---

## 3. Modèle de données

### Options DB (`get_option(NEXXIS_RS_OPT)`)

```
nexxis_rs_options = {
    // Profil & localisation
    profile:             solo | multi | service_area
    country:             FR | BE | CH | LU | MC | CA | US | GB | …
    language:            '' (auto) | fr | fr-BE | en | …

    // Identité juridique
    legalName:           string
    slogan:              string
    identifier:          { type: SIREN | SIRET | VAT | …, value: string }
    vatID:               string
    awards:              [{ name, url }]
    sameAs:              string[]  (profils sociaux)
    knowsAbout:          string[]  (domaines d'expertise)

    // Zones & Catalogue global
    areaServed_global:   [{ type, name }]  (AdministrativeArea / City / Country)
    makesOffer_global:   [{ name, description, type }]

    // Registre d'agences (multi-agences)
    agencies: [
        {
            id:           slug unique
            name:         string
            description:  string (alimente IA sub-types)
            schemaType:   string[]  (ex: ["HVACBusiness", "Plumber"])
            phone, email, website, placeId, siret, priceRange, foundingDate, employees
            address:      { streetAddress, addressLocality, postalCode, addressRegion, addressCountry }
            geo:          { latitude, longitude }
            radius_km:    string
            openingHoursOverride: [{ dayOfWeek, opens, closes }]
            reviews: {
                source:     trustindex | google-gmb | manuel
                rating, count
                last_sync:  unix timestamp
                items:      [{ author, author_url, author_photo, rating, text, date, language }] max 5
            }
            areaServed:   (préservé DB, UI d'édition en backlog)
        }
    ]

    // Stratégie SEO
    strategy:            forteresse | reseau | conquete | hyper
    cities:              [{ name, lat, lng, place_id }]

    // IA & API
    openaiapikey:        chiffré sodium (fallback DB uniquement, wp-config prioritaire)
    openai_model:        gpt-4o-mini | gpt-4o | gpt-3.5-turbo
    supported_post_types: ['page', 'post', …]  (CPT auto-détectés)

    // Legacy bridge (préservés, miroirés depuis agencies[0])
    gmb_place_id, gmb_street, gmb_city, gmb_postcode, gmb_country, gmb_region,
    gmb_lat, gmb_lng, gmb_phone, gmb_website, gmb_price_range,
    gmb_opening_hours, gmb_opening_hours_override
}
```

### ACF fields par page

| Nom | Type | Description |
|---|---|---|
| `nx_schema_type` | select | Type Schema.org principal : service / localbusiness / aboutpage / webpage / collectionpage / article / product / review / faq / howto / recipe / course / event / video / job / other |
| `nx_service_description` | textarea | **Permanente tous types** — description SEO injectée dans le JSON-LD (priorité sur excerpt WP). Bouton IA « Générer Description & Type via IA ». |
| `nx_service_type` | text | Libellé libre (uniquement Service/LB) — devient `serviceType` |
| `nx_agency_id` | select (Select2 + clear-X) | Agence liée (slug) — uniquement Service/LB |
| `nx_haspart_pages` | relationship | **CollectionPage uniquement** — pages distribuées via `hasPart` |
| `nx_city_context` | true_false | City page activée ? |
| `nx_city_name`, `nx_custom_lat/lng/radius` | text/number | Coords + rayon city page |
| `nx_hide_reviews`, `nx_local_rating`, `nx_local_review_count` | — | Overrides reviews (masqués si agence liée) |
| `nx_has_faq`, `nx_faqs` | true_false + repeater | FAQ (accordion pliable) |
| `nx_event_*`, `nx_product_*`, `nx_review_*`, `nx_howto_*`, `nx_video_*`, `nx_recipe_*`, `nx_course_*`, `nx_job_*` | — | Champs par type |

---

## 4. Fonctionnalités v4.0.0

### Briques 1-8 (refonte multi-agences)

- **Brique 1** — Profil & Localisation (solo/multi/service_area, pays, langue)
- **Brique 2.A** — Registre d'agences (repeater admin complet)
- **Brique 2.B** — Auto-fill Google Places + API gouv.fr (NAP + horaires + SIRET) par agence
- **Brique 3** — Identité juridique (legalName, identifier, vatID, awards)
- **Brique 4** — Liaison page ↔ agence via ACF `nx_agency_id` + rendu JSON-LD enrichi par agence
- **Brique 5** — Zones & Catalogue global (Organization-level areaServed + makesOffer)
- **Brique 6** — Organization HP enrichie (subOrganization auto, warnings cohérence par page)
- **Brique 7.A** — Horaires + reviews par agence + défense en profondeur source='manuel'
- **Brique 7.B** — Reviews Google Business Profile (via Google Places Details API) + items détaillés (Review[])
- **Brique 8** — Admin UX : colonne Schema liste pages + row action Rich Results Test + hooks filters exposés

### Phase UX (2026-04)

- Layout admin 2 colonnes (main + sidebar sticky)
- Dashboard « 📊 État du graphe » : 12 cards status (logo, téléphone, OpenAI, GPlaces, profil, agences, identité juridique, SIRET, zones, offres, stratégie, CPT)
- Sticky save bar + Ctrl/Cmd+S
- Info icons déroulants (remplacent les blocs bleus fixes)
- Auto-détection CPT publics (checkbox list)
- Réorganisation sections avec wrap JS `.nrs-section` + CSS Grid
- Config API (snippets wp-config.php) côte à côte avec IA, masquée quand les 2 clés sont en wp-config

### Type `CollectionPage` (hub / pilier)

- Pattern topic cluster / pillar page / hub géo
- Auto-émission `hasPart` : fallback enfants WP → priorité au champ ACF `nx_haspart_pages` explicite
- Filter `nexxis_rs_collectionpage_haspart` pour customisation devs

### Description permanente tous types

- Champ `nx_service_description` visible sur tous les types de page (WebPage, AboutPage, Article, Product, Event, etc.)
- Alimente `description` JSON-LD en priorité sur excerpt WP
- Bouton IA générique « Générer Description & Type via IA » disponible partout

### Bugfixes critiques audit

Voir [CHANGELOG.md](CHANGELOG.md) — 14 correctifs livrés lors de l'audit multi-agences (SIRET silent discard, cache transient stale object cache, areaServed mal scopé, `nx_hide_reviews` stale, rename slug cascade, etc.)

---

## 5. Schema Types supportés

| `nx_schema_type` | `@type` émis | Reviews ? | Agence liée ? |
|---|---|---|---|
| `service` | `Service` | via agence liée (si présente) | Optionnel |
| `localbusiness` | Sous-type de l'agence (`HVACBusiness`, `Plumber`, etc.) ou `LocalBusiness` | via agence liée | **Recommandé** |
| `aboutpage` | `AboutPage` + `about: #organization` | ❌ non-reviewable | ❌ |
| `webpage` | `WebPage` | ❌ | ❌ |
| `collectionpage` | `CollectionPage` + `hasPart` | ❌ | ❌ |
| `article` | `Article` | ❌ (sauf si review object) | ❌ |
| `product` | `Product` | ✅ via overrides per-page | ❌ |
| `review` | `Review` | — | ❌ |
| `faq` | `FAQPage` | ❌ | ❌ |
| `howto`, `recipe`, `course`, `event`, `video`, `job` | `HowTo` / `Recipe` / `Course` / `Event` / `VideoObject` / `JobPosting` | dépend | ❌ |
| `other` | Aucun nœud principal | ❌ | ❌ |

---

## 6. Hooks & Filtres exposés

### Filters (PHP)

```php
// Désactiver l'auto-injection globale.
add_filter('nrs_should_auto_inject', '__return_false');

// Customiser le modèle OpenAI utilisé.
add_filter('nrs_openai_model', function() { return 'gpt-4o'; });

// Filtrer le nœud Organization avant insertion dans le graphe.
add_filter('nexxis_rs_organization_node', function($node, $post_id) {
    $node['additionalType'] = 'CertifiedCompany';
    return $node;
}, 10, 2);

// Filtrer le nœud principal (Service, LocalBusiness, Article, …) avant insertion.
add_filter('nexxis_rs_page_main_node', function($main, $type, $linked_agency, $post_id) {
    // …
    return $main;
}, 10, 4);

// Filtrer le graphe complet avant sérialisation JSON.
add_filter('nexxis_rs_page_graph', function($graph, $post_id) {
    // $graph est la structure finale : { '@context': …, '@graph': [...] }
    return $graph;
}, 10, 2);

// Customiser la liste `hasPart` d'une CollectionPage.
add_filter('nexxis_rs_collectionpage_haspart', function($items, $post_id) {
    // $items = [{'@type':'WebPage', '@id':..., 'url':..., 'name':...}, …]
    return $items;
}, 10, 2);
```

### Actions

- `save_post` → invalidation transient graphe (`delete_transient('nrs_graph_' . $post_id)`)
- `wp_head` priority 999 → injection `<script type="application/ld+json">...</script>`

### AJAX endpoints

- `nrs_generate_faq` — génération FAQ via IA (cascade direct/targeted/global)
- `nrs_generate_desc` — génération description + type Service via IA
- `nrs_agency_fetch_google_places` — auto-fill agence depuis Google Places
- `nrs_agency_fetch_inpi` — auto-fill agence/entité depuis Recherche Entreprises FR
- `nrs_agency_resync_reviews` — resync manuelle reviews Google d'une agence
- `nrs_agency_suggest_types` — suggestion IA @type Schema.org d'une agence
- `nrs_knowsabout_suggest` — suggestion IA domaines d'expertise globaux
- `nrs_clear_faq_snapshot` — reset snapshot FAQ d'une page

---

## 7. Configuration wp-config.php

```php
// Clés API (RECOMMANDÉ en wp-config, pas en BDD)
define( 'NEXXIS_RS_OPENAI_KEY',  'sk-proj-xxx...' );  // OpenAI
define( 'NEXXIS_RS_GPLACES_KEY', 'AIzaSy...' );       // Google Places

// Optionnels
define( 'NEXXIS_RS_USE_CACHE',   true );   // Active le cache transient 24h
define( 'NEXXIS_RS_REQUIRE_FAQ_ID', true ); // FAQ uniquement depuis section id=faq
define( 'NEXXIS_RS_IA_DEBUG',    true );   // Log IA dans debug.log (dev/staging only)
define( 'NXT_DEBUG',             true );   // Active le panneau « 🔍 Debug reviews/agence »
                                             // dans la metabox éditeur (dev only)
define( 'NEXXIS_RS_THEME_ADAPTER', 'enfold' ); // Bascule le parseur FAQ vers Enfold
```

---

## 8. Cron jobs

| Hook | Fréquence | Callback | Action |
|---|---|---|---|
| `nexxis_rs_cron_sync_reviews` | hebdomadaire | `nrs_cron_sync_reviews_weekly` | Sync Google reviews de toutes les agences avec `source=google-gmb` + `placeId` |
| `nexxis_rs_cron_sync_trustindex` | mensuelle | (reviews-trustindex-sync.php) | Legacy Trustindex global (rétrocompat v3) |

**Schedule `monthly`** ajouté via filter `cron_schedules`. Exécution silencieuse, erreurs loggées via `nrs_ia_log`.

---

## 9. Cache & invalidation

### Transient graphe

- **Clé** : `nrs_graph_{post_id}`
- **TTL** : 24 h
- **Invalidation** :
  - `save_post` → suppression transient du post
  - `nexxis_rs_update_options` → suppression de TOUS les transients graphe (option change)
  - `nrs_sync_agency_reviews` → suppression ciblée des transients des pages liées à l'agence resync

### Compat object cache (Redis / Memcache)

Toutes les invalidations utilisent `delete_transient()` plutôt que du bulk SQL DELETE — garantit que l'object cache est nettoyé.

### Options cache

- Static cache PHP dans `nexxis_rs_get_options()` — flushé via `nexxis_rs_flush_options_cache()` à chaque update.

---

## 10. Historique détaillé

Voir **[CHANGELOG.md](CHANGELOG.md)** pour l'historique complet livraison par livraison (~40 entrées v4.0.0).

### Versions marquantes

- **v3.2.5** — Monoagence final (pré-refonte)
- **v4.0.0 Brique 1** — Profil & localisation (multi-agences scaffold)
- **v4.0.0 Brique 4** — Liaison page ↔ agence opérationnelle
- **v4.0.0 Brique 7.B** — Reviews Google Business Profile par agence
- **v4.0.0 Phase UX** — Admin modernisé (layout 2-col, dashboard, sticky save)
- **v4.0.0 Audit** — 14 bugfixes + CollectionPage livrés lors du test en conditions réelles

---

## Glossaire

- **NAP** : Name-Address-Phone (triptyque Google My Business)
- **LB** : LocalBusiness (schema.org)
- **GBP** : Google Business Profile (ex-GMB)
- **E-E-A-T** : Experience, Expertise, Authoritativeness, Trustworthiness (signaux qualité Google)
- **city context** : page ACF marquée `nx_city_context = true` → city page SEO
- **hub / pilier** : page qui agrège et distribue vers des pages spécialisées (topic cluster pattern)

---

## Ressources externes

- [Schema.org officiel](https://schema.org)
- [Google Rich Results Test](https://search.google.com/test/rich-results)
- [Google Search Console — Enhancements](https://search.google.com/search-console)
- [Validator JSON-LD](https://validator.schema.org/)
- [API gouv.fr Recherche Entreprises](https://annuaire-entreprises.data.gouv.fr/)
