210 lines
19 KiB
Markdown
210 lines
19 KiB
Markdown
# 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` — gros (≈1400 lignes) : setup, i18n, contexte Twig, AJAX, filtres de requête, customisations admin
|
||
- `inc/` — helpers PHP par contexte (voir détails plus bas)
|
||
- `templates/` + `templates/partials/` — Twig
|
||
- `scss/` → `css/` — SASS compilé **manuellement** par l'utilisateur (CSS commité)
|
||
- `js/` — scripts frontend et `adminDashboardMods.js` (admin)
|
||
- `vendor/` — Composer (Timber 2.x). `composer install` après clone
|
||
|
||
### 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).
|
||
- **`thalim-newsletter/`** — composition et export HTML des digests mensuels. Admin : Outils → Newsletter. Voir le README du plugin pour les constantes de catégories.
|
||
|
||
### 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 `functions.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 `functions.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 `functions.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 |
|
||
|---|---|
|
||
| `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). |
|
||
| `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 `functions.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 (HEAD request avec `d=404`) — résultat (positif ou négatif) caché 1 semaine en transient `thalim_gravatar_{ID}`
|
||
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 `<tr>` porte `data-name`, `data-status`, `data-affiliation`, `data-roles` (séparés par `|`), `data-avatar`, `data-domaines`, `data-autres-domaines` — consommés par `membresFilters.js` (filtre rôle + recherche texte sur name/status/affiliation, tri colonne), `membresPopover.js` (popover au hover), `seanceToggle.js` (collapse de groupe)
|
||
- **Zébrage** : classe `.is-even-row` posée en JS après chaque filtre/tri — pas `nth-of-type`, qui compte les lignes masquées et casse l'alternance
|
||
|
||
### Single post
|
||
|
||
`single-helpers.php` fait quasi tout le travail. À noter :
|
||
|
||
- Hiérarchie de catégorie : `wp_get_post_categories()` → exclut cat 12 (séance) et 31, prend les ancêtres pour `parent_slug` (color theme) et `parent_name`
|
||
- Posts en catégorie racine sans sous-catégorie → leur lien vers la liste devient `/category/{slug}/autres/` (rewrite rule dédiée)
|
||
- `documents_joints` (Pods relationship file) est splitté par mime-type en `images` (taille `large`, marqué `portrait` si `h > w`) et `documents`. Les captions et titres passent par `|bilingual`
|
||
- Toggle « afficher le titre des images en légende » : meta `afficher_le_titre_des_images_en_legende`
|
||
- Séances de séminaire (`seances` meta = array de post IDs) sont **redirigées via `template_redirect`** vers le parent séminaire avec ancre `#seance-{ID}`. Les helpers post-card et agenda-card font la même substitution pour les liens
|
||
|
||
### Front page
|
||
|
||
`is_front_page()` charge : logo animé (`animatedLogo.js`, aussi sur 404), hero avec mots colorés (`coloredWordsHero.js`), Swiper d'annonces (`annoncesSwiper.js`), `messageLabo.js`, nuage de mots-clés (`keywordCloud.js`, exclut les tags marqués `ne_pas_afficher_dans_le_nuage`), `quickLinks.js`. Tags localisés dans `thalimTags` (window) via `wp_localize_script`.
|
||
|
||
### Catégorie / archives / agenda
|
||
|
||
`is_category()` charge Swiper + `agendaView.js` (slider d'événements à venir, scroll horizontal centré sur aujourd'hui via `today_offset` renvoyé par l'AJAX).
|
||
|
||
Toutes les pages d'archives (catégorie, taxonomie, tag, `/annonces`, search) chargent :
|
||
- `infiniteScroll.js` — AJAX paginé via `wp_ajax_load_more_posts`. Params POST : `page`, `category`, `axe`, `date_from`/`date_to`, `taxonomy`/`term`, `filter_cat`/`filter_autres`, `exclude_cats`, `search`, `lang`. Recherche passe par Relevanssi (`'relevanssi' => true`, `orderby => 'relevance'`)
|
||
- `categoryFilters.js` — UI filtres
|
||
|
||
Sur les pages de catégorie, les **posts épinglés** sont sortis du flux principal (`epingler_dans_la_categorie` = `1` ET (`date_de_fin_depinglage` vide OU ≥ aujourd'hui)) — affichés en tête, exclus de la pagination AJAX via `post__not_in`.
|
||
|
||
Sur les pages de taxonomie/tag, les **séances de séminaire (cat 12)** sont exclues : on liste les posts taggés, pas leurs séances.
|
||
|
||
## Restrictions / sécurité contenus
|
||
|
||
- **Vie du labo (cat 9)** : exclue des requêtes pour les non-connectés via `pre_get_posts`, redirige vers `/` si on visite la page de catégorie, retirée du menu via `wp_nav_menu_objects`
|
||
- **Contributeurs** :
|
||
- Liste admin filtrée : posts qu'ils ont écrits **OU** où ils sont dans `membres`/`autre_membres` (filtre `pre_get_posts` + override de `wp_count_posts` pour des compteurs cohérents)
|
||
- Peuvent **éditer** les posts où ils figurent comme membre (filtre `user_has_cap` — accorde les caps primitives le temps du check, sans modifier la table des rôles). Gère le flow save : capture `post_ID` depuis `$_POST` quand WP teste `edit_others_posts` sans `$args[2]`
|
||
- Login redirige vers `edit.php` (pas le dashboard). Dashboard et menu Outils masqués pour les non-admins
|
||
- **Filtre catégorie admin** : par défaut WP inclut les sous-catégories, on force `include_children => false` (`thalim_exact_category_filter`)
|
||
- **Axes filtres** : pour les contributeurs, restreints au premier groupe (le plus récent) via `thalim_get_axes_filter_groups()` slice
|
||
|
||
## Customisations admin
|
||
|
||
`js/adminDashboardMods.js` (≈900 lignes) + `css/admin.css`. L'admin est très modifié.
|
||
|
||
### Patterns transversaux
|
||
|
||
- **Cacher des éléments** : `css/admin.css` avec `display: none !important`. Pour les boutons TinyMCE, **sélecteurs stables uniquement** (`aria-label`, `.mce-i-bold`, `.mce-i-italic`) — jamais `#mceu_*` qui dépendent du nombre d'éditeurs sur la page
|
||
- **Force du mode visuel** : filtre PHP `user_can_richedit` → `true` (sinon les users avec « désactiver l'éditeur visuel » dans leur profil sont bloqués en mode code, car nos CSS masquent les onglets Visual/Code). `ensureVisualMode(editorId)` (JS) retry jusqu'à 15× pour passer de `html-active` à `tmce-active`
|
||
- **TinyMCE sur champs conditionnels** : un éditeur initialisé dans un élément `display:none` a son iframe cassée (vide, non interactive). `reinitEditor(editorId)` détruit et recrée l'instance quand le champ devient visible. Utilisé pour `body_en` (au premier clic onglet EN) et pour `reference_bibliographique` (via MutationObserver sur le `style` de la ligne Pods)
|
||
- **Rename « Article » → « Annonce »** : `renameArticlesToAnnonces()` parcourt les nœuds texte des éléments connus (menu, h1, notices, submitbox, screen options, search) plus l'admin bar (côté PHP). Réplique de plusieurs casses
|
||
|
||
### Page d'édition de post
|
||
|
||
`initPostEditPage()` orchestre :
|
||
|
||
- Désactivation d'options de catégorie (`1`, `12`, `5`, `20`) dans le select Pods
|
||
- Réordonnancement des metaboxes : `type-dannonce` en haut du `#side-sortables`, `affichage-sur-laccueil` et `thematique` en bas, `champs-contextuels` en haut du `#normal-sortables`, `documents-joints` juste après, `submitdiv` en bas
|
||
- **Onglets FR/EN sur le corps** : barre de tabs injectée avant `#postdivrich`. La metabox `#pods-meta-body-en` est déplacée juste après l'éditeur natif (le déplacement DOM casse son iframe TinyMCE — réparé par `reinitEditor()` au premier clic EN). Phase 1 = DOM avant `t=100ms`, Phase 2 = wiring après
|
||
- **Groupement des axes thématiques** : la liste des cases à cocher est regroupée par période (label = `annee_debut – annee_fin` ou « Axes antérieurs »), via `groupAxesCheckboxes()`. Données passées en `thalimAxesGroups` via `wp_localize_script`. Re-grouping déclenché si la ligne devient visible (MutationObserver)
|
||
- **Visibilité conditionnelle des Pods boxes** : `updatePostboxVisibility()` cache une `.postbox` Pods si tous ses `<tr>` ont `display:none` (sauf `body-en` qui est piloté par les onglets). Re-déclenché au changement de catégorie
|
||
- **Séparateur dans la grille Membres** : `<tr class="membres-grid-separator">` injecté en fin de tbody, affiché uniquement si une ligne `autre_*` est visible
|
||
- **Popover date picker Gutenberg** : le composant Popover de Gutenberg se ferme sur outside-click via `focusout`, mais si le focus n'y entre jamais, il reste ouvert. `initDatePickerPopoverFix()` force le focus dès que `.components-popover__content` apparaît dans le DOM
|
||
- **Info-popovers** (`INFO_TIPS` array) : boutons d'aide attachés à des sélecteurs spécifiques. Type `info` (cercle, neutre) ou `translate` (globe, vert) pour rappeler la convention `FR // EN`. Champ `page` (`post` | `user` | `taxonomy`) limite le scope
|
||
|
||
### Page de profil utilisateur
|
||
|
||
`initProfileEditors()` :
|
||
|
||
- Réordonne les sections natives WP en lignes deux-colonnes (`PROFILE_ORDER`)
|
||
- Masque le `<h2>` « À propos du compte »
|
||
- Renomme le label `Rôle` en `Rôle sur le site`
|
||
- Force le mode visuel sur tous les WYSIWYG Pods (`load` du window)
|
||
- `do_action('show_user_profile', new WP_User(0))` sur `user-new.php` pour que les champs Pods s'affichent à la création d'utilisateur. `user_register` rejoue `personal_options_update` pour sauver ces champs avec le nouvel ID
|
||
- Auto-sync `display_name` ← `first_name + last_name` à chaque save (`profile_update`, `user_register`, et hook Pods)
|
||
|
||
### Pods modal (création de séance via popup)
|
||
|
||
Quand l'URL contient `pods_modal`, la catégorie est verrouillée sur `12` (séance de séminaire) et le select est désactivé pour les autres valeurs.
|
||
|
||
### Colonnes admin custom
|
||
|
||
- Taxonomy `programme_de_recherche` : remplace « Description » par « Type de programme » (term meta `type_de_programme`). Select de filtre injecté côté JS dans la search-form, filtré côté serveur via `pre_get_terms`
|
||
- Taxonomy `post_tag` : remplace « Description » par « Exclure du nuage » (term meta `ne_pas_afficher_dans_le_nuage`)
|
||
|
||
### Autres tweaks admin
|
||
|
||
- Champ `etiquettes` (post_tag pick) : autocomplete déplafonnée (filtre `pods_form_ui_field_pick_autocomplete_limit` retourne `-1`)
|
||
- Admin bar : retire `wp-logo`, `customize`, `wpforms-menu`, et rename « article » → « annonce » dans tous les nodes
|
||
|
||
## Search panel (`templates/partials/search-panel.twig`)
|
||
|
||
Réutilisée dans `search.twig`. L'icône loupe `iconoir-search` est un `<button type="submit" class="search-panel__icon-btn">` (positionné en absolute dans `.search-panel__input-wrap` qui est relative — styles dans `scss/_header.scss`).
|
||
|
||
## Rewrite rules
|
||
|
||
- `/category/{slug}/autres` → posts directement dans la catégorie racine (sans sous-catégorie). Query var `thalim_direct_posts=1`. Utilisé pour le lien depuis les `post-card` quand la catégorie est racine
|
||
|
||
## Conventions Pods
|
||
|
||
- **Membres / autre_membres** : champs `pick` (user) multi-valeur sur les posts. `membres` = membres Thalim, `autre_membres` = externes. Les helpers résolvent en `[{name, url}]`. Quand `membres` est vide, `autre_membres` est utilisé en fallback pour les cards
|
||
- **Liens externes** : convention `lien_externe_1/2/3` + `titre_du_lien_externe_1/2/3` (bilingual). Fallback titre : nom de domaine
|
||
- **Champs `type_*`** (`type_colloque_journee_d_etude`, `type_soutenance`, etc.) : un seul est rempli par post, le premier non-vide donne le `type_label`. Idem pour `fonction_*` et `autre_fonction_*`. Pour les posts anciens sans ces champs, fallback sur un mapping `_pods_categorie ID → label` codé en dur dans `single-helpers.php`
|
||
- **Axes thématiques** (taxonomy `axe_thematique`) : term meta `annee_debut`, `annee_fin`, `ordre_daffichage`. Un axe est « courant » si `annee_fin >= année courante`. Affiché dans le menu de navigation (`axes_courants` dans le contexte)
|
||
|
||
## Workflow
|
||
|
||
- **NE PAS compiler le SCSS/CSS.** L'utilisateur le fait manuellement. Éditer uniquement les `.scss`
|
||
- **CSS commité** dans le dépôt (`css/*.css`) pour que le site fonctionne après clone
|
||
- **SQL direct OK** pour les tweaks ponctuels — phpMyAdmin sur http://localhost:8021
|