Improve SportsPress Open Graph images

This commit is contained in:
2026-04-27 12:43:26 -05:00
parent 1df307dfbe
commit 2afe98bc99
6 changed files with 1868 additions and 344 deletions

View File

@@ -0,0 +1,93 @@
Copyright © 2010 by Dharma Type.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,51 +1,156 @@
<?php
/*
Plugin Name: Custom Open Graph Tags with SportsPress Integration
Description: Adds custom Open Graph tags to posts based on their type, specifically handling sp_event post types with methods from the SportsPress SP_Event class.
Version: 1.0
Author: Your Name
*/
/**
* Open Graph tags for SportsPress events.
*
* @package Tonys_Sportspress_Enhancements
*/
add_action('wp_head', 'custom_open_graph_tags_with_sportspress_integration');
add_action( 'wp_head', 'custom_open_graph_tags_with_sportspress_integration' );
function asc_sp_event_matchup_image_url( $post ) {
if ( is_numeric( $post ) ) {
$post = get_post( $post );
}
/**
* Build the dynamic matchup image URL for an event.
*
* @param int|WP_Post $post Event post or ID.
* @param string $variant Image variant.
* @return string
*/
function asc_sp_event_matchup_image_url( $post, $variant = 'wide' ) {
$post = asc_sp_event_get_post( $post );
if ( ! $post || 'sp_event' !== $post->post_type ) {
if ( ! $post ) {
return '';
}
return get_site_url() . '/head-to-head?post=' . $post->ID;
$args = array(
'post' => $post->ID,
);
if ( 'wide' !== $variant ) {
$args['variant'] = $variant;
}
if ( function_exists( 'asc_sp_event_image_url_version' ) ) {
$args['v'] = asc_sp_event_image_url_version();
}
return add_query_arg( $args, home_url( '/head-to-head' ) );
}
function asc_generate_sp_event_title( $post ) {
// See https://github.com/ThemeBoy/SportsPress/blob/770fa8c6654d7d6648791e877709c2428677635b/includes/admin/post-types/class-sp-admin-cpt-event.php#L99C40-L99C55
if ( is_numeric( $post ) ) {
$post = get_post( $post );
}
if ( ! $post || $post->post_type !== 'sp_event' ) {
return get_the_title();
/**
* Build Open Graph image descriptors for an event.
*
* @param WP_Post $post Event post.
* @return array<int,array<string,string>>
*/
function asc_sp_event_open_graph_images( WP_Post $post ) {
return array(
array(
'url' => asc_sp_event_matchup_image_url( $post, 'wide' ),
'width' => '1200',
'height' => '628',
),
array(
'url' => asc_sp_event_matchup_image_url( $post, 'square' ),
'width' => '1200',
'height' => '1200',
),
);
}
/**
* Normalize an event post argument.
*
* @param int|WP_Post|null $post Post object or ID.
* @return WP_Post|null
*/
function asc_sp_event_get_post( $post = null ) {
if ( null === $post ) {
$post = get_post();
} elseif ( is_numeric( $post ) ) {
$post = get_post( absint( $post ) );
}
$teams = get_post_meta( $post->ID, 'sp_team', false );
$teams = array_filter( $teams );
if ( ! $post instanceof WP_Post || 'sp_event' !== $post->post_type ) {
return null;
}
return $post;
}
/**
* Get event team IDs in SportsPress display order.
*
* @param int|WP_Post $post Event post or ID.
* @return int[]
*/
function asc_sp_event_team_ids( $post ) {
$post = asc_sp_event_get_post( $post );
if ( ! $post ) {
return array();
}
$teams = get_post_meta( $post->ID, 'sp_team', false );
$team_ids = array();
$team_names = array();
foreach ( $teams as $team ) {
while ( is_array( $team ) ) {
$team = array_shift( array_filter( $team ) );
}
$team = absint( $team );
if ( $team > 0 ) {
$team_names[] = sp_team_short_name( $team );
$team_ids[] = $team;
}
}
$team_names = array_unique( $team_names );
$team_ids = array_values( array_unique( $team_ids ) );
if ( get_option( 'sportspress_event_reverse_teams', 'no' ) === 'yes' ) {
$team_names = array_reverse( $team_names );
if ( 'yes' === get_option( 'sportspress_event_reverse_teams', 'no' ) ) {
$team_ids = array_reverse( $team_ids );
}
return $team_ids;
}
/**
* Get a safe team short name with fallbacks for test and partial SportsPress environments.
*
* @param int $team_id Team post ID.
* @return string
*/
function asc_sp_team_short_name( $team_id ) {
$name = '';
if ( function_exists( 'sp_team_short_name' ) ) {
$name = (string) sp_team_short_name( $team_id );
}
if ( '' === trim( $name ) ) {
$name = get_the_title( $team_id );
}
return '' !== trim( $name ) ? $name : __( 'Team TBD', 'tonys-sportspress-enhancements' );
}
/**
* Generate a matchup title from event teams.
*
* @param int|WP_Post $post Event post or ID.
* @return string
*/
function asc_generate_sp_event_title( $post ) {
$post = asc_sp_event_get_post( $post );
if ( ! $post ) {
return get_the_title();
}
$team_names = array_map( 'asc_sp_team_short_name', asc_sp_event_team_ids( $post ) );
$team_names = array_values( array_filter( array_unique( $team_names ) ) );
if ( empty( $team_names ) ) {
return get_the_title( $post );
}
$delimiter = ' ' . get_option( 'sportspress_event_teams_delimiter', 'vs' ) . ' ';
@@ -53,147 +158,343 @@ function asc_generate_sp_event_title( $post ) {
return implode( $delimiter, $team_names );
}
/**
* Generate compact event date text.
*
* @param int|WP_Post $post Event post or ID.
* @param bool $withTime Include time.
* @return string
*/
function asc_generate_short_date( $post, $withTime = true ) {
$formatted_date = get_the_date('D n/j/y', $post);
$post = asc_sp_event_get_post( $post );
if (!$withTime){
return $formatted_date;
}
if ( ! $post ) {
return '';
}
if ( get_the_date('i', $post) == "00") {
$formatted_time = get_the_date('gA', $post);
} else {
$formatted_time = get_the_date('g:iA', $post);
}
return $formatted_date . " " . $formatted_time ;
$formatted_date = get_the_date( 'D n/j/y', $post );
if ( ! $withTime ) {
return $formatted_date;
}
$formatted_time = '00' === get_the_date( 'i', $post ) ? get_the_date( 'gA', $post ) : get_the_date( 'g:iA', $post );
return trim( $formatted_date . ' ' . $formatted_time );
}
/**
* Get venue name for an event.
*
* @param WP_Post $post Event post.
* @return string
*/
function asc_sp_event_venue_name( WP_Post $post ) {
$venue_terms = get_the_terms( $post->ID, 'sp_venue' );
if ( is_wp_error( $venue_terms ) || empty( $venue_terms ) ) {
return __( 'Venue TBD', 'tonys-sportspress-enhancements' );
}
return $venue_terms[0]->name;
}
/**
* Normalize event body content for meta descriptions.
*
* @param WP_Post $post Event post.
* @return string
*/
function asc_sp_event_body_excerpt( WP_Post $post ) {
$content = strip_shortcodes( $post->post_content );
$content = wp_strip_all_tags( $content, true );
$content = html_entity_decode( $content, ENT_QUOTES, get_bloginfo( 'charset' ) );
$content = preg_replace( '/\s+/', ' ', $content );
$content = trim( (string) $content );
if ( '' === $content ) {
return '';
}
return wp_trim_words( $content, 35, '' );
}
/**
* Safely instantiate a SportsPress event object.
*
* @param WP_Post $post Event post.
* @return object|null
*/
function asc_sp_event_object( WP_Post $post ) {
if ( ! class_exists( 'SP_Event' ) ) {
return null;
}
try {
return new SP_Event( $post->ID );
} catch ( Throwable $e ) {
return null;
}
}
/**
* Get the SportsPress event status with fallbacks.
*
* @param WP_Post $post Event post.
* @param object|null $event SportsPress event object.
* @return string
*/
function asc_sp_event_status( WP_Post $post, $event = null ) {
if ( $event && is_callable( array( $event, 'status' ) ) ) {
try {
$status = (string) $event->status();
if ( '' !== $status ) {
return $status;
}
} catch ( Throwable $e ) {
return '';
}
}
return 'future' === $post->post_status ? 'future' : '';
}
/**
* Get SportsPress result rows safely.
*
* @param object|null $event SportsPress event object.
* @return array
*/
function asc_sp_event_results( $event = null ) {
if ( ! $event || ! is_callable( array( $event, 'results' ) ) ) {
return array();
}
try {
$results = $event->results();
return is_array( $results ) ? $results : array();
} catch ( Throwable $e ) {
return array();
}
}
/**
* Convert a result row into outcome labels.
*
* @param array $result Result row.
* @return array
*/
function asc_sp_event_result_outcomes( array $result ) {
$result_outcome = isset( $result['outcome'] ) ? $result['outcome'] : null;
if ( ! is_array( $result_outcome ) ) {
return array();
}
$outcomes = array();
foreach ( $result_outcome as $outcome ) {
$the_outcome = get_page_by_path( $outcome, OBJECT, 'sp_outcome' );
if ( $the_outcome instanceof WP_Post ) {
$outcome_abbreviation = get_post_meta( $the_outcome->ID, 'sp_abbreviation', true );
if ( ! $outcome_abbreviation ) {
$outcome_abbreviation = function_exists( 'sp_substr' ) ? sp_substr( $the_outcome->post_title, 0, 1 ) : substr( $the_outcome->post_title, 0, 1 );
}
$outcomes[] = array(
'title' => $the_outcome->post_title,
'abbreviation' => $outcome_abbreviation,
);
}
}
return $outcomes;
}
/**
* Build a result title/description from SportsPress result data.
*
* @param WP_Post $post Event post.
* @param array $results Event results data.
* @param string $description Existing description.
* @return array{title:string,description:string}|null
*/
function asc_sp_event_result_meta( WP_Post $post, array $results, $description ) {
unset( $results[0] );
$results = array_filter( $results );
if ( count( $results ) < 2 ) {
return null;
}
if ( 'yes' === get_option( 'sportspress_event_reverse_teams', 'no' ) ) {
$results = array_reverse( $results, true );
}
$teams_result_array = array();
foreach ( $results as $team_id => $result ) {
if ( ! is_array( $result ) ) {
continue;
}
$outcomes = asc_sp_event_result_outcomes( $result );
$first_outcome = ! empty( $outcomes ) ? $outcomes[0] : array( 'title' => __( 'Result', 'tonys-sportspress-enhancements' ), 'abbreviation' => '' );
$team_name = asc_sp_team_short_name( $team_id );
$team_score = isset( $result['r'] ) && '' !== $result['r'] ? $result['r'] : null;
$team_score = null !== $team_score ? (string) $team_score : '';
if ( '' === $team_score ) {
continue;
}
$teams_result_array[] = array(
'score' => $team_score,
'outcome' => $first_outcome['title'],
'outcome_abbreviation' => $first_outcome['abbreviation'],
'team_name' => $team_name,
);
}
if ( count( $teams_result_array ) < 2 ) {
return null;
}
$special_result_suffix_abbreviation = '';
$special_result_suffix = '';
foreach ( $teams_result_array as $team ) {
$outcome_abbreviation = strtoupper( (string) $team['outcome_abbreviation'] );
if ( 'TF-W' === $outcome_abbreviation ) {
$special_result_suffix_abbreviation = 'TF-W';
$special_result_suffix = 'Technical Forfeit Win';
break;
}
if ( 'TF-L' === $outcome_abbreviation ) {
$special_result_suffix_abbreviation = 'TF';
$special_result_suffix = 'Technical Forfeit';
break;
}
if ( 'F-W' === $outcome_abbreviation || 'F-L' === $outcome_abbreviation ) {
$special_result_suffix_abbreviation = 'Forfeit';
$special_result_suffix = 'Forfeit';
break;
}
}
$publish_date = asc_generate_short_date( $post, false );
$title = sprintf(
'%1$s %2$s-%3$s %4$s%s',
$teams_result_array[0]['team_name'],
$teams_result_array[0]['score'],
$teams_result_array[1]['score'],
$teams_result_array[1]['team_name'],
$publish_date ? ' - ' . $publish_date : ''
);
if ( $special_result_suffix ) {
$title .= " ({$special_result_suffix_abbreviation})";
}
$description .= sprintf(
' %1$s (%2$s), %3$s (%4$s).',
$teams_result_array[0]['team_name'],
$teams_result_array[0]['outcome'],
$teams_result_array[1]['team_name'],
$teams_result_array[1]['outcome']
);
return array(
'title' => $title,
'description' => $description,
);
}
/**
* Build all Open Graph values for an event.
*
* @param int|WP_Post $post Event post or ID.
* @return array<string,mixed>
*/
function asc_sp_event_open_graph_data( $post ) {
$post = asc_sp_event_get_post( $post );
if ( ! $post ) {
return array();
}
$event = asc_sp_event_object( $post );
$venue_name = asc_sp_event_venue_name( $post );
$publish_date_and_time = get_the_date( 'F j, Y g:i A', $post );
$description = trim( "{$publish_date_and_time} at {$venue_name}." );
$title = asc_generate_sp_event_title( $post );
$sp_status = get_post_meta( $post->ID, 'sp_status', true );
$status = asc_sp_event_status( $post, $event );
if ( in_array( $sp_status, array( 'postponed', 'cancelled', 'tbd' ), true ) ) {
$status_label = strtoupper( $sp_status );
$description = "{$status_label} - {$description}";
$title = trim( "{$status_label} - {$title} - " . asc_generate_short_date( $post ) . " - {$venue_name}", ' -' );
} elseif ( 'future' === $status ) {
$title = trim( $title . ' - ' . asc_generate_short_date( $post ) . " - {$venue_name}", ' -' );
} elseif ( 'results' === $status ) {
$result_meta = asc_sp_event_result_meta( $post, asc_sp_event_results( $event ), $description );
if ( $result_meta ) {
$title = $result_meta['title'];
$description = $result_meta['description'];
}
}
$body_excerpt = asc_sp_event_body_excerpt( $post );
if ( '' !== $body_excerpt ) {
$description = trim( $description . ' ' . $body_excerpt );
}
return array(
'type' => 'article',
'images' => asc_sp_event_open_graph_images( $post ),
'image' => asc_sp_event_matchup_image_url( $post, 'wide' ),
'image_width' => '1200',
'image_height' => '628',
'title' => $title,
'description' => $description,
'url' => get_permalink( $post ),
);
}
/**
* Echo Open Graph meta tags for single SportsPress events.
*/
function custom_open_graph_tags_with_sportspress_integration() {
if (is_single()) {
global $post;
if ($post->post_type === 'sp_event') {
// Instantiate SP_Event object
$event = new SP_Event($post->ID);
if ( ! is_single() ) {
return;
}
// Fetch details using SP_Event methods
$publish_date = get_the_date('F j, Y', $post);
$venue_terms = get_the_terms($post->ID, 'sp_venue');
$venue_name = $venue_terms ? $venue_terms[0]->name : 'Venue TBD';
$results = $event->results(); // Using SP_Event method
$title = asc_generate_sp_event_title($post);
$sp_status = get_post_meta( $post->ID, 'sp_status', true );
$status = $event->status(); // Using SP_Event method
$publish_date_and_time = get_the_date('F j, Y g:i A', $post);
$description = "{$publish_date_and_time} at {$venue_name}.";
if ( 'postponed' == $sp_status || 'cancelled' == $sp_status || 'tbd' == $sp_status) {
$description = strtoupper($sp_status) . "" . $description;
$title = strtoupper($sp_status) . "" . $title . "" . asc_generate_short_date($post) . "" . $venue_name;
}
$post = asc_sp_event_get_post();
if ( 'future' == $status ) {
$description = $description;
$title = $title . "" . asc_generate_short_date($post) . "" . $venue_name;
}
if ( ! $post ) {
return;
}
if ( 'results' == $status ) { // checks if there is a final score
// Get event result data
$data = $event->results();
$meta = asc_sp_event_open_graph_data( $post );
// The first row should be column labels
$labels = $data[0];
if ( empty( $meta ) ) {
return;
}
// Remove the first row to leave us with the actual data
unset( $data[0] );
$data = array_filter( $data );
if ( empty( $data ) ) {
return false;
}
// Initialize
$i = 0;
$result_string = '';
$title_string = '';
// Reverse teams order if the option "Events > Teams > Order > Reverse order" is enabled.
$reverse_teams = get_option( 'sportspress_event_reverse_teams', 'no' ) === 'yes' ? true : false;
if ( $reverse_teams ) {
$data = array_reverse( $data, true );
}
$teams_result_array = [];
foreach ( $data as $team_id => $result ) :
$outcomes = array();
$result_outcome = sp_array_value( $result, 'outcome' );
if ( ! is_array( $result_outcome ) ) :
$outcomes = array( '&mdash;' );
else :
foreach ( $result_outcome as $outcome ) :
$the_outcome = get_page_by_path( $outcome, OBJECT, 'sp_outcome' );
if ( is_object( $the_outcome ) ) :
$outcomes[] = $the_outcome->post_title;
endif;
endforeach;
endif;
unset( $result['outcome'] );
$team_name = sp_team_short_name( $team_id );
$team_abbreviation = sp_team_abbreviation( $team_id );
$outcome_abbreviation = get_post_meta( $the_outcome->ID, 'sp_abbreviation', true );
if ( ! $outcome_abbreviation ) {
$outcome_abbreviation = sp_substr( $the_outcome->post_title, 0, 1 );
}
array_push($teams_result_array, [
"result" => $result,
"outcome" => $the_outcome->post_title,
"outcome_abbreviation" => $outcome_abbreviation,
"team_name" => $team_name,
"team_abbreviation" => $team_abbreviation
]
);
$i++;
endforeach;
$publish_date = asc_generate_short_date($post, false);
$special_result_suffix_abbreviation = '';
$special_result_suffix= '';
foreach ( $teams_result_array as $team ) {
$outcome_abbreviation = strtoupper( $team['outcome_abbreviation'] ); // Normalize case
if ( $outcome_abbreviation === 'TF-W' ) {
$special_result_suffix_abbreviation = 'TF-W';
$special_result_suffix = 'Technical Forfeit Win';
break;
} elseif ( $outcome_abbreviation === 'TF-L' ) {
$special_result_suffix_abbreviation = 'TF';
$special_result_suffix = 'Technical Forfeit';
break;
} elseif ( $outcome_abbreviation === 'F-W' || $outcome_abbreviation === 'F-L' ) {
$special_result_suffix_abbreviation = 'Forfeit';
$special_result_suffix = 'Forfeit';
break;
}
}
$title = "{$teams_result_array[0]['team_name']} {$teams_result_array[0]['result']['r']}-{$teams_result_array[1]['result']['r']} {$teams_result_array[1]['team_name']}{$publish_date}" . ($special_result_suffix ? "({$special_result_suffix_abbreviation})" : "");
$description .= " " . "{$teams_result_array[0]['team_name']} ({$teams_result_array[0]['outcome']}), {$teams_result_array[1]['team_name']} ({$teams_result_array[1]['outcome']})." ;
}
$description .= " " . $post->post_content;
$image = asc_sp_event_matchup_image_url( $post );
echo '<meta property="og:type" content="article" />' . "\n";
echo '<meta property="og:image" content="'. $image . '" />' . "\n";
echo '<meta property="og:title" content="' . $title . '" />' . "\n";
echo '<meta property="og:description" content="' . $description . '" />' . "\n";
echo '<meta property="og:url" content="' . get_permalink() . '" />' . "\n";
}
}
echo '<meta property="og:type" content="' . esc_attr( $meta['type'] ) . '" />' . "\n";
foreach ( $meta['images'] as $image ) {
echo '<meta property="og:image" content="' . esc_url( $image['url'] ) . '" />' . "\n";
echo '<meta property="og:image:width" content="' . esc_attr( $image['width'] ) . '" />' . "\n";
echo '<meta property="og:image:height" content="' . esc_attr( $image['height'] ) . '" />' . "\n";
}
echo '<meta property="og:title" content="' . esc_attr( $meta['title'] ) . '" />' . "\n";
echo '<meta property="og:description" content="' . esc_attr( $meta['description'] ) . '" />' . "\n";
echo '<meta property="og:url" content="' . esc_url( $meta['url'] ) . '" />' . "\n";
}
?>

View File

@@ -0,0 +1,222 @@
<?php
/**
* Tests for the SportsPress event image generator.
*
* @package Tonys_Sportspress_Enhancements
*/
/**
* Featured image generator tests.
*/
class Test_Featured_Image_Generator extends WP_UnitTestCase {
/**
* Temp files created during a test.
*
* @var string[]
*/
private $temp_files = array();
/**
* Clean up temp files.
*/
public function tear_down(): void {
foreach ( $this->temp_files as $file ) {
if ( file_exists( $file ) ) {
unlink( $file );
}
}
$this->temp_files = array();
parent::tear_down();
}
/**
* Create a post.
*
* @param string $type Post type.
* @param string $title Title.
* @return int
*/
private function create_post_of_type( $type, $title ) {
return self::factory()->post->create(
array(
'post_type' => $type,
'post_title' => $title,
'post_status' => 'publish',
)
);
}
/**
* Create a small raster fixture.
*
* @param string $extension File extension.
* @return string
*/
private function create_raster_fixture( $extension ) {
$image = imagecreatetruecolor( 24, 24 );
$red = imagecolorallocate( $image, 200, 0, 0 );
imagefilledrectangle( $image, 0, 0, 23, 23, $red );
$file = tempnam( sys_get_temp_dir(), 'sp-img-' );
$path = $file . '.' . $extension;
rename( $file, $path );
switch ( $extension ) {
case 'jpg':
imagejpeg( $image, $path );
break;
case 'gif':
imagegif( $image, $path );
break;
case 'webp':
imagewebp( $image, $path );
break;
case 'png':
default:
imagepng( $image, $path );
break;
}
asc_sp_event_image_destroy( $image );
$this->temp_files[] = $path;
return $path;
}
/**
* Invalid IDs and non-event posts produce request errors.
*/
public function test_invalid_and_non_event_requests_prepare_404_errors() {
$this->assertWPError( asc_sp_event_prepare_image_request( 999999 ) );
$post_id = $this->create_post_of_type( 'post', 'Regular Post' );
$error = asc_sp_event_prepare_image_request( $post_id );
$this->assertWPError( $error );
$this->assertSame( 'invalid_event', $error->get_error_code() );
}
/**
* Missing team logo paths fall back to generated text and valid dimensions.
*/
public function test_missing_logo_path_generates_png_with_expected_dimensions() {
$image_data = generate_bisected_image( '#123456', '#abcdef', '/missing-left.png', '/missing-right.png', 'Hawks', 'Electrons' );
$image = imagecreatefromstring( $image_data );
$this->assertNotFalse( $image );
$this->assertSame( 1200, imagesx( $image ) );
$this->assertSame( 628, imagesy( $image ) );
asc_sp_event_image_destroy( $image );
}
/**
* Square image variant generates square PNG dimensions.
*/
public function test_square_variant_generates_expected_dimensions() {
$dimensions = asc_sp_event_image_variant_dimensions( 'square' );
$image_data = generate_bisected_image( '#123456', '#abcdef', '/missing-left.png', '/missing-right.png', 'Hawks', 'Electrons', $dimensions['width'], $dimensions['height'] );
$image = imagecreatefromstring( $image_data );
$this->assertNotFalse( $image );
$this->assertSame( 1200, imagesx( $image ) );
$this->assertSame( 1200, imagesy( $image ) );
asc_sp_event_image_destroy( $image );
}
/**
* Raster loader supports common GD-backed formats.
*/
public function test_raster_loader_supports_common_formats_when_available() {
$formats = array(
'png' => 'imagecreatefrompng',
'jpg' => 'imagecreatefromjpeg',
'gif' => 'imagecreatefromgif',
);
if ( function_exists( 'imagewebp' ) && function_exists( 'imagecreatefromwebp' ) ) {
$formats['webp'] = 'imagecreatefromwebp';
}
foreach ( $formats as $extension => $function ) {
if ( ! function_exists( $function ) ) {
continue;
}
$path = $this->create_raster_fixture( $extension );
$image = asc_sp_event_image_create_from_file( $path );
$this->assertNotFalse( $image, "Failed loading {$extension}" );
$this->assertSame( 24, imagesx( $image ) );
$this->assertSame( 24, imagesy( $image ) );
asc_sp_event_image_destroy( $image );
}
}
/**
* Bundled sporty font is available for fallback text.
*/
public function test_bundled_bebas_neue_font_is_available() {
$this->assertFileExists( asc_sp_event_image_font_path() );
$this->assertIsReadable( asc_sp_event_image_font_path() );
}
/**
* Prepared event request includes fallback text for missing logos.
*/
public function test_prepare_image_request_uses_team_short_name_fallbacks() {
$team1 = $this->create_post_of_type( 'sp_team', 'Hawks' );
$team2 = $this->create_post_of_type( 'sp_team', 'Electrons' );
$event = $this->create_post_of_type( 'sp_event', 'Hawks vs Electrons' );
add_post_meta( $event, 'sp_team', $team1 );
add_post_meta( $event, 'sp_team', $team2 );
$request = asc_sp_event_prepare_image_request( $event );
$this->assertIsArray( $request );
$this->assertSame( 'Hawks', $request['team1_fallback'] );
$this->assertSame( 'Electrons', $request['team2_fallback'] );
$this->assertSame( '', $request['team1_logo'] );
$this->assertSame( '', $request['team2_logo'] );
}
/**
* Invalid colors are safely normalized.
*/
public function test_invalid_colors_fall_back_to_configured_defaults() {
$this->assertSame( '#4B5563', asc_sp_event_image_color( 'not-a-color' ) );
$this->assertSame( '#6B7280', asc_sp_event_image_color( 'not-a-color', '#6B7280' ) );
$this->assertSame( '#112233', asc_sp_event_image_color( '#112233' ) );
}
/**
* Image cache keys include the generator version and style hash.
*/
public function test_prepare_image_request_uses_versioned_style_cache_key() {
$team1 = $this->create_post_of_type( 'sp_team', 'Hawks' );
$team2 = $this->create_post_of_type( 'sp_team', 'Electrons' );
$event = $this->create_post_of_type( 'sp_event', 'Hawks vs Electrons' );
add_post_meta( $event, 'sp_team', $team1 );
add_post_meta( $event, 'sp_team', $team2 );
$request = asc_sp_event_prepare_image_request( $event );
$this->assertStringStartsWith( 'team_image_v' . ASC_SP_EVENT_IMAGE_CACHE_VERSION . '_' . asc_sp_event_image_cache_style_hash(), $request['cache_key'] );
$this->assertSame( 'wide', $request['variant'] );
$this->assertSame( 1200, $request['width'] );
$this->assertSame( 628, $request['height'] );
$square_request = asc_sp_event_prepare_image_request( $event, 'square' );
$this->assertStringContainsString( '_square_', $square_request['cache_key'] );
$this->assertSame( 'square', $square_request['variant'] );
$this->assertSame( 1200, $square_request['width'] );
$this->assertSame( 1200, $square_request['height'] );
}
}

View File

@@ -0,0 +1,271 @@
<?php
/**
* Tests for SportsPress event Open Graph output.
*
* @package Tonys_Sportspress_Enhancements
*/
if ( ! class_exists( 'SP_Event' ) ) {
/**
* Minimal SportsPress event test double.
*/
class SP_Event {
/**
* Event post ID.
*
* @var int
*/
private $id;
/**
* Status values by event ID.
*
* @var array<int,string>
*/
public static $statuses = array();
/**
* Result values by event ID.
*
* @var array<int,array>
*/
public static $results = array();
/**
* Constructor.
*
* @param int $id Event post ID.
*/
public function __construct( $id ) {
$this->id = absint( $id );
}
/**
* Get event status.
*
* @return string
*/
public function status() {
return self::$statuses[ $this->id ] ?? '';
}
/**
* Get event results.
*
* @return array
*/
public function results() {
return self::$results[ $this->id ] ?? array();
}
}
}
/**
* Open Graph tests.
*/
class Test_Open_Graph_Tags extends WP_UnitTestCase {
/**
* Reset mock SportsPress state.
*/
public function set_up(): void {
parent::set_up();
if ( property_exists( 'SP_Event', 'statuses' ) ) {
SP_Event::$statuses = array();
SP_Event::$results = array();
}
update_option( 'sportspress_event_reverse_teams', 'no' );
update_option( 'sportspress_event_teams_delimiter', 'vs' );
}
/**
* Create a team.
*
* @param string $name Team name.
* @return int
*/
private function create_team( $name ) {
return self::factory()->post->create(
array(
'post_type' => 'sp_team',
'post_title' => $name,
)
);
}
/**
* Create an event.
*
* @param array $args Post args.
* @return int
*/
private function create_event( array $args = array() ) {
return self::factory()->post->create(
wp_parse_args(
$args,
array(
'post_type' => 'sp_event',
'post_title' => 'Test Event',
'post_status' => 'future',
'post_date' => '2026-05-02 13:00:00',
'post_content' => 'First pitch at one.',
)
)
);
}
/**
* Future event emits complete Open Graph data.
*/
public function test_future_event_emits_core_open_graph_values() {
$home = $this->create_team( 'Hawks' );
$away = $this->create_team( 'Electrons' );
$event = $this->create_event();
add_post_meta( $event, 'sp_team', $home );
add_post_meta( $event, 'sp_team', $away );
if ( property_exists( 'SP_Event', 'statuses' ) ) {
SP_Event::$statuses[ $event ] = 'future';
}
$meta = asc_sp_event_open_graph_data( $event );
$this->assertSame( 'article', $meta['type'] );
$this->assertStringContainsString( 'Hawks vs Electrons', $meta['title'] );
$this->assertStringContainsString( 'First pitch at one.', $meta['description'] );
$this->assertCount( 2, $meta['images'] );
$this->assertSame( '1200', $meta['images'][0]['width'] );
$this->assertSame( '628', $meta['images'][0]['height'] );
$this->assertSame( '1200', $meta['images'][1]['width'] );
$this->assertSame( '1200', $meta['images'][1]['height'] );
$this->assertSame( '1200', $meta['image_width'] );
$this->assertSame( '628', $meta['image_height'] );
$this->assertStringContainsString( '/head-to-head?post=' . $event, $meta['image'] );
$this->assertStringContainsString( 'variant=square', $meta['images'][1]['url'] );
$this->assertNotEmpty( $meta['url'] );
}
/**
* Postponed, cancelled, and TBD labels appear in title and description.
*
* @dataProvider status_provider
*
* @param string $status Status slug.
*/
public function test_schedule_status_appears_in_title_and_description( $status ) {
$home = $this->create_team( 'Hawks' );
$away = $this->create_team( 'Electrons' );
$event = $this->create_event();
add_post_meta( $event, 'sp_team', $home );
add_post_meta( $event, 'sp_team', $away );
update_post_meta( $event, 'sp_status', $status );
$meta = asc_sp_event_open_graph_data( $event );
$label = strtoupper( $status );
$this->assertStringStartsWith( $label, $meta['title'] );
$this->assertStringStartsWith( $label, $meta['description'] );
}
/**
* Status provider.
*
* @return array
*/
public function status_provider() {
return array(
array( 'postponed' ),
array( 'cancelled' ),
array( 'tbd' ),
);
}
/**
* Result events with scores emit score titles.
*/
public function test_result_event_with_scores_emits_score_title() {
$home = $this->create_team( 'Hawks' );
$away = $this->create_team( 'Electrons' );
$event = $this->create_event( array( 'post_status' => 'publish' ) );
add_post_meta( $event, 'sp_team', $home );
add_post_meta( $event, 'sp_team', $away );
if ( property_exists( 'SP_Event', 'statuses' ) ) {
SP_Event::$statuses[ $event ] = 'results';
SP_Event::$results[ $event ] = array(
0 => array( 'r' => 'R' ),
$home => array( 'r' => '7' ),
$away => array( 'r' => '4' ),
);
}
$meta = asc_sp_event_open_graph_data( $event );
$this->assertStringContainsString( 'Hawks 7-4 Electrons', $meta['title'] );
}
/**
* Missing teams/results/outcomes still produce valid data.
*/
public function test_missing_sportspress_data_does_not_break_meta_generation() {
$event = $this->create_event(
array(
'post_title' => 'Sparse Event',
'post_content' => '',
)
);
if ( property_exists( 'SP_Event', 'statuses' ) ) {
SP_Event::$statuses[ $event ] = 'results';
SP_Event::$results[ $event ] = array();
}
$meta = asc_sp_event_open_graph_data( $event );
$this->assertSame( 'Sparse Event', $meta['title'] );
$this->assertNotEmpty( $meta['description'] );
$this->assertSame( '1200', $meta['image_width'] );
}
/**
* HTML-heavy post content is stripped and escaped in rendered tags.
*/
public function test_description_strips_html_and_rendered_tags_are_escaped() {
$home = $this->create_team( 'Hawks "A"' );
$away = $this->create_team( 'Electrons <B>' );
$event = $this->create_event(
array(
'post_content' => '<script>alert("x")</script><p>Bring <strong>bats</strong> & gloves.</p>',
)
);
add_post_meta( $event, 'sp_team', $home );
add_post_meta( $event, 'sp_team', $away );
$meta = asc_sp_event_open_graph_data( $event );
$this->assertStringNotContainsString( '<script', $meta['description'] );
$this->assertStringContainsString( 'Bring bats & gloves.', $meta['description'] );
$GLOBALS['post'] = get_post( $event );
$GLOBALS['wp_query']->is_single = true;
ob_start();
custom_open_graph_tags_with_sportspress_integration();
$output = ob_get_clean();
$this->assertStringContainsString( 'og:image:width', $output );
$this->assertSame( 2, substr_count( $output, 'property="og:image" content=' ) );
$this->assertStringContainsString( 'content="628"', $output );
$this->assertStringContainsString( 'content="1200"', $output );
$this->assertStringContainsString( 'variant=square', $output );
$this->assertStringContainsString( 'Hawks &quot;A&quot;', $output );
$this->assertStringNotContainsString( '<B>', $output );
$this->assertStringNotContainsString( '<script', $output );
}
}