# CLAUDE.md Guidance pour Claude Code sur ce dépôt. ## Project overview Site WordPress du laboratoire THALIM (Théorie et Histoire des Arts et des Littératures de la Modernité). Stack Docker + thème Timber/Twig customisé + deux plugins maison (HAL importer, Newsletter). Le site est bilingue FR/EN via un système maison (Polylang a été retiré). ## Dev environment ```bash docker-compose up -d # WordPress, MySQL, phpMyAdmin docker-compose down ``` - WordPress : http://localhost:8020 - phpMyAdmin : http://localhost:8021 - MySQL : `localhost:3307` → `db:3306` - Identifiants dans `.env` - WordPress monté depuis `./wp-data/` ## Architecture ### Thème : `wp-data/wp-content/themes/thalim/` Timber/Twig. Chaque `*.php` charge un Twig de `templates/`. `base.twig` est le layout, les autres l'étendent. - `functions.php` — simple chargeur : init Timber + `require_once` des modules `inc/*` (l'ordre compte : `config.php` et `i18n.php` d'abord) - `inc/` — toute la logique PHP, un module par responsabilité (voir détails plus bas) - `templates/` + `templates/partials/` — Twig - `scss/` → `css/` — SASS compilé **manuellement** par l'utilisateur (CSS commité) - `js/` — scripts frontend ; `js/admin/` — scripts admin par contexte de page (voir « Customisations admin ») - `assets/vendor/` — dépendances tierces auto-hébergées (Swiper 12.2.0, Iconoir 7.11.0) — plus aucun CDN - `vendor/` — Composer (Timber 2.x). `composer install` après clone - `tests/run-tests.php` — tests des fonctions pures, à exécuter via `docker exec wordpress php /var/www/html/wp-content/themes/thalim/tests/run-tests.php` (idem dans chaque plugin) ### Plugins maison - **`thalim-hal-importer/`** — import publications HAL (structure 254015). Admin : Outils → HAL Import. Voir le README du plugin pour le mapping doc types → catégories (10 types : ART, COUV, OUV, COMM, ISSUE, PROCEEDINGS, THESE, HDR, SON, VIDEO). Les catégories sont résolues **par slug** et les IDs Pods **par nom** ; toute écriture de relation Pods passe par `includes/class-pods-storage.php` (dépendance dure à Pods 3.x documentée dans le fichier — ne pas écrire `wp_podsrel` ailleurs). - **`thalim-newsletter/`** — composition et export HTML des digests mensuels. Admin : Outils → Newsletter. Les constantes `THALIM_NL_CAT_*` sont résolues par slug au chargement (transient 1 j, fallback IDs historiques) ; le pod/champ `categorie` est résolu par nom dans `class-admin-page.php`. ### Plugins WP requis - **Pods** — types de contenu + champs personnalisés (tout est en Pods) - **Members** — gestion fine des rôles/capacités - **Simple Local Avatars** — avatars uploadés (vs Gravatar) - **Relevanssi** — recherche ## Multilingue (système maison) Polylang a été **remplacé** par un système custom. Toute la logique vit dans `inc/i18n.php`. - **Détection** : `thalim_current_language()` retourne `'en'` si `THALIM_ORIGINAL_URI` commence par `/en/`, sinon `'fr'`. Le préfixe `/en/` est strippé via `do_parse_request` avant la résolution d'URL WP, et `redirect_canonical` est désactivé sur les URLs `/en/` pour éviter que WP ne supprime le préfixe - **Champs texte bilingues** : convention `"FR // EN"` dans un même champ. `thalim_bilingual($value, $lang)` (exposé comme filtre Twig `|bilingual`) renvoie la bonne moitié. Utilisé partout : titres de posts, noms de termes de taxonomie, sous-titres, lieux, fonctions, types, etc. - **Champs longs séparés** : pour le corps des posts, c'est un champ Pods `body_en` à part. Idem pour `biographie_en`, `presentation_en`, `recherches_en_cours_en`, `resume_de_la_these_en`, `autres_domaines_de_recherches_en` sur les utilisateurs, et `presentation_en` / `presentation_detail_en` sur le post `contenu_general` - **URLs préfixées** : `thalim_en_url($url)` ajoute `/en` aux URLs internes en mode EN, attaché à `term_link`, `post_link`, `page_link`, `post_type_link`, `author_link`. Filtre Twig `|en_url` aussi disponible. No-op en admin (URI commence par `/wp-admin/`) - **Noms de catégories** : `thalim_cat_name($cat)` (filtre Twig `|cat_name`) lit le term meta `titre_anglais` en mode EN. Aussi appliqué au title de l'onglet navigateur via `document_title_parts` - **Menus** : slugs `Navigation` / `Navigation-en` et `Footer` / `Footer-en` (pas `menu-principal` — c'est un legacy obsolète) - **Contenu général** : un seul post de type `contenu_general` fournit les blocs UMR/thalim/siècles/présentation au layout via `gc` dans le contexte Twig - **Sélecteur de langue** : `thalim_language_switcher()` retourne `['fr' => ['slug','url','current_lang'], 'en' => …]` — remplace `pll_the_languages()` - **Override pour AJAX** : poser `$GLOBALS['thalim_lang_override'] = $lang` avant `thalim_current_language()` (utilisé par les handlers `load_more_posts` / `load_more_agenda` qui reçoivent la langue en POST) ## Dates et tri par événement Les annonces ont une `date_de_debut` / `date_de_fin` (champ date Pods) ou un `datetime` (communications). Les listes et l'agenda doivent **prioriser ces dates sur `post_date`**. - **Activation** : ajouter `'thalim_event_date_order' => true` aux args d'un `WP_Query` (ou Timber) - **Filtre de plage** : `'thalim_event_date_filter' => ['from' => 'YYYY-MM-DD', 'to' => 'YYYY-MM-DD']` - **Implémentation** : filtres `posts_join` + `posts_orderby` + `posts_where` dans `inc/event-dates.php`. LEFT JOIN sur `postmeta` puis `CASE WHEN date_de_debut ELSE datetime ELSE post_date END DESC`. Les valeurs `0000-00-00...` sont traitées comme NULL - **Format d'affichage** : `thalim_format_date($raw, $lang)` dans `inc/single-helpers.php` renvoie `date_i18n('j F Y', $ts)`. Les abréviations de mois (3-lettres) pour les vignettes agenda sont codées en dur dans `inc/ajax.php` (`thalim_get_agenda_card_data()`) et dans `inc/single-helpers.php` (séances) - **Construction du `date_label`** : la logique « Le X de H1 à H2 / Du X au Y / Jusqu'au X / X à H » vit dans `thalim_get_agenda_card_data()` — à dupliquer prudemment si besoin ailleurs ## Helpers PHP (`inc/`) | Fichier | Rôle | |---|---| | `config.php` | **Identifiants centralisés** : `thalim_term_id_by_slug()` (slug → term_id, cache statique), `thalim_cat_id($cle)` (catégories structurelles par clé logique — attention, le slug de la cat 9 est `vie-du-labo-intranet`), `thalim_excluded_role_ids()`, `thalim_category_color_slug()` (seule map encore indexée sur term_id). **Ne jamais réintroduire d'ID numérique en dur** : passer par ces fonctions | | `i18n.php` | Multilingue maison complet (cf. section dédiée) | | `assets.php` | Enqueue front + admin (scripts admin conditionnels par écran via `get_current_screen()`) | | `context.php` | `add_to_context()` (contexte Twig global) | | `event-dates.php` | Filtres `posts_join/orderby/where` du tri par événement, `thalim_get_active_pinned_ids()`, `thalim_get_axes_filter_groups()` | | `seance-helpers.php` | `thalim_get_seance_parent_id()` (mémoïsée — la seule résolution séance→séminaire, utilisée partout), `thalim_get_seance_link()`, redirection `template_redirect` séance→parent | | `ajax.php` | Handlers `load_more_posts` / `load_more_agenda` + helpers partagés `thalim_ajax_read_filters()` / `thalim_ajax_build_query_args()`, `thalim_get_agenda_card_data()` | | `archive-filters.php` | Helpers partagés des 4 contrôleurs d'archives (category/taxonomy/search/page-annonces) : exclusions, lecture des filtres GET, rubrique active, posts « directs », listes de filtres (liens injectés en closure par chaque contrôleur) | | `access-control.php` | Restrictions contributeurs (`pre_get_posts`, `user_has_cap`, `wp_count_posts`), Vie du labo, redirections login/dashboard | | `admin-tweaks.php` | Synchro `display_name`, Pods sur user-new, colonnes taxonomies, filtre catégorie exact, rewrite `/autres`, admin bar, `user_can_richedit` | | `avatars.php` | `thalim_get_user_avatar_url()` (voir « Avatars ») | | `single-helpers.php` | `thalim_get_single_data($post_id)` résout tous les champs Pods d'un post en tableau prêt pour Twig : dates formatées, images vs documents (split par mime-type), membres résolus en `{name, url}`, taxonomies (axes, étiquettes, programmes), séances triées en `seances_a_venir` / `seances_passees`, hiérarchie de catégorie, `type_label` et `fonction_label` dérivés des champs `type_*` / `fonction_*` ou (legacy) du `_pods_categorie`, liens externes (1–3), Canal-U / YouTube embeds. Inclut aussi `thalim_format_date()` (partagée). **Champs profil/HTML passés par `wp_kses_post`** (autoescape Twig désactivé) | | `author-helpers.php` | `thalim_get_author_data($user_id)` (profil membre) et `thalim_get_author_posts_by_category($user_id)` (posts liés au membre, groupés par catégorie primaire, plus un groupe spécial « séances de séminaire » via cat 12). Tri inter-groupes par `ordre_profil` (term meta) puis par nombre de posts | | `membres-helpers.php` | Page `/membres` : `thalim_get_membres_groups()` regroupe par slugs de rôle taxonomy (mapping codé en dur dans `$group_definitions` — voir « Page membres » plus bas) | | `post-card-helpers.php` | `thalim_get_card_data($post_id)` / `thalim_get_cards_data($posts)` : données pour `partials/post-card.twig`. Inclut la résolution catégorie parente pour le code couleur (`parent_slug`), la première image (medium), la `card_event_date`, et la redirection `#seance-{ID}` pour les séances de séminaire | | `pods-conditional-required.php` | Patch : Pods n'évalue pas sa propre logique conditionnelle côté serveur lors de la validation des champs *required*. On la rejoue dans `pods_api_pre_save_pod_item_post` et on désactive `required` sur les champs masqués | | `pods-save-error-handler.php` | Quand Pods déclenche un `wp_die()` à la sauvegarde admin, intercepter (`pods_error_die`), stocker tous les `pods_meta_*` dans un transient, rediriger vers `post.php?action=edit`, annuler le statut si le post passait à `publish`, et restaurer les champs côté React via `get_post_metadata` + JS dans `admin_footer`. **Mécanisme partagé** avec : | | `post-title-required.php` | Force un titre non-vide à la sauvegarde, réutilise le même mécanisme transient/restore | | `admin-users-filter.php` | Dropdown « Statut » sur `users.php`, filtre via meta `role_1`/`role_2`/`role_3` | ### Avatars (chaîne de fallback) `thalim_get_user_avatar_url($user_id)` dans `inc/avatars.php` : 1. Simple Local Avatar (`simple_local_avatar` user meta) — résolu via `media_id` quand dispo (pour survivre aux changements de domaine), sinon URL réécrite 2. Gravatar — lu **uniquement** depuis le transient `thalim_gravatar_{ID}` (1 semaine). Aucune requête réseau dans le rendu : le HEAD `d=404` est fait par le cron quotidien `thalim_warm_gravatar_cache` (+ réchauffage unitaire `thalim_warm_gravatar_user` planifié à la volée en cas de cache manquant) 3. Chaîne vide → templates fallback initiales ## Pages spécifiques ### Page Membres (`page-membres.php`) Pilotée par `inc/membres-helpers.php`. - Groupes basés sur les **slugs** de la taxonomy `role` (pas les term IDs — survit aux migrations) - Groupe « Direction » en tête, ordre fixe : `directeur` puis `directeur_adjoint`, lus depuis les post meta de la page `le-laboratoire` - Tri intra-groupe alphabétique sur le nom de famille via `Collator('fr_FR')` à `PRIMARY` (insensible accents/casse). Pour les noms composés (« Duclaux de l'Estoile »), `thalim_get_sort_key()` prend le premier mot du meta `last_name` - Cas spécial « Personnel d'accompagnement à la recherche » : les statuts contenant « Gestion et pilotage » remontent en tête - Chaque `