Salta ai contenuti

Nuovo CPT in 8 passi

Esempio reale: vogliamo mostrare citazioni di clienti. Il CPT si chiama testimonianza, la collezione testimonianze.

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).

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.

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.

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,
};
}

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.

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.

src/pages/index.astro:

const testimonianze = (await getCollection("testimonianze"))
.map((t) => t.data)
.sort((a, b) => a.sortOrder - b.sortOrder);
<Testimonials items={testimonianze} />

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.

  • 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 quality verde
  • 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 imageMetaKeys per i campi immagine.
  • Mettere il fetch nel componente invece che nel loader.