Salta ai contenuti

Mu-plugin WordPress

Plugin normale Mu-plugin
Attivazione manuale automatica
Posizione wp-content/plugins/ wp-content/mu-plugins/
Disattivabile da admin sì no
Hook di attivazione sì no
Ordine di caricamento dopo prima dei plugin normali
Aggiornamento marketplace/zip deploy via FTP

Per un contratto dati che il frontend presuppone, il mu-plugin è la scelta giusta: se il plugin che registra i CPT non è attivo, la build fallisce. Con un plugin normale sarebbe un errore umano silenzioso.

<?php
/**
* Plugin Name: Headless CMS Setup
* Description: CPT, meta e impostazioni per il frontend Astro.
* Version: 1.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
final class Headless_CMS {
public static function init(): void {
add_action('init', [self::class, 'register_post_types']);
add_action('init', [self::class, 'register_meta']);
add_action('save_post', [self::class, 'save_meta'], 10, 2);
}
// ...
}
Headless_CMS::init();

Regole:

  • guard ABSPATH sempre;
  • niente output, niente HTML nell’admin se non nei metabox;
  • classi con metodi statici, oppure funzioni prefissate (headless_*);
  • nessuna credenziale nel codice;
  • versione nel docblock e nel repository, così sai cosa è deployato.
  1. CPT con register_post_type: show_in_rest => true, rest_base esplicito, supports con custom-fields. Senza custom-fields i meta non vengono salvati né esposti.
  2. Meta con register_post_meta: single, type, schema REST (string, array, integer, boolean), auth_callback => '__return_true' per la lettura pubblica.
  3. Metabox: generati da una configurazione dichiarativa (CPT + PAGE_META), quindi aggiungere un campo = aggiungere una riga, non scrivere HTML a mano.
  4. Salvataggio: nonce, autosave, capability edit_post, sanitizzazione. I campi JSON vengono decodificati in array; se il JSON non è valido il salvataggio viene rifiutato con un avviso in admin.
  5. Impostazioni sito: opzioni headless_*, pagina in Impostazioni, rotta pubblica /headless/v1/settings in sola lettura.

Alcuni contenuti sono liste strutturate: agenda, paragrafi, benefici, opzioni di prezzo. Un campo testo con JSON è il compromesso più semplice senza ACF:

['key' => 'agenda', 'label' => 'Agenda (JSON)', 'type' => 'json', 'rest' => [
'type' => 'array',
'items' => ['type' => 'object', 'properties' => [
'time' => ['type' => 'string'],
'activity' => ['type' => 'string'],
]],
]],

La REST restituisce un array, non una stringa: lo schema Zod lo vede già come { time, activity }[].

  1. Aggiungi la riga in CPT[...]['fields'].
  2. Rigenera i permalink non serve, ma ricarica la pagina di modifica.
  3. Aggiorna lo schema Zod (con o senza .default()), il mapper e il dominio.
  4. Aggiorna la fixture e il test.
  5. Se il campo è obbligatorio, non mettere default: la build deve fallire quando manca.
  1. In SITE_SETTINGS aggiungi 'chiave' => ['label' => '...', 'type' => 'string'].
  2. In src/cms/wordpress/wordpress-settings.schema.ts aggiungi il campo snake_case.
  3. Nel mapper aggiungi il camelCase.
  4. In SiteSettings.ts e in getSiteSettings() gestisci il fallback.
  • Registra il CPT contatto privato (non in REST): è una inbox, non un contenuto.
  • Espone POST /headless/v1/contact.
  • Valida e sanitizza con le funzioni WP (sanitize_text_field, sanitize_email, sanitize_textarea_field).
  • Honeypot: se il campo azienda è compilato, risponde 201 senza salvare (i bot non capiscono di essere stati scoperti).
  • Rate limit: transient di 60 secondi per IP, risposta 429.
  • Invia una mail a headless_email (fallback: email admin).
  • CORS aperto solo per la rotta contatti, utile in sviluppo locale.
  • Ascolta wp_after_insert_post e updated_option.
  • Filtra i post type rilevanti e le opzioni headless_*.
  • Debounce di 60 secondi con transient: una raffica di salvataggi produce una dispatch.
  • Chiama GitHub repository_dispatch con blocking: false su shutdown, così l’admin non aspetta la rete.
  • Senza costanti HEADLESS_WEBHOOK_* non fa nulla: in locale non serve configurarlo.

Vedi Build e concorrenza per il comportamento della coda.

  • permission_callback => '__return_true' solo su letture pubbliche e sul form.
  • Ogni scrittura passa da nonce + capability.
  • Sanitizza in ingresso, escapa in uscita (esc_html, esc_attr, esc_textarea).
  • Nessun eval, nessuna query SQL custom.
  • Token e password in wp-config.php, mai nel repository né nel database.
  • XML-RPC disattivato.
  • Tieni WordPress e i plugin aggiornati: il backend è la parte esposta.

I mu-plugin si caricano via FTP in wp-content/mu-plugins/ e restano attivi. Dopo un aggiornamento di headless-cms.php: salva i permalink una volta, poi verifica /wp-json/wp/v2/{cpt}.

Versiona i file nel repository (come qui) e annota nel commit cosa è stato deployato.

Se la funzionalità non è essenziale per il contratto dati (es. un redirect, un widget social, un’integrazione marketing), un plugin normale è più gestibile: si attiva, si disattiva, si aggiorna. Il mu-plugin è per ciò che il frontend pretende che esista.