Skip to content

Commit

Permalink
Events: Redirect query-filter URLs to a "pretty" version
Browse files Browse the repository at this point in the history
  • Loading branch information
iandunn committed Jan 26, 2024
1 parent 4c9cbd2 commit 22c8ea7
Showing 1 changed file with 182 additions and 33 deletions.
215 changes: 182 additions & 33 deletions public_html/wp-content/themes/wporg-events-2023/inc/events-query.php
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
<?php

namespace WordPressdotorg\Events_2023;
use WP_Query, WP_Post, WP_Block;
use WP, WP_Query, WP_Post, WP_Block;
use WordPressdotorg\MU_Plugins\Google_Map;

defined( 'WPINC' ) || die();

// Match URLs like `{pagename}/filtered/{facets}`, e.g., `/upcoming-events/filtered/type/meetup/format/in-person/month/05/country/US/`.
// This intentionally doesn't have the starting/ending delimiters and flags, so that it can be used with
// `add_rewrite_rule()`.
const FILTERED_URL_PATTERN = '([\w-]+)/filtered/(.+)';
const PRETTY_URL_VALUE_DELIMITER = '-';

// Misc.
add_action( 'init', __NAMESPACE__ . '\register_post_types' );
add_filter( 'posts_pre_query', __NAMESPACE__ . '\inject_events_into_query', 10, 2 );

// Query filters.
add_action( 'init', __NAMESPACE__ . '\add_rewrite_rules' );
add_filter( 'query_vars', __NAMESPACE__ . '\add_query_vars' );
add_action( 'parse_request', __NAMESPACE__ . '\set_query_vars_from_pretty_url' );
add_action( 'wp', __NAMESPACE__ . '\redirect_to_pretty_query_vars' );
add_action( 'wporg_query_filter_in_form', __NAMESPACE__ . '\inject_other_filters' );
add_filter( 'document_title_parts', __NAMESPACE__ . '\add_filters_to_page_title' );
add_filter( 'wporg_query_total_label', __NAMESPACE__ . '\update_query_total_label', 10, 3 );
Expand Down Expand Up @@ -129,21 +138,74 @@ function inject_events_into_query( $posts, WP_Query $query ) {
* This converts them to the keys that the Google Map block uses. The map block will sanitize/validate them.
*/
function get_query_var_facets(): array {
$facets = array(
'search' => 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';
Expand All @@ -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 <input> 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.
*
Expand All @@ -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;
}

Expand Down Expand Up @@ -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 ) {
Expand Down Expand Up @@ -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. */
Expand All @@ -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',
Expand All @@ -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. */
Expand All @@ -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',
Expand All @@ -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. */
Expand All @@ -367,15 +514,15 @@ 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 );
}

return array(
'label' => $label,
'title' => __( 'Month', 'wporg' ),
'key' => 'month',
'action' => is_search() ? '' : home_url( '/upcoming-events/' ),
'action' => build_form_action_url(),
'options' => $months,
'selected' => $selected,
);
Expand All @@ -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. */
Expand All @@ -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,
);
Expand Down

0 comments on commit 22c8ea7

Please sign in to comment.