Google was getting two hreflang annotations that contradicted each other
next-intl's middleware builds its alternates from the locale prefix: it takes the pathname and puts /en in front of it. That is right for every template sharing a slug across languages, and wrong for the only one with a translated slug, /servicios against /en/services. On /servicios it advertised /en/servicios in the header, a URL that resolves but whose canonical points somewhere else, so the same response argued with itself.
return stripIntlAlternateLinks(intlMiddleware(request))
function stripIntlAlternateLinks(response: NextResponse): NextResponse {
const link = response.headers.get('link')
if (!link) return response
const kept = link
.split(/,\s*(?=<)/)
.filter((entry) => !/rel="?alternate"?/i.test(entry))
.join(', ')
if (kept) {
response.headers.set('link', kept)
} else {
response.headers.delete('link')
}
return response
}One annotation, the one in the head