"columns"
@@ -1758,6 +1771,7 @@ function cooked_render_recipe_fields( $post_id ) {
id ()
+ include_ids ()
title ()
limit ()
columns ()
@@ -1768,7 +1782,7 @@ function cooked_render_recipe_fields( $post_id ) {
- [cooked-related-recipes limit="4" columns="2" title="Related Recipes"]
+ [cooked-related-recipes include_ids="101,102" limit="4" columns="2" title="Related Recipes"]
diff --git a/includes/class.cooked-related-recipes.php b/includes/class.cooked-related-recipes.php
index b2493a8..9f5f2db 100644
--- a/includes/class.cooked-related-recipes.php
+++ b/includes/class.cooked-related-recipes.php
@@ -31,6 +31,7 @@ class Cooked_Related_Recipes {
public static function get_default_atts() {
$default_atts = [
'id' => false,
+ 'include_ids' => false,
'title' => __( 'Related Recipes', 'cooked' ),
'limit' => 4,
'columns' => 2,
@@ -72,64 +73,68 @@ public static function get_related_recipes( $recipe_id, $atts ) {
}
/**
- * Find related recipes: one WP_Query with tax_query (all taxonomies OR) and orderby rand.
+ * Find related recipes: pinned include_ids first, then one WP_Query with
+ * tax_query (all taxonomies OR) and orderby rand to fill remaining slots.
*
* @param array $source_recipe Source recipe data.
* @param array $atts Shortcode attributes.
* @return array [ ['id' => int], ... ]
*/
public static function find_related_recipes( $source_recipe, $atts ) {
- $recipe_id = $source_recipe['id'];
- $limit = isset( $atts['limit'] ) ? max( 1, (int) $atts['limit'] ) : 4;
-
- // Build OR clause for each taxonomy that is enabled via match_* and where the source recipe has terms.
- $clauses = [];
- $taxonomy_atts = [
- 'cp_recipe_category' => 'match_categories',
- 'cp_recipe_cuisine' => 'match_cuisines',
- 'cp_recipe_cooking_method' => 'match_cooking_methods',
- 'cp_recipe_tags' => 'match_tags',
- 'cp_recipe_diet' => 'match_diets',
- ];
- foreach ( $taxonomy_atts as $taxonomy => $att_key ) {
- if ( empty( $atts[ $att_key ] ) || $atts[ $att_key ] === 'false' ) {
- continue;
+ $recipe_id = $source_recipe['id'];
+ $limit = isset( $atts['limit'] ) ? max( 1, (int) $atts['limit'] ) : 4;
+ $include_ids = array_slice( self::parse_include_ids( $atts, $recipe_id ), 0, $limit );
+ $remaining = $limit - count( $include_ids );
+ $related_ids = [];
+
+ if ( $remaining > 0 ) {
+ $clauses = [];
+ $taxonomy_atts = [
+ 'cp_recipe_category' => 'match_categories',
+ 'cp_recipe_cuisine' => 'match_cuisines',
+ 'cp_recipe_cooking_method' => 'match_cooking_methods',
+ 'cp_recipe_tags' => 'match_tags',
+ 'cp_recipe_diet' => 'match_diets',
+ ];
+ foreach ( $taxonomy_atts as $taxonomy => $att_key ) {
+ if ( ! isset( $atts[ $att_key ] ) || ! wp_validate_boolean( $atts[ $att_key ] ) ) {
+ continue;
+ }
+ $terms = self::get_recipe_terms( $recipe_id, $taxonomy );
+ if ( ! empty( $terms ) ) {
+ $clauses[] = [ 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $terms ];
+ }
}
- $terms = self::get_recipe_terms( $recipe_id, $taxonomy );
- if ( ! empty( $terms ) ) {
- $clauses[] = [ 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $terms ];
- }
- }
-
- if ( empty( $clauses ) ) {
- return [];
- }
- $tax_query = array_merge( [ 'relation' => 'OR' ], $clauses );
-
- $query_args = [
- 'post_type' => 'cp_recipe',
- 'post_status' => 'publish',
- 'post__not_in' => [ $recipe_id ],
- 'posts_per_page' => $limit,
- 'orderby' => 'rand',
- 'fields' => 'ids',
- ];
-
- $query_args['tax_query'] = $tax_query;
+ if ( empty( $clauses ) && empty( $include_ids ) ) {
+ return [];
+ }
- $current_language = false;
- if ( class_exists( 'Cooked_Multilingual' ) && Cooked_Multilingual::is_multilingual_active() ) {
- $current_language = Cooked_Multilingual::get_current_language();
+ if ( ! empty( $clauses ) ) {
+ $query_args = [
+ 'post_type' => 'cp_recipe',
+ 'post_status' => 'publish',
+ 'post__not_in' => array_merge( [ $recipe_id ], $include_ids ),
+ 'posts_per_page' => $remaining,
+ 'orderby' => 'rand',
+ 'fields' => 'ids',
+ 'tax_query' => array_merge( [ 'relation' => 'OR' ], $clauses ),
+ ];
+
+ $current_language = false;
+ if ( class_exists( 'Cooked_Multilingual' ) && Cooked_Multilingual::is_multilingual_active() ) {
+ $current_language = Cooked_Multilingual::get_current_language();
+ }
+ $query_args = apply_filters( 'cooked_related_recipes_query_args', $query_args, $current_language );
+
+ $query = new \WP_Query( $query_args );
+ $related_ids = ! empty( $query->posts ) ? $query->posts : [];
+ wp_reset_postdata();
+ }
}
- $query_args = apply_filters( 'cooked_related_recipes_query_args', $query_args, $current_language );
-
- $query = new \WP_Query( $query_args );
- $ids = ! empty( $query->posts ) ? $query->posts : [];
- wp_reset_postdata();
$result = [];
- foreach ( $ids as $id ) {
+ foreach ( array_merge( $include_ids, $related_ids ) as $id ) {
$result[] = [ 'id' => (int) $id ];
}
@@ -145,6 +150,36 @@ public static function find_related_recipes( $source_recipe, $atts ) {
return apply_filters( 'cooked_related_recipes_result', $result, $source_recipe, $atts );
}
+ /**
+ * Parse include_ids into unique positive IDs, excluding the source recipe.
+ *
+ * @param array $atts Shortcode attributes.
+ * @param int $recipe_id Source recipe ID.
+ * @return int[]
+ */
+ private static function parse_include_ids( $atts, $recipe_id ) {
+ if ( empty( $atts['include_ids'] ) ) {
+ return [];
+ }
+
+ $raw = $atts['include_ids'];
+ if ( ! is_array( $raw ) ) {
+ $raw = explode( ',', str_replace( ' ', '', (string) $raw ) );
+ }
+
+ $ids = [];
+ $recipe_id = (int) $recipe_id;
+ foreach ( $raw as $id ) {
+ $id = absint( $id );
+ if ( ! $id || $id === $recipe_id || in_array( $id, $ids, true ) ) {
+ continue;
+ }
+ $ids[] = $id;
+ }
+
+ return $ids;
+ }
+
/**
* Get recipe term IDs for a taxonomy.
*
diff --git a/includes/class.cooked-seo.php b/includes/class.cooked-seo.php
index 613048e..5fd7227 100644
--- a/includes/class.cooked-seo.php
+++ b/includes/class.cooked-seo.php
@@ -101,8 +101,8 @@ public static function schema_values( $recipe = false ) {
endif;
endif;
- $cook_time = isset($recipe['cook_time']) && $recipe['cook_time'] ? esc_html( $recipe['cook_time'] ) : 0;
- $prep_time = isset($recipe['prep_time']) && $recipe['prep_time'] ? esc_html( $recipe['prep_time'] ) : 0;
+ $cook_time = isset($recipe['cook_time']) ? Cooked_Measurements::normalize_minutes( $recipe['cook_time'] ) : 0;
+ $prep_time = isset($recipe['prep_time']) ? Cooked_Measurements::normalize_minutes( $recipe['prep_time'] ) : 0;
$total_time = $cook_time + $prep_time;
$unsaturatedFatAmount = (isset($recipe['nutrition']['monounsaturated_fat']) && $recipe['nutrition']['monounsaturated_fat'] ? $recipe['nutrition']['monounsaturated_fat'] : 0) + (isset($recipe['nutrition']['polyunsaturated_fat']) && $recipe['nutrition']['polyunsaturated_fat'] ? $recipe['nutrition']['polyunsaturated_fat'] : 0);
diff --git a/includes/class.cooked-shortcodes.php b/includes/class.cooked-shortcodes.php
index 42d9259..7c967ff 100644
--- a/includes/class.cooked-shortcodes.php
+++ b/includes/class.cooked-shortcodes.php
@@ -732,7 +732,7 @@ public static function cooked_info_prep_time( $recipe ) {
global $_cooked_settings;
if (!empty($_cooked_settings['recipe_info_display_options']) && in_array('timing_prep',$_cooked_settings['recipe_info_display_options'])) {
- $prep_time = isset($recipe['prep_time']) ? esc_html( $recipe['prep_time'] ) : 0;
+ $prep_time = isset($recipe['prep_time']) ? Cooked_Measurements::normalize_minutes( $recipe['prep_time'] ) : 0;
echo $prep_time ? '
' . esc_html__('Prep Time','cooked') . '' . wp_kses_post( Cooked_Measurements::time_format( $prep_time ) ) . '' : '';
}
}
@@ -741,7 +741,7 @@ public static function cooked_info_cook_time( $recipe ) {
global $_cooked_settings;
if (!empty($_cooked_settings['recipe_info_display_options']) && in_array('timing_cook', $_cooked_settings['recipe_info_display_options'])) {
- $cook_time = isset($recipe['cook_time']) ? esc_html( $recipe['cook_time'] ) : 0;
+ $cook_time = isset($recipe['cook_time']) ? Cooked_Measurements::normalize_minutes( $recipe['cook_time'] ) : 0;
echo $cook_time ? '
' . esc_html__('Cook Time','cooked') . '' . wp_kses_post( Cooked_Measurements::time_format( $cook_time ) ) . '' : '';
}
}
@@ -750,13 +750,13 @@ public static function cooked_info_total_time( $recipe ) {
global $_cooked_settings;
if (!empty($_cooked_settings['recipe_info_display_options']) && in_array('timing_total',$_cooked_settings['recipe_info_display_options'])) {
- $total_time = isset($recipe['total_time']) ? esc_html( $recipe['total_time'] ) : 0;
+ $total_time = isset($recipe['total_time']) ? Cooked_Measurements::normalize_minutes( $recipe['total_time'] ) : 0;
if ( $total_time ) {
echo $total_time ? '
' . esc_html__('Total Time','cooked') . '' . wp_kses_post( Cooked_Measurements::time_format( $total_time ) ) . '' : '';
} else {
- $prep_time = isset($recipe['prep_time']) ? esc_html( $recipe['prep_time'] ) : 0;
- $cook_time = isset($recipe['cook_time']) ? esc_html( $recipe['cook_time'] ) : 0;
+ $prep_time = isset($recipe['prep_time']) ? Cooked_Measurements::normalize_minutes( $recipe['prep_time'] ) : 0;
+ $cook_time = isset($recipe['cook_time']) ? Cooked_Measurements::normalize_minutes( $recipe['cook_time'] ) : 0;
if ( $prep_time && $cook_time ) {
$total_time = $prep_time + $cook_time;
@@ -1245,9 +1245,9 @@ public function cooked_related_recipes_shortcode($atts, $content = null) {
// Display recipes in grid
$columns = intval($atts['columns']);
- $hide_image = $atts['hide_image'] && $atts['hide_image'] !== 'false';
- $hide_excerpt = $atts['hide_excerpt'] && $atts['hide_excerpt'] !== 'false';
- $hide_author = $atts['hide_author'] && $atts['hide_author'] !== 'false';
+ $hide_image = wp_validate_boolean( $atts['hide_image'] );
+ $hide_excerpt = wp_validate_boolean( $atts['hide_excerpt'] );
+ $hide_author = wp_validate_boolean( $atts['hide_author'] );
echo '