Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Events: Make filter URLs pretty #1221

Merged
merged 1 commit into from
Jan 26, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 190 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 );
@@ -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';
@@ -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.
*
@@ -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,32 @@ 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 {
if ( is_search() ) {
$url = home_url();
} elseif ( is_front_page() ) {
$url = home_url( 'upcoming-events' );
} else {
$url = get_permalink();
}

return $url;
}

/**
* 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 +458,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 +473,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 +490,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 +506,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,15 +522,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,
);
@@ -387,16 +542,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 +566,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,
);