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 Userwp_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
/**
* 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
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
}.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;
}
}
}