Benutzerdefinierte Feeds in WordPress anlegen

WordPress bietet standardmäßig RSS Feeds für Beiträge, Kommentare, Taxonomien, Archive und Custom Post Types an. In „Alle Feeds einer WordPress-Installation“ hat Vladimir diese mal aufgelistet.

Die Inhalte der vorhandenen Feeds können mit den richtigen Filtern angepasst werden. Doch wie kann ein eigener Feed angelegt werden?

Mit add_feed() einen eigenen Feed anlegen

Die seit WordPress 2.1.0 mitgelieferte Funktion add_feed() befindet sich in wp-includes/rewrite.php und ist, wie der Name schon verspricht, für das Anlegen neuer Feeds zuständig. Die Funktion ist folgendermaßen aufgebaut:

/**
 * Add a new feed type like /atom1/.
 *
 * @since 2.1.0
 *
 * @param string $feedname
 * @param callback $function Callback to run on feed display.
 * @return string Feed action name.
 */
function add_feed($feedname, $function) {
	global $wp_rewrite;
	if ( ! in_array($feedname, $wp_rewrite->feeds) ) //override the file if it is
		$wp_rewrite->feeds[] = $feedname;
	$hook = 'do_feed_' . $feedname;
	// Remove default function hook
	remove_action($hook, $hook, 10, 1);
	add_action($hook, $function, 10, 1);
	return $hook;
}

Zunächst erwartet die Funktion einen Slug für den Feed. Es ist der Name, unter welchem der Feed später aufrufbar sein wird. Er sollte deswegen nur aus den Zeichensatz [a-z0-9_-] bestehen. Die Feed URL wird bei aktivierten Permalinks im Format example.com/feed/{feed_slug} sein.
Der zweite Teil ist die Callback-Funktion. Diese Funktion sollte den Feed rendern und wird beim Aufruf des Feeds aufgegriffen.

Das feed-rss2.php Template

feed-rss2.php ist ein Template in WordPress und übernimmt die Darstellung für jegliche Art von Post Type im RSS2 Format.

Das Template ist somit die optimale Ausgangslage für die Darstellung des eigenen Feeds und kann deswegen in der Callback-Funktion geladen werden.

Anregung für PHP 5.3:

function ds_custom_feed() {
	add_feed( 'custom', function() {
		load_template( ABSPATH . WPINC . '/feed-rss2.php' );
	} );
}
add_action( 'init', 'ds_custom_feed' );

Feed mit eigenen Inhalt füllen

Was nun noch fehlt, ist der Inhalt des Feeds. Dazu als Beispiel folgende Klasse anschauen:

PHP / RAW / github:gist
<?php
/**
 * Add a custom feed to WordPress.
 *
 * The feed will be rendered through the wp-includes/feed-rss2.php template
 * and avaiable under example.com/feed/{$feed_slug}.
 *
 * Note: Don't forget to flush the rewrite rules once.
 *
 * @author Dominik Schilling
 * @link   https://dominikschilling.de/897
 */
class DS_Custom_Feed {
	/**
	 * Sets the feed slug.
	 *
	 * @var string
	 */
	public static $feed_slug = 'custom';

	/**
	 * Registers the feed and the pre_get_posts action
	 */
	public static function init() {
		add_feed( self::$feed_slug, array( __CLASS__, 'feed_template' ) );

		add_action( 'pre_get_posts', array( __CLASS__, 'feed_content' ) );
	}

	/**
	 * Customizes the query.
	 * It will bail if $query is not an object, filters are suppressed and it's not
	 * our feed query.
	 *
	 * @param  WP_Query $query The current query
	 */
	public static function feed_content( $query ) {
		// Bail if $query is not an object or of incorrect class
		if ( ! $query instanceof WP_Query )
			return;

		// Bail if filters are suppressed on this query
		if ( $query->get( 'suppress_filters' ) )
			return;

		// Bail if it's not our feed
		if ( ! $query->is_feed( self::$feed_slug ) )
			return;

		// Change the feed content
		// Example: A feed for pages
		$query->set( 'post_type', array( 'page' ) );
	}

	/**
	 * Loads the feed template which is placed in wp-includes/feed-rss2.php.
	 */
	public static function feed_template() {
		load_template( ABSPATH . WPINC . '/feed-rss2.php' );
	}
}

/**
 * Hooks into `init`.
 *
 * Note: add_feed() needs access to the global $wp_rewrite
 */
add_action( 'init', array( 'DS_Custom_Feed', 'init' ) );

Es wird Gebrauch vom pre_get_posts Hook gemacht. Dieser erlaubt es die Query nach Bedarf anzupassen – hier wird z.B. der Post Type auf page gesetzt. Was alles angepasst werden kann, kann dem Eintrag im Codex entnommen werden.

Als Beispiel kann ich auf meinen Feed für den WPD Planet hinweisen, welcher nur Beiträge mit einem bestimmten Custom Field anzeigt – Nachzulesen im GitHub Repo zum Theme.

Zum Thema

Cache-Busting durch Versionierung im Dateinamen

Von Haus aus versioniert WordPress eingebundene Skripte und Styles. So sieht der Output für die jQuery Bibliothek beispielhaft folgendermaßen aus:
http://example.com/wp-includes/js/jquery/jquery.js?ver=1.8.3

Wofür die Versionierung? Statische Dateien, die eine hohe Auslaufzeit (Expires-Header) haben, werden vom Browser nicht mehr vom Server geladen, sondern direkt aus dem Browsercache eingebunden. Wird die Resource aber auf dem Server geändert, bekommt der Browser dies nicht mit und lädt weiterhin die alte Version aus dem Browsercache. Abhilfe schafft die Versionierung.

WordPress hängt an die Resource eine Query String mit dem Parameter ver. Diese Art von Versionierung wird allerdings nicht empfohlen.
Google schreibt dazu:

Most proxies, most notably Squid up through version 3.0, do not cache resources with a “?” in their URL even if a Cache-control: public header is present in the response. To enable proxy caching for these resources, remove query strings from references to static resources, and instead encode the parameters into the file names themselves.

Die Version soll also mit in den Dateinamen gepackt werden.

Plugin: Dateinamen um die Version erweitern

Um die Dateien nicht physisch zu ändern hilft das mod_rewrite Modul und folgendes Plugin, welches sich in die WordPress Filter script_loader_src und style_loader_src einklinkt.

PHP / RAW / github:gist
<?php
/**
 * Plugin Name: Filename-based cache busting
 * Version: 0.3
 * Description: Filename-based cache busting for WordPress scripts/styles.
 * Author: Dominik Schilling
 * Author URI: http://wphelper.de/
 * Plugin URI: https://dominikschilling.de/880/
 *
 * License: GPLv2 or later
 * License URI: http://www.gnu.org/licenses/gpl-2.0.html
 *
 *
 * Extend your .htaccess file with these lines:
 *
 *   <IfModule mod_rewrite.c>
 *     RewriteEngine On
 *     RewriteBase /
 *
 *     RewriteCond %{REQUEST_FILENAME} !-f
 *     RewriteCond %{REQUEST_FILENAME} !-d
 *     RewriteRule ^(.+)\.(.+)\.(js|css)$ $1.$3 [L]
 *   </IfModule>
 */

/**
 * Moves the `ver` query string of the source into
 * the filename. Doesn't change admin scripts/styles and sources
 * with more than the `ver` arg.
 *
 * @param  string $src The original source.
 * @return string
 */
function ds_filename_based_cache_busting( $src ) {
	// Don't touch admin scripts.
	if ( is_admin() ) {
		return $src;
	}

	$_src = $src;
	if ( '//' === substr( $_src, 0, 2 ) ) {
		$_src = 'http:' . $_src;
	}

	$_src = parse_url( $_src );

	// Give up if malformed URL.
	if ( false === $_src ) {
		return $src;
	}

	// Check if it's a local URL.
	$wp = parse_url( home_url() );
	if ( isset( $_src['host'] ) && $_src['host'] !== $wp['host'] ) {
		return $src;
	}

	return preg_replace(
		'/\.(js|css)\?ver=(.+)$/',
		'.$2.$1',
		$src
	);
}
add_filter( 'script_loader_src', 'ds_filename_based_cache_busting' );
add_filter( 'style_loader_src', 'ds_filename_based_cache_busting' );

Wie im Plugin Header dokumentiert, muss die .htaccess ergänzt werden.
Wer auf nginx setzt kann folgende Zeilen nutzen, danke Sergej:

location ~ ^(.+)\.(.+)\.(js|css)$ {
    alias $1.$3;
}

Zum Thema

WebKit XSS Filter blockt Inlineframes und Flash Objekte in der WordPress Artikelvorschau

Cross-Site-Scripting (XSS), also das Einfügen von schädlichen Skripter einer Seite A in Seite B, ist ein Sicherheitsfaktor, den es gilt zu unterbinden. Primär sind dafür die Entwickler der jeweiligen Webseite zuständig.
Doch auch heutige Browser klinken sich in die Thematik ein.

Seit geraumer Zeit besitzen WebKit Browser, siehe Chrome und Safari, einen XSS Filter, der überprüft, ob ein Skript, welches auf einer Webseite ausgeführt werden soll, sich auch schon in der POST Anfrage befand. Ist dem so, kann dies ein Indiz für eine XSS-Attacke sein und wird geblockt.

Mit folgenden Beispiel kann der XSS Filter selbst getestet werden:

<?php
if( ! $_POST ) :
?>
<form method="post">
	<input name="xss" value="<iframe src='http://example.com'></iframe>" />
	<input type="submit" />
</form>
<?php
else :
	echo $_POST&#91;'xss'&#93;;
endif;&#91;/code&#93;

<h4>Was hat das nun mit der Artikelvorschau in WordPress zu tun?</h4>
Ganz einfach: Starte Chrome oder Safari, erstelle einen Artikel mit einem YouTube Video und klicke auf den Button <em>Vorschau</em>.

Das Ergebnis:

[caption id="attachment_980" align="aligncenter" width="700"]<img src="https://dominikschilling.de/wp-content/uploads/2012/09/wordpress-preview-webkit.png" alt="WordPress Vorschau im Webkit Browser" title="Artikel Vorschau eines YouTube Videos im Webkit Browser" width="700" height="472" class="size-full wp-image-980" /> Artikel Vorschau eines YouTube Videos im Webkit Browser[/caption]

Man sucht vergeblich das YouTube Video, die Konsole wirft ein <span style="color:red">Refused to execute a JavaScript script. Source code of script found within request.</span> aus und nach einem Reload des Fensters ist das YouTube Video wieder da.

[caption id="attachment_982" align="alignright" width="256"]<img src="https://dominikschilling.de/wp-content/uploads/2012/09/wordpress-aktikelvorschau-weg.png" alt="WordPress Artikelvorschau" title="WordPress Artikelvorschau" width="256" height="388" class="size-full wp-image-982" /> Visualisierung der Artikelvorschau[/caption]

Warum? Der XSS-Filter hat zugeschlagen.

Beim Klick auf den <em>Vorschau</em> Button wird eine POST Abfrage an <code>/wp-admin/post.php</code> gestartet und intern als <code>post.php?action=preview</code> aufgenommen.

Dadurch wird erreicht, dass eventuelle Änderungen in einem Entwurf oder einer Revision (Autosave) gespeichert werden. Genaueres kann man der Funktion <code>post_preview()</code> entnehmen.

Das Wichtige an der Funktion ist die Rückgabe. Er enthält die Weiterleitung zur eigentlichen Vorschau.

Wie bekannt ist, zeigt die Vorschau eine 1:1 Kopie des Artikels samt Änderungen. Dazu zählt auch in unserem Fall der vorher per POST gesendete Artikelinhalt mit dem YouTube Video.
Wir erinnern uns zurück: Der XSS-Filter schlägt zu, wenn ein Skript in einer POST Abfrage gefunden wird und gleichzeitig vom Browser gerendert werden soll.
Trotz dem Umweges ist dies nun bei WordPress genau der Fall.

Und jetzt?

<h4>Den WebKit XSS Filter umgehen</h4>

Mit Hilfe eines nicht-standardisierten HTTP-Headerfeldes <code>X-XSS-Protection</code> kann dem Browser mitteilt werden, dass schon auf der Client-Seite für die nötige Sicherheit gesorgt wurde. Der Wert des Feldes sollte dann mit <code>0</code> belegt werden.

Folgende Zeilen setzen das Vorhaben für unsere Artikelvorschau um und sollte in einem Plugin Platz finden.

[code gist="plugin.php"]

Führt man das obige Szenarion nun erneut aus, so sollte das YouTube Video direkt angezeigt werden.

Zum Thema

jQuery im Footer von WordPress laden — Die elegantere Variante

Seit WordPress 3.9 funktioniert diese Variante leider nicht mehr.

WordPress wird mit einigen JavaScript Bibliotheken ausgeliefert. Dazu gehören zum Beispiel script.aculo.us oder jQuery.
Plugins und Themes müssen diese Bibliotheken dann nicht mehr selbst mitausliefern, sondern können diese über die WP_Scripts API einbinden.

Direkt zur Lösung.

Registierung der Standardbibliotheken

Zuständig für die Registrierung der Standardbibliotheken ist die Funktion wp_default_scripts() in /wp-includes/script-loader.php. Ein Auszug:

<?php
/**
 * Register all WordPress scripts.
 *
 * @since 2.6.0
 *
 * @param object $scripts WP_Scripts object.
 */
function wp_default_scripts( &$scripts ) {
// &#91;…&#93;
	$scripts->add( 'colorpicker', "/wp-includes/js/colorpicker$suffix.js", array('prototype'), '3517m' );

	$scripts->add( 'editor', "/wp-admin/js/editor$suffix.js", array('utils','jquery'), false, 1 );
// […]
	// not used in core, replaced by Jcrop.js
	$scripts->add( 'cropper', '/wp-includes/js/crop/cropper.js', array('scriptaculous-dragdrop') );

	$scripts->add( 'jquery', '/wp-includes/js/jquery/jquery.js', array(), '1.7.2' );

	// full jQuery UI
	$scripts->add( 'jquery-ui-core', '/wp-includes/js/jquery/ui/jquery.ui.core.min.js', array('jquery'), '1.8.20', 1 );
	$scripts->add( 'jquery-effects-core', '/wp-includes/js/jquery/ui/jquery.effects.core.min.js', array('jquery'), '1.8.20', 1 );
// […]
}

WP_Dependencies->add() erwartet also die Argumente in der Reihenfolge Skriptname, Pfad zum Skript, Abhängigkeit zu anderen Skripten, Version des Skripts und zum Schluss das entscheidene Argument Einreihung in den Header oder Footer.

Nach dem selben Prinzip arbeiten übrigens auch wp_register_script() und wp_enqueue_script().

Warum JavaScript eigentlich in den Footer gehört

With scripts, progressive rendering is blocked for all content below the script. Moving scripts as low in the page as possible means there’s more content above the script that is rendered sooner.

The second problem caused by scripts is blocking parallel downloads.

Steve Souders in „High Performance Web Sites: Rule 6 – Move Scripts to the Bottom

Zusammengefasst: Erkennt der Browser ein Skript, wird das Skript zunächst abgearbeitet und alles andere muss warten.

jQuery für den Footer registrieren

Schaut man sich nun nochmal obige markierte Zeile an, so erkennt man schnell, dass der letzte Parameter bei der jQuery Registrierung nicht gesetzt wurde.
Daraus folgt, dass, wenn jQuery eingebunden wird, die Skripteinbindung automatisch in den Header gelegt wird.

Gerade für Theme-Entwickler und im Bezug auf die Performance einer Seite ist dies sehr ungünstig. Wird ein zusätzliches Skript mit jQuery Abhängigkeit in den Footer eingebunden, bleibt jQuery weiterhin im Header.

Um jQuery trotzdem in den Footer zu bekommen, wird im Netz recht häufig die Variante über wp_deregister_script( 'jquery' ) angepriesen. — Erst die originale Skriptdefinition löschen und dann wieder mit dem letzten Parameter neu definieren. Sehr unflexibel, da für die Neuregistrierung die jQuery Version und der Pfad zur Datei gebraucht wird.

Aus diesem Grund möchte ich nun auf die elegantere Variante eingehen.

PHP / RAW / github:gist
<?php
/**
 * Prints jQuery in footer on front-end.
 */
function ds_print_jquery_in_footer( &$scripts) {
	if ( ! is_admin() )
		$scripts->add_data( 'jquery', 'group', 1 );
}
add_action( 'wp_default_scripts', 'ds_print_jquery_in_footer' );

Bei dieser Methode klinkt man sich in den wp_default_scripts Hook ein. Dieser übergibt als Parameter ein Objekt vom Typ WP_Scripts, wo unter anderem die registrierten Skripte enthalten sind.
Die Definitionen zu jQuery sind daher in $scripts->registered['jquery'] deklariert:

[jquery] => _WP_Dependency Object
	(
		[handle] => jquery
		[src] => /wp-includes/js/jquery/jquery.js
		[deps] => Array
 			(
			)
 		[ver] => 1.7.2
		[args] => 
		[extra] => Array
			(
			)
	)

Damit WordPress jQuery nun im Footer einbindet, muss das Feld extra um dem Wert group => 1 ergänzt werden.
Dazu steht die Methode WP_Dependencies->add_data( $handle, $key, $value ) zur Verfügung.

Zusammengefasst ergibt sich schlussendlich $scripts->add_data( 'jquery', 'group', 1 );.

Wer eine PHP Version größer 5.3 einsetzt kann übrigens auch auf eine anonyme Funktion setzen:

PHP / RAW / github:gist
<?php
/**
 * Prints jQuery in footer on front-end.
 * For PHP > 5.3
 */
add_action(
	'wp_default_scripts',
	function( &$scripts ) {
		if ( ! is_admin() )
			$scripts->add_data( 'jquery', 'group', 1 );
	}
);

Zum Thema

Gist-Schnipsel von Github in WordPress einbinden

In letzter Zeit wurde ich immer häufiger gefragt, wie ich denn die Codeschnipsel in meine Erklärungen hier einbinde. Folgend des Rätsels Lösung.

Das Konzept

Zunächst werden die Codeschnipsel als Gist auf github.com ausgelagert.
Um diese in den Beiträgen einbinden zu können, müssen die Schnipsel von Github in WordPress importiert werden.

Im Backend soll dafür ein Eingabefeld für die Gist-ID angelegt werden, welches die Gist-Daten in ein benutzerdefiniertes Feld per AJAX lädt.
Die Dateinamen sollen dann an die gewünschte Stelle eingesetzt werden und im Frontend umgewandelt werden.

Die Umsetzung

Metabox

Zunächst die benötigte Metabox für das Backend. Im Eingabefeld wird die Gist-ID eingefügt bzw. aus der Datenbank geladen. Per Button werden die Daten von Github in die Datenbank als benutzerdefinierte Felder gespeichert. Die Dateinamen werden als Liste im Format {Dateiname} ausgegeben.

PHP / RAW / github:gist
<?php
/*
 * Add a metabox the post edit screen with an input field for the gist ID
 * and show the file list.
 */
function ds_gist_metabox_cb( $data ) {
	wp_nonce_field( 'ds_gist', 'ds-gist-nonce' );

	$gist_id   = get_post_meta( $data->ID, '_gist_id', true ) ;
	$gist_id   = ! empty( $gist_id ) ? $gist_id : '';
	$gist_data = get_post_meta( $data->ID, '_gist_data', true );
	$gist_data = ! empty( $gist_data ) ? $gist_data : array();

	$files = '';
	if ( ! empty( $gist_data ) ) {
		foreach ( $gist_data as $file => $data )
			$files .= '<li><code>{' . $file . '}</code></li>';
	}
	?>
	<p>
		<label>Gist ID: <input type="text" class="small-text" style="width: 180px;" name="gist-id" id="gist-id" value="<?php echo esc_attr( $gist_id ); ?>" /></label>
		<input type="button" value="Fetch" id="gist-update" class="button" />
		<img src="<?php echo esc_url( admin_url( 'images/wpspin_light.gif' ) ); ?>" class="ajax-loading" id="gist-ajax-loading" alt="" />
	</p>
	<ul id="gist-files">
		<?php echo $files;?>
	</ul>
	<?php
}

AJAX Callback

Der AJAX Callback dient dazu, die Codeschnipsel in die lokale Datenbank zu übertragen.
Der Callback übernimmt auch das Umformen der empfangen Daten, denn die Gist API gibt mehrere Dateien leider nur als einen String zurück. Dadurch wird es erst möglich, die Schnipsel mit {Dateiname} einzubinden.
Gespeichert werden die Daten als benutzerdefinierte Felder unter den Namen _gist_id bzw. _gist_data.

PHP / RAW / github:gist
<?php
/*
 * Replace {Filename} with associated snippet from the
 * custom fields. 
 */
function ds_gistify() {
	$post_id = intval( $_POST['post_id'] );
	$gist_id = sanitize_key( $_POST['gist_id'] );
	if ( empty( $post_id ) || empty( $gist_id ) )
		die( json_encode( array(
			'id'    => $gist_id,
			'error' => 'Empty Post ID or Gist ID.'
		) ) );

	check_ajax_referer( 'ds_gist' );

	$gist_body = wp_remote_retrieve_body(
		wp_remote_get(
				$gist_url = sprintf( "https://gist.github.com/%s.json", $gist_id )
		)
	);
	$gist_body = json_decode( $gist_body );

	if ( empty( $gist_body ) )
		die( json_encode( array(
			'id'    => $gist_id,
			'error' => 'Empty Body'
		) ) );

	if ( ! empty( $gist_body->error ) ) {
		die( json_encode( array(
			'id'    => $gist_id,
			'error' => $gist_body->error
		) ) );
	}

	update_post_meta( $post_id, '_gist_id', $gist_id );

	$gist_files = $gist_body->files;

	preg_match_all( '/<pre>(.+?)</pre>/is', $gist_body->div, $gist_divs );
	$gist_divs = $gist_divs[0];

	foreach ( $gist_files as $i => $gist_file )
		$gist_data[ $gist_file ] = '<div class="gist-syntax">' . $gist_divs[$i] . '</div>';

	foreach ( $gist_data as $file => $gist_item ) {
		$gist_new_meta = sprintf(
			'<div><p class="gist-meta"><a class="gist-raw" href="%s">RAW</a> · <a class="gist-download" href="%s">Download</a> · <a class="gist-github" href="%s">Gist@GitHub</a></p></div>',
			esc_url( "https://gist.github.com/raw/{$gist_id}/{$file}" ),
			esc_url( "https://gist.github.com/gists/{$gist_id}/download"),
			esc_url( "https://gist.github.com/{$gist_id}")
		);
		$gist_data[ $file ] .= $gist_new_meta;
	}

	update_post_meta( $post_id, '_gist_data', $gist_data );

	die( json_encode( array(
		'id'    => $gist_id,
		'files' => $gist_files
	) ) );
}

jQuery Handler

Der AJAX Callback wird mit Hilfe der Javascript Bibliothek jQuery ausgeführt. Beim Klick des Buttons beginnt die Ausführung.

JavaScript / RAW / github:gist
// Handle AJAX call
( function( $ ) {
	$( '#gist-update' ).click( function( e ) {
		e.preventDefault();

		$( '#gist-ajax-loading' ).css( 'visibility', 'visible' );

		data = {
			'action' : 'gist',
			'_ajax_nonce' : $('#ds-gist-nonce').val(),
			'post_id' : $('#post_ID').val(),
			'gist_id' : $('#gist-id').val()
		};

		$.post( ajaxurl, data,
			function( res ) {
				res = $.parseJSON( res );

				if ( res.error ) {
					$( '#gist-ajax-loading' ).css( 'visibility', 'hidden' );
					alert( 'Gist ID "' + res.id + '": ' + res.error );
					return;
				}

				if ( res.files ) {
					$( '#gist-files' ).empty();
					$.each( res.files, function( key, val ) {
						$( '#gist-files' ).append( '<li><code>{' + val + '}</code></li>' );
					});
					$( '#gist-ajax-loading' ).css( 'visibility', 'hidden' );
				}

			}
		)
	});
} )( jQuery );

Umwandlung im Frontend

Im eigentlichen Artikel befinden sich die Codeschnipsel noch im Format {Dateiname}. Diese müssen nun mit dem eigentlich Schnipsel aus dem benutzerdefinierten Feld ersetzt werden.

PHP / RAW / github:gist
<?php
/*
 * AJAX callback function which handles the API response. Saves the data
 * into custom fields
 */
function ds_convert_to_gist( $content ) {
	$gist_data = get_post_meta( get_the_ID(), '_gist_data', true );

	if ( empty( $gist_data ) )
		return $content;

	wp_enqueue_style('gist-css');

	foreach ( $gist_data as $file => $file_data ) {
		$files[] = '/{' . $file . '}/';
		$data[]  = $file_data;
	}

	$content = preg_replace(
		$files,
		$data,
		$content
	);

	return $content;
}

Hooks und Skripts

Die Funktionen müssen jetzt noch in das WordPress System eingebunden werden, außerdem das Gist Stylesheet sowie die benötigte Javascript Datei registriert und eingebunden werden.

PHP / RAW / github:gist
<?php
/*
 * Hook into the backend and load scripts and
 * init metabox.
 */
function ds_gist2wordpress_admin() {
		add_action( 'wp_ajax_gist', 'ds_gistify' );

		add_action( 'admin_print_scripts-post.php', 'ds_gist_js' );
		add_action( 'admin_print_scripts-post-new.php', 'ds_gist_js' );

		wp_register_script(
			'gist-js',
			get_bloginfo( 'template_url' ) . "/gist.js", // Path, needs some update from you
			array( 'jquery' ),
			0.1,
			true
		);

		add_meta_box(
				'ds_gist',
				'Gist',
				'ds_gist_metabox_cb',
				'post'
		);
}
add_action( 'admin_init', 'ds_gist2wordpress_admin' );


/*
 * Hook into the frontend and load scripts.
 */
function ds_gist2wordpress() {
		wp_register_style(
			'gist-css',
			'https://gist.github.com/stylesheets/gist/embed.css'
		);

		add_filter( 'the_content', 'ds_convert_to_gist', 99 );
}
add_action( 'init', 'ds_gist2wordpress' );


function ds_gist_js() {
	wp_enqueue_script( 'gist-js' );
}

Das Ergebnis

Zum Schluss nochmal alles zusammen, wie es zum Beispiel in einem Theme oder in einem Plugin aussehen könnte.

PHP / RAW / github:gist
<?php
/*
 * Hook into the backend and load scripts and
 * init metabox.
 */
function ds_gist2wordpress_admin() {
	add_action( 'wp_ajax_gist', 'ds_gistify' );

	add_action( 'admin_print_scripts-post.php', 'ds_gist_js' );
	add_action( 'admin_print_scripts-post-new.php', 'ds_gist_js' );

	wp_register_script(
		'gist-js',
		get_bloginfo( 'template_url' ) . "/gist.js",
		array( 'jquery' ),
		0.1,
		true
	);

	add_meta_box(
			'ds_gist',
			'Gist',
			'ds_gist_metabox_cb',
			'post'
	);
}
add_action( 'admin_init', 'ds_gist2wordpress_admin' );

/*
 * Hook into the frontend and load scripts.
 */
function ds_gist2wordpress() {
		wp_register_style(
			'gist-css',
			'https://gist.github.com/stylesheets/gist/embed.css'
		);

		add_filter( 'the_content', 'ds_convert_to_gist', 99 );
}
add_action( 'init', 'ds_gist2wordpress' );

/*
 * Helper for enqeueing script
 */
function ds_gist_js() {
	wp_enqueue_script( 'gist-js' );
}

/*
 * Add a metabox the post edit screen with an input field for the gist ID
 * and show the file list.
 */
function ds_gist_metabox_cb( $data ) {
	wp_nonce_field( 'ds_gist', 'ds-gist-nonce' );

	$gist_id   = get_post_meta( $data->ID, '_gist_id', true ) ;
	$gist_id   = ! empty( $gist_id ) ? $gist_id : '';
	$gist_data = get_post_meta( $data->ID, '_gist_data', true );
	$gist_data = ! empty( $gist_data ) ? $gist_data : array();

	$files = '';
	if ( ! empty( $gist_data ) ) {
		foreach ( $gist_data as $file => $data )
			$files .= '<li><code>{' . $file . '}</code></li>';
	}
	?>
	<p>
		<label>Gist ID: <input type="text" class="small-text" style="width: 180px;" name="gist-id" id="gist-id" value="<?php echo esc_attr( $gist_id ); ?>" /></label>
		<input type="button" value="Fetch" id="gist-update" class="button" />
		<img src="<?php echo esc_url( admin_url( 'images/wpspin_light.gif' ) ); ?>" class="ajax-loading" id="gist-ajax-loading" alt="" />
	</p>
	<ul id="gist-files">
		<?php echo $files;?>
	</ul>
	<?php
}

/*
 * Replace {Filename} with associated snippet from the
 * custom fields. 
 */
function ds_gistify() {
	$post_id = intval( $_POST['post_id'] );
	$gist_id = sanitize_key( $_POST['gist_id'] );
	if ( empty( $post_id ) || empty( $gist_id ) )
		die( json_encode( array(
			'id'    => $gist_id,
			'error' => 'Empty Post ID or Gist ID.'
		) ) );

	check_ajax_referer( 'ds_gist' );

	$gist_body = wp_remote_retrieve_body(
		wp_remote_get(
				$gist_url = sprintf( "https://gist.github.com/%s.json", $gist_id )
		)
	);
	$gist_body = json_decode( $gist_body );

	if ( empty( $gist_body ) )
		die( json_encode( array(
			'id'    => $gist_id,
			'error' => 'Empty Body'
		) ) );

	if ( ! empty( $gist_body->error ) ) {
		die( json_encode( array(
			'id'    => $gist_id,
			'error' => $gist_body->error
		) ) );
	}

	update_post_meta( $post_id, '_gist_id', $gist_id );

	$gist_files = $gist_body->files;

	preg_match_all( '/<pre>(.+?)</pre>/is', $gist_body->div, $gist_divs );
	$gist_divs = $gist_divs[0];

	foreach ( $gist_files as $i => $gist_file )
		$gist_data[ $gist_file ] = '<div class="gist-syntax">' . $gist_divs[$i] . '</div>';

	foreach ( $gist_data as $file => $gist_item ) {
		$gist_new_meta = sprintf(
			'<div><p class="gist-meta"><a class="gist-raw" href="%s">RAW</a> · <a class="gist-download" href="%s">Download</a> · <a class="gist-github" href="%s">Gist@GitHub</a></p></div>',
			esc_url( "https://gist.github.com/raw/{$gist_id}/{$file}" ),
			esc_url( "https://gist.github.com/gists/{$gist_id}/download"),
			esc_url( "https://gist.github.com/{$gist_id}")
		);
		$gist_data[ $file ] .= $gist_new_meta;
	}

	update_post_meta( $post_id, '_gist_data', $gist_data );

	die( json_encode( array(
		'id'    => $gist_id,
		'files' => $gist_files
	) ) );
}

/*
 * AJAX callback function which handles the API response. Saves the data
 * into custom fields
 */
function ds_convert_to_gist( $content ) {
	$gist_data = get_post_meta( get_the_ID(), '_gist_data', true );

	if ( empty( $gist_data ) )
		return $content;

	wp_enqueue_style('gist-css');

	foreach ( $gist_data as $file => $file_data ) {
		$files[] = '/{' . $file . '}/';
		$data[]  = $file_data;
	}

	$content = preg_replace(
		$files,
		$data,
		$content
	);

	return $content;
}