Problem/Motivation
When an entity has available translations, in the HTML head, list the links to all available languages using the 'rel="alternate" hreflang="x"' approach described in https://support.google.com/webmasters/answer/189077.
This is necessary for SEO. When content is translated into multiple languages, search engines look for link tags with hreflang attributes in the HTML head to determine available translations so that they may surface results appropriate to the searcher's location and language.
This is a follow-up to #1164682: links with a known language need language identifier. See that issue for more background, discussion, and earlier attempts to achieve this functionality. At the end only the l() function got this feature and no header changes were made there.
Proposed resolution
Add the links to HTML head.
Remaining tasks
Do it. Tests. Reviews.
User interface changes
None.
API changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #38 | interdiff.txt | 3.9 KB | paulmckibben |
| #38 | hreflang-2303525-38.patch | 3.66 KB | paulmckibben |
| #26 | hreflang-2303525-25.patch | 3.59 KB | tim.plunkett |
| #17 | interdiff-14-to-17.txt | 1.23 KB | paulmckibben |
| #17 | hreflang-in-head-2303525-17.patch | 3.82 KB | paulmckibben |
Comments
Comment #1
gábor hojtsyTagged, moved to a task and updated issue summary with template
Comment #2
paulmckibbenI am running into a stumbling block with this.
In the 8.0.x branch, I created my own branch, and I am attempting to implement hook_page_alter() in content_translation.module. Here's some code:
I've translated a page node and a tag taxonomy term. For both, I see the message "This is a content entity," but I don't see "and it is translatable."
Stepping into it with a debugger, I'm seeing that $entity->isTranslatable() is indeed returning false.
What am I missing?
Comment #3
paulmckibbenI tried a different approach and succeeded. I looked at content_translation_overview() as an example of how to identify translations for an entity.
Patch attached!
I realize I need to add an automated test. I'm new to Drupal 8 core development. For this case, where is the best place to add a test, and is there a good tutorial somebody can point me to, or example code? Thanks!
Comment #5
gábor hojtsyThis logic is VERY convoluted. Instead IMHO you should use getTranslationLanguages() which gets you all the right languages :)
Comment #6
paulmckibbenGabor, thanks! If I had only known about that method.
New patch attached.
Comment #7
gábor hojtsyThis looks as good as it can be as far as I see for the code (if it works :D). As for tests, NodeTranslationUITest has nodes translated in various languages and already tests for read more, comment, etc. links. You could test the node pages there for the alternate links as well, should only be a few lines added there.
Comment #9
gábor hojtsyAs for the fails, looks like getOption() on the route may return null if there was no such option. So blindly foreach()ing on it would not work.
Comment #10
paulmckibbenGabor, thanks as always for the helpful advice.
New patch attached, fixing the foreach() issue that was breaking tests, and also updating NodeTranslationUITest.php to test for the hreflang links.
Comment #11
gábor hojtsyWe usually couple patch updates on drupal.org with interdiffs, so it is easier to review what changed. It may look too much for such small patches, but it would still help a lot. See https://www.drupal.org/documentation/git/interdiff I think you changed this:
to this:
That makes sense. :) Using an interdiff, it is much easier to tell.
Looking forward to test results.
Comment #13
gábor hojtsyFrom http://php.net/manual/en/function.empty.php
Drupal requires PHP 5.4.x, 5.5 is not required, so this should work with 5.4.x.
Comment #14
paulmckibbenGabor, thanks again for your help and patience. Let's try this again.
New patch attached, fixing the PHP 5.4 syntax error, plus two interdiffs:
interdiff-6-to-10.txt shows the diff from the patch in comment 6 to comment 10.
interdiff-10-to-14.txt shows the diff from comment 10 to this comment.
Let's see how the testbot does with this!
Comment #15
gábor hojtsyPutting on sprint board, since you are working on this :)
Comment #16
gábor hojtsyThis looks very good except this small bit:
There is a bit too much concrete markup assumption about how Drupal formats these tags exactly, down to the number of whitespace... If you use an xpath expression, you can test essentially the same thing but with more room for Drupal to adjust markup without needing to fix every detail in tests. Eg. something like
Good job! :)
Comment #17
paulmckibbenGabor, thank you! I did not think of using xpath.
New patch uploaded with interdiff. I used a slightly different xpath expression, '/head/link', to specify that the link be a child of the head element.
Comment #20
paulmckibbenTesting failed because of a git error. After re-queueing, tests passed. Setting back to Needs Review.
Comment #21
gábor hojtsyLooks good to me now. Thanks!
Comment #22
sunGlad to see you were able to locate the right bracket after which to break: ;-)
You can avoid excessive nesting by using (1) negated conditions (+
continue;if applicable) and/or (2) early-returns.Comment #23
paulmckibbenSun, thanks for the feedback. Did you want this to go back to "needs work"? Should I submit a new patch?
Comment #24
tim.plunkettSomething like this?
EDIT: @paulmckibben Ahhh! I crossposted with you, and I didn't notice you were assigned. Sorry :(
Comment #26
tim.plunkettWhoops. My test code snuck in there.
Comment #27
tim.plunkettComment #28
gábor hojtsyThanks for reformatting the code for even better readability :) Yay for machine parseable output.
Comment #31
tim.plunkettTestbot ate that run.
Comment #32
alexpottWhat about a view that is translated?
How come we do this? When do we find more than one content entity? Perhaps the comment needs updating to say don't check anymore parameters after we've found the one we're looking for :)
Comment #33
tim.plunkettWhen rewriting the code, the
breakcalled out in #22 from the patch in #17 became areturn.I added the comment because that's what it does, I'm not sure if it was intended.
Comment #34
sunI was confused by @alexpott's reference to views - which shouldn't apply here; except if the view itself would be translated (disregarding translatable entities within).
The quoted condition, however,
…seemingly tries to address a different problem space: If the current route contains multiple upcasted entity parameters, then we don't know what the "actual" (primary) entity of the current page is; e.g.:
/node/{node}/revisions/{node_revision}So we "blindly" choose the first, in the hope that it's the actual/primary. (which isn't necessarily the case, as e.g. in aforementioned node revision route example)
Comment #35
gábor hojtsy@alexpott: a view that is translated in itself does not really guarantee that that page will have language alternate content. That would be assuming quite a bit about how that view is built. Eg. that it is dependent on the page's language. That a view has some of its config translated does not guarantee it actually is displaying language dependent data.
@sun: do you know a way to tell the "primary" entity on the route from the "non-primary" one(s)?
Comment #36
paulmckibbenI am having trouble making sense of this concern as well. As far as I can tell, there would only be one entity parameter, even if the entity is a node with revisions, or even if the path has a revision ID in it.
@sun, can you explain a way I can test for a condition where there would be multiple entity parameters in the route?
Thank you.
Comment #37
gábor hojtsy@paulmckibben: well, looks like we need the comment updated as per #32.
Comment #38
paulmckibbenRerolled patch against updated 8.0.x branch, and updated the comment. Patch and interdiff attached. Interdiff seemed to have some issues with NodeTranslationUITest.php, but there are no new changes to that file. The only real change is the one comment line.
Thanks!
Paul
Comment #39
gábor hojtsyI think we cleared out above that config entities are not really suitable for this. Also that there is no way to tell which part of the route should be the primary entity, so we don't have a better way but to pick the first. The requested comment update was done, so moving back to RTBC.
Comment #40
paulmckibbenThanks, Gabor. I'd like to add, regarding "picking the first entity," in every test I tried, the route only had one entity. In @sun's example of /node/{node}/revisions/{node_revision}, there was no entity on the route at all. Bottom line: I could not find a scenario where the route had more than one entity. If somebody knows of such a scenario, I'd like to learn about it.
Comment #41
alexpottCommitted 2e7b455 and pushed to 8.0.x. Thanks!
@Gábor Hojtsy and I discussed whether or not there should be an alternate link to the same language as currently being viewed.
I updated the variable name on commit since $altlangcode should be snaked.
Comment #43
gábor hojtsySuperb, thanks for sticking around on this @paulmckibben, this should be a great move to Drupal 8 multilingual SEO!
Comment #45
mfbIt's not clear to me why the hreflang link tags should only be generated for translated entities. Other pages, such as views, and untranslated content with a translated user interface, need them too.
For another solution, this module adds hreflang link tags to all pages on the site: https://www.drupal.org/project/hreflang
Comment #46
andypost@mfb please file new feature request for 8.1.x branch!
Comment #47
roam2345 commentedShould this not be in the head of the html page? The language switcher is not in the of the page, that is what the description on the google page requires.