'',
'link' => 'false',
'target' => '_blank',
'class' => '',
'default' => '',
),
$atts,
'a1tools_var'
);
if ( empty( $atts['key'] ) ) {
return '';
}
$value = a1tools_get_variable( $atts['key'], $atts['default'] );
if ( empty( $value ) ) {
return esc_html( $atts['default'] );
}
// Wrap in link if requested and value is a URL.
if ( 'true' === $atts['link'] && filter_var( $value, FILTER_VALIDATE_URL ) ) {
$class_attr = ! empty( $atts['class'] ) ? ' class="' . esc_attr( $atts['class'] ) . '"' : '';
$rel_attr = '_blank' === $atts['target'] ? ' rel="noopener noreferrer"' : '';
return '' . esc_html( $value ) . ' ';
}
// Wrap in span if class provided.
if ( ! empty( $atts['class'] ) ) {
return '' . esc_html( $value ) . ' ';
}
return esc_html( $value );
}
add_shortcode( 'a1tools_var', 'a1tools_shortcode_var' );
/**
* Shortcode: [a1tools_website_url]
* Outputs the website URL as text or a clickable link.
*
* @since 1.5.0
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_website_url( $atts ) {
$atts = shortcode_atts(
array(
'link' => 'false',
'target' => '_blank',
'class' => '',
'text' => '',
),
$atts,
'a1tools_website_url'
);
$url = home_url( '/' );
// If link="true", output as a clickable link.
if ( 'true' === $atts['link'] ) {
$link_text = ! empty( $atts['text'] ) ? esc_html( $atts['text'] ) : esc_html( $url );
$class_attr = ! empty( $atts['class'] ) ? ' class="' . esc_attr( $atts['class'] ) . '"' : '';
$rel_attr = '_blank' === $atts['target'] ? ' rel="noopener noreferrer"' : '';
return '' . $link_text . ' ';
}
// Plain text output.
if ( ! empty( $atts['class'] ) ) {
return '' . esc_html( $url ) . ' ';
}
return esc_html( $url );
}
add_shortcode( 'a1tools_website_url', 'a1tools_shortcode_website_url' );
/**
* Shortcode: [a1tools_address format="full"]
* Outputs formatted address.
*
* @since 1.0.0
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_address( $atts ) {
$atts = shortcode_atts(
array(
'format' => 'full',
'separator' => ' ',
),
$atts,
'a1tools_address'
);
$vars = a1tools_get_site_variables();
if ( ! $vars ) {
return '';
}
$parts = array();
switch ( $atts['format'] ) {
case 'street':
$parts = array_filter(
array(
isset( $vars['address_line1'] ) ? $vars['address_line1'] : '',
isset( $vars['address_line2'] ) ? $vars['address_line2'] : '',
)
);
break;
case 'city_state_zip':
$city_state = array_filter(
array(
isset( $vars['city'] ) ? $vars['city'] : '',
isset( $vars['state'] ) ? $vars['state'] : '',
)
);
$zip = isset( $vars['zip'] ) ? $vars['zip'] : '';
$parts = array( implode( ', ', $city_state ) . ' ' . $zip );
break;
case 'full':
default:
$city = isset( $vars['city'] ) ? $vars['city'] : '';
$state = isset( $vars['state'] ) ? $vars['state'] : '';
$zip = isset( $vars['zip'] ) ? $vars['zip'] : '';
$state_zip = trim( $state . ' ' . $zip );
$city_line = $city ? $city . ( $state_zip ? ', ' . $state_zip : '' ) : $state_zip;
$parts = array_filter(
array(
isset( $vars['address_line1'] ) ? $vars['address_line1'] : '',
isset( $vars['address_line2'] ) ? $vars['address_line2'] : '',
$city_line,
isset( $vars['country'] ) ? $vars['country'] : '',
)
);
break;
}
// Handle separator - allow for HTML output, otherwise escape.
$separator = $atts['separator'];
if ( ' ' === $separator || ' ' === $separator || ' ' === $separator ) {
return implode( ' ', array_map( 'esc_html', $parts ) );
}
return esc_html( implode( $separator, $parts ) );
}
add_shortcode( 'a1tools_address', 'a1tools_shortcode_address' );
/**
* Shortcode: [a1tools_full_address]
* Outputs the complete address in a single line.
*
* @since 1.2.0
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_full_address( $atts ) {
$atts = shortcode_atts(
array(
'separator' => ', ',
'class' => '',
),
$atts,
'a1tools_full_address'
);
$vars = a1tools_get_site_variables();
if ( ! $vars ) {
return '';
}
// Use standalone full_address field if set.
if ( ! empty( $vars['full_address'] ) ) {
$output = esc_html( $vars['full_address'] );
if ( ! empty( $atts['class'] ) ) {
$output = '' . $output . ' ';
}
return $output;
}
// Fallback: assemble from individual address fields.
$parts = array();
// Address Line 1.
if ( ! empty( $vars['address_line1'] ) ) {
$parts[] = $vars['address_line1'];
}
// Address Line 2.
if ( ! empty( $vars['address_line2'] ) ) {
$parts[] = $vars['address_line2'];
}
// City.
if ( ! empty( $vars['city'] ) ) {
$parts[] = $vars['city'];
}
// State + ZIP (combined).
$state = ! empty( $vars['state'] ) ? $vars['state'] : '';
$zip = ! empty( $vars['zip'] ) ? $vars['zip'] : '';
if ( $state || $zip ) {
$parts[] = trim( $state . ' ' . $zip );
}
if ( empty( $parts ) ) {
return '';
}
$output = esc_html( implode( $atts['separator'], $parts ) );
if ( ! empty( $atts['class'] ) ) {
$output = '' . $output . ' ';
}
return $output;
}
add_shortcode( 'a1tools_full_address', 'a1tools_shortcode_full_address' );
/**
* Shortcode: [a1tools_hours]
* Outputs formatted operating hours table.
*
* @since 1.0.0
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_hours( $atts ) {
$atts = shortcode_atts(
array(
'format' => 'table',
'class' => 'a1tools-hours',
),
$atts,
'a1tools_hours'
);
$vars = a1tools_get_site_variables();
if ( ! $vars || empty( $vars['operating_hours'] ) ) {
return '';
}
$hours = $vars['operating_hours'];
$days = array(
'mon' => __( 'Monday', 'a1-tools' ),
'tue' => __( 'Tuesday', 'a1-tools' ),
'wed' => __( 'Wednesday', 'a1-tools' ),
'thu' => __( 'Thursday', 'a1-tools' ),
'fri' => __( 'Friday', 'a1-tools' ),
'sat' => __( 'Saturday', 'a1-tools' ),
'sun' => __( 'Sunday', 'a1-tools' ),
);
$output = '';
if ( 'list' === $atts['format'] ) {
$output = '
';
foreach ( $days as $key => $label ) {
$time = isset( $hours[ $key ] ) && ! empty( $hours[ $key ] ) ? esc_html( $hours[ $key ] ) : esc_html__( 'Closed', 'a1-tools' );
$output .= '' . esc_html( $label ) . ': ' . $time . ' ';
}
$output .= ' ';
} else {
$output = '';
foreach ( $days as $key => $label ) {
$time = isset( $hours[ $key ] ) && ! empty( $hours[ $key ] ) ? esc_html( $hours[ $key ] ) : esc_html__( 'Closed', 'a1-tools' );
$output .= '' . esc_html( $label ) . ' ' . $time . ' ';
}
$output .= '
';
}
return $output;
}
add_shortcode( 'a1tools_hours', 'a1tools_shortcode_hours' );
/**
* Get SVG markup for icons not available in Font Awesome 5.
*
* @since 1.5.0
* @param string $icon_key The SVG icon key (e.g. 'svg-x').
* @return string SVG markup or empty string.
*/
function a1tools_get_svg_icon( $icon_key ) {
$svgs = array(
'svg-x' => ' ',
);
return isset( $svgs[ $icon_key ] ) ? $svgs[ $icon_key ] : '';
}
/**
* Get icon HTML (SVG or Font Awesome tag).
*
* @since 1.5.0
* @param string $icon Icon class or SVG key.
* @return string HTML markup for the icon.
*/
function a1tools_get_icon_html( $icon ) {
$svg = a1tools_get_svg_icon( $icon );
if ( $svg ) {
return $svg;
}
return ' ';
}
/**
* Shortcode: [a1tools_social_links]
* Outputs social media icons/links with optional styling.
*
* @since 1.0.0
* @since 1.2.0 Added styling parameters.
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_social_links( $atts ) {
$atts = shortcode_atts(
array(
'class' => 'a1tools-social-links',
'icon_class' => '',
'style' => '', // default, outline, minimal, light, dark
'shape' => 'rounded', // rounded, circle, square
'size' => '40', // button size in px
'icon_size' => '18', // icon size in px
'spacing' => '8', // spacing between icons in px
'alignment' => 'left', // left, center, right
'color' => '', // custom color (hex)
'hover' => 'scale', // scale, lift, rotate, pulse, none
),
$atts,
'a1tools_social_links'
);
$vars = a1tools_get_site_variables();
if ( ! $vars ) {
return '';
}
$platforms = array(
'facebook_url' => array( 'label' => 'Facebook', 'icon' => 'fab fa-facebook-f', 'color' => '#1877F2' ),
'instagram_url' => array( 'label' => 'Instagram', 'icon' => 'fab fa-instagram', 'color' => '#E4405F' ),
'youtube_url' => array( 'label' => 'YouTube', 'icon' => 'fab fa-youtube', 'color' => '#FF0000' ),
'twitter_url' => array( 'label' => 'X (Twitter)', 'icon' => 'svg-x', 'color' => '#000000' ),
'linkedin_url' => array( 'label' => 'LinkedIn', 'icon' => 'fab fa-linkedin-in', 'color' => '#0A66C2' ),
'tiktok_url' => array( 'label' => 'TikTok', 'icon' => 'fab fa-tiktok', 'color' => '#000000' ),
'yelp_url' => array( 'label' => 'Yelp', 'icon' => 'fab fa-yelp', 'color' => '#D32323' ),
'google_business_url' => array( 'label' => 'Google', 'icon' => 'fab fa-google', 'color' => '#4285F4' ),
'pinterest_url' => array( 'label' => 'Pinterest', 'icon' => 'fab fa-pinterest-p', 'color' => '#E60023' ),
'bbb_url' => array( 'label' => 'BBB', 'icon' => 'fas fa-shield-alt', 'color' => '#006CB7' ),
'nextdoor_url' => array( 'label' => 'Nextdoor', 'icon' => 'fas fa-home', 'color' => '#8ED500' ),
'houzz_url' => array( 'label' => 'Houzz', 'icon' => 'fab fa-houzz', 'color' => '#4DBC15' ),
'angi_url' => array( 'label' => 'Angi', 'icon' => 'fas fa-tools', 'color' => '#FF6153' ),
'thumbtack_url' => array( 'label' => 'Thumbtack', 'icon' => 'fas fa-thumbtack', 'color' => '#009FD9' ),
'homeadvisor_url' => array( 'label' => 'HomeAdvisor', 'icon' => 'fas fa-home', 'color' => '#EF6C00' ),
);
// If no style is set, use simple output (backwards compatible).
if ( empty( $atts['style'] ) ) {
$output = '';
return $output;
}
// Styled output.
$style = $atts['style'];
$shape = $atts['shape'];
$size = (int) $atts['size'];
$icon_size = (int) $atts['icon_size'];
$spacing = (int) $atts['spacing'];
$alignment = $atts['alignment'];
$custom_color = $atts['color'];
$hover_effect = $atts['hover'];
// Generate unique ID.
static $instance_count = 0;
$instance_count++;
$widget_id = 'a1tools-social-sc-' . $instance_count;
// Border radius based on shape.
$border_radius = '8px';
if ( 'circle' === $shape ) {
$border_radius = '50%';
} elseif ( 'square' === $shape ) {
$border_radius = '0';
}
// Build CSS for this instance.
$css = '#' . esc_attr( $widget_id ) . ' { text-align:' . esc_attr( $alignment ) . '; }';
$css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon { ';
$css .= 'width:' . (int) $size . 'px;height:' . (int) $size . 'px;';
$css .= 'margin:0 ' . (int) ( $spacing / 2 ) . 'px ' . (int) $spacing . 'px;';
$css .= 'font-size:' . (int) $icon_size . 'px;';
$css .= 'display:inline-flex;align-items:center;justify-content:center;';
$css .= 'text-decoration:none;transition:all 0.3s ease;';
$css .= 'border-radius:' . esc_attr( $border_radius ) . ';';
$css .= '}';
// Style-specific CSS.
if ( 'default' === $style || 'official' === $style ) {
$css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon { color: #fff; }';
} elseif ( 'outline' === $style ) {
$css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon { background: transparent; border: 2px solid; }';
} elseif ( 'minimal' === $style ) {
$css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon { background: transparent; }';
} elseif ( 'light' === $style ) {
$css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon { background: #f5f5f5; }';
} elseif ( 'dark' === $style ) {
$css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon { background: #333; color: #fff; }';
}
// Hover effects.
if ( 'scale' === $hover_effect ) {
$css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon:hover { transform: scale(1.1); }';
} elseif ( 'lift' === $hover_effect ) {
$css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon:hover { transform: translateY(-3px); box-shadow: 0 4px 12px rgba(0,0,0,0.2); }';
} elseif ( 'rotate' === $hover_effect ) {
$css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon:hover { transform: rotate(15deg); }';
} elseif ( 'pulse' === $hover_effect ) {
$css .= '#' . esc_attr( $widget_id ) . ' .a1tools-social-icon:hover { animation: a1tools-pulse 0.5s ease; }';
$css .= '@keyframes a1tools-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.15); } }';
}
// Ensure the style handle exists before adding inline CSS.
if ( ! wp_style_is( 'a1tools-social', 'registered' ) ) {
wp_register_style( 'a1tools-social', false, array(), A1TOOLS_VERSION );
}
if ( ! wp_style_is( 'a1tools-social', 'enqueued' ) ) {
wp_enqueue_style( 'a1tools-social' );
}
wp_add_inline_style( 'a1tools-social', $css );
$output = '';
return $output;
}
add_shortcode( 'a1tools_social_links', 'a1tools_shortcode_social_links' );
/**
* Shortcode: [a1tools_city_name]
* Outputs the city name (for geo-targeting).
*
* @since 1.0.8
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_city_name( $atts ) {
$atts = shortcode_atts(
array(
'default' => '',
'class' => '',
),
$atts,
'a1tools_city_name'
);
$value = a1tools_get_variable( 'city_name', $atts['default'] );
if ( empty( $value ) ) {
return esc_html( $atts['default'] );
}
if ( ! empty( $atts['class'] ) ) {
return '' . esc_html( $value ) . ' ';
}
return esc_html( $value );
}
add_shortcode( 'a1tools_city_name', 'a1tools_shortcode_city_name' );
/**
* Shortcode: [a1tools_state]
* Outputs the state/location name (for geo-targeting).
*
* @since 1.0.8
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_state( $atts ) {
$atts = shortcode_atts(
array(
'default' => '',
'class' => '',
),
$atts,
'a1tools_state'
);
$value = a1tools_get_variable( 'location_name', $atts['default'] );
if ( empty( $value ) ) {
return esc_html( $atts['default'] );
}
if ( ! empty( $atts['class'] ) ) {
return '' . esc_html( $value ) . ' ';
}
return esc_html( $value );
}
add_shortcode( 'a1tools_state', 'a1tools_shortcode_state' );
/**
* Shortcode: [a1tools_city_link]
* Outputs the city name as a clickable link using the configured city_url.
* If no city_url is set, falls back to plain text (same as [a1tools_city_name]).
*
* @since 1.9.2
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_city_link( $atts ) {
$atts = shortcode_atts(
array(
'default' => '',
'text' => '',
'target' => '_self',
'class' => '',
),
$atts,
'a1tools_city_link'
);
$city_name = a1tools_get_variable( 'city_name', $atts['default'] );
$city_url = a1tools_get_variable( 'city_url', '' );
$text = ! empty( $atts['text'] ) ? $atts['text'] : $city_name;
if ( empty( $text ) ) {
return '';
}
if ( ! empty( $city_url ) ) {
$class_attr = ! empty( $atts['class'] ) ? ' class="' . esc_attr( $atts['class'] ) . '"' : '';
return '' . esc_html( $text ) . ' ';
}
// No URL set — plain text fallback.
if ( ! empty( $atts['class'] ) ) {
return '' . esc_html( $text ) . ' ';
}
return esc_html( $text );
}
add_shortcode( 'a1tools_city_link', 'a1tools_shortcode_city_link' );
/**
* Shortcode: [a1tools_state_link]
* Outputs the state name as a clickable link using the configured state_url.
* If no state_url is set, falls back to plain text (same as [a1tools_state]).
*
* @since 1.9.2
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_state_link( $atts ) {
$atts = shortcode_atts(
array(
'default' => '',
'text' => '',
'target' => '_self',
'class' => '',
),
$atts,
'a1tools_state_link'
);
$state_name = a1tools_get_variable( 'location_name', $atts['default'] );
$state_url = a1tools_get_variable( 'state_url', '' );
$text = ! empty( $atts['text'] ) ? $atts['text'] : $state_name;
if ( empty( $text ) ) {
return '';
}
if ( ! empty( $state_url ) ) {
$class_attr = ! empty( $atts['class'] ) ? ' class="' . esc_attr( $atts['class'] ) . '"' : '';
return '' . esc_html( $text ) . ' ';
}
// No URL set — plain text fallback.
if ( ! empty( $atts['class'] ) ) {
return '' . esc_html( $text ) . ' ';
}
return esc_html( $text );
}
add_shortcode( 'a1tools_state_link', 'a1tools_shortcode_state_link' );
/**
* Shortcode: [a1tools_google_map]
* Outputs a Google Maps embed or link.
*
* @since 1.0.8
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_google_map( $atts ) {
$atts = shortcode_atts(
array(
'type' => 'link',
'width' => '100%',
'height' => '400',
'class' => 'a1tools-google-map',
'text' => __( 'View on Google Maps', 'a1-tools' ),
'target' => '_blank',
),
$atts,
'a1tools_google_map'
);
$url = a1tools_get_variable( 'google_maps_url', '' );
if ( empty( $url ) ) {
return '';
}
// If type is 'embed', create an iframe
if ( 'embed' === $atts['type'] ) {
// Check if URL is already an embed URL or convert it
$embed_url = $url;
// If it's a regular Google Maps URL, try to convert to embed format
if ( false !== strpos( $url, 'google.com/maps' ) && false === strpos( $url, '/embed' ) ) {
// Extract place or coordinates and create embed URL
if ( preg_match( '/place\/([^\/]+)/', $url, $matches ) ) {
$place = $matches[1];
$embed_url = 'https://www.google.com/maps/embed/v1/place?key=&q=' . urlencode( $place );
} elseif ( preg_match( '/@(-?\d+\.\d+),(-?\d+\.\d+)/', $url, $matches ) ) {
$lat = $matches[1];
$lng = $matches[2];
$embed_url = 'https://maps.google.com/maps?q=' . $lat . ',' . $lng . '&output=embed';
}
}
return '';
}
// Default: return a link
$rel = '_blank' === $atts['target'] ? ' rel="noopener noreferrer"' : '';
return '' . esc_html( $atts['text'] ) . ' ';
}
add_shortcode( 'a1tools_google_map', 'a1tools_shortcode_google_map' );
// ============================================================================
// CITY PAGE SHORTCODES
// ============================================================================
/**
* Shortcode: [a1tools_city_phone]
* Outputs the city-specific phone number for the current page.
* Automatically detects which city page it's on via get_the_ID().
* Falls back to the site's primary phone if no city-specific phone is set.
*
* @since 1.7.9
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_city_phone( $atts ) {
$atts = shortcode_atts(
array(
'default' => '',
'link' => 'no',
'class' => '',
'page_id' => '',
),
$atts,
'a1tools_city_phone'
);
$page_id = ! empty( $atts['page_id'] ) ? intval( $atts['page_id'] ) : get_the_ID();
$city_pages = a1tools_get_site_city_pages();
// Get city-specific phone.
$phone = '';
if ( ! empty( $city_pages ) && isset( $city_pages[ $page_id ] ) ) {
$phone = isset( $city_pages[ $page_id ]['phone'] ) ? $city_pages[ $page_id ]['phone'] : '';
}
// Fallback to site primary phone if no city-specific phone.
if ( empty( $phone ) ) {
if ( ! empty( $atts['default'] ) ) {
return esc_html( $atts['default'] );
}
// Use the site's primary phone as fallback.
$phone = a1tools_get_variable( 'phone_primary', '' );
if ( empty( $phone ) ) {
return '';
}
}
// Output as tel: link if requested.
if ( 'yes' === $atts['link'] ) {
$clean = preg_replace( '/[^0-9+]/', '', $phone );
$class_attr = ! empty( $atts['class'] ) ? ' class="' . esc_attr( $atts['class'] ) . '"' : '';
return '' . esc_html( $phone ) . ' ';
}
if ( ! empty( $atts['class'] ) ) {
return '' . esc_html( $phone ) . ' ';
}
return esc_html( $phone );
}
add_shortcode( 'a1tools_city_phone', 'a1tools_shortcode_city_phone' );
/**
* Shortcode: [a1tools_city_address]
* Outputs the city-specific address for the current page.
* Automatically detects which city page it's on via get_the_ID().
*
* Attributes:
* - format: 'full' (multi-line), 'inline' (single-line), or a specific field name
* (e.g., 'city', 'state', 'zip', 'address_line1')
*
* @since 1.7.9
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_city_address( $atts ) {
$atts = shortcode_atts(
array(
'default' => '',
'format' => 'full',
'class' => '',
'page_id' => '',
),
$atts,
'a1tools_city_address'
);
$page_id = ! empty( $atts['page_id'] ) ? intval( $atts['page_id'] ) : get_the_ID();
$city_pages = a1tools_get_site_city_pages();
// Try city-specific address first.
$city_data = null;
if ( ! empty( $city_pages ) && isset( $city_pages[ $page_id ] ) ) {
$city_data = $city_pages[ $page_id ];
}
// Check if city data has any address fields set.
$has_city_address = false;
if ( $city_data ) {
$has_city_address = ! empty( $city_data['address_line1'] )
|| ! empty( $city_data['city'] )
|| ! empty( $city_data['state'] );
}
// If format is a specific field name, return that field from city data or site fallback.
$single_fields = array( 'address_line1', 'address_line2', 'city', 'state', 'zip', 'country' );
if ( in_array( $atts['format'], $single_fields, true ) ) {
$value = '';
if ( $has_city_address && isset( $city_data[ $atts['format'] ] ) ) {
$value = $city_data[ $atts['format'] ];
}
// Fallback to site variable.
if ( empty( $value ) ) {
$value = a1tools_get_variable( $atts['format'], '' );
}
if ( empty( $value ) ) {
return esc_html( $atts['default'] );
}
if ( ! empty( $atts['class'] ) ) {
return '' . esc_html( $value ) . ' ';
}
return esc_html( $value );
}
// Determine source: city-specific or site fallback.
if ( $has_city_address ) {
$line1 = isset( $city_data['address_line1'] ) ? $city_data['address_line1'] : '';
$line2 = isset( $city_data['address_line2'] ) ? $city_data['address_line2'] : '';
$city = isset( $city_data['city'] ) ? $city_data['city'] : '';
$state = isset( $city_data['state'] ) ? $city_data['state'] : '';
$zip = isset( $city_data['zip'] ) ? $city_data['zip'] : '';
$country = isset( $city_data['country'] ) ? $city_data['country'] : '';
} else {
// Fallback to site address from Information tab.
$vars = a1tools_get_site_variables();
$line1 = isset( $vars['address_line1'] ) ? $vars['address_line1'] : '';
$line2 = isset( $vars['address_line2'] ) ? $vars['address_line2'] : '';
$city = isset( $vars['city'] ) ? $vars['city'] : '';
$state = isset( $vars['state'] ) ? $vars['state'] : '';
$zip = isset( $vars['zip'] ) ? $vars['zip'] : '';
$country = isset( $vars['country'] ) ? $vars['country'] : '';
}
if ( 'inline' === $atts['format'] ) {
// Single-line comma-separated.
$parts = array_filter( array( $line1, $line2, $city, trim( $state . ' ' . $zip ), $country ) );
$output = esc_html( implode( ', ', $parts ) );
} else {
// Multi-line (full format).
$lines = array();
if ( ! empty( $line1 ) ) {
$lines[] = esc_html( $line1 );
}
if ( ! empty( $line2 ) ) {
$lines[] = esc_html( $line2 );
}
$city_state_zip = trim( $city . ( ! empty( $state ) ? ', ' . $state : '' ) . ( ! empty( $zip ) ? ' ' . $zip : '' ) );
if ( ! empty( $city_state_zip ) ) {
$lines[] = esc_html( $city_state_zip );
}
if ( ! empty( $country ) && 'USA' !== $country ) {
$lines[] = esc_html( $country );
}
$output = implode( ' ', $lines );
}
if ( empty( $output ) ) {
return esc_html( $atts['default'] );
}
$class_attr = ! empty( $atts['class'] ) ? ' class="' . esc_attr( $atts['class'] ) . '"' : '';
return '' . $output . ' ';
}
add_shortcode( 'a1tools_city_address', 'a1tools_shortcode_city_address' );
/**
* Shortcode: [a1tools_city_list]
* Renders a linked list of city pages, optionally grouped by group_name.
* Uses get_permalink() for page URLs. Groups are assigned in the Flutter app.
*
* Attributes:
* - layout: 'list' (single column) or 'columns' (multi-column CSS columns)
* - columns: Number of columns (1-4, default 3, used when layout=columns)
* - show_groups: 'yes' or '' — show group headings
* - group: Filter to a specific group name
* - type: 'all', 'single_city', or 'location_city'
* - show_bullets: 'yes' or '' — show list bullets
* - target: Link target: '_self' or '_blank'
* - append_state: 'yes' to append state suffix after city name
* - state: State suffix text (e.g., 'NJ')
* - class: Additional CSS class for the wrapper
*
* @since 1.8.1
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_city_list( $atts ) {
$atts = shortcode_atts(
array(
'layout' => 'list',
'columns' => '3',
'show_groups' => 'yes',
'group' => '',
'type' => 'all',
'show_bullets' => 'yes',
'target' => '_self',
'append_state' => '',
'state' => '',
'class' => '',
'accordion' => '',
'accordion_default' => 'collapsed',
'accordion_mode' => 'multiple',
'accordion_icon' => 'yes',
'accordion_speed' => '300',
'show_count' => '',
'show_search' => '',
'search_placeholder' => 'Search cities...',
'_elementor' => '',
),
$atts,
'a1tools_city_list'
);
// Get ALL WordPress city pages from the local WP database (includes every
// published page by "Single City" / "Location City" authors, excludes "SEO City").
$wp_city_pages = a1tools_get_wp_city_pages();
// Get saved data from the A1 Tools API (phone, address, group — only for
// cities that have been edited in the app). Keyed by wp_page_id.
$saved_data = a1tools_get_site_city_pages();
if ( ! is_array( $saved_data ) ) {
$saved_data = array();
}
// Merge: start with all WP pages, overlay saved data (phone/address/group).
// If WP query returned results, use it as the base (shows ALL city pages).
// Otherwise fall back to API-only data (only pages with saved data).
$base_pages = ! empty( $wp_city_pages ) ? $wp_city_pages : $saved_data;
if ( empty( $base_pages ) ) {
return '';
}
$cities = array();
foreach ( $base_pages as $wp_page_id => $wp_data ) {
$permalink = get_permalink( intval( $wp_page_id ) );
if ( empty( $permalink ) ) {
continue; // Skip cities whose WP page no longer exists.
}
// Overlay saved data if this city has been edited in the app.
if ( ! empty( $wp_city_pages ) && isset( $saved_data[ $wp_page_id ] ) ) {
$wp_data = array_merge( $wp_data, $saved_data[ $wp_page_id ] );
}
$wp_data['wp_page_id'] = $wp_page_id;
$wp_data['permalink'] = $permalink;
$cities[] = $wp_data;
}
// Filter by type.
if ( 'all' !== $atts['type'] && ! empty( $atts['type'] ) ) {
$filter_type = $atts['type'];
$cities = array_filter(
$cities,
function ( $c ) use ( $filter_type ) {
return isset( $c['page_type'] ) && $c['page_type'] === $filter_type;
}
);
}
// Filter by specific group.
if ( ! empty( $atts['group'] ) ) {
$filter_group = $atts['group'];
$cities = array_filter(
$cities,
function ( $c ) use ( $filter_group ) {
return isset( $c['group_name'] ) && $c['group_name'] === $filter_group;
}
);
}
if ( empty( $cities ) ) {
return '';
}
// Sort by group_order, group_name, then page_title.
usort(
$cities,
function ( $a, $b ) {
$ao = intval( $a['group_order'] ?? 0 );
$bo = intval( $b['group_order'] ?? 0 );
if ( $ao !== $bo ) {
return $ao - $bo;
}
$gn = strcasecmp( $a['group_name'] ?? '', $b['group_name'] ?? '' );
if ( 0 !== $gn ) {
return $gn;
}
return strcasecmp( $a['page_title'] ?? '', $b['page_title'] ?? '' );
}
);
// Parse settings.
$show_groups = 'yes' === $atts['show_groups'];
$show_bullets = 'yes' === $atts['show_bullets'];
$target = esc_attr( $atts['target'] );
$is_columns = 'columns' === $atts['layout'];
$accordion = 'yes' === $atts['accordion'] && $show_groups;
$acc_expanded = 'expanded' === $atts['accordion_default'];
$acc_mode = $atts['accordion_mode'];
$acc_icon = 'yes' === $atts['accordion_icon'];
$acc_speed = max( 0, intval( $atts['accordion_speed'] ) );
$show_count = 'yes' === $atts['show_count'] && $show_groups;
$show_search = 'yes' === $atts['show_search'];
// List style attributes.
$list_style = $show_bullets ? '' : ' style="list-style:none;padding-left:0;"';
$from_elementor = 'yes' === $atts['_elementor'];
$col_style = '';
if ( $is_columns && ! $from_elementor ) {
// Standalone shortcode: apply columns via inline style.
// When rendered by the Elementor widget, Elementor's responsive
// selectors handle columns + column-gap (supports mobile/tablet
// breakpoints), so we must NOT add an inline style that would
// override them due to CSS specificity.
$columns = max( 1, min( 4, intval( $atts['columns'] ) ) );
$col_style = ' style="columns:' . $columns . ';column-gap:20px;"';
}
// Wrapper class and data attributes.
$wrapper_class = 'a1tools-city-list-wrapper';
if ( ! empty( $atts['class'] ) ) {
$wrapper_class .= ' ' . sanitize_html_class( $atts['class'] );
}
$data_attrs = '';
if ( $accordion ) {
$data_attrs .= ' data-accordion="1"'
. ' data-accordion-mode="' . esc_attr( $acc_mode ) . '"'
. ' data-accordion-speed="' . esc_attr( $acc_speed ) . '"';
}
if ( $show_search ) {
$data_attrs .= ' data-search="1"';
}
$output = '';
// Search bar.
if ( $show_search ) {
$placeholder = ! empty( $atts['search_placeholder'] ) ? $atts['search_placeholder'] : 'Search cities...';
$output .= '
'
. ' '
. '
';
}
// Helper to render a single city link.
$render_link = function ( $city ) use ( $atts, $target ) {
$label = isset( $city['page_title'] ) ? $city['page_title'] : '';
if ( 'yes' === $atts['append_state'] && ! empty( $atts['state'] ) ) {
$label .= ', ' . $atts['state'];
}
return '
' . esc_html( $label ) . ' ';
};
if ( $show_groups ) {
// Group cities by group_name.
$groups = array();
$ungrouped = array();
foreach ( $cities as $city ) {
$gn = isset( $city['group_name'] ) ? $city['group_name'] : '';
if ( ! empty( $gn ) ) {
if ( ! isset( $groups[ $gn ] ) ) {
$groups[ $gn ] = array();
}
$groups[ $gn ][] = $city;
} else {
$ungrouped[] = $city;
}
}
// Render each group.
foreach ( $groups as $group_name => $group_cities ) {
$city_count = count( $group_cities );
$output .= '
'; // .a1tools-city-group
}
// Render ungrouped at the end with "Ungrouped" heading.
if ( ! empty( $ungrouped ) ) {
$ug_count = count( $ungrouped );
$output .= '
';
}
} else {
// Flat list.
$output .= '
';
}
$output .= '
';
// Base styles (output once per page via static flag).
static $styles_output = false;
if ( ! $styles_output ) {
$output .= '';
$styles_output = true;
}
// Inline JS for accordion + search (output once per page).
static $js_output = false;
if ( ! $js_output && ( $accordion || $show_search ) ) {
$output .= '';
$js_output = true;
}
return $output;
}
add_shortcode( 'a1tools_city_list', 'a1tools_shortcode_city_list' );
// ============================================================================
// FRANCHISE LOCATION & STORE LOCATOR SHORTCODES
// ============================================================================
/**
* Shortcode: [a1tools_franchise]
* Outputs franchise location cards with name, phone, address, and map embed.
* Supports multiple franchises per site in a responsive grid layout.
*
* Attributes:
* show_phone - yes/no (default: yes)
* show_address - yes/no (default: yes)
* show_map - yes/no (default: yes)
* show_directions - yes/no (default: yes)
* map_height - pixels (default: 300)
* map_zoom - 1-20 (default: 15)
* columns - 1-3 grid columns (default: 1)
* id - show only a specific franchise by ID (optional)
* class - custom CSS class (default: a1tools-franchise-card)
*
* @since 1.7.0
* @updated 1.7.1 Multi-franchise support with grid layout.
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_franchise( $atts ) {
$atts = shortcode_atts(
array(
'show_title' => 'yes',
'show_phone' => 'yes',
'show_address' => 'yes',
'show_map' => 'yes',
'show_directions' => 'yes',
'map_height' => '300',
'map_zoom' => '15',
'columns' => '1',
'id' => '',
'class' => 'a1tools-franchise-card',
),
$atts,
'a1tools_franchise'
);
$franchises = a1tools_get_site_franchises();
if ( empty( $franchises ) ) {
return '';
}
// If a specific franchise ID is requested, filter to just that one.
if ( ! empty( $atts['id'] ) ) {
$target_id = intval( $atts['id'] );
$franchises = array_filter(
$franchises,
function ( $f ) use ( $target_id ) {
return intval( $f['id'] ) === $target_id;
}
);
if ( empty( $franchises ) ) {
return '';
}
}
$columns = max( 1, min( 3, intval( $atts['columns'] ) ) );
// Grid wrapper.
$output = ''; // .a1tools-franchise-grid
// Inline styles (output once).
$output .= '';
return $output;
}
add_shortcode( 'a1tools_franchise', 'a1tools_shortcode_franchise' );
/**
* Shortcode: [a1tools_store_locator]
* Interactive store locator with Google Maps JS API, custom markers, and split-panel layout.
* Falls back to iframe embeds when no Google Maps API key is configured.
*
* @since 1.7.0
* @updated 1.7.6 Google Maps JS API with custom markers, image cards, professional layout.
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_store_locator( $atts ) {
$atts = shortcode_atts(
array(
'map_height' => '500',
'show_search' => 'yes',
'show_filters' => 'yes',
'show_images' => 'yes',
'marker_icon' => '',
'api_key' => '',
'class' => 'a1tools-store-locator',
),
$atts,
'a1tools_store_locator'
);
$stores = a1tools_get_group_stores();
if ( empty( $stores ) ) {
return ' ' . esc_html__( 'No store locations found.', 'a1-tools' ) . '
';
}
// Resolve API key and marker icon (shortcode attr > WP option > empty).
$api_key = ! empty( $atts['api_key'] ) ? $atts['api_key'] : get_option( 'a1tools_google_maps_api_key', '' );
$marker_icon = ! empty( $atts['marker_icon'] ) ? $atts['marker_icon'] : get_option( 'a1tools_marker_icon_url', '' );
$use_js_api = ! empty( $api_key );
// Collect unique states for filter dropdown.
$states = array();
foreach ( $stores as $store ) {
if ( ! empty( $store['state'] ) && ! in_array( $store['state'], $states, true ) ) {
$states[] = $store['state'];
}
}
sort( $states );
// Build JSON data for JavaScript.
$stores_json = array();
foreach ( $stores as $store ) {
$addr_parts = array();
if ( ! empty( $store['address_line1'] ) ) {
$addr_parts[] = $store['address_line1'];
}
if ( ! empty( $store['address_line2'] ) ) {
$addr_parts[] = $store['address_line2'];
}
$csz = array();
if ( ! empty( $store['city'] ) ) {
$csz[] = $store['city'];
}
if ( ! empty( $store['state'] ) ) {
$csz[] = $store['state'];
}
if ( ! empty( $store['zip'] ) ) {
$csz[] = $store['zip'];
}
if ( ! empty( $csz ) ) {
$addr_parts[] = implode( ', ', $csz );
}
$stores_json[] = array(
'id' => intval( $store['id'] ?? 0 ),
'name' => $store['name'] ?? '',
'address' => implode( ', ', $addr_parts ),
'city' => strtolower( $store['city'] ?? '' ),
'state' => strtolower( $store['state'] ?? '' ),
'zip' => $store['zip'] ?? '',
'phone' => $store['phone'] ?? '',
'email' => $store['email'] ?? '',
'website' => $store['website_url'] ?? '',
'mapsUrl' => $store['google_maps_url'] ?? '',
'image' => $store['image_url'] ?? '',
'lat' => ! empty( $store['latitude'] ) ? floatval( $store['latitude'] ) : null,
'lng' => ! empty( $store['longitude'] ) ? floatval( $store['longitude'] ) : null,
);
}
$unique_id = 'a1sl-' . wp_unique_id();
$map_height = intval( $atts['map_height'] );
$output = '';
// ── Search & Filters ──
if ( 'yes' === $atts['show_search'] || 'yes' === $atts['show_filters'] ) {
$output .= '
';
}
// ── Two-panel layout ──
$output .= '
'; // .a1tools-sl-layout
$output .= '
'; // .a1tools-store-locator
// ── Inline styles ──
$output .= '';
// ── JavaScript ──
$stores_js = wp_json_encode( $stores_json );
$marker_js = $marker_icon ? "'" . esc_js( $marker_icon ) . "'" : 'null';
if ( $use_js_api ) {
// Enqueue Google Maps JS API (only once per page).
$output .= '';
}
$output .= '';
return $output;
}
add_shortcode( 'a1tools_store_locator', 'a1tools_shortcode_store_locator' );
// ============================================================================
// SERVICES, REVIEWS, BEFORE/AFTER, TEAM & FAQ SHORTCODES
// ============================================================================
/**
* Shortcode: [a1tools_services]
* Outputs a grid or list of service cards with title, description, phone, and icon/image.
*
* Merges local WP service pages (by "Large Service" authors) with API data,
* following the same merge pattern as the city list shortcode.
*
* @since 1.9.0
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_services( $atts ) {
$atts = shortcode_atts(
array(
'layout' => 'grid',
'columns' => '3',
'show_description' => 'yes',
'show_icon' => 'yes',
'show_image' => 'yes',
'show_phone' => 'yes',
'featured_only' => 'no',
'target' => '_self',
'class' => '',
'_elementor' => '',
),
$atts,
'a1tools_services'
);
// Get ALL WordPress service pages from local WP database.
$wp_service_pages = a1tools_get_wp_service_pages();
// Get saved data from the A1 Tools API (description, icon, phone, image).
$saved_data = a1tools_get_site_services();
if ( ! is_array( $saved_data ) ) {
$saved_data = array();
}
// Merge: start with all WP pages, overlay saved data.
$base_pages = ! empty( $wp_service_pages ) ? $wp_service_pages : $saved_data;
if ( empty( $base_pages ) ) {
return '';
}
$services = array();
foreach ( $base_pages as $wp_page_id => $wp_data ) {
$permalink = get_permalink( intval( $wp_page_id ) );
if ( empty( $permalink ) ) {
continue;
}
// Overlay saved data if this service has been edited in the app.
if ( ! empty( $wp_service_pages ) && isset( $saved_data[ $wp_page_id ] ) ) {
$wp_data = array_merge( $wp_data, $saved_data[ $wp_page_id ] );
}
$wp_data['wp_page_id'] = $wp_page_id;
$wp_data['permalink'] = $permalink;
$services[] = $wp_data;
}
// Filter featured only.
if ( 'yes' === $atts['featured_only'] ) {
$services = array_filter(
$services,
function ( $s ) {
return ! empty( $s['featured'] );
}
);
}
if ( empty( $services ) ) {
return '';
}
// Sort alphabetically by page_title.
usort(
$services,
function ( $a, $b ) {
return strcasecmp( $a['page_title'] ?? '', $b['page_title'] ?? '' );
}
);
// Parse settings.
$show_desc = 'yes' === $atts['show_description'];
$show_icon = 'yes' === $atts['show_icon'];
$show_image = 'yes' === $atts['show_image'];
$show_phone = 'yes' === $atts['show_phone'];
$target = esc_attr( $atts['target'] );
$is_list = 'list' === $atts['layout'];
$columns = max( 1, min( 4, intval( $atts['columns'] ) ) );
$from_elementor = 'yes' === $atts['_elementor'];
$wrapper_class = 'a1tools-services-grid';
if ( $is_list ) {
$wrapper_class = 'a1tools-services-list';
}
if ( ! empty( $atts['class'] ) ) {
$wrapper_class .= ' ' . sanitize_html_class( $atts['class'] );
}
$grid_style = '';
if ( ! $is_list && ! $from_elementor ) {
$grid_style = ' style="grid-template-columns:repeat(' . $columns . ',1fr);"';
}
$output = '';
foreach ( $services as $service ) {
$title = isset( $service['page_title'] ) ? $service['page_title'] : '';
$output .= '
'; // .a1tools-service-card
}
$output .= '
';
// Inline CSS (output once per page).
static $services_styles_output = false;
if ( ! $services_styles_output ) {
$output .= '';
$services_styles_output = true;
}
return $output;
}
add_shortcode( 'a1tools_services', 'a1tools_shortcode_services' );
/**
* Shortcode: [a1tools_reviews]
* Outputs a grid of review cards with star rating, reviewer name, review text, and source badge.
*
* @since 1.9.0
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_reviews( $atts ) {
$atts = shortcode_atts(
array(
'layout' => 'grid',
'columns' => '3',
'show_stars' => 'yes',
'show_source' => 'yes',
'show_date' => 'yes',
'show_image' => 'yes',
'featured_only' => 'no',
'limit' => '0',
'class' => '',
),
$atts,
'a1tools_reviews'
);
$reviews = a1tools_get_group_reviews();
if ( empty( $reviews ) || ! is_array( $reviews ) ) {
return '';
}
// Filter featured only.
if ( 'yes' === $atts['featured_only'] ) {
$reviews = array_filter(
$reviews,
function ( $r ) {
return ! empty( $r['featured'] );
}
);
}
// Apply limit.
$limit = intval( $atts['limit'] );
if ( $limit > 0 ) {
$reviews = array_slice( $reviews, 0, $limit );
}
if ( empty( $reviews ) ) {
return '';
}
// Parse settings.
$show_stars = 'yes' === $atts['show_stars'];
$show_source = 'yes' === $atts['show_source'];
$show_date = 'yes' === $atts['show_date'];
$show_image = 'yes' === $atts['show_image'];
$columns = max( 1, min( 4, intval( $atts['columns'] ) ) );
$wrapper_class = 'a1tools-reviews-grid';
if ( ! empty( $atts['class'] ) ) {
$wrapper_class .= ' ' . sanitize_html_class( $atts['class'] );
}
$output = '';
foreach ( $reviews as $review ) {
$output .= '
'; // .a1tools-review-card
}
$output .= '
';
// Inline CSS (output once per page).
static $reviews_styles_output = false;
if ( ! $reviews_styles_output ) {
$output .= '';
$reviews_styles_output = true;
}
return $output;
}
add_shortcode( 'a1tools_reviews', 'a1tools_shortcode_reviews' );
/**
* Shortcode: [a1tools_before_after]
* Outputs before/after comparison items as interactive sliders or side-by-side grid.
*
* Slider mode uses CSS clip-path with a JS drag handle for interactive comparison.
* Grid mode shows before/after image pairs side by side with labels.
*
* @since 1.9.0
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_before_after( $atts ) {
$atts = shortcode_atts(
array(
'layout' => 'slider',
'columns' => '2',
'show_title' => 'yes',
'show_description' => 'yes',
'class' => '',
),
$atts,
'a1tools_before_after'
);
$items = a1tools_get_group_before_after();
if ( empty( $items ) || ! is_array( $items ) ) {
return '';
}
$show_title = 'yes' === $atts['show_title'];
$show_desc = 'yes' === $atts['show_description'];
$is_slider = 'slider' === $atts['layout'];
$columns = max( 1, min( 4, intval( $atts['columns'] ) ) );
$wrapper_class = $is_slider ? 'a1tools-ba-slider-wrapper' : 'a1tools-ba-grid-wrapper';
if ( ! empty( $atts['class'] ) ) {
$wrapper_class .= ' ' . sanitize_html_class( $atts['class'] );
}
$grid_style = '';
if ( ! $is_slider ) {
$grid_style = ' style="grid-template-columns:repeat(' . $columns . ',1fr);"';
}
$output = '';
foreach ( $items as $index => $item ) {
$before_url = isset( $item['before_image'] ) ? $item['before_image'] : '';
$after_url = isset( $item['after_image'] ) ? $item['after_image'] : '';
$title = isset( $item['title'] ) ? $item['title'] : '';
if ( empty( $before_url ) || empty( $after_url ) ) {
continue;
}
if ( $is_slider ) {
// Slider mode: interactive comparison slider.
$output .= '
';
} else {
// Grid mode: side-by-side image pairs.
$output .= '
';
}
}
$output .= '
';
// Inline CSS (output once per page).
static $ba_styles_output = false;
if ( ! $ba_styles_output ) {
$output .= '';
$ba_styles_output = true;
}
// Inline JS for slider interaction (output once per page).
static $ba_js_output = false;
if ( ! $ba_js_output && $is_slider ) {
$output .= '';
$ba_js_output = true;
}
return $output;
}
add_shortcode( 'a1tools_before_after', 'a1tools_shortcode_before_after' );
/**
* Shortcode: [a1tools_team]
* Outputs a grid or list of team member cards with photo, name, role, bio, contact info,
* and certification badges.
*
* @since 1.9.0
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_team( $atts ) {
$atts = shortcode_atts(
array(
'layout' => 'grid',
'columns' => '3',
'show_photo' => 'yes',
'photo_shape' => 'circle',
'show_role' => 'yes',
'show_bio' => 'yes',
'show_phone' => 'yes',
'show_email' => 'yes',
'show_certifications' => 'yes',
'featured_only' => 'no',
'category' => '',
'category_order' => '',
'show_category_heading' => 'yes',
'class' => '',
'_elementor' => '',
),
$atts,
'a1tools_team'
);
$members = a1tools_get_group_team();
if ( empty( $members ) || ! is_array( $members ) ) {
return '';
}
// Filter featured only.
if ( 'yes' === $atts['featured_only'] ) {
$members = array_filter(
$members,
function ( $m ) {
return ! empty( $m['featured'] );
}
);
}
if ( empty( $members ) ) {
return '';
}
// Filter by category if specified.
if ( ! empty( $atts['category'] ) ) {
$cat_filter = strtolower( trim( $atts['category'] ) );
$members = array_filter(
$members,
function ( $m ) use ( $cat_filter ) {
return isset( $m['category'] ) && strtolower( trim( $m['category'] ) ) === $cat_filter;
}
);
}
if ( empty( $members ) ) {
return '';
}
// Parse settings.
$show_photo = 'yes' === $atts['show_photo'];
$photo_shape = in_array( $atts['photo_shape'], array( 'circle', 'square' ), true ) ? $atts['photo_shape'] : 'circle';
$show_role = 'yes' === $atts['show_role'];
$show_bio = 'yes' === $atts['show_bio'];
$show_phone = 'yes' === $atts['show_phone'];
$show_email = 'yes' === $atts['show_email'];
$show_certs = 'yes' === $atts['show_certifications'];
$is_list = 'list' === $atts['layout'];
$columns = max( 1, min( 6, intval( $atts['columns'] ) ) );
$show_cat_heading = 'yes' === $atts['show_category_heading'];
$category_order = array_filter( array_map( 'trim', explode( ',', $atts['category_order'] ) ) );
$is_elementor = 'yes' === ( $atts['_elementor'] ?? '' );
$wrapper_class = $is_list ? 'a1tools-team-list' : 'a1tools-team-grid';
if ( ! empty( $atts['class'] ) ) {
$wrapper_class .= ' ' . sanitize_html_class( $atts['class'] );
}
$grid_style = '';
if ( ! $is_list && ! $is_elementor ) {
// Only output inline grid columns for non-Elementor usage.
// Elementor handles columns via its own selectors to support responsive breakpoints.
$grid_style = ' style="grid-template-columns:repeat(' . $columns . ',1fr);"';
}
// Group members by category.
$grouped = array();
foreach ( $members as $member ) {
$cat = ( ! empty( $member['category'] ) ) ? trim( $member['category'] ) : 'Uncategorized';
if ( ! isset( $grouped[ $cat ] ) ) {
$grouped[ $cat ] = array();
}
$grouped[ $cat ][] = $member;
}
// Sort categories: use category_order if provided, else by category_sort_order, else alphabetical.
if ( ! empty( $category_order ) ) {
$sorted_groups = array();
foreach ( $category_order as $cat_name ) {
if ( isset( $grouped[ $cat_name ] ) ) {
$sorted_groups[ $cat_name ] = $grouped[ $cat_name ];
unset( $grouped[ $cat_name ] );
}
}
// Append remaining categories.
foreach ( $grouped as $cat_name => $cat_members ) {
$sorted_groups[ $cat_name ] = $cat_members;
}
$grouped = $sorted_groups;
} else {
// Sort by category_sort_order from the first member in each group.
uksort(
$grouped,
function ( $a, $b ) use ( $grouped ) {
if ( 'Uncategorized' === $a ) {
return 1;
}
if ( 'Uncategorized' === $b ) {
return -1;
}
$a_order = isset( $grouped[ $a ][0]['category_sort_order'] ) ? intval( $grouped[ $a ][0]['category_sort_order'] ) : 0;
$b_order = isset( $grouped[ $b ][0]['category_sort_order'] ) ? intval( $grouped[ $b ][0]['category_sort_order'] ) : 0;
if ( $a_order !== $b_order ) {
return $a_order - $b_order;
}
return strcasecmp( $a, $b );
}
);
}
$output = ''; // .a1tools-team-wrapper
// Inline CSS (output once per page).
static $team_styles_output = false;
if ( ! $team_styles_output ) {
$output .= '';
$team_styles_output = true;
}
return $output;
}
add_shortcode( 'a1tools_team', 'a1tools_shortcode_team' );
/**
* Shortcode: [a1tools_faq]
* Outputs an accordion of FAQ items with optional category filtering
* and FAQPage JSON-LD structured data.
*
* Reuses the same accordion CSS classes and JS pattern from the city list shortcode.
*
* @since 1.9.0
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_faq( $atts ) {
$atts = shortcode_atts(
array(
'category' => '',
'show_category' => 'yes',
'accordion_icon' => 'yes',
'schema' => 'yes',
'class' => '',
),
$atts,
'a1tools_faq'
);
$faqs = a1tools_get_group_faq();
if ( empty( $faqs ) || ! is_array( $faqs ) ) {
return '';
}
// Filter by category.
if ( ! empty( $atts['category'] ) ) {
$filter_cat = $atts['category'];
$faqs = array_filter(
$faqs,
function ( $f ) use ( $filter_cat ) {
return isset( $f['category'] ) && $f['category'] === $filter_cat;
}
);
}
if ( empty( $faqs ) ) {
return '';
}
$show_cat = 'yes' === $atts['show_category'];
$acc_icon = 'yes' === $atts['accordion_icon'];
$schema = 'yes' === $atts['schema'];
$wrapper_class = 'a1tools-faq-wrapper a1tools-city-list-wrapper';
if ( ! empty( $atts['class'] ) ) {
$wrapper_class .= ' ' . sanitize_html_class( $atts['class'] );
}
$output = '';
// Schema data collector.
$schema_items = array();
// Group by category if show_category is enabled.
if ( $show_cat ) {
$categories = array();
$uncategorized = array();
foreach ( $faqs as $faq ) {
$cat = isset( $faq['category'] ) && ! empty( $faq['category'] ) ? $faq['category'] : '';
if ( ! empty( $cat ) ) {
$categories[ $cat ][] = $faq;
} else {
$uncategorized[] = $faq;
}
}
// Render categorized FAQs.
foreach ( $categories as $cat_name => $cat_faqs ) {
$output .= '
'
. '';
foreach ( $cat_faqs as $faq ) {
$output .= a1tools_render_faq_item( $faq, $acc_icon, $schema_items );
}
$output .= '
';
}
// Render uncategorized.
foreach ( $uncategorized as $faq ) {
$output .= a1tools_render_faq_item( $faq, $acc_icon, $schema_items );
}
} else {
// Flat list.
foreach ( $faqs as $faq ) {
$output .= a1tools_render_faq_item( $faq, $acc_icon, $schema_items );
}
}
$output .= '
';
// FAQPage JSON-LD schema.
if ( $schema && ! empty( $schema_items ) ) {
$schema_data = array(
'@context' => 'https://schema.org',
'@type' => 'FAQPage',
'mainEntity' => $schema_items,
);
$output .= '';
}
// Inline CSS for FAQ-specific styles (output once per page).
// Reuses accordion classes from city list.
static $faq_styles_output = false;
if ( ! $faq_styles_output ) {
$output .= '';
$faq_styles_output = true;
}
// Inline JS reuses the city list accordion JS. Output a small init
// that delegates to the same pattern (only if city list JS has not loaded).
static $faq_js_output = false;
if ( ! $faq_js_output ) {
$output .= '';
$faq_js_output = true;
}
return $output;
}
/**
* Helper: Render a single FAQ accordion item.
*
* @since 1.9.0
* @param array $faq FAQ data with 'question' and 'answer' keys.
* @param bool $acc_icon Whether to show accordion icon.
* @param array &$schema_items Reference to schema items array for JSON-LD.
* @return string HTML for the FAQ item.
*/
function a1tools_render_faq_item( $faq, $acc_icon, &$schema_items ) {
$question = isset( $faq['question'] ) ? $faq['question'] : '';
$answer = isset( $faq['answer'] ) ? $faq['answer'] : '';
if ( empty( $question ) ) {
return '';
}
// Collect for schema.
$schema_items[] = array(
'@type' => 'Question',
'name' => $question,
'acceptedAnswer' => array(
'@type' => 'Answer',
'text' => $answer,
),
);
$html = '';
return $html;
}
add_shortcode( 'a1tools_faq', 'a1tools_shortcode_faq' );
// ============================================================================
// POST TITLE SHORTCODE
// ============================================================================
/**
* Shortcode: [a1tools_post_title]
*
* Outputs the current post/page title. Useful in templates, Elementor,
* or anywhere a shortcode can be used.
*
* Attributes:
* tag - HTML tag to wrap the title (default: none, outputs plain text)
* Example: tag="h1", tag="h2", tag="span"
* class - CSS class(es) for the wrapper tag (default: none)
* id - Specific post ID to get the title for (default: current post)
* before - Text/HTML to prepend before the title
* after - Text/HTML to append after the title
*
* @since 2.0.7
*/
function a1tools_shortcode_post_title( $atts ) {
$atts = shortcode_atts(
array(
'tag' => '',
'class' => '',
'id' => 0,
'before' => '',
'after' => '',
),
$atts,
'a1tools_post_title'
);
$post_id = absint( $atts['id'] );
if ( $post_id > 0 ) {
$title = get_the_title( $post_id );
} else {
$title = get_the_title();
}
if ( empty( $title ) ) {
return '';
}
$title = esc_html( $title );
$output = $atts['before'] . $title . $atts['after'];
// Wrap in tag if specified
if ( ! empty( $atts['tag'] ) ) {
$tag = sanitize_key( $atts['tag'] );
$class = ! empty( $atts['class'] ) ? ' class="' . esc_attr( $atts['class'] ) . '"' : '';
$output = '<' . $tag . $class . '>' . $output . '' . $tag . '>';
}
return $output;
}
add_shortcode( 'a1tools_post_title', 'a1tools_shortcode_post_title' );
// ============================================================================
// SEO LOCATION SHORTCODES
// ============================================================================
/**
* Shortcode: [a1tools_location_phone]
* Outputs the SEO location-specific phone number for the current page.
* Falls back to site primary phone if no location-specific phone is set.
*
* Attributes:
* - default: Fallback text if no phone found
* - link: 'yes' to wrap in tel: link
* - class: CSS class for the element
* - page_id: Override automatic page ID detection
*
* @since 2.1.0
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_location_phone( $atts ) {
$atts = shortcode_atts(
array(
'default' => '',
'link' => 'no',
'class' => '',
'page_id' => '',
),
$atts,
'a1tools_location_phone'
);
$page_id = ! empty( $atts['page_id'] ) ? intval( $atts['page_id'] ) : get_the_ID();
$locations = a1tools_get_seo_locations();
// Get location-specific phone.
$phone = '';
if ( ! empty( $locations ) && isset( $locations[ $page_id ] ) ) {
$phone = isset( $locations[ $page_id ]['phone'] ) ? $locations[ $page_id ]['phone'] : '';
}
// Fallback to default or site primary phone.
if ( empty( $phone ) ) {
if ( ! empty( $atts['default'] ) ) {
return esc_html( $atts['default'] );
}
// Use the site's primary phone as fallback.
$phone = a1tools_get_variable( 'phone_primary', '' );
if ( empty( $phone ) ) {
return '';
}
}
// Output as tel: link if requested.
if ( 'yes' === $atts['link'] ) {
$clean = preg_replace( '/[^0-9+]/', '', $phone );
$class_attr = ! empty( $atts['class'] ) ? ' class="' . esc_attr( $atts['class'] ) . '"' : '';
return '' . esc_html( $phone ) . ' ';
}
if ( ! empty( $atts['class'] ) ) {
return '' . esc_html( $phone ) . ' ';
}
return esc_html( $phone );
}
add_shortcode( 'a1tools_location_phone', 'a1tools_shortcode_location_phone' );
/**
* Shortcode: [a1tools_location_web]
* Outputs the SEO location-specific website URL for the current page.
*
* Attributes:
* - default: Fallback text if no URL found
* - link: 'yes' to wrap in anchor tag
* - text: Custom link text (used when link=yes)
* - target: Link target attribute (default: _blank)
* - class: CSS class for the element
* - page_id: Override automatic page ID detection
*
* @since 2.1.0
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_location_web( $atts ) {
$atts = shortcode_atts(
array(
'default' => '',
'link' => 'no',
'text' => '',
'target' => '_blank',
'class' => '',
'page_id' => '',
),
$atts,
'a1tools_location_web'
);
$page_id = ! empty( $atts['page_id'] ) ? intval( $atts['page_id'] ) : get_the_ID();
$locations = a1tools_get_seo_locations();
$url = '';
if ( ! empty( $locations ) && isset( $locations[ $page_id ] ) ) {
$url = isset( $locations[ $page_id ]['location_web'] ) ? $locations[ $page_id ]['location_web'] : '';
}
if ( empty( $url ) ) {
return esc_html( $atts['default'] );
}
if ( 'yes' === $atts['link'] ) {
// Priority: text attribute > full_address field > URL.
$label = '';
if ( ! empty( $atts['text'] ) ) {
$label = esc_html( $atts['text'] );
} elseif ( ! empty( $locations[ $page_id ]['full_address'] ) ) {
$label = esc_html( $locations[ $page_id ]['full_address'] );
} else {
$label = esc_html( $url );
}
$class_attr = ! empty( $atts['class'] ) ? ' class="' . esc_attr( $atts['class'] ) . '"' : '';
return '' . $label . ' ';
}
return esc_html( $url );
}
add_shortcode( 'a1tools_location_web', 'a1tools_shortcode_location_web' );
/**
* Shortcode: [a1tools_city_web]
* Outputs the SEO location-specific city website URL for the current page.
*
* Attributes:
* - default: Fallback text if no URL found
* - link: 'yes' to wrap in anchor tag
* - text: Custom link text (used when link=yes)
* - target: Link target attribute (default: _blank)
* - class: CSS class for the element
* - page_id: Override automatic page ID detection
*
* @since 2.1.0
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_city_web( $atts ) {
$atts = shortcode_atts(
array(
'default' => '',
'link' => 'no',
'text' => '',
'target' => '_blank',
'class' => '',
'page_id' => '',
),
$atts,
'a1tools_city_web'
);
$page_id = ! empty( $atts['page_id'] ) ? intval( $atts['page_id'] ) : get_the_ID();
$locations = a1tools_get_seo_locations();
$url = '';
if ( ! empty( $locations ) && isset( $locations[ $page_id ] ) ) {
$url = isset( $locations[ $page_id ]['city_web'] ) ? $locations[ $page_id ]['city_web'] : '';
}
if ( empty( $url ) ) {
return esc_html( $atts['default'] );
}
if ( 'yes' === $atts['link'] ) {
// Priority: text attribute > city field > URL.
$label = '';
if ( ! empty( $atts['text'] ) ) {
$label = esc_html( $atts['text'] );
} elseif ( ! empty( $locations[ $page_id ]['city'] ) ) {
$label = esc_html( $locations[ $page_id ]['city'] );
} else {
$label = esc_html( $url );
}
$class_attr = ! empty( $atts['class'] ) ? ' class="' . esc_attr( $atts['class'] ) . '"' : '';
return '' . $label . ' ';
}
return esc_html( $url );
}
add_shortcode( 'a1tools_city_web', 'a1tools_shortcode_city_web' );
/**
* Shortcode: [a1tools_state_web]
* Outputs the SEO location-specific state website URL for the current page.
*
* Attributes:
* - default: Fallback text if no URL found
* - link: 'yes' to wrap in anchor tag
* - text: Custom link text (used when link=yes)
* - target: Link target attribute (default: _blank)
* - class: CSS class for the element
* - page_id: Override automatic page ID detection
*
* @since 2.1.0
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_state_web( $atts ) {
$atts = shortcode_atts(
array(
'default' => '',
'link' => 'no',
'text' => '',
'target' => '_blank',
'class' => '',
'page_id' => '',
),
$atts,
'a1tools_state_web'
);
$page_id = ! empty( $atts['page_id'] ) ? intval( $atts['page_id'] ) : get_the_ID();
$locations = a1tools_get_seo_locations();
$url = '';
if ( ! empty( $locations ) && isset( $locations[ $page_id ] ) ) {
$url = isset( $locations[ $page_id ]['state_web'] ) ? $locations[ $page_id ]['state_web'] : '';
}
if ( empty( $url ) ) {
return esc_html( $atts['default'] );
}
if ( 'yes' === $atts['link'] ) {
// Priority: text attribute > state field > URL.
$label = '';
if ( ! empty( $atts['text'] ) ) {
$label = esc_html( $atts['text'] );
} elseif ( ! empty( $locations[ $page_id ]['state'] ) ) {
$label = esc_html( $locations[ $page_id ]['state'] );
} else {
$label = esc_html( $url );
}
$class_attr = ! empty( $atts['class'] ) ? ' class="' . esc_attr( $atts['class'] ) . '"' : '';
return '' . $label . ' ';
}
return esc_html( $url );
}
add_shortcode( 'a1tools_state_web', 'a1tools_shortcode_state_web' );
/**
* Shortcode: [a1tools_location_city]
* Outputs the city name for the current SEO City page.
*
* Attributes:
* - default: Fallback text if no city found
* - class: CSS class for the wrapper span
* - page_id: Override automatic page ID detection
*
* @since 2.1.1
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_location_city( $atts ) {
$atts = shortcode_atts(
array(
'default' => '',
'class' => '',
'page_id' => '',
),
$atts,
'a1tools_location_city'
);
$page_id = ! empty( $atts['page_id'] ) ? intval( $atts['page_id'] ) : get_the_ID();
$locations = a1tools_get_seo_locations();
$city = '';
if ( ! empty( $locations ) && isset( $locations[ $page_id ] ) ) {
$city = isset( $locations[ $page_id ]['city'] ) ? $locations[ $page_id ]['city'] : '';
}
if ( empty( $city ) ) {
$city = ! empty( $atts['default'] ) ? $atts['default'] : a1tools_get_variable( 'city_name', '' );
}
if ( empty( $city ) ) {
return '';
}
$class_attr = ! empty( $atts['class'] ) ? ' class="' . esc_attr( $atts['class'] ) . '"' : '';
return '' . esc_html( $city ) . ' ';
}
add_shortcode( 'a1tools_location_city', 'a1tools_shortcode_location_city' );
/**
* Shortcode: [a1tools_location_address]
* Outputs the full address for the current SEO City page.
*
* Attributes:
* - default: Fallback text if no address found
* - class: CSS class for the wrapper span
* - page_id: Override automatic page ID detection
*
* @since 2.1.1
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_location_address( $atts ) {
$atts = shortcode_atts(
array(
'default' => '',
'class' => '',
'page_id' => '',
),
$atts,
'a1tools_location_address'
);
$page_id = ! empty( $atts['page_id'] ) ? intval( $atts['page_id'] ) : get_the_ID();
$locations = a1tools_get_seo_locations();
$address = '';
if ( ! empty( $locations ) && isset( $locations[ $page_id ] ) ) {
$address = isset( $locations[ $page_id ]['full_address'] ) ? $locations[ $page_id ]['full_address'] : '';
}
if ( empty( $address ) ) {
$address = ! empty( $atts['default'] ) ? $atts['default'] : a1tools_get_variable( 'full_address', '' );
}
if ( empty( $address ) ) {
return '';
}
$class_attr = ! empty( $atts['class'] ) ? ' class="' . esc_attr( $atts['class'] ) . '"' : '';
return '' . esc_html( $address ) . ' ';
}
add_shortcode( 'a1tools_location_address', 'a1tools_shortcode_location_address' );
/**
* Shortcode: [a1tools_location_state]
* Outputs the state for the current SEO City page.
*
* Attributes:
* - default: Fallback text if no state found
* - class: CSS class for the wrapper span
* - page_id: Override automatic page ID detection
*
* @since 2.1.1
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_location_state( $atts ) {
$atts = shortcode_atts(
array(
'default' => '',
'class' => '',
'page_id' => '',
),
$atts,
'a1tools_location_state'
);
$page_id = ! empty( $atts['page_id'] ) ? intval( $atts['page_id'] ) : get_the_ID();
$locations = a1tools_get_seo_locations();
$state = '';
if ( ! empty( $locations ) && isset( $locations[ $page_id ] ) ) {
$state = isset( $locations[ $page_id ]['state'] ) ? $locations[ $page_id ]['state'] : '';
}
if ( empty( $state ) ) {
$state = ! empty( $atts['default'] ) ? $atts['default'] : a1tools_get_variable( 'state', '' );
}
if ( empty( $state ) ) {
return '';
}
$class_attr = ! empty( $atts['class'] ) ? ' class="' . esc_attr( $atts['class'] ) . '"' : '';
return '' . esc_html( $state ) . ' ';
}
add_shortcode( 'a1tools_location_state', 'a1tools_shortcode_location_state' );
/**
* Shortcode: [a1tools_map_address]
* Outputs the SEO location-specific map address for the current page.
* Falls back to site full_address if no location-specific address is set.
*
* Attributes:
* - default: Fallback text if no address found
* - link: 'yes' to wrap in Google Maps link
* - target: Link target attribute (default: _blank)
* - class: CSS class for the element
* - page_id: Override automatic page ID detection
*
* @since 2.1.0
* @param array $atts Shortcode attributes.
* @return string Shortcode output.
*/
function a1tools_shortcode_map_address( $atts ) {
$atts = shortcode_atts(
array(
'default' => '',
'link' => 'no',
'target' => '_blank',
'class' => '',
'page_id' => '',
),
$atts,
'a1tools_map_address'
);
$page_id = ! empty( $atts['page_id'] ) ? intval( $atts['page_id'] ) : get_the_ID();
$locations = a1tools_get_seo_locations();
// Get location-specific map address.
$address = '';
if ( ! empty( $locations ) && isset( $locations[ $page_id ] ) ) {
$address = isset( $locations[ $page_id ]['map_address'] ) ? $locations[ $page_id ]['map_address'] : '';
}
// Fallback to default or site full_address.
if ( empty( $address ) ) {
if ( ! empty( $atts['default'] ) ) {
$address = $atts['default'];
} else {
$address = a1tools_get_variable( 'full_address', '' );
}
}
if ( empty( $address ) ) {
return '';
}
if ( 'yes' === $atts['link'] ) {
$maps_url = 'https://www.google.com/maps/search/' . urlencode( $address );
$class_attr = ! empty( $atts['class'] ) ? ' class="' . esc_attr( $atts['class'] ) . '"' : '';
return '' . esc_html( $address ) . ' ';
}
$class_attr = ! empty( $atts['class'] ) ? ' class="' . esc_attr( $atts['class'] ) . '"' : '';
return '' . esc_html( $address ) . ' ';
}
add_shortcode( 'a1tools_map_address', 'a1tools_shortcode_map_address' );
/**
* Shortcode: [a1tools_viewpoint]
* Outputs a Google Street View iframe for the current SEO location page.
* Supports both full