User tests: Successful: Unsuccessful:
Pull Request resolves #38047.
HtmlDocument::addCustomTag() is documented as adding markup "to the head block", but its output is actually rendered by ScriptsRenderer. Templates that place <jdoc:include type="scripts" /> at the end of the body — a common performance optimisation — therefore end up with head-only markup incorrectly duplicated into the body.
In this issue that markup is the x-default hreflang <link> tag added by plg_system_languagefilter, which then fails HTML validation: a <link> element without itemprop or an allowed rel value must not appear inside <body>.
addHeadLink() could not be used instead, because HtmlDocument::$_links is keyed by href, and the x-default link normally shares its href with an already-added language-specific alternate link — so it would silently overwrite that entry.
The fix:
HtmlDocument::addHeadTag() method and $_customHead property, wired into getHeadData() / setHeadData() / mergeHeadData() / resetHeadData() for module output-caching parity with the existing _custom handling.MetasRenderer renders $_customHead. MetasRenderer is always part of the <head> output, unlike scripts, which templates may relocate.plg_system_languagefilter now calls addHeadTag() instead of addCustomTag() for the x-default link.This is intentionally not a change to addCustomTag() / ScriptsRenderer / $_custom. Third-party extensions may rely on the existing (documented-as-head-but-actually-scripts-position) behaviour, so it is left completely untouched. This PR only adds a new opt-in API and switches core's own languagefilter plugin to use it — no behavioural change for any existing addCustomTag() caller.
plg_system_languagefilter, enable "Remove URL Language Code" (or set xdefault).index.php to move <jdoc:include type="scripts" /> to just above </body>, keeping <jdoc:include type="metas" /> and <jdoc:include type="styles" /> in <head>.<link rel="alternate" hreflang="x-default"> tag.<link rel="alternate" hreflang="..."> tags (added via addHeadLink()) are still present and correct.addCustomTag() output — e.g. an analytics <script> snippet added by another extension — still renders at the scripts jdoc position, unchanged.The x-default link is rendered by ScriptsRenderer, so it follows the relocated scripts position and lands inside the body:
<body>
...
<link rel="alternate" hreflang="x-default" href="https://example.org/">
<script src="/media/system/js/core.min.js"></script>
</body>
The W3C validator reports:
Error: Element link is missing required attribute property.
Error: The element link must not appear as a descendant of the body element.
The x-default link is rendered by MetasRenderer and stays in the head, regardless of where the template places the scripts position:
<head>
...
<link rel="alternate" hreflang="en-GB" href="https://example.org/en/">
<link rel="alternate" hreflang="nl-NL" href="https://example.org/nl/">
<link rel="alternate" hreflang="x-default" href="https://example.org/">
</head>
The per-language alternates are unaffected, existing addCustomTag() output still renders at the scripts position, and the validator reports no link-in-body error.
Please select:
Documentation link for guide.joomla.org:
No documentation changes for guide.joomla.org needed
Pull Request link for manual.joomla.org:
No documentation changes for manual.joomla.org needed
| Status | New | ⇒ | Pending |
| Category | ⇒ | Libraries Front End Plugins |
| Title |
|
||||||
| Labels |
Added:
PR-5.4-dev
|
||||||
@richard67 body text adjusted to the template
@hans2103 Please keep the structure of the description of this PR like in our pull request template, and acknowledge the Generative AI policy . Thanks in advance.