Files
thalim-stack/CLAUDE.md
2026-05-12 23:32:48 +02:00

19 KiB
Raw Blame History

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

docker-compose up -d     # WordPress, MySQL, phpMyAdmin
docker-compose down

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 (13), 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_richedittrue (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_namefirst_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