Nuovo CPT in 8 passi
Esempio reale: vogliamo mostrare citazioni di clienti. Il CPT si chiama
testimonianza, la collezione testimonianze.
1. Registra il CPT nel mu-plugin
Sezione intitolata “1. Registra il CPT nel mu-plugin”In wordpress/mu-plugins/headless-cms.php, dentro private const CPT, aggiungi:
'testimonianza' => [ 'labels' => ['name' => 'Testimonianze', 'singular_name' => 'Testimonianza'], 'menu_icon' => 'dashicons-format-quote', 'rewrite' => 'testimonianze', 'fields' => [ ['key' => 'quote', 'label' => 'Citazione', 'type' => 'textarea'], ['key' => 'author', 'label' => 'Autore', 'type' => 'text'], ['key' => 'role', 'label' => 'Ruolo / azienda', 'type' => 'text'], ['key' => 'photo', 'label' => 'Foto (ID media)', 'type' => 'image'], ['key' => 'sort_order', 'label' => 'Ordine', 'type' => 'number'], ],],Il titolo del post resta il campo “titolo” nativo: usalo per un nome interno
(es. “Mario Rossi”) e quote per il testo mostrato.
Poi, in WordPress: Impostazioni → Permalink → Salva (rigenera le rewrite rule).
2. Aggiungi il modello di dominio
Sezione intitolata “2. Aggiungi il modello di dominio”src/domain/Testimonianza.ts:
export interface Testimonianza { id: number; slug: string; quote: string; author: string; role: string; photo: string; sortOrder: number;}Il dominio non conosce WordPress: nomi in camelCase, campi già risolti.
3. Scrivi lo schema Zod
Sezione intitolata “3. Scrivi lo schema Zod”src/cms/wordpress/wordpress-testimonianza.schema.ts:
import { z } from "zod";import { WordPressPostSchema } from "./wordpress-post.schema";
const MetaSchema = z.object({ quote: z.string().default(""), author: z.string().default(""), role: z.string().default(""), photo: z.string().default(""), sort_order: z.number().default(0),});
export const WordPressTestimonianzaSchema = WordPressPostSchema.extend({ meta: MetaSchema,});
export const WordPressTestimonianzeSchema = z.array(WordPressTestimonianzaSchema);
export type WordPressTestimonianza = z.infer<typeof WordPressTestimonianzaSchema>;Se il campo è obbligatorio per il senso del contenuto, togli il default: la build fallirà quando manca, invece di mostrare una card vuota.
4. Scrivi il mapper
Sezione intitolata “4. Scrivi il mapper”src/cms/wordpress/wordpress-testimonianza.mapper.ts:
import type { Testimonianza } from "../../domain/Testimonianza";import type { WordPressTestimonianza } from "./wordpress-testimonianza.schema";
export function mapWordPressTestimonianzaToTestimonianza( wp: WordPressTestimonianza, media: Map<string, string>,): Testimonianza { return { id: wp.id, slug: wp.slug, quote: wp.meta.quote, author: wp.meta.author, role: wp.meta.role, photo: media.get(wp.meta.photo) ?? "", sortOrder: wp.meta.sort_order, };}5. Registra la collezione
Sezione intitolata “5. Registra la collezione”src/content.config.ts:
const testimonianze = defineCollection({ loader: createWordPressCollectionLoader({ name: "wordpress-testimonianze-loader", endpoint: "/wp-json/wp/v2/testimonianza", schema: WordPressTestimonianzeSchema, imageMetaKeys: ["photo"], map: mapWordPressTestimonianzaToTestimonianza, }), schema: z.object({ id: z.number(), slug: z.string(), quote: z.string(), author: z.string(), role: z.string(), photo: z.string(), sortOrder: z.number(), }),});
export const collections = { // ... testimonianze,};imageMetaKeys è la lista dei campi che contengono ID media: il loader li risolve in URL.
6. Crea il componente
Sezione intitolata “6. Crea il componente”src/components/Testimonials.astro:
---import type { Testimonianza } from "../domain/Testimonianza";
interface Props { items: Testimonianza[];}
const { items } = Astro.props;---
{items.length > 0 && ( <section class="bg-panel py-24"> <div class="mx-auto max-w-7xl px-6"> <div class="grid gap-8 md:grid-cols-2"> {items.map((item) => ( <figure class="rounded-2xl border border-line bg-surface p-8"> <blockquote class="text-muted">“{item.quote}”</blockquote> <figcaption class="mt-5 text-sm"> <span class="font-bold text-fg">{item.author}</span> {item.role && <span class="text-muted"> — {item.role}</span>} </figcaption> </figure> ))} </div> </div> </section>)}Niente fetch, niente stato, niente colori: solo token.
7. Usalo in una pagina
Sezione intitolata “7. Usalo in una pagina”src/pages/index.astro:
const testimonianze = (await getCollection("testimonianze")) .map((t) => t.data) .sort((a, b) => a.sortOrder - b.sortOrder);
<Testimonials items={testimonianze} />8. Aggiungi il test
Sezione intitolata “8. Aggiungi il test”tests/fixtures/testimonianza-valida.json con un JSON realistico, poi in
tests/wordpress-schema.test.ts:
it("testimonianza valida passa e mappa al dominio", () => { const [wp] = WordPressTestimonianzeSchema.parse([testimonianzaValida]); const item = mapWordPressTestimonianzaToTestimonianza(wp!, media); expect(item.author).toBe("Mario Rossi");});Chiudi con npm run quality.
Checklist finale
Sezione intitolata “Checklist finale”- CPT registrato (e permalink rigenerati)
- Campi con tipo corretto (
text,textarea,number,checkbox,json,image) - Modello di dominio
- Schema Zod allineato 1:1 ai meta
- Mapper (e media)
- Collezione in
content.config.ts - Componente con stato vuoto
- Pagina/sezione
- Fixture + test
-
npm run qualityverde
Errori da non fare
Sezione intitolata “Errori da non fare”- Registrare il CPT in un plugin normale invece che nel mu-plugin: la build non lo vede finché il plugin non è attivo.
- Aggiungere campi al metabox senza registrarli con
register_post_meta: non arrivano nella REST. - Usare una select con opzioni diverse dai valori attesi nel codice.
- Dimenticare
imageMetaKeysper i campi immagine. - Mettere il fetch nel componente invece che nel loader.