float, 'label' => '+107%', 'improved' => bool] or null.
*/
function sahcs_metric_change(array $item) {
$before = sahcs_parse_number(isset($item['before']) ? $item['before'] : '');
$after = sahcs_parse_number(isset($item['after']) ? $item['after'] : '');
if ($before === null || $after === null || (float) $before === 0.0) {
return null;
}
$pct = ($after - $before) / abs($before) * 100;
if (abs($pct) < 0.05) {
return null;
}
$better_lower = isset($item['better']) && $item['better'] === 'lower';
$improved = $better_lower ? $pct < 0 : $pct > 0;
$rounded = abs($pct) >= 10 ? round(abs($pct)) : round(abs($pct), 1);
$label = ($pct > 0 ? '+' : "\u{2212}") . number_format_i18n($rounded, (abs($pct) >= 10 ? 0 : 1)) . '%';
return ['pct' => $pct, 'label' => $label, 'improved' => $improved];
}
function sahcs_format_value($value, array $item = []) {
$value = sahcs_trim((string) $value);
if ($value === '') {
return '';
}
$prefix = isset($item['prefix']) ? (string) $item['prefix'] : '';
$suffix = isset($item['suffix']) ? (string) $item['suffix'] : '';
return $prefix . $value . $suffix;
}
/* ------------------------------------------------------------------
* Small helpers
* ---------------------------------------------------------------- */
/** Safe list of allowed heading tags for cards and section titles. */
function sahcs_heading_tag($tag, $default = 'h3') {
return in_array($tag, ['h2', 'h3', 'h4', 'p'], true) ? $tag : $default;
}
/**
* A short summary of a case study: the manual excerpt if there is one,
* otherwise the "At a glance" text or the first paragraph.
*/
function sahcs_card_excerpt($post, $words = 24) {
$post = get_post($post);
if (!$post) {
return '';
}
if (has_excerpt($post)) {
return wp_trim_words(get_the_excerpt($post), $words);
}
$text = '';
$walk = function ($blocks) use (&$walk, &$text) {
foreach ($blocks as $b) {
if ($text !== '') {
return;
}
if ($b['blockName'] === 'sahcs/hero' && !empty($b['attrs']['intro'])) {
$text = $b['attrs']['intro'];
return;
}
if ($b['blockName'] === 'core/paragraph') {
$t = sahcs_trim(wp_strip_all_tags($b['innerHTML']));
if ($t !== '') {
$text = $t;
return;
}
}
if (!empty($b['innerBlocks'])) {
$walk($b['innerBlocks']);
}
}
};
$walk(parse_blocks($post->post_content));
return wp_trim_words($text, $words);
}
/** Terms of a case study, cached by WordPress. */
function sahcs_post_terms($post_id) {
$terms = get_the_terms($post_id, SAHCS_TAX);
return (is_array($terms)) ? $terms : [];
}
/* ------------------------------------------------------------------
* Case study card
* ---------------------------------------------------------------- */
/**
* @param int $post_id
* @param array $args heading (h2|h3|h4), excerpt (bool), eager (bool), stat (bool)
*/
function sahcs_card($post_id, array $args = []) {
$args = array_merge(['heading' => 'h3', 'excerpt' => true, 'eager' => false, 'priority' => false, 'stat' => true], $args);
$post = get_post($post_id);
if (!$post) {
return '';
}
$tag = sahcs_heading_tag($args['heading'], 'h3');
$url = get_permalink($post);
$title = get_the_title($post);
$terms = sahcs_post_terms($post->ID);
$media = '';
if (has_post_thumbnail($post)) {
$img_attrs = [
'class' => 'sahcs-card__img',
'loading' => $args['eager'] ? 'eager' : 'lazy',
'sizes' => '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw',
'alt' => '',
// Some themes add inline sizes to every image; these win over them.
'style' => 'width:100%;height:100%;max-width:100%',
];
if ($args['priority']) {
$img_attrs['fetchpriority'] = 'high';
}
$media = get_the_post_thumbnail($post, 'medium_large', $img_attrs);
}
if ($media === '') {
$initial = function_exists('mb_substr') ? mb_substr(wp_strip_all_tags($title), 0, 1) : substr(wp_strip_all_tags($title), 0, 1);
$media = '' . esc_html($initial) . '';
}
$term_slugs = wp_list_pluck($terms, 'slug');
$out = '';
$out .= '' . $media . '
';
$out .= '';
if ($terms) {
$out .= '
';
foreach (array_slice($terms, 0, 2) as $t) {
$out .= '' . esc_html($t->name) . '';
}
$out .= '
';
}
$out .= '<' . $tag . ' class="sahcs-card__title">
' . esc_html(wp_strip_all_tags($title)) . '' . $tag . '>';
if ($args['stat']) {
$hl = get_post_meta($post->ID, '_sahcs_highlight', true);
if (is_array($hl) && !empty($hl['value'])) {
$out .= '
' . esc_html($hl['value']) . ' ' . esc_html(isset($hl['label']) ? $hl['label'] : '') . '
';
}
}
if ($args['excerpt']) {
$excerpt = sahcs_card_excerpt($post);
if ($excerpt !== '') {
$out .= '
' . esc_html($excerpt) . '
';
}
}
$out .= '
' . esc_html__('Read case study', 'siteaudithub-case-studies') . '→';
$out .= '
';
return $out;
}
/* ------------------------------------------------------------------
* Grid (used by the shortcode, the "Case Studies" block and Related)
* ---------------------------------------------------------------- */
function sahcs_grid_defaults() {
return [
'limit' => 6,
'category' => '',
'ids' => '',
'exclude' => '', // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- shortcode attribute default, not a query argument
'columns' => 3,
'layout' => 'grid', // grid | list
'filters' => false,
'orderby' => 'latest', // latest | oldest | title | random | menu
'excerpt' => true,
'button' => false,
'button_text' => '',
'heading' => '', // '' = automatic: h2 without a title, h3 under a title
'title' => '',
];
}
function sahcs_bool($v) {
if (is_bool($v)) {
return $v;
}
return in_array(strtolower((string) $v), ['1', 'yes', 'true', 'on'], true);
}
function sahcs_grid(array $args) {
$a = array_merge(sahcs_grid_defaults(), $args);
$a['limit'] = max(1, min(60, (int) $a['limit']));
$a['columns'] = max(1, min(4, (int) $a['columns']));
$a['layout'] = $a['layout'] === 'list' ? 'list' : 'grid';
$a['filters'] = sahcs_bool($a['filters']);
$a['excerpt'] = sahcs_bool($a['excerpt']);
$a['button'] = sahcs_bool($a['button']);
$query = [
'post_type' => SAHCS_CPT,
'post_status' => 'publish',
'posts_per_page' => $a['limit'],
'ignore_sticky_posts' => true,
'no_found_rows' => true,
];
$ids = array_values(array_unique(array_filter(array_map('sahcs_localize_id', array_map('absint', explode(',', (string) $a['ids']))))));
if ($ids) {
$query['post__in'] = $ids;
$query['orderby'] = 'post__in';
$query['posts_per_page'] = count($ids);
} else {
switch ($a['orderby']) {
case 'oldest':
$query['orderby'] = 'date';
$query['order'] = 'ASC';
break;
case 'title':
$query['orderby'] = 'title';
$query['order'] = 'ASC';
break;
case 'random':
$query['orderby'] = 'rand';
break;
case 'menu':
$query['orderby'] = ['menu_order' => 'ASC', 'date' => 'DESC'];
break;
default:
$query['orderby'] = 'date';
$query['order'] = 'DESC';
}
}
$cats = array_filter(array_map('sanitize_title', explode(',', (string) $a['category'])));
if ($cats) {
$query['tax_query'] = [[ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
'taxonomy' => SAHCS_TAX,
'field' => 'slug',
'terms' => $cats,
]];
}
$exclude = array_filter(array_map('absint', explode(',', (string) $a['exclude'])));
if ($exclude && (int) $query['posts_per_page'] > 0) {
// Ask for a few extra and drop the excluded ones here (faster than NOT IN).
$query['posts_per_page'] = (int) $query['posts_per_page'] + count($exclude);
}
$q = new WP_Query($query);
if ($exclude) {
$keep = (int) $a['limit'];
$q->posts = array_values(array_filter($q->posts, function ($p) use ($exclude) {
return !in_array((int) $p->ID, $exclude, true);
}));
if ($keep > 0 && empty($ids)) {
$q->posts = array_slice($q->posts, 0, $keep);
}
$q->post_count = count($q->posts);
}
sahcs_enqueue_front();
if (!$q->have_posts()) {
return '';
}
// Heading levels follow the page outline: the page title is the H1, a grid
// title is an H2 and its cards H3; without a grid title the cards are H2.
$card_heading = (isset($a['heading']) && $a['heading'] !== '')
? sahcs_heading_tag($a['heading'], 'h3')
: ($a['title'] !== '' ? 'h3' : 'h2');
$out = '
';
if ($a['title'] !== '') {
$out .= '
' . esc_html($a['title']) . '
';
}
if ($a['filters'] && !$ids) {
$terms = [];
foreach ($q->posts as $p) {
foreach (sahcs_post_terms($p->ID) as $t) {
$terms[$t->slug] = $t->name;
}
}
if (count($terms) > 1) {
asort($terms);
$out .= '
';
$out .= '';
foreach ($terms as $slug => $name) {
$out .= '';
}
$out .= '
';
}
}
$cols_class = $a['layout'] === 'list' ? '' : ' sahcs-cols-' . (int) $a['columns'];
$out .= '
';
foreach ($q->posts as $p) {
$out .= sahcs_card($p->ID, ['heading' => $card_heading, 'excerpt' => $a['excerpt']]);
}
$out .= '
';
$out .= '
' . esc_html__('No case studies in this category yet.', 'siteaudithub-case-studies') . '
';
if ($a['button']) {
$text = $a['button_text'] !== '' ? $a['button_text'] : __('View all case studies', 'siteaudithub-case-studies');
$out .= '
' . esc_html($text) . '
';
}
$out .= '
';
wp_reset_postdata();
return $out;
}
/* ------------------------------------------------------------------
* Listing page (archive and category pages)
* ---------------------------------------------------------------- */
function sahcs_render_archive() {
global $wp_query;
$s = sahcs_get_settings();
sahcs_enqueue_front();
$term = is_tax(SAHCS_TAX) ? get_queried_object() : null;
if ($term) {
/* translators: %s: category name, e.g. Shopify */
$title = sprintf(__('%s case studies', 'siteaudithub-case-studies'), $term->name);
$intro = $term->description !== '' ? $term->description : $s['archive_intro'];
} else {
$title = $s['archive_title'] !== '' ? $s['archive_title'] : __('Case Studies', 'siteaudithub-case-studies');
$intro = $s['archive_intro'];
}
$out = '' . sahcs_page_script();
$out .= '';
if (!empty($s['show_filters'])) {
$out .= sahcs_archive_filters($term);
}
$out .= '
';
if (have_posts()) {
$layout = (isset($s['archive_layout']) && $s['archive_layout'] === 'list') ? 'list' : 'grid';
$out .= '
';
$i = 0;
while (have_posts()) {
the_post();
$out .= sahcs_card(get_the_ID(), ['heading' => 'h2', 'eager' => $i < (int) $s['columns'], 'priority' => $i === 0]);
$i++;
}
$out .= '
';
$links = paginate_links([
'type' => 'list',
'prev_text' => '
← ' . esc_html__('Previous', 'siteaudithub-case-studies'),
'next_text' => esc_html__('Next', 'siteaudithub-case-studies') . '
→',
'total' => (int) $wp_query->max_num_pages,
]);
if ($links) {
$out .= '';
}
wp_reset_postdata();
} else {
$out .= '
' . esc_html__('No case studies found yet.', 'siteaudithub-case-studies') . '
';
}
$out .= '
';
return $out;
}
function sahcs_archive_filters($current_term = null) {
$terms = get_terms(['taxonomy' => SAHCS_TAX, 'hide_empty' => true]);
if (is_wp_error($terms) || count($terms) < 1) {
return '';
}
$all_url = get_post_type_archive_link(SAHCS_CPT);
$out = '';
return $out;
}
/* ------------------------------------------------------------------
* Related case studies
* ---------------------------------------------------------------- */
/**
* Related case studies.
* $args['ids'] - chosen case studies in this order (empty = automatic: same category first).
* $args['layout'] - 'grid' or 'list'; empty = the Settings choice.
*/
function sahcs_related($post_id, $count, $title, $args = []) {
$count = max(1, min(12, (int) $count));
$chosen = isset($args['ids']) ? array_values(array_unique(array_filter(array_map('intval', (array) $args['ids'])))) : [];
$layout = isset($args['layout']) && in_array($args['layout'], ['grid', 'list'], true) ? $args['layout'] : sahcs_get_settings('related_layout');
$layout = $layout === 'list' ? 'list' : 'grid';
$ids = [];
if ($chosen) {
// Only published case studies, never the one being viewed, in the chosen order.
foreach ($chosen as $id) {
$id = sahcs_localize_id($id); // same case study in the page's language
if ($id !== (int) $post_id && get_post_type($id) === SAHCS_CPT && get_post_status($id) === 'publish') {
$ids[] = $id;
}
}
}
if (!$ids) {
$term_ids = wp_list_pluck(sahcs_post_terms($post_id), 'term_id');
$query = [
'post_type' => SAHCS_CPT,
'post_status' => 'publish',
'posts_per_page' => $count + 1,
'fields' => 'ids',
'ignore_sticky_posts' => true,
'no_found_rows' => true,
];
$ids = [];
if ($term_ids) {
$query['tax_query'] = [[ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
'taxonomy' => SAHCS_TAX,
'field' => 'term_id',
'terms' => $term_ids,
]];
$ids = array_diff(array_map('intval', (new WP_Query($query))->posts), [(int) $post_id]);
$ids = array_slice(array_values($ids), 0, $count);
unset($query['tax_query']);
}
// Top up with the latest case studies if the category has too few.
if (count($ids) < $count) {
$skip = array_merge([(int) $post_id], $ids);
$query['posts_per_page'] = $count + count($skip);
$more = array_diff(array_map('intval', (new WP_Query($query))->posts), $skip);
$ids = array_merge($ids, array_slice(array_values($more), 0, $count - count($ids)));
}
}
if (!$ids) {
return '';
}
// Columns follow the number asked for ("How many"), not the number found,
// so one related case study keeps its normal card size and the other
// places stay empty instead of one card stretching across the row.
$cols = min(3, max(count($ids), $count));
$grid_class = $layout === 'list' ? 'sahcs-grid sahcs-grid--list' : 'sahcs-grid sahcs-grid--grid sahcs-cols-' . (int) $cols;
$out = '';
return $out;
}