From 22c8ea7152810f6ccc3489b6441fe6cbc3c00405 Mon Sep 17 00:00:00 2001 From: Ian Dunn Date: Tue, 23 Jan 2024 11:42:23 -0800 Subject: [PATCH] Events: Redirect query-filter URLs to a "pretty" version --- .../wporg-events-2023/inc/events-query.php | 215 +++++++++++++++--- 1 file changed, 182 insertions(+), 33 deletions(-) diff --git a/public_html/wp-content/themes/wporg-events-2023/inc/events-query.php b/public_html/wp-content/themes/wporg-events-2023/inc/events-query.php index db13afdfa8..62407688bc 100644 --- a/public_html/wp-content/themes/wporg-events-2023/inc/events-query.php +++ b/public_html/wp-content/themes/wporg-events-2023/inc/events-query.php @@ -1,17 +1,26 @@ get_query_var( 's', '' ), - 'type' => get_query_var( 'event_type', array() ), - 'format' => get_query_var( 'format_type', array() ), - 'month' => get_query_var( 'month', array() ), - 'country' => get_query_var( 'country', array() ), - ); + global $wp; + + // This needs to be retrieved from `$wp->query_vars`, not `$wp_query->query_vars`. Otherwise the previously + // applied facet will get wiped out when a new request is submitted with an additional facet. + $pretty_facets = $wp->query_vars['event_facets'] ?? ''; + + // The query-filters form submission has key-value pairs in the URL, but then that request is redirected to a + // "pretty" URL. This function needs to handle both types of requests. + // @see redirect_to_pretty_query_vars(). + if ( $pretty_facets ) { + preg_match_all( '#([\w\-]+/[\w\-,]+)#', $pretty_facets, $matches ); + + foreach ( (array) $matches[0] as $match ) { + $parts = explode( '/', $match ); + $var_key = $parts[0]; + + // We need a delimiter to separate the values, but Google discourages using commas, colons, or + // anything else in URLs, so we're use a dash like they want. We also need a dash in some of the + // values themselves, like `in-person`. Luckily `in-person` is the only value that needs one at + // the moment, so the easiest thing is to just make an exception. + // @link https://developers.google.com/search/blog/2014/02/faceted-navigation-best-and-5-of-worst#worst-practice-1:-non-standard-url-encoding-for-parameters,-like-commas-or-brackets,-instead-of-key=value-pairs. + if ( 'in-person' === $parts[1] ) { + $var_values = array( 'in-person' ); + } else { + $var_values = explode( PRETTY_URL_VALUE_DELIMITER, $parts[1] ); + } + + $facets[ $var_key ] = $var_values; + } + + $facets['search'] = get_query_var( 's', '' ); + + } else { + $facets = array( + 'search' => (string) get_query_var( 's', '' ), + 'type' => (array) get_query_var( 'event_type', array() ), + 'format' => (array) get_query_var( 'format_type', array() ), + 'month' => (array) get_query_var( 'month', array() ), + 'country' => (array) get_query_var( 'country', array() ), + ); + } + + $facets = array_filter( $facets ); // Remove empty values. return $facets; } +/** + * Register rewrite rules. + */ +function add_rewrite_rules(): void { + // The regex can't explicitly match each facet because they're all optional, so the `$matches` indices aren't + // predictable. Instead, this just matches all the facets into a single var, and they'll be parsed out of that + // into individual facets later. + // @see set_query_vars_from_pretty_url(). + add_rewrite_rule( FILTERED_URL_PATTERN, 'index.php?pagename=$matches[1]&event_facets=$matches[2]', 'top' ); +} + /** * Add in our custom query vars. */ -function add_query_vars( $query_vars ) { +function add_query_vars( array $query_vars ): array { + // This holds the combined facets. + // @see `add_rewrite_rules()`. + $query_vars[] = 'event_facets'; + + // These are the individual query vars that will be populated from `event_facets`. + // @see `set_query_vars_from_pretty_url()`. $query_vars[] = 'format_type'; $query_vars[] = 'event_type'; $query_vars[] = 'month'; @@ -152,6 +214,83 @@ function add_query_vars( $query_vars ) { return $query_vars; } +/** + * Set the individual query vars from the combined `event_facets` query var. + * + * @see add_rewrite_rules() + * @see add_query_vars() + */ +function set_query_vars_from_pretty_url( WP $wp ): void { + $facets = get_query_var_facets(); + + foreach ( $facets as $key => $value ) { + if ( 'format' === $key ) { + $key = 'format_type'; + } + + if ( 'type' === $key ) { + $key = 'event_type'; + } + + // Set it on `WP` because this is the main request. `WP_Query` will populate itself from this. + $wp->set_query_var( $key, $value ); + } +} + +/** + * Redirect URLs with facet query vars to the pretty version of the URL. + * + * The `query-filter` block sets the query vars as fields, so the browser creates an key-value pair in the + * URL, like `/upcoming-events/?month%5B%5D=02&month%5B%5D=03&event_type%5B%5D=wordcamp&event_type%5B%5D=other&format_type%5B%5D=in-person&country%5B%5D=US`. + * This converts that to a "pretty" URL, like `/upcoming-events/filtered/type/wordcamp-other/format/in-person/month/02-03/country/US/`. + */ +function redirect_to_pretty_query_vars(): void { + global $wp; + + if ( preg_match( '#' . FILTERED_URL_PATTERN . '#', $wp->request ) ) { + return; + } + + if ( is_search() ) { + return; + } + + $facets = get_query_var_facets(); + + if ( empty( $facets ) ) { + return; + } + + // Ensure a consistent order for the facets and their values, so that URLs build from this array are consistent. + // @link https://developers.google.com/search/blog/2014/02/faceted-navigation-best-and-5-of-worst#existing-sites. + $facets = sort_facets( $facets ); + $url = get_permalink() . 'filtered/'; + + foreach ( $facets as $key => $values ) { + $values = implode( PRETTY_URL_VALUE_DELIMITER, (array) $values ); + $url .= trailingslashit( $key . '/' . $values ); + } + + wp_safe_redirect( trailingslashit( $url ) ); + exit; +} + +/** + * Sort the facets and their values alphabetically, to ensure a consistent order. + */ +function sort_facets( array $facets ): array { + ksort( $facets ); + + array_walk( + $facets, + function ( &$facet ) { + sort( $facet ); + } + ); + + return $facets; +} + /** * Add in the other existing filters as hidden inputs in the filter form. * @@ -161,17 +300,17 @@ function add_query_vars( $query_vars ) { * * @param string $key The key for the current filter. */ -function inject_other_filters( $key ) { +function inject_other_filters( string $key ): void { global $wp_query; $query_vars = array( 'event_type', 'format_type', 'month', 'country' ); foreach ( $query_vars as $query_var ) { - if ( ! isset( $wp_query->query[ $query_var ] ) ) { + if ( $key === $query_var ) { continue; } - if ( $key === $query_var ) { + if ( ! get_query_var( $query_var ) ) { continue; } @@ -207,6 +346,7 @@ function add_filters_to_page_title( array $parts ): array { unset( $facets['search'] ); $facets = array_filter( $facets ); // Remove empty. + $facets = sort_facets( $facets ); $extra_terms = array(); foreach ( $facets as $facet => $values ) { @@ -280,17 +420,24 @@ function update_query_total_label( string $label, int $found_posts, WP_Block $bl return $label; } +/** + * Build the `action` attribute for the `query-filters` form. + */ +function build_form_action_url(): string { + return is_search() ? home_url() : get_permalink(); +} + /** * Sets up our Query filter for format_type. * * @return array */ function get_format_type_options( array $options ): array { - global $wp_query; - $selected = isset( $wp_query->query['format_type'] ) ? (array) $wp_query->query['format_type'] : array(); + $facets = get_query_var_facets(); + $selected = $facets['format'] ?? array(); $count = count( $selected ); + $label = __( 'Format', 'wporg' ); - $label = __( 'Format', 'wporg' ); if ( $count > 0 ) { $label = sprintf( /* translators: The dropdown label for filtering, %s is the selected term count. */ @@ -303,7 +450,7 @@ function get_format_type_options( array $options ): array { 'label' => $label, 'title' => __( 'Format', 'wporg' ), 'key' => 'format_type', - 'action' => is_search() ? '' : home_url( '/upcoming-events/' ), + 'action' => build_form_action_url(), 'options' => array( 'online' => 'Online', 'in-person' => 'In Person', @@ -318,11 +465,11 @@ function get_format_type_options( array $options ): array { * @return array */ function get_event_type_options( array $options ): array { - global $wp_query; - $selected = isset( $wp_query->query['event_type'] ) ? (array) $wp_query->query['event_type'] : array(); + $facets = get_query_var_facets(); + $selected = $facets['type'] ?? array(); $count = count( $selected ); + $label = __( 'Type', 'wporg' ); - $label = __( 'Type', 'wporg' ); if ( $count > 0 ) { $label = sprintf( /* translators: The dropdown label for filtering, %s is the selected term count. */ @@ -335,7 +482,7 @@ function get_event_type_options( array $options ): array { 'label' => $label, 'title' => __( 'Type', 'wporg' ), 'key' => 'event_type', - 'action' => is_search() ? '' : home_url( '/upcoming-events/' ), + 'action' => build_form_action_url(), 'options' => array( 'meetup' => 'Meetup', 'wordcamp' => 'WordCamp', @@ -351,11 +498,11 @@ function get_event_type_options( array $options ): array { * @return array */ function get_month_options( array $options ): array { - global $wp_query; - $selected = isset( $wp_query->query['month'] ) ? (array) $wp_query->query['month'] : array(); + $facets = get_query_var_facets(); + $selected = $facets['month'] ?? array(); $count = count( $selected ); + $label = __( 'Month', 'wporg' ); - $label = __( 'Month', 'wporg' ); if ( $count > 0 ) { $label = sprintf( /* translators: The dropdown label for filtering, %s is the selected term count. */ @@ -367,7 +514,7 @@ function get_month_options( array $options ): array { $months = array(); for ( $i = 1; $i <= 12; $i++ ) { - $month = strtotime( "2023-$i-1" ); + $month = strtotime( "2023-$i-1" ); $months[ gmdate( 'm', $month ) ] = gmdate( 'F', $month ); } @@ -375,7 +522,7 @@ function get_month_options( array $options ): array { 'label' => $label, 'title' => __( 'Month', 'wporg' ), 'key' => 'month', - 'action' => is_search() ? '' : home_url( '/upcoming-events/' ), + 'action' => build_form_action_url(), 'options' => $months, 'selected' => $selected, ); @@ -387,16 +534,18 @@ function get_month_options( array $options ): array { * @return array */ function get_country_options( array $options ): array { - global $wp_query; - $selected = isset( $wp_query->query['country'] ) ? (array) $wp_query->query['country'] : array(); - $count = count( $selected ); - + $facets = get_query_var_facets(); + $selected = $facets['country'] ?? array(); + $count = count( $selected ); $countries = wcorg_get_countries(); + $label = __( 'Country', 'wporg' ); - // Re-index to match the format expected by the query-filters block. - $countries = array_combine( array_keys( $countries ), array_column( $countries, 'name' ) ); + // Re-index to match the format expected by the query-filters block. e.g., `DE` => `Germany`. + $countries = array_combine( + array_keys( $countries ), + array_column( $countries, 'name' ) + ); - $label = __( 'Country', 'wporg' ); if ( $count > 0 ) { $label = sprintf( /* translators: The dropdown label for filtering, %s is the selected term count. */ @@ -409,7 +558,7 @@ function get_country_options( array $options ): array { 'label' => $label, 'title' => __( 'Country', 'wporg' ), 'key' => 'country', - 'action' => is_search() ? '' : home_url( '/upcoming-events/' ), + 'action' => build_form_action_url(), 'options' => $countries, 'selected' => $selected, );