AJAX in WordPress

1. Was ist AJAX? Warum? Use Cases?

AJAX = Asynchronous JavaScript and XML. Erlaubt dem Browser, im Hintergrund Daten vom Server zu holen/senden, ohne die Seite neu zu laden.

Typische Frontend-Use-Cases in WordPress:

  • Load More – Beiträge nachladen (dein Fall)
  • Live-Suche – Ergebnisse beim Tippen anzeigen
  • Formulare absenden – z.B. Kontaktformular ohne Reload
  • Filter/Sortierung – Posts nach Kategorie/Tag filtern
  • Infinite Scroll – automatisches Nachladen beim Scrollen
  • Warenkorb-Updates – Produkte hinzufügen ohne Seitenreload

2. Wie regelt WordPress AJAX? Warum?

WordPress routet alle AJAX-Requests über eine zentrale Datei: wp-admin/admin-ajax.php.

Warum? Wenn du eine eigene PHP-Datei anlegen würdest, hättest du keinen Zugriff auf WordPress-Funktionen (WP_Query, get_posts, etc.). Über admin-ajax.php ist der gesamte WordPress-Core geladen — du kannst alles nutzen.

Das System funktioniert über Action-Hooks:

  • wp_ajax_{action} → für eingeloggte User
  • wp_ajax_nopriv_{action} → für nicht eingeloggte User (wichtig fürs Frontend!)
  • Du brauchst beide, wenn alle Besucher den Load More nutzen sollen

Im Frontend gibt es kein globales ajaxurl (im Admin schon). Du musst die URL selbst übergeben — dazu dient wp_add_inline_script() (offiziell seit WP 4.5 empfohlen, nicht wp_localize_script, obwohl letzteres noch überall gezeigt wird). Warum wp_add_inline_script()?

3. Load More Posts – komplettes Beispiel

  • Alles in einem PHP Snippet
  • Ohne nonce – da nur ready only. Dadurch keine Caching Probleme
  • Alles zentral verwaltbar
  • Via Shortcode – Skript lädt nur, wenn Shortdode auf der Seite
PHP
<?php

/**
 * Shortcode: [load_more_posts]
 */
function gw_load_more_shortcode() {
    global $gw_loadmore_active;
    $gw_loadmore_active = true;

    $query = new WP_Query( [
        'post_type'      => 'post',
        'posts_per_page' => 2,
        'paged'          => 1,
        'post_status'    => 'publish',
    ] );

    if ( ! $query->have_posts() ) {
        return '<p>Keine Beiträge gefunden.</p>';
    }

    ob_start();
    echo '<div class="js-posts-container">';
    while ( $query->have_posts() ) {
        $query->the_post();
        gw_render_post_card();
    }
    echo '</div>';

    if ( $query->max_num_pages > 1 ) {
        echo '<button class="js-load-more">Mehr laden</button>';
    }

    wp_reset_postdata();
    return ob_get_clean();
}
add_shortcode( 'load_more_posts', 'gw_load_more_shortcode' );


/**
 * Post-Card HTML
 */
function gw_render_post_card() {
    ?>
    <article class="post-card">
        <h3><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h3>
        <p><?php echo get_the_excerpt(); ?></p>
    </article>
    <?php
}


/**
 * Load More Posts – JS nur im Footer wenn Shortcode aktiv
 */
function gw_loadmore_footer_script() {
    global $gw_loadmore_active;
    if ( empty( $gw_loadmore_active ) ) return;

    $ajax_url = admin_url( 'admin-ajax.php' );
    ?>
    <script id="load-more-posts">
    document.addEventListener('DOMContentLoaded', () => {
        const btn = document.querySelector('.js-load-more');
        const container = document.querySelector('.js-posts-container');
        let currentPage = 1;
        if (!btn || !container) return;

        btn.addEventListener('click', async () => {
            currentPage++;
            btn.textContent = 'Laden…';
            btn.disabled = true;

            const data = new FormData();
            data.append('action', 'gw_load_more');
            data.append('page', currentPage);

            try {
                const res = await fetch('<?php echo esc_url( $ajax_url ); ?>', {
                    method: 'POST',
                    body: data
                });
                const result = await res.json();

                if (result.success) {
                    container.insertAdjacentHTML('beforeend', result.data.html);
                    if (!result.data.has_more) {
                        btn.remove();
                    } else {
                        btn.textContent = 'Mehr laden';
                        btn.disabled = false;
                    }
                } else {
                    btn.remove();
                }
            } catch (e) {
                console.error(e);
                btn.textContent = 'Fehler';
                btn.disabled = false;
            }
        });
    });
    </script>
    <?php
}
add_action( 'wp_footer', 'gw_loadmore_footer_script' );


/**
 * Load More Posts – AJAX Handler
 */
function gw_load_more_posts() {
    $paged = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;

    $query = new WP_Query( [
        'post_type'      => 'post',
        'posts_per_page' => 2,
        'paged'          => $paged,
        'post_status'    => 'publish',
    ] );

    if ( $query->have_posts() ) {
        ob_start();
        while ( $query->have_posts() ) {
            $query->the_post();
            gw_render_post_card();
        }
        wp_reset_postdata();

        wp_send_json_success( [
            'html'     => ob_get_clean(),
            'has_more' => $paged < $query->max_num_pages,
        ] );
    } else {
        wp_send_json_error( 'Keine weiteren Beiträge.' );
    }
}
add_action( 'wp_ajax_gw_load_more', 'gw_load_more_posts' );
add_action( 'wp_ajax_nopriv_gw_load_more', 'gw_load_more_posts' );

4. Einige andere HTML Card Templates

PHP
function gw_render_post_card() {
    $thumbnail_url = get_the_post_thumbnail_url( get_the_ID(), 'large' );
    ?>
    <article class="load-more-card post">
        <?php if ( $thumbnail_url ) : ?>
            <div class="image" style="background-image: url('<?php echo esc_url( $thumbnail_url ); ?>');"></div>
        <?php endif; ?>
        <div class="content">
            <h3 class="title"><?php the_title(); ?></h3>
            <div class="meta">
                <time class="date"><?php echo get_the_date(); ?></time>
                <span class="category"><?php echo get_the_category_list( ', ' ); ?></span>
            </div>
            <p class="excerpt"><?php echo wp_trim_words( get_the_excerpt(), 40 ); ?></p>
            <a href="<?php the_permalink(); ?>" class="link">Read more</a>
        </div>
    </article>
    <?php
}
SCSS
.load-more-card {
    .image {
        aspect-ratio: 16/9;
    }
    .content {
        padding: 1rem;
    }
    .meta {
        display: flex;
        gap: 1rem;
    }
    .excerpt {
        margin: 0.5rem 0;
    }
    .title {
        color: red;
    }
		// falls du mehrere laod-more-cards hast für verschiedene post-types
    &.page {
        .excerpt {
            display: none;
        }
        .category {
            display: none;
        }
        a {
            color: black;
        }
    }
}