Dual-Language Breadcrumbs for WordPress

<h3 class="entry-title">Dual-Language Breadcrumbs for WordPress</h3>
Posted on



Background

One of my side projects is creating an English/Spanish dual language WordPress site that uses the encuencaecuador.com domain to load the Spanish language content and the the domain incuencaecuador.com for the English language content. Using the plugin Advanced Custom Fields allows for a single page/post to have the content written in both languages.

Due to the dual-domain, I was unable to find any plugin that would correctly generate the breadcrumbs. Most likely due the custom rewrites that I implemented for the Spanish language domain (see below)

$bugsCPT = [
		'arañas' => 'spiders',
		'moscas' => 'flys',
		'mariposas-y-polillas' => 'butterflys-and-moths',
		'abejas-avispas-y-hormigas' => 'bees-wasps-and-ants',
		'escarabajos' => 'beetles',
		'hemípteros' => 'true-bugs',
		'efímeras' => 'mayflies',
		'fásmidos' => 'phasmids',
		'de-hecho-no-es-un-bicho' => 'not-actually-a-bug'
	];

	// Parent Category - Needs to be placed after subcategoriess.
	foreach ($bugsCPT as $key => $value) {
        add_rewrite_rule('^bichos/' . $key . '/([0-9]+)/?$', 'index.php?post_type=bug&p=$matches[1]', 'top');
        add_rewrite_rule('^bichos/' . $key . '/([^/]+)/?$', 'index.php?post_type=bug&es_alternate_uri=$matches[1]', 'top');
		add_rewrite_rule('^bichos/' . $key .'/?$', 'index.php?category_name=' . $value , 'top');

	}

One thing I noticed is that the character ñ seemingly cannot be used in the es_alternate_name as WordPress will be unable to locate the post. So for names such as araña need to be changed to arana. When arañas  is the category name, there is no issue since it is not being included in the rewrite.

For the insects, I have a parent category page (bugs/bichos) for displaying all insect groups and a category page for the specific insect group (e.g bugs/spiders or bichos/arañas). To accomplish this, I hook into the WordPress filter category_template and check to see if the current category is one of the insects.

add_filter( 'category_template', 'custom_category_template' );
function custom_category_template( $template ) {
	$bug_categories = array('bees-wasps-and-ants',
							'beetles', 
							'butterflys-and-moths',
							'dragonflies-and-damselfies',
							'flys', 'not-actually-a-bug',
							'phasmids', 
							'spiders', 
							'stick-bugs', 
							'true-bugs');
	$bug_parent_category = array('bugs');

    if (is_category($bug_categories) ) {
		$template =  locate_template( 'page-templates/specie-template.php'); 
	} else if ( is_category($bug_parent_category)  ) {
		$template = locate_template( 'archive-bugs.php' ); 
	} 
    return $template;
}

Goals

Generate Breadcrumbs for both the English Site as well as the Spanish site for the Bugs custom post type

Generate valid JSON-LD structured data to alert the search engines of the presence of the breadcrumbs.

Current Implementation

The current implementation works for the Bugs custom post type with the custom category templates. Further testing will be needed to extended the breadcrumbs to the rest of the site.

function get_breadcrumb($posttype) {
    global $wp_query;
    
    $bullet = ' • ';
    $double_right_angles = "   »   ";

    $breadcrumbs = array(
        "@context" => "http://schema.org",
        "@type" => "BreadcrumbList",
        "itemListElement"=> array()
    );
    
    $query_cat_template = get_query_var( 'cat' );
    $lang_prefix = is_spanish() ? 'es' : 'en';
    $home = is_spanish() ? 'Inicio' : 'Home';
    
    
    $breadcrumb_len = 1;
    $home_item = array(
        "@type" => "ListItem",
        "position" => $breadcrumb_len,
        "item" => array(
            "@id"=> home_url(),
            "name"=> $home     
            )
    );
    array_push($breadcrumbs{'itemListElement'}, $home_item);
    $breadcrumb_len++;
   
    echo '<a href="' . home_url() . '" rel="nofollow">'. $home .'</a>';

    if($query_cat_template !== '') {
        // Handle Custom Archive Template
        $cat = get_category( get_query_var( 'cat' ) );
        $label = get_category_label($cat, $lang_prefix);
        
        if($cat->parent) {
            echo $bullet;
            $parent = get_category($cat->parent);
            $parent_uri = get_category_uri_2($parent, $lang_prefix);
            $parent_label = get_category_label($parent, $lang_prefix);

            $item = array(
                "@type" => "ListItem",
                "position" => $breadcrumb_len,
                "item" => array(
                    "@id"=>$parent_uri,
                    "name"=> $parent_label     
                )
            );
            array_push($breadcrumbs{'itemListElement'}, $item);
            $breadcrumb_len++;

            echo '<a href="' . $parent_uri  .'">' . $parent_label . '</a>';
        }
        echo $double_right_angles;
        echo $label;
    }
    elseif (is_category() || is_any_single()) {
        
        $cats = get_the_category();
        // Ensure that categorie parent is the first 
        // in the list.
        usort($cats, 'compare_term_id');
            
        $len = count($cats);
        $i = 0;
        
        echo $double_right_angles;
        foreach($cats as $idx=>$cat) {
            $uri = get_category_uri_2($cat, $lang_prefix);
            $label =  get_category_label($cat, $lang_prefix);

            $item = array(
                "@type" => "ListItem",
                "position" => $breadcrumb_len,
                "item" => array(
                    "@id"=>$uri,
                    "name"=> $label     
                )
            );
            array_push($breadcrumbs{'itemListElement'}, $item);
            $breadcrumb_len++;

            echo '<a href="' . $uri  .'">' .$label . '</a>';
            if($i !== $len -1) {
                echo $bullet;
            }
            $i++;
        }
        if (is_any_single()) {
            $post_name = is_spanish() ? get_field('es_alternate_name') ?: 'Sin Nombre' : get_the_title();
            echo $double_right_angles;
            echo $post_name;
        }
    } elseif (is_page()) {
        echo $double_right_angles;
        echo is_spanish() ? get_field('es_alternate_name') : get_the_title();
    }
    // JSON-D Schema
    echo '<script type="application/ld+json">' . json_encode($breadcrumbs) .'</script>';
}

Screenshots

Check out the screenshots below or visit the site to see the breadcrumbs in action. To test the structured data you can use the tool provided by Google.
Take note of the differences in the site address as well as the breadcrumbs

English

Structured Data - English

Spanish

Structured Data - Spanish

Explanation of Utility Functions

The WordPress function get_the_category()does not return the Post Categories with the category parent first, but rather alphabetically by category name. A quick sort by comparing the term_id  helps.

function compare_term_id($a, $b) {
    return $a->term_id > $b->term_id;
}

One of my favorite operators is the ternary operator and I use the function below to determine which language content needs to be loaded.

function is_spanish() {
    $actual_link = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
  return strpos($actual_link, 'encuencaecuador');
}

An issue I ran into, was when the Spanish uri matched the rewrite rule:

add_rewrite_rule('^bichos/' . $key . '/([^/]+)/?$', 'index.php?post_type=bug&es_alternate_uri=$matches[1]', 'top');

The function is_single() would return false and fail to generate the the breadcrumbs. The work-around I found on StackOverflow was to check and see if the query_var was set. I'm not positive that it is the best solution, but it works for my use case.

function is_any_single() {
    $post_type = get_query_var('post_type');
    return is_single() || !empty($post_type);
 }