19 KiB
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
- 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 admininc/— helpers PHP par contexte (voir détails plus bas)templates/+templates/partials/— Twigscss/→css/— SASS compilé manuellement par l'utilisateur (CSS commité)js/— scripts frontend etadminDashboardMods.js(admin)vendor/— Composer (Timber 2.x).composer installaprè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'siTHALIM_ORIGINAL_URIcommence par/en/, sinon'fr'. Le préfixe/en/est strippé viado_parse_requestavant la résolution d'URL WP, etredirect_canonicalest 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 pourbiographie_en,presentation_en,recherches_en_cours_en,resume_de_la_these_en,autres_domaines_de_recherches_ensur les utilisateurs, etpresentation_en/presentation_detail_ensur le postcontenu_general - URLs préfixées :
thalim_en_url($url)ajoute/enaux URLs internes en mode EN, attaché àterm_link,post_link,page_link,post_type_link,author_link. Filtre Twig|en_urlaussi 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 metatitre_anglaisen mode EN. Aussi appliqué au title de l'onglet navigateur viadocument_title_parts - Menus : slugs
Navigation/Navigation-enetFooter/Footer-en(pasmenu-principal— c'est un legacy obsolète) - Contenu général : un seul post de type
contenu_generalfournit les blocs UMR/thalim/siècles/présentation au layout viagcdans le contexte Twig - Sélecteur de langue :
thalim_language_switcher()retourne['fr' => ['slug','url','current_lang'], 'en' => …]— remplacepll_the_languages() - Override pour AJAX : poser
$GLOBALS['thalim_lang_override'] = $langavantthalim_current_language()(utilisé par les handlersload_more_posts/load_more_agendaqui 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' => trueaux args d'unWP_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_wheredansfunctions.php. LEFT JOIN surpostmetapuisCASE WHEN date_de_debut ELSE datetime ELSE post_date END DESC. Les valeurs0000-00-00...sont traitées comme NULL - Format d'affichage :
thalim_format_date($raw, $lang)dansinc/single-helpers.phprenvoiedate_i18n('j F Y', $ts). Les abréviations de mois (3-lettres) pour les vignettes agenda sont codées en dur dansfunctions.php(thalim_get_agenda_card_data()) et dansinc/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 dansthalim_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 :
- Simple Local Avatar (
simple_local_avataruser meta) — résolu viamedia_idquand dispo (pour survivre aux changements de domaine), sinon URL réécrite - Gravatar (HEAD request avec
d=404) — résultat (positif ou négatif) caché 1 semaine en transientthalim_gravatar_{ID} - 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 :
directeurpuisdirecteur_adjoint, lus depuis les post meta de la pagele-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 metalast_name - Cas spécial « Personnel d'accompagnement à la recherche » : les statuts contenant « Gestion et pilotage » remontent en tête
- Chaque
<tr>portedata-name,data-status,data-affiliation,data-roles(séparés par|),data-avatar,data-domaines,data-autres-domaines— consommés parmembresFilters.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-rowposée en JS après chaque filtre/tri — pasnth-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 pourparent_slug(color theme) etparent_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 enimages(taillelarge, marquéportraitsih > w) etdocuments. 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 (
seancesmeta = array de post IDs) sont redirigées viatemplate_redirectvers 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é viawp_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 viawp_nav_menu_objects - Contributeurs :
- Liste admin filtrée : posts qu'ils ont écrits OU où ils sont dans
membres/autre_membres(filtrepre_get_posts+ override dewp_count_postspour 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 : capturepost_IDdepuis$_POSTquand WP testeedit_others_postssans$args[2] - Login redirige vers
edit.php(pas le dashboard). Dashboard et menu Outils masqués pour les non-admins
- Liste admin filtrée : posts qu'ils ont écrits OU où ils sont dans
- 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.cssavecdisplay: 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 dehtml-activeàtmce-active - TinyMCE sur champs conditionnels : un éditeur initialisé dans un élément
display:nonea son iframe cassée (vide, non interactive).reinitEditor(editorId)détruit et recrée l'instance quand le champ devient visible. Utilisé pourbody_en(au premier clic onglet EN) et pourreference_bibliographique(via MutationObserver sur lestylede 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-dannonceen haut du#side-sortables,affichage-sur-laccueiletthematiqueen bas,champs-contextuelsen haut du#normal-sortables,documents-jointsjuste après,submitdiven bas - Onglets FR/EN sur le corps : barre de tabs injectée avant
#postdivrich. La metabox#pods-meta-body-enest déplacée juste après l'éditeur natif (le déplacement DOM casse son iframe TinyMCE — réparé parreinitEditor()au premier clic EN). Phase 1 = DOM avantt=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_finou « Axes antérieurs »), viagroupAxesCheckboxes(). Données passées enthalimAxesGroupsviawp_localize_script. Re-grouping déclenché si la ligne devient visible (MutationObserver) - Visibilité conditionnelle des Pods boxes :
updatePostboxVisibility()cache une.postboxPods si tous ses<tr>ontdisplay:none(saufbody-enqui 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 ligneautre_*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__contentapparaît dans le DOM - Info-popovers (
INFO_TIPSarray) : boutons d'aide attachés à des sélecteurs spécifiques. Typeinfo(cercle, neutre) outranslate(globe, vert) pour rappeler la conventionFR // EN. Champpage(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ôleenRôle sur le site - Force le mode visuel sur tous les WYSIWYG Pods (
loaddu window) do_action('show_user_profile', new WP_User(0))suruser-new.phppour que les champs Pods s'affichent à la création d'utilisateur.user_registerrejouepersonal_options_updatepour 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 metatype_de_programme). Select de filtre injecté côté JS dans la search-form, filtré côté serveur viapre_get_terms - Taxonomy
post_tag: remplace « Description » par « Exclure du nuage » (term metane_pas_afficher_dans_le_nuage)
Autres tweaks admin
- Champ
etiquettes(post_tag pick) : autocomplete déplafonnée (filtrepods_form_ui_field_pick_autocomplete_limitretourne-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 varthalim_direct_posts=1. Utilisé pour le lien depuis lespost-cardquand 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}]. Quandmembresest vide,autre_membresest 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 letype_label. Idem pourfonction_*etautre_fonction_*. Pour les posts anciens sans ces champs, fallback sur un mapping_pods_categorie ID → labelcodé en dur danssingle-helpers.php - Axes thématiques (taxonomy
axe_thematique) : term metaannee_debut,annee_fin,ordre_daffichage. Un axe est « courant » siannee_fin >= année courante. Affiché dans le menu de navigation (axes_courantsdans 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