Five New Optimizely Certifications are Here! Validate your expertise and advance your career with our latest certification exams. Click here to find out more

Canonical with fallback languages

Vote:
 

Hello!

We've recently implemented the standard Optimizely methods (see below) for generating canonicals and alternate URL:s and it works pretty well.

@Html.CanonicalLink()
@Html.AlternateLinks()

However, we're wondering why we're not getting links for the pages which have replacement language / fallback?

For instance we have this page about sustainability which exists in Spanish (see below)

When visiting this page in the browser, on the mexican site i works fine and shows the spanish fallback / replacement content

 

But the alternate / canonicals are not using the es-mx urls, but the es-es URL instead, and the alternate links do not even include the link(s) that have fallback.

Are we missing something?

#336654
Feb 11, 2025 13:55
Vote:
 

We had the same issue and had to write our own logic to handle this.

Try this code

using System;
using System.Globalization;
using System.Linq;

using EPiServer.Core;
using EPiServer.Web;
using EPiServer.Web.Routing;

namespace yadayada;

public class CanonicalUrlFactory
{
    private readonly IUrlResolver urlResolver;
    private readonly ISiteDefinitionResolver siteDefinitionResolver;

    public CanonicalUrlFactory(
        IUrlResolver urlResolver,
        ISiteDefinitionResolver siteDefinitionResolver
    )
    {
        this.urlResolver = urlResolver ?? throw new ArgumentNullException(nameof(urlResolver));
        this.siteDefinitionResolver = siteDefinitionResolver ?? throw new ArgumentNullException(nameof(siteDefinitionResolver));
    }

    //Modified and copied from http://www.dodavinkeln.se/post/how-to-get-the-external-url-to-content
    //This method also works for MediaData.
    public virtual string Create(
        ContentReference contentLink,
        CultureInfo contentLanguage,
        bool absoluteUrl = true
    )
    {
        var result = this.urlResolver.GetUrl(
            contentLink,
            contentLanguage.Name,
            new VirtualPathArguments
            {
                ContextMode = ContextMode.Default,
                ForceCanonical = absoluteUrl
            });

        // HACK: Temprorary fix until GetUrl and ForceCanonical works as expected,
        // i.e returning an absolute URL even if there is a HTTP context that matches the content's site definition and host.
        if(!absoluteUrl)
        {
            return result;
        }

        if(!Uri.TryCreate(result, UriKind.RelativeOrAbsolute, out var relativeUri))
        {
            return result;
        }

        if(relativeUri.IsAbsoluteUri)
        {
            return result;
        }

        var siteDefinition = this.siteDefinitionResolver.GetByContent(
            contentLink: contentLink,
            fallbackToWildcard: true,
            fallbackToEmpty: true);
        var hosts = siteDefinition.GetHosts(
                language: contentLanguage,
                fallbackToUnmapped: true)
            .ToList();
        var host = hosts.FirstOrDefault(h => h.Type == HostDefinitionType.Primary) ??
                   hosts.FirstOrDefault(h => h.Type == HostDefinitionType.Undefined);
        var baseUri = siteDefinition.SiteUrl;

        // Avoid exception if host is missing, i.e. page without startpage
        if(baseUri == null)
        {
            return string.Empty;
        }

        if(host != null && host.Name.Equals("*") == false)
        {
            // Try to create a new base URI from the host with the site's URI scheme. Name should be a valid
            // authority, i.e. have a port number if it differs from the URI scheme's default port number.
            Uri.TryCreate(siteDefinition.SiteUrl.Scheme + "://" + host.Name, UriKind.Absolute, out baseUri);
        }

        var absoluteUri = new Uri(baseUri, relativeUri);

        return absoluteUri.AbsoluteUri;
    }
}
#336846
Feb 17, 2025 21:10
sebp - Feb 18, 2025 7:14
Thank you Eric, I will give this a go :)
Johan Petersson - Feb 24, 2025 9:57
This code seems to be a bit outdated. Instead of using ForceCanonical, you can use ForceAbsolute to always get an absolute URL (if that's what you want).
Vote:
 

It seems this is working out pretty well. So I wrote my own method for doing the same for alternate links, not sure it's the best solution but it seems to work! Here it is if anyone needs it :)

public Dictionary<string, string> GetAlternateLinks(ContentReference contentReference)
{
    var isEditMode = _contextModeResolver.CurrentMode.EditOrPreview();
    if (isEditMode)
    {
        return null;
    }

    var alternates = new Dictionary<string, string>();
    var allLanguages = _languageBranchRepository.ListEnabled().Select(x => x.Culture);

    var startPage = _contentLoader.Get<StartPage>(ContentReference.StartPage);
    var masterLanguage = startPage.MasterLanguage;

    foreach (var lang in allLanguages)
    {
        var url = GetCanonicalUrl(contentReference, lang);

        if (!string.IsNullOrEmpty(url))
        {
            var urlBuilder = new UrlBuilder(url);
            var routedContent = _urlResolver.Route(urlBuilder);
            var isPublished = _publishedStateAssessor.IsPublished(routedContent, PublishedStateCondition.None);

            if (routedContent != null && !ContentReference.IsNullOrEmpty(routedContent.ContentLink) && isPublished)
            {
                alternates[lang.Name] = url;

                if (lang == masterLanguage)
                {
                    alternates["x-default"] = url;
                }
            }
        }
    }

    return alternates;
}

#336906
Edited, Feb 19, 2025 10:49
Eric Herlitz - Feb 19, 2025 11:14
Awesome, I'm sure it will serve you just fine!
Vote:
 

Canonical links should not contain fallback or replacement languages/URLs. The purpose of the canonical links is to tell search engines where the original content is located so it doesn't index duplicates. Es-es and es-mx are duplicates in this case, hence es-es should be the canonical URL of this page.

 

If you navigate to /es-es/sobre-xxx/sustainability/, is there an alternate link rendered to /es-mx/sobre-xxx/sustainability/ with hreflang set to 'es-mx'?

#336987
Edited, Feb 24, 2025 9:55
sebp - Mar 07, 2025 6:52
When the client first approached me with their request, my initial response was to suggest checking with our SEO expert to determine what actually needed to be addressed. The feedback I received was that they were using "Screaming Frog," and the software flagged an issue: the canonical tag was set to "es-es" even when accessing the page from "es-mx."

That said, I think your explanation makes the most sense. If the canonical tag is meant to refer to the original content, it absolutely should remain "es-es," even when the page is accessed from "es-mx."

Also, I can confirm that the "es-mx" alternate link does render correctly when viewing the sustainability page in "es-es."
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.