Alessandro Aili - WordPress Developer
Alessandro Aili - WordPress DeveloperPlay Alessandro Aili - WordPress DeveloperDrag Alessandro Aili - WordPress DeveloperView
Alessandro Aili - WordPress Developer
  • Homepage
  • Chi sono
  • Portfolio
  • Aree di Attività
    • Sviluppo di Siti Web Personalizzati
    • Ottimizzazione delle Prestazioni
    • Sicurezza del Sito Web
    • Aggiornamenti e Manutenzione
    • Sviluppo di Temi e Plugin Personalizzati
    • Migrazione e Trasferimento del Sito
  • Blog
  • Contatti
Alessandro Aili - WordPress Developer
Alessandro Aili - WordPress Developer Alessandro Aili - WordPress Developer
  • Homepage
  • Chi sono
  • Portfolio
  • Aree di Attività
    • Sviluppo di Siti Web Personalizzati
    • Ottimizzazione delle Prestazioni
    • Sicurezza del Sito Web
    • Aggiornamenti e Manutenzione
    • Sviluppo di Temi e Plugin Personalizzati
    • Migrazione e Trasferimento del Sito
  • Blog
  • Contatti
MENU Alessandro Aili - WordPress Developer
  • Home
  • Blog
  • Temi e Plugin
  • Template WordPress personalizzati: single, page, archive, 404 e loop (Parte 3)

Template WordPress personalizzati: single, page, archive, 404 e loop (Parte 3)

Alessandro Aili - WordPress Developer
Alessandro Aili WordPress Developer

Data di pubblicazione

28 Maggio 2025

Tempo di lettura

13 minuti

Categoria

Temi e Plugin

Commenti

0

Template WordPress personalizzati: single, page, archive e 404

Nella Parte 2 abbiamo visto la regola: WordPress cerca nel tema una lista ordinata di nomi di file e carica il primo che trova. Qui passiamo dalla regola alla pratica e scriviamo i file che quella lista si aspetta — single.php, page.php, archive.php, 404.php — più il meccanismo che li anima tutti: il loop.

C’è poi una sezione che vale da sola il tempo di lettura, ed è quella su WP_Query. Non tanto per come si usa, quanto per quando non va usato: è l’errore più diffuso nello sviluppo WordPress, produce paginazioni rotte e query duplicate, e si trova in una quantità impressionante di temi in produzione.

Se sei arrivato qui direttamente, il percorso completo parte da come creare un tema WordPress da zero.

single.php: il template dell’articolo

WordPress usa single.php per ogni singolo articolo del blog. È il template su cui atterra chi arriva dalla ricerca, quindi è quello dove la struttura conta di più.

<?php get_header(); ?>

<main id="primary" class="site-main">

<?php while ( have_posts() ) : the_post(); ?>

    <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>

        <header class="entry-header">
            <h1 class="entry-title"><?php the_title(); ?></h1>
            <p class="entry-meta">
                <time datetime="<?php echo esc_attr( get_the_date( 'c' ) ); ?>">
                    <?php echo esc_html( get_the_date() ); ?>
                </time>
                <span class="byline"><?php the_author(); ?></span>
            </p>
        </header>

        <?php if ( has_post_thumbnail() ) : ?>
            <figure class="entry-thumbnail">
                <?php the_post_thumbnail( 'large' ); ?>
            </figure>
        <?php endif; ?>

        <div class="entry-content">
            <?php the_content(); ?>
        </div>

        <footer class="entry-footer">
            <?php the_tags( '<ul class="entry-tags"><li>', '</li><li>', '</li></ul>' ); ?>
        </footer>

    </article>

    <?php the_post_navigation( array(
        'prev_text' => '&laquo; %title',
        'next_text' => '%title &raquo;',
    ) ); ?>

    <?php
    if ( comments_open() || get_comments_number() ) {
        comments_template();
    }
    ?>

<?php endwhile; ?>

</main>

<?php get_footer(); ?>

Tre scelte in questo file meritano una spiegazione, perché sono le stesse che distinguono un template scritto con criterio da uno copiato.

  • Qui il titolo è <h1>, in index.php era <h2>. Non è un dettaglio di stile: su un archivio i titoli sono voci di un elenco e l’h1 è il titolo dell’archivio, su un articolo il titolo è l’intestazione principale della pagina. Un solo h1 per pagina è il controllo 3 della checklist SEO on-page, e in un tema custom dipende interamente da come scrivi i template. In un tema a blocchi la stessa decisione si prende da un menu a tendina, con lo stesso esito se la si sbaglia.
  • La data ha un attributo datetime in formato ISO. get_the_date( 'c' ) produce la versione leggibile dalle macchine, mentre get_the_date() senza argomenti rispetta il formato scelto nelle impostazioni del sito. Servono entrambe: una per i browser e i crawler, l’altra per le persone.
  • comments_template() è protetto da una condizione. Senza, WordPress carica il template dei commenti anche su articoli in cui sono chiusi e non ce n’è nessuno, stampando un contenitore vuoto.

Su un tema che cresce, il contenuto dell’<article> si sposta presto in un frammento richiamato con get_template_part(), così da riusarlo altrove. Finché il tema è piccolo, tenerlo inline è più leggibile.

page.php e i template di pagina selezionabili

page.php gestisce le pagine statiche. La struttura somiglia a single.php, con due differenze: niente data e niente autore, perché su “Chi sono” o “Contatti” non dicono nulla di utile.

<?php get_header(); ?>

<main id="primary" class="site-main">

<?php while ( have_posts() ) : the_post(); ?>

    <article id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
        <h1 class="entry-title"><?php the_title(); ?></h1>
        <div class="entry-content">
            <?php the_content(); ?>
        </div>
    </article>

<?php endwhile; ?>

</main>

<?php get_footer(); ?>

La parte davvero utile arriva ora, ed è quella che nella Parte 2 avevo lasciato in sospeso: il template di pagina personalizzato, cioè quello che compare nel menu a tendina dell’editor e che chi gestisce il sito può assegnare a una pagina qualsiasi. Si dichiara con un commento in testa al file, e il nome del file è libero.

<?php
/**
 * Template Name: Pagina a larghezza piena
 * Template Post Type: page
 */

get_header();
?>

<main id="primary" class="site-main is-full-width">
    <?php
    while ( have_posts() ) :
        the_post();
        the_content();
    endwhile;
    ?>
</main>

<?php get_footer(); ?>

Basta questo: salvi il file nel tema e la voce “Pagina a larghezza piena” compare nell’editor, sotto Attributi pagina. È il meccanismo con cui consegni flessibilità al cliente senza dargli accesso al codice, ed è anche il livello più alto della gerarchia — vince su page-{slug}.php e su tutto il resto.

Template Post Type è opzionale e serve a rendere il template assegnabile anche ad articoli o custom post type: senza quella riga, WordPress lo propone solo per le pagine.

archive.php: un file per tutti gli elenchi

archive.php è il template più redditizio del tema, perché con un file solo copri categorie, tag, autori, archivi per data, custom post type e tassonomie personalizzate.

<?php get_header(); ?>

<main id="primary" class="site-main">

<?php if ( have_posts() ) : ?>

    <header class="page-header">
        <?php
        the_archive_title( '<h1 class="page-title">', '</h1>' );
        the_archive_description( '<div class="archive-description">', '</div>' );
        ?>
    </header>

    <div class="post-list">
        <?php while ( have_posts() ) : the_post(); ?>
            <?php get_template_part( 'template-parts/content', 'excerpt' ); ?>
        <?php endwhile; ?>
    </div>

    <?php the_posts_pagination( array(
        'mid_size'  => 1,
        'prev_text' => __( 'Precedente', 'nome-tema' ),
        'next_text' => __( 'Successiva', 'nome-tema' ),
    ) ); ?>

<?php else : ?>

    <p><?php esc_html_e( 'Nessun contenuto in questo archivio.', 'nome-tema' ); ?></p>

<?php endif; ?>

</main>

<?php get_footer(); ?>

the_archive_title() è la funzione che rende possibile il file unico: capisce da sola in che tipo di archivio si trova e stampa “Categoria: Guide”, “Tag: WordPress” o “Archivi dell’autore: Alessandro” senza che tu scriva una condizione. the_archive_description() stampa la descrizione della categoria, se l’hai compilata — ed è un buon motivo per compilarla, perché altrimenti l’archivio è una pagina senza testo proprio.

Se in futuro una categoria specifica avrà bisogno di un layout diverso, crei category-guide.php e la gerarchia lo preferirà automaticamente. Non serve toccare archive.php.

404.php: la pagina che deve restituire 404

Il template dell’errore costa dieci minuti e recupera visitatori che altrimenti chiudono la scheda.

<?php get_header(); ?>

<main id="primary" class="site-main error-404">

    <h1><?php esc_html_e( 'Pagina non trovata', 'nome-tema' ); ?></h1>

    <p><?php esc_html_e( 'L&rsquo;indirizzo che hai aperto non esiste o &egrave; stato spostato.', 'nome-tema' ); ?></p>

    <?php get_search_form(); ?>

    <h2><?php esc_html_e( 'Articoli recenti', 'nome-tema' ); ?></h2>
    <ul>
        <?php wp_get_archives( array( 'type' => 'postbypost', 'limit' => 5 ) ); ?>
    </ul>

</main>

<?php get_footer(); ?>

L’elenco degli articoli recenti viene da wp_get_archives(): la stessa funzione, con 'type' => 'alpha' e senza limit, elenca tutti gli articoli in ordine alfabetico, ed è uno dei mattoni di una mappa del sito costruita con le funzioni del core.

Qui va corretto un consiglio che circola parecchio, e che io stesso avevo riportato in una versione precedente di questo articolo: non serve aggiungere <meta name="robots" content="noindex"> alla pagina 404. Una pagina che risponde con lo stato HTTP 404 non viene indicizzata, punto: il codice di stato è già l’istruzione, e il meta tag è rumore.

Il problema vero è l’opposto, e ha un nome: soft 404. Succede quando un URL inesistente restituisce 200 OK pur mostrando un messaggio di errore — tipicamente perché un plugin o una regola di redirect manda tutti gli indirizzi sconosciuti alla homepage o a una pagina generica. Per Google quella è una pagina valida e vuota, la segnala in Search Console come Soft 404, e nel frattempo la sta scansionando come se fosse contenuto.

La verifica dura cinque secondi: apri un indirizzo inventato sul tuo sito e controlla il codice di stato.

curl -sI https://iltuosito.it/indirizzo-che-non-esiste/ | head -1

Deve rispondere 404. Se risponde 200, il template 404 non entrerà mai in funzione e hai un problema di configurazione, non di tema; se risponde 301 verso un articolo con uno slug simile a quello che hai scritto, non è un errore: è WordPress che prova a indovinare l’indirizzo, e conviene sapere come funziona prima di trarre conclusioni. E come regola generale: non reindirizzare gli errori 404 alla homepage. Un 301 ha senso quando esiste una pagina che sostituisce davvero quella vecchia; per tutto il resto, il 404 è la risposta corretta.

Il loop: cosa succede davvero

In tutti i template scritti finora compare lo stesso schema: while ( have_posts() ) : the_post();. Vale la pena capire su cosa opera, perché è il presupposto della sezione successiva.

Prima ancora di caricare il template, WordPress ha già interpretato l’URL, costruito una query SQL ed eseguito quella che si chiama query principale. I risultati sono già in memoria quando il tuo file viene incluso. have_posts() chiede a quella query se ci sono altri elementi; the_post() avanza di uno e imposta il post corrente nella variabile globale che tutte le funzioni the_*() leggono.

Da qui discende una conseguenza che spiega metà dei problemi che seguono: il loop non decide quali contenuti mostrare, li mostra e basta. Cosa viene estratto è stato deciso prima, dall’URL.

WP_Query per le query secondarie

Quando ti serve un elenco in aggiunta al contenuto principale — tre articoli in evidenza in homepage, i post correlati sotto un articolo, gli ultimi eventi in una pagina statica — quella è una query secondaria, e si costruisce con WP_Query.

<?php
$in_evidenza = new WP_Query( array(
    'post_type'      => 'post',
    'posts_per_page' => 3,
    'category_name'  => 'guide',
    'no_found_rows'  => true,
) );

if ( $in_evidenza->have_posts() ) : ?>

    <section class="in-evidenza">
        <h2><?php esc_html_e( 'Guide in evidenza', 'nome-tema' ); ?></h2>

        <?php while ( $in_evidenza->have_posts() ) : $in_evidenza->the_post(); ?>
            <?php get_template_part( 'template-parts/card' ); ?>
        <?php endwhile; ?>
    </section>

    <?php wp_reset_postdata(); ?>

<?php endif; ?>

Due righe di questo blocco sono quelle che fanno la differenza tra codice che funziona e codice che sembra funzionare.

  • wp_reset_postdata() non è opzionale. Dopo il ciclo, il post globale è rimasto fermo sull’ultimo elemento della query secondaria. Tutto ciò che viene dopo — i commenti, la navigazione tra articoli, un secondo blocco, a volte perfino il footer — si riferirà a quel post invece che a quello vero. È un bug che non genera errori e si manifesta come “i commenti sbagliati sotto l’articolo”: difficilissimo da diagnosticare se non sai che esiste.
  • no_found_rows => true quando non ti serve la paginazione. Di default WordPress esegue anche un conteggio totale delle righe per sapere quante pagine ci sono. Se stai mostrando tre articoli in evidenza quel conteggio non lo userai mai, e su tabelle grandi è la parte più costosa della query. È uno degli interventi meno noti tra quelli che alleggeriscono davvero il database — il quadro completo è nelle sei cause reali di un sito WordPress lento.

Quando NON usare WP_Query: pre_get_posts

Ecco l’errore annunciato all’inizio. Serve modificare cosa compare in un archivio — dodici articoli invece di dieci nella categoria “guide”, un ordinamento diverso, l’esclusione di una categoria dalla home. La soluzione istintiva è aprire archive.php e sostituire il loop con un new WP_Query() configurato come serve.

Sembra funzionare. Non funziona, per tre motivi che si presentano in momenti diversi.

  • La paginazione si rompe. WordPress ha già calcolato il numero di pagine sulla query principale, quella che stai ignorando. Alla pagina 2 il conteggio non corrisponde più a quello che mostri, e a seconda dei casi ottieni contenuti ripetuti o un 404.
  • Il database lavora il doppio. La query principale viene eseguita comunque, prima che il template venga caricato. La tua è la seconda: una delle due è puro spreco su ogni singola visita.
  • I tag condizionali mentono. Funzioni come is_category() continuano a riferirsi alla query principale, quindi la logica costruita attorno a loro smette di corrispondere a ciò che viene mostrato.

La soluzione corretta è non sostituire la query principale ma modificarla prima che venga eseguita, con l’hook pre_get_posts in functions.php:

function nometema_modifica_query( $query ) {

    if ( is_admin() || ! $query->is_main_query() ) {
        return;
    }

    if ( $query->is_category( 'guide' ) ) {
        $query->set( 'posts_per_page', 12 );
        $query->set( 'orderby', 'title' );
        $query->set( 'order', 'ASC' );
    }
}
add_action( 'pre_get_posts', 'nometema_modifica_query' );

Così la query eseguita è una sola, la paginazione resta coerente e il template non cambia di una riga: continua a fare while ( have_posts() ) come prima.

Le prime due righe della funzione sono obbligatorie. pre_get_posts scatta su ogni query, comprese quelle del backend e ogni query secondaria della pagina. Senza il controllo su is_admin() ti ritrovi l’elenco degli articoli in amministrazione ordinato per titolo e limitato a dodici, e senza is_main_query() modifichi anche i tuoi stessi blocchi “in evidenza”. Sono due righe che sembrano difensive e non lo sono: senza, l’hook fa danni ovunque.

La regola in una frase: se stai cambiando quali contenuti mostra una pagina, usa pre_get_posts. Se stai aggiungendo un elenco a quello che la pagina già mostra, usa WP_Query.

Un’ultima insidia, se proprio ti serve paginare una query secondaria: il parametro da leggere è get_query_var( 'paged' ), ma su una pagina statica impostata come prima pagina del sito diventa page. Due nomi diversi per la stessa cosa, ed è la ragione per cui una paginazione personalizzata funziona ovunque tranne che in homepage.

SEO, performance e accessibilità dentro i template

In un tema custom queste tre cose non si aggiungono dopo con un plugin: nascono da come scrivi i file di questa pagina.

  • Un solo <h1> per pagina. Nei template singoli è il titolo del contenuto, negli archivi è il titolo dell’archivio prodotto da the_archive_title(). L’errore classico è lasciare h1 anche sui titoli delle voci in elenco.
  • Un solo <main> per pagina, e la navigazione dentro un <nav> con etichetta. Sono i punti di riferimento su cui si muove chi usa uno screen reader: il contesto completo è in accessibilità in WordPress.
  • Non scrivere i tag <img> a mano. the_post_thumbnail() genera già srcset e sizes con tutte le dimensioni disponibili, più il loading="lazy" che WordPress aggiunge da solo. Un <img> scritto a mano perde tutto questo.
  • Sui dati strutturati, prima verifica cosa esiste già. Se il sito usa Rank Math o Yoast, lo schema Article viene già emesso, e aggiungerne uno a mano nel template significa averne due in conflitto. Il markup manuale nel tema ha senso per i tipi che il plugin non copre — una FAQPage costruita da campi personalizzati, per esempio. La distinzione tra i due approcci è in schema.org in WordPress.

C’è una verifica che chiude questa sezione e che non si fa leggendo il codice: percorrere una pagina generata da questi template usando solo il Tab. È lì che vengono fuori il «Leggi tutto» ripetuto negli archivi, il menu che si apre solo al passaggio del mouse e il focus che il tema ha disattivato — il test da tastiera passo per passo richiede dieci minuti su una pagina.

Il passo successivo: la Parte 4

Il tema ora copre tutti i tipi di contenuto: articoli, pagine, archivi, errori. Quello che manca è la parte che lo rende consegnabile.

Nella Parte 4 — modularizzazione, ACF, Gutenberg e distribuzione spezziamo i template in frammenti riutilizzabili, aggiungiamo il supporto all’editor a blocchi e a theme.json, vediamo quando ACF serve davvero, e chiudiamo con la checklist da percorrere prima di consegnare o pubblicare il tema.

Domande frequenti

Perché la paginazione non funziona sulla mia query personalizzata?

Quasi sempre perché stai usando WP_Query dove servirebbe pre_get_posts: la paginazione che WordPress stampa è calcolata sulla query principale, che nel tuo template stai ignorando. Se invece la query secondaria deve davvero essere paginata, ti manca il parametro paged — e ricorda che su una homepage statica il valore da leggere si chiama page, non paged.

Serve creare category.php se ho già archive.php?

Solo se le categorie devono apparire diverse dagli altri archivi. La gerarchia fa ricadere le categorie su archive.php senza che tu faccia niente, quindi un category.php identico ad archive.php è un file in più da mantenere allineato — e prima o poi le due versioni divergeranno per dimenticanza.

Devo mettere noindex sulla pagina 404?

No. Una pagina che risponde con lo stato HTTP 404 non viene indicizzata già di suo, e il meta tag non aggiunge nulla. Quello che va verificato è che il 404 sia un 404 vero: se un URL inesistente risponde 200 o viene reindirizzato alla homepage, hai un soft 404, e quello sì è un problema che Search Console segnala.

Qual è la differenza tra the_content() e the_excerpt()?

the_content() stampa il contenuto completo, con i blocchi resi e gli shortcode eseguiti: si usa nei template singoli. the_excerpt() stampa il riassunto — quello scritto a mano nell’editor o, se manca, i primi 55 caratteri di testo ripulito da ogni markup: si usa negli elenchi. Usare the_content() in un archivio significa stampare venti articoli interi in una pagina sola, con tutte le immagini che li accompagnano.

Dove sei arrivato

Il tema gestisce ogni tipo di richiesta con il template giusto, offre template di pagina selezionabili dall’editor, e sai distinguere una query principale da una secondaria — che è la distinzione da cui dipendono paginazione, performance e metà dei bug difficili da spiegare.

Se hai un tema in cui la paginazione fa cose strane o gli archivi si comportano in modo imprevedibile, quasi sempre la causa è una di quelle viste qui. Sviluppo temi e plugin WordPress su misura: posso dare un’occhiata al codice o affiancarti nel sistemarlo.

Indice dei contenuti

  • single.php: il template dell’articolo
  • page.php e i template di pagina selezionabili
  • archive.php: un file per tutti gli elenchi
  • 404.php: la pagina che deve restituire 404
  • Il loop: cosa succede davvero
  • WP_Query per le query secondarie
  • Quando NON usare WP_Query: pre_get_posts
  • SEO, performance e accessibilità dentro i template
  • Il passo successivo: la Parte 4
  • Domande frequenti
    • Dove sei arrivato

    Tag in Evidenza

    TemaTemi WordPressWordPress

    Condividi


    Lascia un commento Annulla risposta

    Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

    Alessandro Aili - WordPress Developer Alessandro Aili - WordPress Developer
    Sono un WordPress developer che affronta tutte le tue sfide online.
    Skills
    • Sviluppo di Siti Web Personalizzati
    • Ottimizzazione delle Prestazioni
    • Sicurezza del Sito Web
    • Aggiornamenti e Manutenzione
    • Sviluppo di Temi e Plugin Personalizzati
    • Migrazione e Trasferimento del Sito
    © Copyright 2026 Alessandro Aili
    • Mappa del sito
    • Privacy Policy
    • Cookie Policy
    • Preferenze cookie
    Alessandro Aili - WordPress Developer