nystudio107 / craft-seomatic

SEOmatic facilitates modern SEO best practices & implementation for Craft CMS 3. It is a turnkey SEO system that is comprehensive, powerful, and flexible.
https://nystudio107.com/plugins/seomatic
Other
165 stars 70 forks source link

hreflang links with custom routes & paginated pages #718

Closed milesjbland closed 3 years ago

milesjbland commented 4 years ago

Question

On our multisite setup, we have a blog index template that lets you filter by topic. The topics use custom routes set in routes.php.

I'm running into a couple of issues with both custom routes and paginated pages.

  1. On paginated pages, the hreflang links are using the correct site URLs, but it is stripping out the page segment:
# http://foo.test/en/resources/blog/p2

# What we expect
<link href="http://foo.test/fr/ressources/blog/p2" rel="alternate" hreflang="fr">
<link href="http://foo.test/en/resources/blog/p2" rel="alternate" hreflang="en">

# What we get
<link href="http://foo.test/fr/ressources/blog" rel="alternate" hreflang="fr">
<link href="http://foo.test/en/resources/blog" rel="alternate" hreflang="en">
  1. With custom routes, hreflang links are using the correct site URLs, but are stripping out the custom segment:
# http://foo.test/en/resources/blog/promote

# What we expect
<link href="http://foo.test/fr/ressources/blog/promouvoir" rel="alternate" hreflang="fr">
<link href="http://foo.test/en/resources/blog/promote" rel="alternate" hreflang="en">

# What we get
<link href="http://foo.test/fr/ressources/blog" rel="alternate" hreflang="fr">
<link href="http://foo.test/en/resources/blog" rel="alternate" hreflang="en">

I can solve both these problems by writing some twig that uses the link meta object function to manually set the links for each site with the custom segment and paginated segment added on the end.

The problem is that this doesn't work for any paginated pages after page 1. It strips the page segment and uses the English URIs for each site:

# http://foo.test/en/resources/blog/promote/p2

# What we expect
<link href="http://foo.test/fr/ressources/blog/promouvoir/p2" rel="alternate" hreflang="fr">
<link href="http://foo.test/en/resources/blog/promote/p2" rel="alternate" hreflang="en">

# What we get
<link href="http://foo.test/fr/resources/blog/promote" rel="alternate" hreflang="fr">
<link href="http://foo.test/en/resources/blog/promote" rel="alternate" hreflang="en">

Is there a best practice approach to achieve the result I'm after?

Additional context

Craft: 3.4.30 SEOmatic: 3.3.8

This might be related to https://github.com/nystudio107/craft-seomatic/issues/342

khalwat commented 4 years ago

This is correct behavior. SEOmatic intentionally does not link to paginate pages, but rather to the root index page.

The reason is that content on the paginated pages changes, and so may be different depending on a variety of factors.

See: https://github.com/nystudio107/craft-seomatic/issues/375#issuecomment-488369209

milesjbland commented 4 years ago

Thanks for the response and the link @khalwat, that's good to know!

I still have the problem that the hreflang links on paginated custom route pages are using the current site's URI for each link.

I'm logging $metaTag->href just before this line and getting the following results:

The links for the page, both paginated and non-paginated, work correctly as you described:

# http://foo.test/en/resources/blog 
# http://foo.test/en/resources/blog/p2

array (
  0 => 'http://foo.test/en/resources/blog',
  1 => 'http://foo.test/en/resources/blog',
  2 => 'http://foo.test/fr/ressources/blog',
)

The links for the page with a custom route are correct on page 1, but it looks like it generates two arrays, with the last one being the correct one:

# http://foo.test/en/resources/blog/promote

array (
  0 => 'http://foo.test/en/resources/blog/promote',
  1 => 'http://foo.test/en/resources/blog/promote',
  2 => 'http://foo.test/fr/resources/blog/promote',
)
array (
  0 => 'http://foo.test/en/resources/blog',
  1 => 'http://foo.test/en/resources/blog',
  2 => 'http://foo.test/fr/ressources/blog',
)

A similar thing happens on the page with a custom route after page 1, but it generates three arrays, with the first and last being incorrect:

# http://foo.test/en/resources/blog/promote/p2

array (
  0 => 'http://foo.test/en/resources/blog/promote',
  1 => 'http://foo.test/en/resources/blog/promote',
  2 => 'http://foo.test/fr/resources/blog/promote',
)
array (
  0 => 'http://foo.test/en/resources/blog',
  1 => 'http://foo.test/en/resources/blog',
  2 => 'http://foo.test/fr/ressources/blog',
)
array (
  0 => 'http://foo.test/en/resources/blog/promote',
  1 => 'http://foo.test/en/resources/blog/promote',
  2 => 'http://foo.test/fr/resources/blog/promote',
)

For context, the custom routes are setup like so:

'englishSite' => [
    'resources/blog/<topicSlug:{slug}>' => ['template' => '_resources/blog/index.twig'],
],
'frenchSite' => [
    'ressources/blog/<topicSlug:{slug}>' => ['template' => '_resources/blog/index.twig'],
],
khalwat commented 4 years ago

When you say "it generates" what do you mean? Can you give me more context in terms of where you are breaking to inspect these values?

milesjbland commented 4 years ago

Sorry, I think that was just a poor choice of words.

I'm inspecting those values here just after the foreach loop: https://github.com/nystudio107/craft-seomatic/blob/v3/src/helpers/DynamicMeta.php#L522

I forgot to mention, in our twig file we're updating the meta data like so for topic pages:

{% set entry = craft.entries.section('blogIndex').one() %}
{% set topic = topicSlug is defined ? craft.entries.section('topic').slug(topicSlug).one() : [] %}

{# SEOMatic for topic sub-pages #}
{% if topic|length %}
    {% do seomatic.helper.loadMetadataForUri(entry.uri) %}
    {# Append topic title to SEO Title etc... #}
{% endif %}

It looks like the twig above is the reason we're getting the correct second array on the topic pages. (Removing it causes us to only get the first incorrect one.)

For some reason, on paginated pages we're seeing a third incorrect array. Not sure if it's a clue, but the third appears just before the closing </body> tag, whilst the first two appear just after the opening <body> tag.

milesjbland commented 4 years ago

Hey @khalwat! Please let me know if there is any more information you need from me on this.

khalwat commented 4 years ago

@milesjbland I've tried reproducing this, but am so far unable to.

Can you show me the actual code you're using here, as well as the code you're using to have a look at these various values? You say, for example, there are 3 arrays, but where are you seeing these arrays? Are they all in one value?

It looks to me like it's adding a tag each time through, but that doesn't seem possible based on the code -- it should just overwrite the existing tag.

khalwat commented 4 years ago

I've tried doing this:

{% do seomatic.helper.loadMetadataForUri('/blog') %}
{% dd seomatic.link.container %}

And I see all of the links in the container, and the alternate link has in it what I'd expect:

nystudio107\seomatic\models\MetaLinkContainer#1
(
    [data] => [
        'canonical' => nystudio107\seomatic\models\metalink\CanonicalLink#2
        (
            [crossorigin] => ''
            [href] => '{seomatic.meta.canonicalUrl}'
            [hreflang] => ''
            [media] => ''
            [rel] => 'canonical'
            [sizes] => ''
            [type] => ''
            [yii\base\Model:_errors] => null
            [yii\base\Model:_validators] => null
            [yii\base\Model:_scenario] => 'default'
            [yii\base\Component:_events] => []
            [yii\base\Component:_eventWildcards] => []
            [yii\base\Component:_behaviors] => []
            [include] => true
            [key] => 'canonical'
            [environment] => null
            [dependencies] => null
        )
        'home' => nystudio107\seomatic\models\metalink\HomeLink#3
        (
            [crossorigin] => ''
            [href] => '{{ seomatic.helper.siteUrl(\"/\") }}'
            [hreflang] => ''
            [media] => ''
            [rel] => 'home'
            [sizes] => ''
            [type] => ''
            [yii\base\Model:_errors] => null
            [yii\base\Model:_validators] => null
            [yii\base\Model:_scenario] => 'default'
            [yii\base\Component:_events] => []
            [yii\base\Component:_eventWildcards] => []
            [yii\base\Component:_behaviors] => []
            [include] => true
            [key] => 'home'
            [environment] => null
            [dependencies] => null
        )
        'author' => nystudio107\seomatic\models\metalink\AuthorLink#4
        (
            [crossorigin] => ''
            [href] => '{{ seomatic.helper.siteUrl(\"/humans.txt\") }}'
            [hreflang] => ''
            [media] => ''
            [rel] => 'author'
            [sizes] => ''
            [type] => 'text/plain'
            [yii\base\Model:_errors] => null
            [yii\base\Model:_validators] => null
            [yii\base\Model:_scenario] => 'default'
            [yii\base\Component:_events] => []
            [yii\base\Component:_eventWildcards] => []
            [yii\base\Component:_behaviors] => []
            [include] => true
            [key] => 'author'
            [environment] => null
            [dependencies] => [
                'frontend_template' => [
                    0 => 'humans'
                ]
            ]
        )
        'publisher' => nystudio107\seomatic\models\MetaLink#5
        (
            [crossorigin] => ''
            [href] => '{seomatic.site.googlePublisherLink}'
            [hreflang] => ''
            [media] => ''
            [rel] => 'publisher'
            [sizes] => ''
            [type] => ''
            [yii\base\Model:_errors] => null
            [yii\base\Model:_validators] => null
            [yii\base\Model:_scenario] => 'default'
            [yii\base\Component:_events] => []
            [yii\base\Component:_eventWildcards] => []
            [yii\base\Component:_behaviors] => []
            [include] => true
            [key] => 'publisher'
            [environment] => null
            [dependencies] => [
                'site' => [
                    0 => 'googlePublisherLink'
                ]
            ]
        )
        'alternate' => nystudio107\seomatic\models\MetaLink#6
        (
            [crossorigin] => null
            [href] => [
                0 => 'http://craft3.test/en/blog'
                1 => 'http://craft3.test/en/blog'
                2 => 'http://craft3.test/es/blog'
                3 => 'http://craft3.test/to-to/blog'
            ]
            [hreflang] => [
                0 => 'en-us'
                1 => 'x-default'
                2 => 'es'
                3 => 'to-to'
            ]
            [media] => null
            [rel] => 'alternate'
            [sizes] => null
            [type] => null
            [yii\base\Model:_errors] => null
            [yii\base\Model:_validators] => null
            [yii\base\Model:_scenario] => 'default'
            [yii\base\Component:_events] => []
            [yii\base\Component:_eventWildcards] => []
            [yii\base\Component:_behaviors] => []
            [include] => true
            [key] => 'alternate'
            [environment] => null
            [dependencies] => null
        )
    ]
    [yii\base\Model:_errors] => null
    [yii\base\Model:_validators] => null
    [yii\base\Model:_scenario] => 'default'
    [yii\base\Component:_events] => []
    [yii\base\Component:_eventWildcards] => []
    [yii\base\Component:_behaviors] => []
    [name] => 'General'
    [description] => 'Link Tags'
    [class] => 'nystudio107\\seomatic\\models\\MetaLinkContainer'
    [handle] => 'general'
    [include] => true
    [dependencies] => []
    [clearCache] => false
)
milesjbland commented 4 years ago

Thanks for looking into it! Here's the current contents of my files:

routes.php

<?php

return [
    'default' => [
        'resources/blog/<topicSlug:{slug}>' => ['template' => '_resources/blog/index.twig'],
    ],
    'frenchSite' => [
        'ressources/blog/<topicSlug:{slug}>' => ['template' => '_resources/blog/index.twig'],
    ],
    'germanSite' => [
        'ressourcen/blog/<topicSlug:{slug}>' => ['template' => '_resources/blog/index.twig'],
    ],
    'italianSite' => [
        'risorse/blog/<topicSlug:{slug}>' => ['template' => '_resources/blog/index.twig'],
    ],
    'portugeseSite' => [
        'recursos/blog/<topicSlug:{slug}>' => ['template' => '_resources/blog/index.twig'],
    ],
    'spanishSite' => [
        'recursos-practicos/blog/<topicSlug:{slug}>' => ['template' => '_resources/blog/index.twig'],
    ]
];

_resources/blog/index.twig

{% extends '_layouts/default' %}

{# We set this because topic pages otherwise won't know #}
{% set entry = craft.entries.section('blogIndex').one() %}
{% set topic = topicSlug is defined ? craft.entries.section('resourceTopic').slug(topicSlug).one() : [] %}

{% if topicSlug is defined and topic|length == 0 %}
    {% exit 404 %}
{% endif %}

{% paginate craft.entries.section('blogArticle').relatedTo(topic).limit(5) as pageInfo, pageEntries %}

{% if topic|length %}
    {% do seomatic.helper.loadMetadataForUri(entry.uri) %}
    {% do seomatic.meta.canonicalUrl(entry.url ~ '/' ~ topicSlug) %}
    {% do seomatic.meta.seoTitle(seomatic.meta.seoTitle ~ ' - ' ~ topic.title) %}
{% endif %}

{% block content %}
    {# Page content goes here. #}
{% endblock %}

I'm adding this code just before this line and immediately after the foreach loop to inspect the values:

echo '<pre>'.var_export($metaTag, true).'<pre>';

I now get one of two possible outputs when loading a paginated topic page (http://foo.test/resources/blog/promote/p2):

Output 1

nystudio107\seomatic\models\MetaLink::__set_state(array(
   'crossorigin' => NULL,
   'href' => 
  array (
    0 => 'http://foo.test/en/resources/blog/promote',
    1 => 'http://foo.test/en/resources/blog/promote',
    2 => 'http://foo.test/fr/resources/blog/promote',
    3 => 'http://foo.test/de/resources/blog/promote',
    4 => 'http://foo.test/it/resources/blog/promote',
    5 => 'http://foo.test/pt/resources/blog/promote',
    6 => 'http://foo.test/es/resources/blog/promote',
  ),
   'hreflang' => 
  array (
    0 => 'en',
    1 => 'x-default',
    2 => 'fr',
    3 => 'de',
    4 => 'it',
    5 => 'pt',
    6 => 'es',
  ),
   'media' => NULL,
   'rel' => 'alternate',
   'sizes' => NULL,
   'type' => NULL,
   '_errors' => NULL,
   '_validators' => NULL,
   '_scenario' => 'default',
   '_events' => 
  array (
  ),
   '_eventWildcards' => 
  array (
  ),
   '_behaviors' => 
  array (
  ),
   'include' => true,
   'key' => 'alternate',
   'environment' => NULL,
   'dependencies' => NULL,
))
nystudio107\seomatic\models\MetaLink::__set_state(array(
   'crossorigin' => NULL,
   'href' => 
  array (
    0 => 'http://foo.test/en/resources/blog',
    1 => 'http://foo.test/en/resources/blog',
    2 => 'http://foo.test/fr/ressources/blog',
    3 => 'http://foo.test/de/ressourcen/blog',
    4 => 'http://foo.test/it/risorse/blog',
    5 => 'http://foo.test/pt/recursos/blog',
    6 => 'http://foo.test/es/recursos-practicos/blog',
  ),
   'hreflang' => 
  array (
    0 => 'en',
    1 => 'x-default',
    2 => 'fr',
    3 => 'de',
    4 => 'it',
    5 => 'pt',
    6 => 'es',
  ),
   'media' => NULL,
   'rel' => 'alternate',
   'sizes' => NULL,
   'type' => NULL,
   '_errors' => NULL,
   '_validators' => NULL,
   '_scenario' => 'default',
   '_events' => 
  array (
  ),
   '_eventWildcards' => 
  array (
  ),
   '_behaviors' => 
  array (
  ),
   'include' => true,
   'key' => 'alternate',
   'environment' => NULL,
   'dependencies' => NULL,
))

Output 2

nystudio107\seomatic\models\MetaLink::__set_state(array(
   'crossorigin' => NULL,
   'href' => 
  array (
    0 => 'http://foo.test/en/resources/blog/promote',
    1 => 'http://foo.test/en/resources/blog/promote',
    2 => 'http://foo.test/fr/resources/blog/promote',
    3 => 'http://foo.test/de/resources/blog/promote',
    4 => 'http://foo.test/it/resources/blog/promote',
    5 => 'http://foo.test/pt/resources/blog/promote',
    6 => 'http://foo.test/es/resources/blog/promote',
  ),
   'hreflang' => 
  array (
    0 => 'en',
    1 => 'x-default',
    2 => 'fr',
    3 => 'de',
    4 => 'it',
    5 => 'pt',
    6 => 'es',
  ),
   'media' => NULL,
   'rel' => 'alternate',
   'sizes' => NULL,
   'type' => NULL,
   '_errors' => NULL,
   '_validators' => NULL,
   '_scenario' => 'default',
   '_events' => 
  array (
  ),
   '_eventWildcards' => 
  array (
  ),
   '_behaviors' => 
  array (
  ),
   'include' => true,
   'key' => 'alternate',
   'environment' => NULL,
   'dependencies' => NULL,
))

In both instances, the hreflangs are always set as:

<link href="http://foo.test/es/resources/blog/promote" rel="alternate" hreflang="es">
<link href="http://foo.test/pt/resources/blog/promote" rel="alternate" hreflang="pt">
<link href="http://foo.test/it/resources/blog/promote" rel="alternate" hreflang="it">
<link href="http://foo.test/de/resources/blog/promote" rel="alternate" hreflang="de">
<link href="http://foo.test/fr/resources/blog/promote" rel="alternate" hreflang="fr">
<link href="http://foo.test/en/resources/blog/promote" rel="alternate" hreflang="x-default">
<link href="http://foo.test/en/resources/blog/promote" rel="alternate" hreflang="en">
khalwat commented 3 years ago

Do we have this sorted @milesjbland ?

milesjbland commented 3 years ago

All sorted. I think it was an issue my side, in the end. Thank you for your help and time on this!

chrismlusk commented 2 years ago

Hello from the future, @milesjbland. I think I'm having a similar issue with custom routes and the hreflang links for a multi-site (multi-language) set up. Like the Output 2 example you posted, my hreflang links all use the route pattern as defined in routes.php for the current site.

I know this issue was a long time ago, but any chance you remember how you solved this?