` pattern. // --------------------------------------------------------------------------- /** * Render an `` tag from an ACF image field, a fallback array, or a URL. * * @param mixed $image ACF image array (`['ID', 'url', 'alt', ...]`), * a `['url', 'alt']` fallback array, or a URL string. * @param string $size WP image size key (e.g. 'portfolio-card', 'hero-full'). * Only applies when an attachment ID is available. * @param array $attrs HTML attributes. `loading` defaults to "lazy", * `decoding` to "async". Pass `loading => "eager"` and * `fetchpriority => "high"` for LCP images. * @return string Escaped HTML — empty string when no image data is present. */ function cn_acf_image_tag( $image, string $size = 'full', array $attrs = array() ): string { $attrs = array_merge( array( 'loading' => 'lazy', 'decoding' => 'async' ), $attrs ); // Best path: WordPress-managed attachment with an ID. Produces a full // tag with srcset/sizes for responsive delivery. if ( is_array( $image ) && ! empty( $image['ID'] ) ) { return wp_get_attachment_image( (int) $image['ID'], $size, false, $attrs ); } // Fallback paths: extract URL + alt, render a bare . $url = ''; $alt = (string) ( $attrs['alt'] ?? '' ); if ( is_array( $image ) && ! empty( $image['url'] ) ) { $url = (string) $image['url']; if ( '' === $alt && ! empty( $image['alt'] ) ) { $alt = (string) $image['alt']; } } elseif ( is_string( $image ) && '' !== $image ) { $url = $image; } if ( '' === $url ) { return ''; } $attrs['alt'] = $alt; $attr_str = ''; foreach ( $attrs as $key => $val ) { if ( null === $val || false === $val ) { continue; } if ( true === $val ) { $attr_str .= ' ' . esc_attr( $key ); continue; } $attr_str .= ' ' . esc_attr( $key ) . '="' . esc_attr( (string) $val ) . '"'; } return ''; } // --------------------------------------------------------------------------- // Page URL resolver // // Templates and partials previously hardcoded `home_url( '/contact' )`, // `home_url( '/studio' )` etc. — fragile because the day someone renames a // Page in WP admin (changes the slug), every footer/header link breaks. // // cn_url() resolves through get_page_by_path() first so it follows the // actual configured permalink, then falls back to the conventional path. // Optional second argument appends an anchor (#design, etc.). // // Internal cache avoids repeated DB lookups when the same slug is used // across many partials in a single request. // --------------------------------------------------------------------------- /** * Resolve the public URL for a theme page by slug, with optional anchor. * * @param string $slug Page slug (e.g. 'contact', 'studio'). No leading slash. * @param string $anchor Optional anchor without the leading '#'. * @return string URL — never empty; falls back to a conventional /{slug}/ path. */ function cn_url( string $slug, string $anchor = '' ): string { static $cache = array(); $slug = trim( $slug, "/ \t\n\r\0\x0B" ); if ( '' === $slug ) { return home_url( '/' ); } if ( ! isset( $cache[ $slug ] ) ) { $cache[ $slug ] = ''; $page = get_page_by_path( $slug ); if ( $page instanceof WP_Post ) { $permalink = get_permalink( $page ); if ( $permalink ) { $cache[ $slug ] = $permalink; } } if ( '' === $cache[ $slug ] ) { $cache[ $slug ] = home_url( '/' . $slug . '/' ); } } $anchor = ltrim( $anchor, '#' ); return $anchor ? $cache[ $slug ] . '#' . rawurlencode( $anchor ) : $cache[ $slug ]; } /** * Resolve the public URL for a WooCommerce product category by slug. * * @param string $slug Term slug (e.g. 'lechuza'). * @return string URL — never empty; always falls back to the conventional path. */ function cn_shop_category_url( string $slug ): string { if ( '' === $slug ) { return home_url( '/shop/' ); } if ( taxonomy_exists( 'product_cat' ) ) { $term = get_term_by( 'slug', $slug, 'product_cat' ); if ( $term && ! is_wp_error( $term ) ) { $link = get_term_link( $term ); if ( ! is_wp_error( $link ) ) { return $link; } } } return home_url( '/product-category/' . $slug . '/' ); } // --------------------------------------------------------------------------- // Disable comments site-wide // // Catapult Nature is a B2B studio site — posts are journal entries, not // discussion threads. Disabling comments removes the surface entirely: // - Comment support is stripped from every post type (no comment box). // - Existing comments (if any imported with content) are forced closed and // hidden from the front end. // - Admin menu / dashboard widget / toolbar entries for Comments are hidden. // - Comment feeds and the EditURI/RSD links are removed from . // - The /comments-feed/ endpoint redirects home to avoid 404 noise in logs. // // If comments are ever needed for a specific post type, drop the call to // `remove_post_type_support()` for that type and ship a comments.php. // --------------------------------------------------------------------------- /** * Strip comment support from every registered post type. */ function cn_disable_comments_post_type_support(): void { foreach ( get_post_types() as $post_type ) { if ( post_type_supports( $post_type, 'comments' ) ) { remove_post_type_support( $post_type, 'comments' ); remove_post_type_support( $post_type, 'trackbacks' ); } } } add_action( 'admin_init', 'cn_disable_comments_post_type_support' ); /** * Force-close comments and pings on the front end. */ function cn_disable_comments_status(): bool { return false; } add_filter( 'comments_open', 'cn_disable_comments_status', 20 ); add_filter( 'pings_open', 'cn_disable_comments_status', 20 ); /** * Hide existing comments from queries on the front end. * * @param array $comments Comments array. * @return array */ function cn_disable_comments_hide_existing( $comments ): array { return array(); } add_filter( 'comments_array', 'cn_disable_comments_hide_existing', 10 ); /** * Remove the Comments item from the admin menu. */ function cn_disable_comments_admin_menu(): void { remove_menu_page( 'edit-comments.php' ); } add_action( 'admin_menu', 'cn_disable_comments_admin_menu' ); /** * Redirect anyone who lands on the comments admin page. */ function cn_disable_comments_admin_menu_redirect(): void { global $pagenow; if ( 'edit-comments.php' === $pagenow ) { wp_safe_redirect( admin_url() ); exit; } } add_action( 'admin_init', 'cn_disable_comments_admin_menu_redirect' ); /** * Drop the comments dashboard widget. */ function cn_disable_comments_dashboard(): void { remove_meta_box( 'dashboard_recent_comments', 'dashboard', 'normal' ); } add_action( 'admin_init', 'cn_disable_comments_dashboard' ); /** * Remove the Comments node from the admin toolbar (front and back end). * * @param WP_Admin_Bar $wp_admin_bar Admin bar instance. */ function cn_disable_comments_admin_bar( $wp_admin_bar ): void { if ( $wp_admin_bar instanceof WP_Admin_Bar ) { $wp_admin_bar->remove_node( 'comments' ); } } add_action( 'admin_bar_menu', 'cn_disable_comments_admin_bar', 999 ); /** * Strip the comment-related tags from . */ remove_action( 'wp_head', 'feed_links_extra', 3 ); remove_action( 'wp_head', 'rsd_link' ); add_filter( 'feed_links_show_comments_feed', '__return_false' ); /** * Send the comment feed endpoint home rather than serving an empty feed. */ function cn_disable_comments_feed_redirect(): void { if ( is_comment_feed() ) { wp_safe_redirect( home_url( '/' ), 301 ); exit; } } add_action( 'template_redirect', 'cn_disable_comments_feed_redirect' ); https://catapultnature.co.ke/post-sitemap.xml 2026-04-14T10:36:50+00:00 https://catapultnature.co.ke/page-sitemap.xml 2026-04-25T10:26:03+00:00 https://catapultnature.co.ke/product-sitemap.xml 2026-04-22T13:54:07+00:00 https://catapultnature.co.ke/category-sitemap.xml 2026-04-14T10:36:50+00:00