|string $atts Shortcode attributes.
* @return string HTML output.
*/
public function render_shortcode($atts)
{
// Global kill switch — if the plugin is disabled, output nothing.
$general = wp_parse_args(
get_option(Aha_Orbit_Testimonials_Settings::OPTION_GENERAL, array()),
array('enabled' => 1)
);
if (empty($general['enabled'])) {
return '';
}
$atts = shortcode_atts(
array(
// Required.
'id' => 0,
// Template style slug ("orbit" = default; add-ons can register more).
'style' => 'orbit',
),
$atts,
'aha_orbit'
);
$post_id = absint($atts['id']);
if (!$post_id || AHA_ORBIT_TESTIMONIALS_CPT !== get_post_type($post_id) || 'publish' !== get_post_status($post_id)) {
return '';
}
$testimonials = get_post_meta($post_id, '_aha_orbit_testimonials_data', true);
if (!is_array($testimonials) || empty($testimonials)) {
return '';
}
// Optimize image and logo URLs for performance in all templates.
foreach ($testimonials as &$item) {
$image_url = isset($item['image']) ? $item['image'] : '';
if (!empty($image_url)) {
$attachment_id = attachment_url_to_postid($image_url);
if ($attachment_id) {
$img_src = wp_get_attachment_image_src($attachment_id, 'aha-orbit-avatar');
if (!$img_src) {
$img_src = wp_get_attachment_image_src($attachment_id, 'thumbnail');
}
if ($img_src) {
$item['image'] = $img_src[0];
}
}
}
$logo_url = isset($item['company_logo']) ? $item['company_logo'] : '';
if (!empty($logo_url)) {
$attachment_id = attachment_url_to_postid($logo_url);
if ($attachment_id) {
$img_src = wp_get_attachment_image_src($attachment_id, 'aha-orbit-logo');
if ($img_src) {
$item['company_logo'] = $img_src[0];
}
}
}
}
unset($item);
// ── Per-orbit settings from metabox ───────────────────────────────────
$meta = get_post_meta($post_id, '_aha_orbit_settings', true);
$meta = wp_parse_args(
(array) $meta,
array(
'layout' => 'default',
'layoutMode' => 'auto',
'showArrows' => 'yes',
'showDots' => 'yes',
'autoplay' => 'yes',
'interval' => 5000,
'transitionSpeed' => 800,
'animationDelay' => 150,
'grayscale' => 'yes',
'draggable' => 'yes',
'containerHeight' => 450,
'grid_columns' => 3,
'carousel_columns' => 1,
'card_bg' => '',
'text_color' => '',
'name_color' => '',
'role_color' => '',
'quote_color' => '',
'arrow_color' => '',
'bullet_color' => '',
'bullet_active_color' => '',
)
);
// ── Build JS options ──────────────────────────────────────────────────
// Note: assets are enqueued by each template (default.php, grid.php, carousel.php)
// so that only the relevant CSS/JS is loaded per layout.
$js_options = array(
'data' => $this->format_testimonials_for_js($testimonials),
'layoutMode' => $meta['layoutMode'],
'showArrows' => 'yes' === $meta['showArrows'],
'showDots' => 'yes' === $meta['showDots'],
'autoplay' => 'yes' === $meta['autoplay'],
'interval' => (int) $meta['interval'],
'transitionSpeed' => (int) $meta['transitionSpeed'],
'animationDelay' => (int) $meta['animationDelay'],
'grayscale' => 'yes' === $meta['grayscale'],
'draggable' => 'yes' === $meta['draggable'],
'columns' => isset( $meta['carousel_columns'] ) ? (int) $meta['carousel_columns'] : 1,
);
$inline_style = '--ot-container-height: ' . absint($meta['containerHeight']) . 'px;';
$js_options = wp_json_encode($js_options);
// ── Resolve layout slug first (needed for inline style handle) ────────
$layout_slug = sanitize_key( $meta['layout'] ?? 'default' );
if ( '' === $layout_slug ) {
$layout_slug = 'default';
}
// ── Design: CSS custom properties from per-orbit meta ─────────────────
$design_css_map = array(
'card_bg' => '--ot-card-bg',
'text_color' => '--ot-text-primary',
'name_color' => '--ot-name-color',
'role_color' => '--ot-role-color',
'quote_color' => '--ot-quote-color',
'arrow_color' => '--ot-arrow-color',
'bullet_color' => '--ot-bullet-color',
'bullet_active_color' => '--ot-bullet-active-color',
);
$design_vars = array();
foreach ($design_css_map as $key => $css_var) {
$val = isset($meta[$key]) ? sanitize_hex_color($meta[$key]) : '';
if ($val) {
$design_vars[$css_var] = $val;
}
}
$design_css = '';
if (!empty($design_vars)) {
$design_css .= '';
}
// ── Template resolution ──────────────────────────────────────────────
/** This filter is documented in aha-orbit-testimonials.php */
$available_layouts = apply_filters( 'aha_orbit_testimonials_layouts', array() );
$template_file = isset( $available_layouts[ $layout_slug ]['template'] )
? $available_layouts[ $layout_slug ]['template']
: AHA_ORBIT_TESTIMONIALS_PATH . 'templates/frontend/default.php';
// 2. Allow a final override via the legacy aha_orbit_testimonials_template
// filter for backward compatibility with add-ons.
/**
* Filter: aha_orbit_testimonials_template
*
* Allows a final override of the resolved template path, for backward
* compatibility with add-ons that hook into this filter.
*
* @since 1.0.0
* @param string $path Absolute path to the template file.
* @param string $layout The layout slug saved on this orbit.
* @param int $post_id ID of the current Orbit Testimonials post.
*/
$template_file = apply_filters(
'aha_orbit_testimonials_template',
$template_file,
$layout_slug,
$post_id
);
// Safety: fall back to built-in template if the resolved path is not readable.
if (!is_string($template_file) || !is_readable($template_file)) {
$template_file = AHA_ORBIT_TESTIMONIALS_PATH . 'templates/frontend/default.php';
}
ob_start();
// phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable -- path is validated above.
include $template_file;
return $design_css . ob_get_clean();
}
/**
* Format testimonials array for JavaScript consumption.
*
* @param array> $testimonials Raw testimonials from post meta.
* @return array>
*/
private function format_testimonials_for_js($testimonials)
{
$formatted = array();
foreach ($testimonials as $item) {
// Fetch optimized image sizes.
$image_url = isset($item['image']) ? $item['image'] : '';
if ( ! empty( $image_url ) ) {
$attachment_id = attachment_url_to_postid( $image_url );
if ( $attachment_id ) {
$img_src = wp_get_attachment_image_src( $attachment_id, 'aha-orbit-avatar' );
if ( ! $img_src ) {
$img_src = wp_get_attachment_image_src( $attachment_id, 'thumbnail' );
}
if ( $img_src ) {
$image_url = $img_src[0];
}
}
}
$logo_url = isset($item['company_logo']) ? $item['company_logo'] : '';
if ( ! empty( $logo_url ) ) {
$attachment_id = attachment_url_to_postid( $logo_url );
if ( $attachment_id ) {
$img_src = wp_get_attachment_image_src( $attachment_id, 'aha-orbit-logo' );
if ( $img_src ) {
$logo_url = $img_src[0];
}
}
}
$slide = array(
'name' => isset($item['name']) ? $item['name'] : '',
'role' => isset($item['role']) ? $item['role'] : '',
'text' => isset($item['text']) ? $item['text'] : '',
'image' => $image_url,
'companyLogo' => $logo_url,
'rating' => isset($item['rating']) ? (float) $item['rating'] : 0,
'socialNetwork' => isset($item['social_network']) ? $item['social_network'] : '',
'socialLink' => isset($item['social_link']) ? $item['social_link'] : '',
'socialNetworkLabel' => isset($item['social_network']) ? $this->get_social_network_label($item['social_network']) : '',
);
if (!empty($item['pos_left']) && !empty($item['pos_top'])) {
$slide['pos'] = array(
'left' => $item['pos_left'],
'top' => $item['pos_top'],
);
}
$formatted[] = $slide;
}
return $formatted;
}
/**
* Get translated label for a social network.
*
* @param string $network The social network slug.
* @return string The translated label, or the capitalized network name as fallback.
*/
private function get_social_network_label($network)
{
$labels = array(
'facebook' => __('View on Facebook', 'aha-orbit-testimonials'),
'instagram' => __('View on Instagram', 'aha-orbit-testimonials'),
'linkedin' => __('View on LinkedIn', 'aha-orbit-testimonials'),
'twitter' => __('View on X (Twitter)', 'aha-orbit-testimonials'),
'youtube' => __('View on YouTube', 'aha-orbit-testimonials'),
'tiktok' => __('View on TikTok', 'aha-orbit-testimonials'),
'google' => __('View on Google', 'aha-orbit-testimonials'),
'trustpilot' => __('View on Trustpilot', 'aha-orbit-testimonials'),
);
return $labels[$network] ?? ucfirst($network);
}
}