WordPress-Postings in zufälliger Reihenfolge

Wenn ein Blog eine Menge Artikel hat, kann es passieren, dass die älteren Beiträge so tief im Archiv versunken liegen, dass der interessierte Leser die kaum noch zur Kenntnis nehmen kann.

Das ist gerade bei Artikeln, die eher zeitlos sind, schon ein richtiges Problem. Mein Blogprojekt „orga-dich“ hatte genau dieses Problem.

Zufällige Reihenfolge

WordPress ermittelt die Postings, die in der Blogroll, also in der Liste der Beiträge, angezeigt werden, über ein wp_query-Objekt. Davon gibt es erfahrungsgemäß mehrere innerhalb eines Blogs, beispielsweise zum Auslesen aller Menüseiten oder innerhalb bestimmter Seitenelemente, die ebenfalls Artikel oder Seiten enthalten sollen.

Aber es gibt auch eine „Master-Query“, die für die Hauptseite zuständig ist. Und eine Query kann auch Parameter haben, die wir übersteuern können…

Oha, functions.php, ick hör dir trapsen

Innerhalb eines WordPress-Monsters können wir uns mit sogenannten Hooks an verschiedenen Stellen der Verarbeitung „beteiligen“.

Und einer davon ist der „posts_orderby„-Hook: Über den können wir zusätzliche Parameter für die Sortierung an das Query-Objekt mitgeben – also frisch ran ans Werk.

Captain Hook

Einen filter zu einem hook zu schreiben ist banal:

add_filter( 'posts_orderby', 'zufaellige_postings' );

Damit haken wir uns in den „posts_orderby„-Hook ein und geben bekannt, dass die PHP-Function „zufaellige_postings“ ausgeführt werden soll, sobald dieser Hook/diese Action ausgeführt wird.

Diese Funktion sieht für unseren Bedarf auch noch recht übersichtlich aus:

function zufaellige_postings( $orderby ) {
   if( is_front_page() ) {
      $orderby = 'RAND';
      return $orderby;
   }
}

Das „is_front_page()“ sorgt dafür, dass wir nur die Query beackern, die für die Anzeige der Startseite mit der Beitragsliste zuständig ist. Wir wollen ja nicht sämtliche DatenbankPost-Queries zufällig gestalten 🙂

That’s it – oder?

Hm – im Prinzip war es das. Aber wir haben ein Problem. Startseiten enthalten ja nicht grundsätzlich alle Postings des Blogs. Sondern immer nur eine Auswahl, der Rest möchte über eine Seitenwahl ausgewählt werden, die im Fachjargon „Pagination“ heißt:

pagination
pagination

Also dieser Bereich mit den Zahlen (1 2 … 4) und Pfeilen. Damit navigiert der geneigte Leser zwischen den vielen hunderttausend Postings eines Blogs.

Wenn wir unsere Zufallsmethode jetzt live schalten, wird auch alles schön gewürfelt – leider aber auf jeder Seite von Neuem! Das kann ja nicht gewollt sein…

Schöne Lösung aus dem Netz der Netze

Und tatsächlich, Ehre wem Ehre gebührt, ein User mit dem klangvollen Namen „hlashbrooke“ (Hugh Lashbrooke) hat auf GitHub eine passende Lösung gefunden.

Er speichert eine zufällig generierte Seed-Zahl in einer Session-Variable und übergibt sie immer erneut an das Query-Objekt als „rand„-Parameter. Dadurch beginnt der Zufallsgenerator immer wieder beim selben Ausgangswert (= Seed) und generiert dieselben Zufallszahlen.

Wie wir wissen, generieren übliche Algorithmen keine ernsthaft zufälligen Zahlenfolgen – sie brauchen einen Ausgangspunkt (den sog. „Seed“) und wenn dieser gleich ist, wird die Reihenfolge der „zufälligen“ Zahlen auch immer gleich sein.

Auf diese Weise wird bei einem seitenweisen Abruf der Postings die einmal erstellte Reihenfolge beibehalten – und liefert entsprechend nicht für jede Seite neue, zufällige Postings sondern liefert die Postings in einer zufälligen Folge innerhalb der gesamten Query.

So sieht das Spaß dann aus:

session_start();

add_filter( 'posts_orderby', 'randomise_with_pagination' );
function randomise_with_pagination( $orderby ) {

	if( is_front_page() ) {

	  	// Reset seed on load of initial archive page
		if( ! get_query_var( 'paged' ) || get_query_var( 'paged' ) == 0 || get_query_var( 'paged' ) == 1 ) {
			if( isset( $_SESSION['seed'] ) ) {
				unset( $_SESSION['seed'] );
			}
		}
	
		// Get seed from session variable if it exists
		$seed = false;
		if( isset( $_SESSION['seed'] ) ) {
			$seed = $_SESSION['seed'];
		}
	
	    	// Set new seed if none exists
	    	if ( ! $seed ) {
	      		$seed = rand();
	      		$_SESSION['seed'] = $seed;
	    	}
	
	    	// Update ORDER BY clause to use seed
	    	$orderby = 'RAND(' . $seed . ')';
	}

	return $orderby;
}
?>

Leider geil – und funktioniert super! Und ist auch angemessen dokumentiert, danke liebe(r) Hugh Lashbrooke (da sind noch mehr coole Sachen zu finden).

So sieht es dann aus

Im Ergebnis kommt das schon ganz nice.

orga-dich postings
orga-dich postings

Ajax Backendanfragen cachen – unmöglich…?

Auf unserer Webseite benutzen wir viele Elemente, die dynamisch mit Content aus dem Backend versorgt werden.

Beispielsweise die Liste unserer Informationsartikel:

ya info
ya info

Die Elemente dieser Liste werden per Ajax aus dem Backend gelutscht – einzeln.

Requests ohne Ende

Also hustet so eine „Grid“-Übersicht mal eben ein paar Dutzend Backendanfragen in Richtung Webserver – und dann passiert das hier:

  • Webserver findet, dass admin-ajax.php eine PHP-Script ist und fährt erst mal eine Instanz des PHP-Interpreters hoch
  • Das Script wiederum möchte gerne das WordPress-Imperium im Zugriff haben, also wird die Maschinerie in Gang gesetzt und das WordPress-Backend macht sich breit (ein paar hundert PHP-Scripte werden geladen und Dutzende Datenbankabfragen losgeschickt – bevor irgendetwas tatsächlich passiert).
  • Dann verarbeitet admin-ajax.php netterweise auch mal den Ajax-Request bzw. leitet den an die zuständige Stelle weiter.
  • Diese fragt dann ihrerseits diverse Tabellen ab – über WordPressroutinen üblicherweise. Also wp_query und Konsorten, die allesamt nicht gerade den Ruf genießen hochperformant optimierten SQL anzuwenden – kann jeder bewundern, wenn er sein MySQL-Slowlog anschmeißt und sich dann mal per explain die dicksten Statements erklären lässt…
  • Wenn dann alle Beteiligten glauben, dass der Content so weit generiert ist, beendet sich das PHP-Konglomerat und stellt eine Antwort für den Ajax-Request bereit.
  • Der wiederum wird dann vom rufenden Javascript in die Seite gelötet.

Das stellen wir uns jetzt mal ein paar Dutzend vor und wir wissen, wie schön es ist, ein sich langsam inkarnierendes Postgrid zu bewundern – Ladezeiten im mehrstelligen Sekundenbereich sind die Folge.

Da möchte man doch mal – Cache?

Klassischer Anwendungsfall eines Caches ist, dass Daten, die sowieso gleich sind, nicht aus der langsamst möglichen Ecke geholt werden sondern von dort, wo es sonnig und warm ist; idealerweise aus dem Arbeitsspeicher, wenn es nicht anders geht, so zumindest aus der Tiefkühltruhe und nur noch schnell in die Micowelle (also servierfertig und nur dezent zu bearbeiten).

Angeblich geht das mit WordPress-Ajax nicht.

Selten so gekichert… natürlich geht das. Man muss nur die schulterlangen Gummihandschuhe anziehen, Nasenklammern drauf und dann in der Suppe wühlen, bis man die richtigen Stellen gefunden hat, an denen man was anflanschen muss.

Jetzt passiert das hier:

  • Ajax-Request schlägt im Backend auf.
  • admin-ajax.php wird immer noch durch eine neue PHP-Interpretinstanz interpre… ausgeführt.
  • ohne WordPress hochzufahren wird erkannt, dass die gewünschte Action mit dem passenden Schlüsselzeugs schon in der „Datenbank“ herumlungert und die Antwort umgehend an den Anfrager zurückgeworfen.

Dabei heißt „Datenbank“ hier ein Key im Redis-Inmemory-Store.

redis cache
redis cache

Aber einmal muss ich durch den Cache…

Die allererste Anfrage muss natürlich durch den gesamten Dschungel, sonst weiß der Redis ja nicht, wie die Abkürzung aussieht.

Das muss aber nicht zwingend mit dem ersten Besucher passieren, der sich auf die Webseite traut.

node.js precache
node.js precache

Das kann ein nächtliches Script machen, was bei der routinemäßigen Wartung mitläuft und die relevanten Ajax-Requests simuliert, die tagsüber so auf das Backend einschlagen. Dieses Script wiederum nutzt node.js und jsdom und diverse andere ähm „Hilfsmittel“ – darüber schreibe ich ein anderes Mal, mir ist schon schlecht 😉