Implementare un content model in WordPress: dalle pagine alle tassonomie, in tre versioni
La versione del mio content model che gira oggi su questo sito è la terza. La prima era sparsa in due plugin di clienti, e usava la stessa parola per dire due cose diverse; la seconda funzionava, ed era elegante, e l’ho buttata lo stesso. Ti racconto il percorso intero, perché le scelte giuste, alla fine, le hanno decise gli errori.
Due plugin, una parola, due significati
Prima del modello c’erano due plugin, ciascuno cucito su un cliente. Li ho scritti nel 2026, il primo a inizio maggio, il secondo a luglio. Namespace diversi, ma gli stessi hook: erano pensati fin dall’inizio per confluire, un giorno, in qualcosa di più grande.
Il cuore di entrambi era un sistema chiamato “Topic”. Un Topic era una pagina WordPress, marcata con un termine is_topic di una tassonomia page_type, con la sua gerarchia di pagine. Un rewrite tag %topic% prefissava gli URL dei contenuti: /servizi/ diventava /{topic}/servizi/. E un motore di relazioni, configurato da array negli argomenti dei post type, generava da solo i gruppi di campi ACF e denormalizzava le relazioni nei meta.
Era un sistema ingegnoso. Il problema non stava nel codice, stava nella parola.
Il primo cliente è un’azienda che eroga servizi sul territorio, rivolti a più target. Per questo la navigazione dava la priorità alle audience, e le audience erano geolocalizzate, città per città: lì “Topic” voleva dire il pubblico. Nel secondo, un’azienda manifatturiera, voleva dire le business unit. Stesso nome, stesso meccanismo, due concetti che non hanno niente in comune. Finché ogni plugin viveva sul suo sito nessuno se ne accorgeva; nel momento in cui dovevano diventare uno solo, la domanda era inevitabile: Topic, esattamente, che cos’è?
Se due progetti usano la stessa parola per due cose diverse, non hai un sistema condiviso. Hai due abitudini con lo stesso nome.
Il modello, prima del codice
La risposta non l’ho cercata nel codice. Ci stavo girando attorno da molto prima dei due plugin, e da tutt’altra finestra: quella del data analyst.
Era novembre 2025. Davanti ai report dei progetti di Creeo mi sono fatto una domanda semplice: che senso ha visualizzare tanti dati legati a tante URL, se quelle URL non sono raggruppate per concetto? Un elenco di pagine con le loro metriche ti dice cosa è successo a ciascuna. Non ti dice di cosa parla il sito, a chi, e a che punto del percorso le persone si fermano.
Da quella domanda ho cominciato a studiare la topical authority e la customer journey. Pian piano, progetto dopo progetto, si è delineato un percorso: prima raggruppare, poi dare un nome ai gruppi, poi capire che quei nomi dovevano valere per tutti, non solo per chi legge i numeri. A fine agosto, dopo i due plugin, quel percorso è diventato un documento: il Bebop Content Model, v1.0.
L’idea, in una riga: un contenuto è un punto in uno spazio a quattro dimensioni.
Content = Identity × Topic × Target × JourneyLe coordinate si decidono prima di scrivere, poi accompagnano il contenuto dal brief fino alla dashboard. Ogni dimensione ha le sue proprietà: Identity (Platform, Type, View), Topic (Primary, Cluster, Relations), Target (Persona, End User), Journey (Customer Journey, Stage, Intento). E sopra tutto un principio: una sola classificazione condivisa, che alimenta navigazione, link interni, template, schema.org, dataLayer e analytics insieme.
Riletti con questa lente, i due plugin si chiarivano da soli. Il pubblico del primo cliente non era un Topic: era un Target. E le business unit del secondo, come concetto a sé, sono sparite: sono diventate anche loro Target, rivolti alle Persona di quei settori, ognuno con una pillar page che porta lo schema Organization della sua business unit. Il modello non aggiungeva complessità: dava a ogni concetto una casa sua, e un nome che non si poteva più confondere.
Restava da capire come farlo girare in WordPress.
La seconda versione: pagine hub, repeater e un motore di query
Il primo tentativo di implementare il modello è stato un modulo dedicato, pensato a lungo. L’impostazione era questa:
- Identity derivata al momento della richiesta, mai salvata;
- Topic e Target come pagine hub, con un repeater per i Cluster e per gli End User;
- Journey come tassonomia;
- un motore di query con variabili sue, indici denormalizzati e sei condizioni per il builder.
C’era anche una primitiva che mi piaceva molto: un repeater sull’entità genitore, e sul contenuto una select condizionata che mostra solo i figli di quel genitore. Riutilizzabile, pulita.
Funzionava. Lo sottolineo, perché è il punto di tutta la storia.
Perché ho buttato una versione che funzionava?
Perché un content model deve poterlo usare anche chi non scrive codice, e quella versione no. Una soluzione funzionante non è un buon motivo per tenerla.
Me ne sono accorto appena ho visto la resa grafica in amministrazione. Associare Target, Topic e Customer Journey a un contenuto era complicato: repeater da compilare, select che dipendevano da altre select, dati sparsi tra pagine diverse. Ma il modello non lo usa solo lo sviluppatore: lo usa chi disegna l’interfaccia, lo usa il cliente che ogni giorno pubblica sul suo sito. Se classificare un contenuto è faticoso, prima o poi non lo fa nessuno, e una classificazione che nessuno compila non alimenta niente.
Serviva un modo più semplice, anche da vedere. Il 7 settembre ho cambiato strada: al posto di pagine hub, repeater e meta, tre tassonomie gerarchiche non pubbliche:
bcm_topic: Topic → Cluster;bcm_target: Persona → End User;bcm_journey: Customer Journey → Stage.
Due livelli al massimo, sempre. Il Primary di ogni dimensione è semplicemente il primo termine selezionato, denormalizzato in un meta per le query veloci. L’ordine lo governa l’autore con il gesto più naturale che ha: l’ordine in cui clicca.
La scelta è partita dall’interfaccia, ma il vantaggio più grande l’ha avuto il codice. Guarda cosa è sparito. Il motore di query dedicato: le variabili del modello diventano una tax_query, cioè WordPress che fa il suo mestiere. I repeater: la gerarchia della tassonomia è già la relazione genitore → figlio. Le pagine hub come contenitori di dati: il dato torna a essere un dato. Meno codice, una sola primitiva, query native.
Semplificando, la registrazione di una delle tre tassonomie si riduce a questo:
<?php
register_taxonomy('bcm_topic', ['post', 'page'], [
'labels' => [
'name' => 'Topic',
],
'hierarchical' => true,
'public' => false,
'show_ui' => true,
'rewrite' => false,
'meta_box_cb' => false,
'sort' => true,
]);Tre scelte da notare. public => false e rewrite => false: la classificazione è un dato, non un insieme di pagine, quindi niente archivi di tassonomia da mantenere e nessun URL nato per sbaglio. meta_box_cb => false: l’assegnazione passa da un campo ACF, non dal box nativo. sort => true: l’ordine di assegnazione si conserva, ed è quello che rende possibile “il primo è il Primary”.
Il vecchio modulo è rimasto nei file, fuori dall’elenco dei moduli attivi. Nessuna migrazione: non avevo ancora cominciato a usarlo. È il momento migliore per buttare qualcosa, e anche il più difficile da riconoscere.
La pillar page: il pezzo che mancava al modello
Se le tassonomie non sono pubbliche, come fa un Topic a diventare navigazione? Con una pillar page: una pagina normale, costruita con il builder, collegata a un termine.
Quando un termine ha una pillar, diventa raggiungibile. Il suo link è la pillar, e gli URL dei contenuti classificati su quel termine si prefissano con il percorso della pillar:
pillar del Topic "Bebop Content Model": /bebop-content-model/
contenuto classificato: /bebop-content-model/blog/content-model-wordpress-tassonomie/
archivio del blog sotto il topic: /bebop-content-model/blog/Qui la terza versione ha fatto pace con una mia vecchia preferenza. Per l’entità principale preferisco un custom post type e uso le tassonomie come filtro, per un motivo pratico: un archivio di tassonomia non lo costruisci con il page builder, una pagina sì. Il sistema “Topic” dei due plugin partiva dalla pagina; la seconda versione dalle pagine hub; la terza separa le due cose. Il dato sta nella tassonomia, la pagina resta una pagina, libera di essere disegnata come serve, e le collega un solo riferimento.
È anche il punto in cui la mia implementazione aggiunge qualcosa al modello. Nel documento, “senza Cluster il contenuto è una pillar page” era un’osservazione. Nel codice la pillar è un oggetto vero: collegata al termine, bloccata su quel termine, radice degli URL. Senza, il Topic resta descrittivo, buono per lo schema.org. Con, diventa struttura.
Cosa ha limato il sito vero
Una volta portato su questo sito, il modello ha incontrato la realtà, e la realtà ha corretto tre cose.
L’Identity è uscita dal modulo
Era nata dentro il modulo di classificazione, ma vale su ogni pagina, anche dove non si classifica niente: una 404, una ricerca, un archivio. La domanda che l’ha spostata è stata semplice: ma chi inietta cosa? Chi consuma l’Identity (dataLayer, CTA, classificazione) non la possiede. Così la separazione tra dichiarato e derivato è diventata netta: Topic, Target e Journey si dichiarano; l’Identity si calcola al momento della richiesta e non si salva mai. Il Type è il post type, la View è la forma della pagina.
Le page non vengono mai riscritte
Sono l’unico post type a cui si può assegnare un Topic senza toccare l’URL. Altrimenti una sottopagina classificata sul Topic della sua stessa pillar corromperebbe il percorso gerarchico. Una regola nata da un caso concreto, e diventata un’eccezione esplicita nel codice, applicata dopo qualsiasi filtro.
La journey del modello non è quella del lettore
Il modello non prova a indovinare a che punto del percorso si trova chi legge: quella è la customer journey reale, e ognuno la percorre a modo suo, entrando da dove capita. Il modello classifica un’altra cosa: la journey con cui, e per cui, il contenuto viene scritto. Lo Stage è una scelta dell’autore, fatta prima di scrivere: questo pezzo nasce per chi scopre un problema, questo per chi lo approfondisce, questo per chi torna a consultare.
La prima proposta di tappe per questo blog ne aveva cinque, pensate sullo stato d’animo del lettore. L’ho scartata proprio per questo: uno stato d’animo non lo dichiari, un’intenzione di scrittura sì. Oggi la Reader Journey ha tre Stage: Scoperta, Approfondimento, Riferimento.
Il percorso reale delle persone, invece, non si dichiara. Si misura, e si confronta con quello progettato. È lì che lo Stage entra nel dataLayer, e ne parlo in un prossimo pezzo della serie.
Prima il codice, poi il pensiero. Ora il contrario
C’è un’altra cosa che è cambiata in questi mesi, e non riguarda il modello: riguarda me. Il plugin l’ho costruito insieme a Claude Code. Io progetto, decido e verifico; l’AI scrive con me.
Prima davo importanza soprattutto al codice. Partivo subito a scriverlo, poi correggevo, integravo, ristrutturavo. Un metodo esperienziale, che con me funzionava: sono molto veloce a scrivere codice, e il pensiero prendeva forma mentre le righe crescevano.
Con Claude Code il concetto si è ribaltato. Scrivere codice non è più il passaggio lento: lo è diventato descrivere ciò che voglio.
Ed è un passaggio scomodo, perché non si può improvvisare. Per ottenere un codice che rispecchi le mie specifiche e il mio modo di scrivere codice, devo avere ben chiaro in testa il risultato che voglio. Se il pensiero è confuso, il codice lo sarà altrettanto, solo scritto più in fretta. Questa storia ne è la prova: il passaggio dalle pagine hub alle tassonomie non è stato un problema di codice, è stato un problema di pensiero. Scriverlo, a quel punto, era la parte facile.
L’AI accelera, non decide.
Dove il codice non arriva ancora
Una descrizione che dice solo “funziona” sarebbe meno utile di un confronto onesto. Quindi:
- Le cardinalità sono culturali, non imposte. “Un solo Primary Topic” e “una sola Persona” sono regole del modello, ma il campo accetta più termini e il Primary è il primo. Il vantaggio: le Relations sono semplicemente gli altri termini, senza un campo in più. Il costo: nessuna validazione ti impedisce di assegnare due Persona incompatibili.
- Non esiste un mappatore generico Identity →
@type. Il tipo schema.org lo decide il modulo di ciascun post type; un post type nuovo ne vuole uno suo. - La View non sceglie il template: quella scelta resta a Breakdance.
- L’Intento oggi è un dato di misura, non guida niente in automatico.
- Non c’è una schermata di content brief prima della scrittura: il gruppo di campi sul contenuto ne fa le veci.
Nessuna di queste è una dimenticanza. Sono il punto in cui il modello chiede ancora qualcosa al codice.
Cosa mi porto via
Pensavo che la parte difficile fosse scrivere il motore: le query, le relazioni, gli indici. Ho scoperto che la parte difficile era scegliere la primitiva, e che la primitiva giusta era quella che WordPress aveva già.
Tre versioni per arrivarci. La prima mi ha insegnato che un nome condiviso non basta se il significato non lo è. La seconda che l’eleganza di una soluzione non la rende necessaria. La terza che il dato e la pagina sono due cose, e conviene tenerle separate, collegate da un solo filo.
Oggi da quella classificazione il plugin deriva URL, query, condizioni dei template, contenuti correlati, schema.org, breadcrumb, canonical, titoli SEO, CTA, dataLayer e l’indice per l’analytics. Nessuna disciplina ha la sua copia del dato: tutte leggono la stessa coordinata. Struttura solida, suonata con swing.
E tu, quante volte hai tenuto una soluzione solo perché funzionava?
Nei prossimi pezzi della serie seguo quella coordinata fuori dal database: prima dentro il grafo schema.org, poi dentro il dataLayer. Se vuoi il modello nella sua forma completa, parti dalla pagina del Bebop Content Model. Se devi spiegarlo a chi coordina il progetto: Il problema non sono i team: è la lingua.
