Chapitre 8.9 — Tutoriel : le bloc « Livre à la une »
⏱️ TL;DR — On assemble la Partie 8 en un bloc réel, branché sur le plugin
wpr-books(Partie 7). Le bloc « Livre à la une » est dynamique (rendu PHP) : dans l’éditeur, l’utilisateur choisit un livre (viauseSelect+getEntityRecords) et règle l’affichage (Inspector) ; sur le front, unrender.phpaffiche titre, image, prix (metawpr_prix). Il combineblock.json,edit/attributs, contrôles, data store et rendu dynamique — tout le chapitre en une pièce.
🎯 Objectifs
- Construire un bloc dynamique de bout en bout.
- Charger et sélectionner une entité (livre) via
@wordpress/data. - Rendre le front en PHP, sécurisé, avec les supports natifs.
- Comprendre le pont Partie 7 → 8 (et vers 9).
1. block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "wpr/livre-une",
"title": "Livre à la une",
"category": "widgets",
"icon": "book",
"textdomain": "wpr-books",
"attributes": {
"livreId": { "type": "number" },
"afficherPrix":{ "type": "boolean", "default": true }
},
"supports": {
"html": false,
"align": ["wide"],
"color": { "background": true, "text": true },
"spacing": { "padding": true }
},
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"render": "file:./render.php"
}Bloc dynamique (render), avec un attribut livreId (le livre choisi) et afficherPrix. Les supports offrent couleur/alignement/espacement gratuitement.
2. edit.js : sélection du livre + réglages
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, ComboboxControl, ToggleControl, Spinner, Placeholder } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { __ } from '@wordpress/i18n';
export default function Edit( { attributes, setAttributes } ) {
const { livreId, afficherPrix } = attributes;
const blockProps = useBlockProps();
// Charger la liste des livres (résolution auto → REST /wp/v2/livre)
const { livres, chargement } = useSelect( ( select ) => {
const query = { per_page: 50, _fields: 'id,title' };
return {
livres: select( coreStore ).getEntityRecords( 'postType', 'livre', query ),
chargement: select( coreStore ).isResolving( 'getEntityRecords', [ 'postType', 'livre', query ] ),
};
}, [] );
// Charger le livre sélectionné (titre + meta)
const livre = useSelect(
( select ) => livreId ? select( coreStore ).getEntityRecord( 'postType', 'livre', livreId ) : null,
[ livreId ]
);
const options = ( livres || [] ).map( ( l ) => ( {
value: l.id,
label: l.title.rendered || `#${ l.id }`,
} ) );
return (
<>
<InspectorControls>
<PanelBody title={ __( 'Réglages', 'wpr-books' ) }>
<ComboboxControl
label={ __( 'Livre', 'wpr-books' ) }
value={ livreId }
options={ options }
onChange={ ( id ) => setAttributes( { livreId: id ? Number( id ) : undefined } ) }
/>
<ToggleControl
label={ __( 'Afficher le prix', 'wpr-books' ) }
checked={ afficherPrix }
onChange={ ( v ) => setAttributes( { afficherPrix: v } ) }
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
{ chargement && ! livres && <Spinner /> }
{ ! livreId && (
<Placeholder icon="book" label={ __( 'Livre à la une', 'wpr-books' ) }
instructions={ __( 'Choisissez un livre dans les réglages.', 'wpr-books' ) } />
) }
{ livreId && ! livre && <Spinner /> }
{ livre && (
<div className="wpr-livre-une__apercu">
<strong>{ livre.title?.rendered }</strong>
{ afficherPrix && livre.meta?.wpr_prix
? <span> — { livre.meta.wpr_prix } €</span> : null }
</div>
) }
</div>
</>
);
}Points clés :
getEntityRecords('postType','livre')charge la liste (le store fait le fetch REST — d’où l’importance deshow_in_restsur le CPT, ch. 7.3).ComboboxControl= un select recherchable pour choisir le livre.- Gestion des états (chargement
Spinner, videPlaceholder, prêt) — vos réflexes React. livre.meta?.wpr_prix: la meta est là parce qu’ elle est enregistrée avecshow_in_rest(ch. 7.4).
⚠️ Piège — la meta absente dans l’entité. Si
livre.metaest vide/undefined, vérifiez quewpr_prixest bienregister_post_meta(..., show_in_rest => true)(Partie 7) : sans ça, la REST (et donc le store) ne renvoie pas la meta.
3. save.js : rien (dynamique)
export default function save() {
return null; // rendu par render.php
}4. render.php : le front, sécurisé
<?php
// $attributes, $content, $block disponibles
$livre_id = isset( $attributes['livreId'] ) ? absint( $attributes['livreId'] ) : 0;
if ( ! $livre_id || get_post_type( $livre_id ) !== 'livre' ) {
return; // rien à afficher
}
$titre = get_the_title( $livre_id );
$url = get_permalink( $livre_id );
$prix = get_post_meta( $livre_id, 'wpr_prix', true );
$img = get_the_post_thumbnail( $livre_id, 'medium' );
$wrapper = get_block_wrapper_attributes( array( 'class' => 'wpr-livre-une' ) );
?>
<div <?php echo $wrapper; ?>>
<a href="<?php echo esc_url( $url ); ?>" class="wpr-livre-une__lien">
<?php echo $img; // HTML d'image généré par le cœur (déjà sûr) ?>
<span class="wpr-livre-une__titre"><?php echo esc_html( $titre ); ?></span>
</a>
<?php if ( ! empty( $attributes['afficherPrix'] ) && $prix ) : ?>
<span class="wpr-livre-une__prix">
<?php echo esc_html( number_format_i18n( (float) $prix, 2 ) ); ?> €
</span>
<?php endif; ?>
</div>get_block_wrapper_attributes()applique lessupports(couleur, alignement, padding) sur le front (ch. 8.5).- Échappement partout (
esc_url,esc_html) — sécurité (ch. 7.7). - Le prix vient de la même meta que la metabox de la Partie 7 : une seule source de vérité.
5. Enregistrer le bloc depuis wpr-books
Dans le plugin wpr-books (Partie 7), on ajoute :
add_action( 'init', function () {
register_block_type( WPR_BOOKS_PATH . 'build/livre-une' ); // dossier de build du bloc
} );Le bloc vit dans le plugin wpr-books : le CPT, la meta et le bloc forment un tout cohérent.
6. Ce qu’on a démontré (et la suite)
- Un bloc dynamique complet : sélection d’entité en React, rendu serveur sécurisé.
- Le data store relie le bloc aux données du CMS sans REST manuel.
- La cohérence Partie 7 (données) → Partie 8 (bloc) : la même meta alimente admin, bloc et — bientôt — le front headless (Partie 9).
💡 Pour un dev React — Vous venez de faire, dans WordPress, ce que vous faites tous les jours : un composant qui charge une liste depuis une API (
getEntityRecords), laisse choisir un élément (Combobox), gère les états (loading/empty/ready), et affiche des données — avec un rendu serveur pour le front. Le storecorevous a épargné tout le câblage API. C’est exactement le genre de compétence (React dans WordPress) qui est rare et bien payée (Partie 13).
✏️ Exercices
- Pourquoi ce bloc est-il dynamique plutôt que statique ?
- Qu’est-ce qui garantit que
getEntityRecords('postType','livre')fonctionne (côté Partie 7) ? - La liste des livres se charge, mais le prix ne s’affiche jamais dans l’aperçu. Quelle cause vérifiez-vous ?
✅ Solution
- Parce que le contenu affiché dépend d’une donnée qui évolue (le livre, son titre, son prix). Un
savestatique figerait ces valeurs à l’insertion et provoquerait une invalidation si le livre change ; le rendu PHP recalcule à chaque affichage, toujours à jour. - Le CPT
livreest enregistré avecshow_in_rest => true(Partie 7, ch. 7.3) : la ressource/wp/v2/livreexiste, et le storecorepeut la charger viagetEntityRecords. - La meta
wpr_prixn’est probablement pas exposée : vérifier qu’elle estregister_post_meta(..., 'show_in_rest' => true)(ch. 7.4). Sans ça,livre.meta.wpr_prixest absent du store.
🧠 Quiz de révision
1. Comment le bloc charge-t-il la liste des livres ?
Via useSelect + getEntityRecords('postType','livre') (le store fait le fetch REST).
2. Que renvoie save() pour ce bloc ?
null : le rendu front est produit par render.php (bloc dynamique).
3. Quel appel applique les supports sur le front ?
get_block_wrapper_attributes() dans render.php.
4. D’où vient le prix affiché ?
De la meta wpr_prix du livre (la même que la metabox de la Partie 7) : une seule source de vérité.
5. Où le bloc est-il enregistré ?
Dans le plugin wpr-books (register_block_type du dossier de build), avec le CPT et la meta.
Partie suivante : Partie 9 — REST API & WordPress headless (Next.js).