Interne Link Suche im WordPress Block Editor erweitern (für Kategorien, Schlagwörter…)

Vor den Umstieg auf den Block Editor aka Gutenberg habe ich im Classic Editor das Plugin Better Internal Link Search verwendet, um im Link-Dialog nicht nur nach internen Beiträgen und Seiten zu suchen, sondern auch nach Kategorien, Schlagwörtern und weiteren Terms. Leider unterstützt das Plugin aber nicht den Block Editor und die Autoren haben wohl auch das Interesse an dem Plugin verloren (letztes Update vor 4 Jahren).

Die Suche z. B. nach Kategorien, Schlagwörtern (zusammengefasst Terms genannt) lässt sich aber recht einfach umsetzen, weil man den WP_REST_Search_Handler recht einfach erweitern kann:

Update

Seit WordPress 5.6 wird jetzt im Block Editor auch standardmäßig nach Terms (Kategorien, Schlagwörter…) gesucht. Ihr müsst jetzt keinen eigenen WP_REST_Taxonomy_Search_Handler mehr schreiben!

Ich habe die Suche nach Links zu Terms als funktionsfähiges WordPress Plugin als Gist veröffentlicht:

Link Suche im Block Editor nach einem Schlagwort
Link Suche im Block Editor nach einem Schlagwort

Search Handler

Als Erstes müssen wir unseren eigenen Search Handler bauen. Dafür erweitern wir unsere eigene WP_REST_Taxonomy_Search_Handler-Klasse, um die abstrakte WP_REST_Search_Handler-Klasse und nutzen diese als Vorlage. Dabei kann man sich gut an der WP_REST_Post_Search_Handler Klasse orientieren.

In der _construct-Funktion geben wir unseren Search Type einen eindeutigen Namen taxonomy und geben als Subtypen alle öffentlichen public => true Taxonomies an:

class WP_REST_Taxonomy_Search_Handler extends WP_REST_Search_Handler {

	public function __construct() {
		$this->type = 'taxonomy';

		$this->subtypes = get_taxonomies( [
			'show_ui' => true,
			'public' => true,
		] );
	}
Code-Sprache: PHP (php)

Search Items

In der Funktion search_items müssen wir die Suche nach zu der Suchanfrage passenden Terms durchführen und die gefunden IDs zurückgebenen:

	public function search_items( WP_REST_Request $request ) {
		// Get the taxonomy types to search for the current request.
		$taxonomy_types = $request[ WP_REST_Search_Controller::PROP_SUBTYPE ];
		if ( in_array( WP_REST_Search_Controller::TYPE_ANY, $taxonomy_types, true ) ) {
			$taxonomy_types = $this->subtypes;
		}

		$offset = ( $request['page'] - 1 ) * $request['per_page'];

		$query_args = array(
			'taxonomy'           	=> $taxonomy_types,
			'number'      			=> (int) $request['per_page'],
			'offset'				=> (int) $offset,
			'fields'              	=> 'ids',
			'hide_empty'			=> false,
		);

		if ( ! empty( $request['search'] ) ) {
			$query_args['search'] = $request['search'];
		}

		$query     = new WP_Term_Query();
		$found_ids = $query->query( $query_args );

		// ToDo: Determine total found taxonomies
		$total = $offset + count( $found_ids );
		if ( count( $found_ids ) === (int) $request['per_page'] ) {
			$total = $total + 1;
		}

		return [
			self::RESULT_IDS   => $found_ids,
			self::RESULT_TOTAL => $total,
		];
	}Code-Sprache: PHP (php)
Info

Im Gegensatz zu WP_Query unterstützt WP_Term_Query allerdings nicht das paged und posts_per_page Argument. Dieses habe ich durch number und offset ersetzt.

Außerdem ist es nicht möglich, die Anzahl aller gefundenen Terms die mit unserer Suchanfrage zusammenhängen, auszugeben. Deswegen wird zur Not immer ein um eins höherer Wert als $total zurückgegeben, als bisher gefunden.

Prepare Items

In der Funktion prepare_item muss jetzt das Item für die Antwort vorbereitet werden. Auch hier kann man sich relativ gut an dem Beispiel für Posts orientieren:

	public function prepare_item( $id, array $fields ) {
		$term = get_term( $id );

		$data = [];

		if ( in_array( WP_REST_Search_Controller::PROP_ID, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_ID ] = (int) $term->term_id;
		}
		if ( in_array( WP_REST_Search_Controller::PROP_TITLE, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_TITLE ] = $term->name;
		}
		if ( in_array( WP_REST_Search_Controller::PROP_URL, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_URL ] = get_term_link( $term );
		}
		if ( in_array( WP_REST_Search_Controller::PROP_TYPE, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_TYPE ] = $this->type;
		}
		if ( in_array( WP_REST_Search_Controller::PROP_SUBTYPE, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_SUBTYPE ] = get_taxonomy( $term->taxonomy )->labels->singular_name;
		}

		return $data;
	}Code-Sprache: PHP (php)

Zu guter Letzt brauchen wir noch einen Link zu dem REST API Endpunkt für die Taxonomie und die einzelnen Terms. Da dieser Teil nicht von dem Block Editor verwendet wird, gebe ich nur ein leeres Array zurück:

public function prepare_item_links( $id ) {

		$links = [];
		return $links;

	}
Code-Sprache: PHP (php)

Insgesamt sieht WP_REST_Taxonomy_Search_Handler jetzt wie folgt aus:

class WP_REST_Taxonomy_Search_Handler extends WP_REST_Search_Handler {

	public function __construct() {
		$this->type = 'taxonomy';

		$this->subtypes = get_taxonomies( [
			'show_ui' => true,
			'public' => true,
		] );
	}

	/**
	 * Searches the object type content for a given search request.
	 *
	 *
	 * @param WP_REST_Request $request Full REST request.
	 * @return array Associative array containing an `WP_REST_Search_Handler::RESULT_IDS` containing
	 *               an array of found IDs and `WP_REST_Search_Handler::RESULT_TOTAL` containing the
	 *               total count for the matching search results.
	 */
	public function search_items( WP_REST_Request $request ) {
		// Get the taxonomy types to search for the current request.
		$taxonomy_types = $request[ WP_REST_Search_Controller::PROP_SUBTYPE ];
		if ( in_array( WP_REST_Search_Controller::TYPE_ANY, $taxonomy_types, true ) ) {
			$taxonomy_types = $this->subtypes;
		}

		$offset = ( $request['page'] - 1 ) * $request['per_page'];

		$query_args = array(
			'taxonomy'           	=> $taxonomy_types,
			// Replace paged with offset
			//'paged'               	=> (int) $request['page'],
			'number'      			=> (int) $request['per_page'],
			'offset'				=> (int) $offset,
			'fields'              	=> 'ids',
			'hide_empty'			=> false,
		);

		if ( ! empty( $request['search'] ) ) {
			$query_args['search'] = $request['search'];
		}

		$query     = new WP_Term_Query();
		$found_ids = $query->query( $query_args );

		$total = $offset + count( $found_ids );
		if ( count( $found_ids ) === (int) $request['per_page'] ) {
			$total = $total + 1;
		}

		return [
			self::RESULT_IDS   => $found_ids,
			self::RESULT_TOTAL => $total,
		];
	}

	/**
	 * Prepares the search result for a given ID.
	 *
	 *
	 * @param int   $id     Item ID.
	 * @param array $fields Fields to include for the item.
	 * @return array Associative array containing all fields for the item.
	 */
	public function prepare_item( $id, array $fields ) {
		$term = get_term( $id );

		$data = [];

		if ( in_array( WP_REST_Search_Controller::PROP_ID, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_ID ] = (int) $term->term_id;
		}
		if ( in_array( WP_REST_Search_Controller::PROP_TITLE, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_TITLE ] = $term->name;
		}
		if ( in_array( WP_REST_Search_Controller::PROP_URL, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_URL ] = get_term_link( $term );
		}
		if ( in_array( WP_REST_Search_Controller::PROP_TYPE, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_TYPE ] = $this->type;
		}
		if ( in_array( WP_REST_Search_Controller::PROP_SUBTYPE, $fields, true ) ) {
			$data[ WP_REST_Search_Controller::PROP_SUBTYPE ] = get_taxonomy( $term->taxonomy )->labels->singular_name;
		}

		return $data;
	}

	/**
	 * Prepares links for the search result of a given ID.
	 *
	 *
	 * @param int $id Item ID.
	 * @return array Links for the given item.
	 */
	public function prepare_item_links( $id ) {

		$links = [];
		return $links;

	}
}Code-Sprache: PHP (php)

Search Handler hinzufügen

Unseren eigenen Search Handler für Taxonomies müssen wir jetzt noch über den Filter wp_rest_search_handlers hinzufügen:

function wp_rest_search_handlers( $handlers ) {

	// Add Taxonomy Handler
	$handlers[] = new WP_REST_Taxonomy_Search_Handler();

	return $handlers;
}
add_filter( 'wp_rest_search_handlers', __NAMESPACE__ . '\wp_rest_search_handlers' );
Code-Sprache: PHP (php)

Search Type

Sucht ihr im Link-Dialog, wird eure Anfrage an den REST API Endpoint /wp-json/wp/v2/search gesendet. Allerdings mit dem Parameter type=post. Dadurch weiß der Server, dass ihr nach ein Post sucht. Leider lässt sich nicht der Type, nach dem man sucht (z.B. neben Post auch Taxonomy), im Link Dialog aussuchen. Deswegen müssen wir uns an dieser Stelle etwas überlegen, um diesen Standard zu überschreiben.

/wp-json/wp/v2/search?search=wordpress&per_page=20&type=post&_locale=user 

Ich habe da die Notation vom Better Internal Link Search Plugin übernommen und wenn ich meine Suchanfrage mit einem -ter oder -tax (ist egal) starte, gefolgt von dem Suchbegriff, z.B. -ter WordPress, wird dieser Request auf dem Server abgefangen, der type auf taxonomy geändert und der Search String angepasst.

Update

Seit WordPress 5.6 wird im Block Editor jeweils für post und term = zwei Requests pro Suche an den Server gesendet d.h. hackt ihr den Request wie ich, musst ihr einen davon abfangen und eine leere Antwort zurückgeben. Sonst wird alles doppelt angezeigt. Ich habe das mit einem „unsupported“ Search Handler gelöst, der für post dann eine leere Antwort zurückgibt

function change_search_types( $result, $server, $request ) {

	if ( '/wp/v2/search' !== $request->get_route() ) {
		return $result;
	}

	$supported = [
		'tax' => 'taxonomy',
		'ter' => 'taxonomy',
		'pl' => 'prettylink',
	];

	$matches = [];
	preg_match( '/^-(\w{2,3}) (.*)/', $request->get_param( 'search' ), $matches );
	if ( 3 === count( $matches ) && isset( $supported[$matches[1]] ) ) {

		$request->set_param( 'type', $supported[$matches[1]] );
		$request->set_param( 'search', $matches[2] );

	}

	return $result;
}
add_filter( 'rest_pre_dispatch', __NAMESPACE__ . '\change_search_types', 10, 3 );Code-Sprache: PHP (php)

Zusammengesetzt findet ihr das Plugin als Gist auf Github.

Ausblick

Neben Kategorien, Schlagwörter und weiteren Taxonomies kann man auch einen Handler für viele weitere Suchen schreiben. Mit dem Better Internal Link Search Plugin kann man zum Beispiel nach Wikipedia-Einträgen, Github Repositories oder Gists, WordPress Plugins… suchen. Das kann man alles mit seinem eigenen Search Handler umsetzen.

Titelbild: Bild von Free-Photos auf Pixabay

5 Reaktionen zu “Interne Link Suche im WordPress Block Editor erweitern (für Kategorien, Schlagwörter…)

  1. Drivingralle sagt:

    Moin!

    Super Plugin hast du da gebaut.
    Habe mich schon darüber geärgert, dass ich nicht nach Terms suchen kann.

    Hat du geplant, das Plugin ins offizielle Repo zu bringen? Wäre für mich ein Plugin, dass ich in alle Websites einbauen würde.

    Es grüßt
    derRALF

  2. Hallo Johannes,
    ich kann mich Ralf nur anschließen, mir fehlt die Funktion auch ständig.
    Mein Wunsch wäre aber die Verankerung im Core.

    Vielen Dank für den Code.
    Jochen

    1. Johannes sagt:

      Ja ich wäre auch eher für eine Lösung im Core. Am besten das man bei der Link Suche den type auswählen kann, nach was man suchen möchte. So ist die API ja eigentlich auch angedacht. Dann würde der Workarount mit -ter wegfallen. Das finde ich aktuell zu unsauber, um es als Plugin zu veröffentlichen.

      Scheinbar wird daran bereits gearbeitet: https://github.com/WordPress/gutenberg/pull/22600 Allerdings nur nach Kategorien und Post Formats aktuell. Finde ich nicht so ganz rund, ich werde mich da mal einbringen.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert