isIndexable() ) { return; } $languages = $this->settings->published(); /* * Язык, на котором эту запись владелец не включил, версии страницы не * имеет (там редирект на исходный язык): alternate на него был бы * обещанием, которого нет. x-default (исходный язык) остаётся всегда. */ if ( null !== $this->pageLanguages ) { $languages = array_filter( $languages, fn( Language $language ): bool => ! $this->pageLanguages->isHiddenOn( $language ) ); } if ( count( $languages ) < 2 ) { return; } foreach ( $languages as $language ) { printf( '' . "\n", esc_attr( $language->bcp47() ), esc_url( $this->urls->canonicalUrlFor( $language ) ) ); } printf( '' . "\n", esc_url( $this->urls->canonicalUrlFor( $this->settings->defaultLanguage() ) ) ); } /** * Подставляет язык текущего запроса в атрибуты тега `html`. * * @param string $output Значение вида `lang="ru-RU"`. */ public function filterLanguageAttributes( $output ): string { $output = (string) $output; $lang = $this->resolver->current()->bcp47(); if ( 1 === preg_match( '/lang="[^"]*"/', $output ) ) { return (string) preg_replace( '/lang="[^"]*"/', 'lang="' . esc_attr( $lang ) . '"', $output ); } return trim( $output . ' lang="' . esc_attr( $lang ) . '"' ); } /** * {@inheritDoc} */ public function apply( HtmlDocument $document, Language $target ): void { $root = $document->root(); if ( null !== $root && 'html' === strtolower( (string) $root->nodeName ) ) { // Страховка на случай темы, которая не вызывает language_attributes(). $root->setAttribute( 'lang', $target->bcp47() ); } $this->fixCanonical( $document, $target ); } /** * Приводит canonical к текущему языковому адресу. * * @param HtmlDocument $document Разобранный документ. * @param Language $target Текущий язык. */ private function fixCanonical( HtmlDocument $document, Language $target ): void { $url = $this->urls->canonicalUrlFor( $target ); $existing = $this->findCanonical( $document ); if ( array() !== $existing ) { $first = array_shift( $existing ); $first->setAttribute( 'href', $url ); // Дубли canonical от нескольких SEO-модулей убираем. foreach ( $existing as $extra ) { $parent = $extra->parentNode; if ( null !== $parent ) { $parent->removeChild( $extra ); } } return; } if ( ! $this->isIndexable() ) { return; } $head = $document->document()->getElementsByTagName( 'head' )->item( 0 ); if ( null === $head ) { return; } $link = $document->document()->createElement( 'link' ); $link->setAttribute( 'rel', 'canonical' ); $link->setAttribute( 'href', $url ); $head->appendChild( $link ); } /** * Находит все теги canonical в документе. * * @param HtmlDocument $document Разобранный документ. * @return list */ private function findCanonical( HtmlDocument $document ): array { $found = array(); foreach ( $document->document()->getElementsByTagName( 'link' ) as $link ) { if ( 'canonical' === strtolower( trim( (string) $link->getAttribute( 'rel' ) ) ) ) { $found[] = $link; } } return $found; } /** * Стоит ли выводить canonical и hreflang на этой странице. * * У страницы ошибки и результатов поиска канонического адреса нет. */ private function isIndexable(): bool { // До хука `wp` главного запроса ещё нет, и условные теги вызывать нельзя. if ( ! did_action( 'wp' ) ) { return false; } return ! is_404() && ! is_search(); } }