# Un bloc Gutenberg qui génère du texte par IA directement dans l’éditeur

> Construire un bloc personnalisé qui appelle une route REST serveur vers un LLM, insère la proposition dans l'éditeur et laisse le rédacteur valider chaque phrase.

- Auteur : Clément Hadrot
- Publié le : 2023-04-10
- Mis à jour le : 2023-04-10
- Catégorie : IA &amp; MCP
- URL : https://wpmoderne.dev.wordpress-developpement.fr/ia-mcp/bloc-gutenberg-generation-texte-ia/

## L’essentiel

- Appel côté serveur, jamais côté client
- Insertion en état "brouillon" éditable
- Aucune clé API dans le navigateur

Construire un bouton « générer avec l'IA » dans l'éditeur de blocs paraît simple sur le papier : un clic, un appel réseau, du texte qui apparaît. La difficulté réelle se situe ailleurs, dans les détails qu'on découvre seulement en le mettant en production — la clé API qu'on a failli exposer côté client, le texte généré qui remplace un paragraphe sans confirmation, ou le bloc qui reste bloqué en chargement si la requête échoue.

Ce tutoriel construit un bloc Gutenberg minimal mais robuste, avec un appel serveur sécurisé et une insertion du texte que le rédacteur peut modifier avant de la garder.

## Architecture générale : le bloc ne parle jamais directement au LLM

Le principe de base, non négociable, est que le navigateur du rédacteur n'appelle jamais l'API du fournisseur de LLM directement. Le bloc appelle une route REST WordPress, qui elle-même appelle le service d'IA depuis le serveur, où la clé API reste stockée en toute sécurité, par exemple via une constante définie dans `wp-config.php`.

```
Navigateur (bloc Gutenberg)
        │  apiFetch( '/wpm/v1/generate' )
        ▼
Serveur WordPress (route REST)
        │  wp_remote_post() avec la clé API
        ▼
API du fournisseur de LLM
```

## Enregistrer le bloc et son panneau de génération

Le bloc s'enregistre classiquement avec `register_block_type()` côté PHP et un fichier `block.json` décrivant ses attributs, dont un champ `content` pour stocker le texte final.

```
registerBlockType( 'wpm/generateur-texte', {
    edit: ( { attributes, setAttributes } ) => {
        const [ isLoading, setIsLoading ] = useState( false );

        const generer = async () => {
            setIsLoading( true );
            const reponse = await apiFetch( {
                path: '/wpm/v1/generate',
                method: 'POST',
                data: { instruction: attributes.instruction },
            } );
            setAttributes( { content: reponse.texte, brouillon: true } );
            setIsLoading( false );
        };

        return (
            <div>
                <Button onClick={ generer } isBusy={ isLoading }>
                    Générer une proposition
                </Button>
                <RichText
                    tagName="p"
                    value={ attributes.content }
                    onChange={ ( content ) => setAttributes( { content } ) }
                />
            </div>
        );
    },
} );
```

> L'essentiel à retenir : Appel côté serveur, jamais côté client ; Insertion en état "brouillon" éditable ; Aucune clé API dans le navigateur

## La route REST côté serveur

Le callback vérifie les permissions, construit le prompt à partir de l'instruction du rédacteur, appelle le modèle et renvoie le texte brut, sans mise en forme imposée : c'est le rédacteur qui décide de l'intégrer tel quel ou de le retravailler.

```
add_action( 'rest_api_init', function () {
    register_rest_route( 'wpm/v1', '/generate', array(
        'methods'             => 'POST',
        'permission_callback' => function () {
            return current_user_can( 'edit_posts' );
        },
        'callback'            => function ( WP_REST_Request $request ) {
            $instruction = sanitize_textarea_field( $request->get_param( 'instruction' ) );

            $reponse = wp_remote_post( 'https://api.openai.com/v1/chat/completions', array(
                'timeout' => 25,
                'headers' => array(
                    'Authorization' => 'Bearer ' . WPM_LLM_API_KEY,
                    'Content-Type'  => 'application/json',
                ),
                'body'    => wp_json_encode( array(
                    'model'    => 'gpt-4o-mini',
                    'messages' => array(
                        array( 'role' => 'user', 'content' => $instruction ),
                    ),
                ) ),
            ) );

            if ( is_wp_error( $reponse ) ) {
                return new WP_Error( 'wpm_llm_error', 'Le service IA est indisponible.' );
            }

            $corps = json_decode( wp_remote_retrieve_body( $reponse ), true );
            $texte = $corps['choices'][0]['message']['content'] ?? '';

            return array( 'texte' => wp_kses_post( $texte ) );
        },
    ) );
} );
```

### Le champ "brouillon" : une décision d'interface, pas de code

L'attribut `brouillon` ajouté au bloc sert à afficher visuellement une bordure orange tant que le rédacteur n'a pas validé le texte. Ce n'est qu'un signal visuel, mais il évite qu'un paragraphe généré automatiquement se retrouve publié sans relecture parce qu'il ressemblait, visuellement, à un paragraphe écrit à la main.

## Gérer les échecs sans bloquer l'éditeur

Un appel réseau peut échouer pour de multiples raisons : quota dépassé, timeout, réponse mal formée. Le bloc affiche alors un message d'erreur clair et remet le bouton dans son état initial, plutôt que de laisser l'indicateur de chargement tourner indéfiniment, un défaut que nous avons corrigé après un retour agacé d'un rédacteur un vendredi après-midi.

- Timeout fixé à 25 secondes côté `wp_remote_post()`.
- Message d'erreur explicite affiché dans le bloc, jamais une page blanche.
- Le contenu déjà présent dans le bloc n'est jamais effacé en cas d'échec de la génération suivante.

## Notre verdict

Un bloc de génération de texte réussi tient davantage à ses garde-fous qu'à la qualité du texte produit par le modèle : clé API hors du navigateur, contenu toujours éditable, échecs gérés proprement. Cette base couvre l'essentiel des cas pratiques rencontrés sur les projets clients où ce type de bloc est resté en production plus de quelques semaines.
