diff --git a/docs/src/app/material-docs-app.ts b/docs/src/app/material-docs-app.ts index 4e27862eebd4..8b8f4d55fe08 100644 --- a/docs/src/app/material-docs-app.ts +++ b/docs/src/app/material-docs-app.ts @@ -11,10 +11,11 @@ import {Component, OnDestroy, ViewEncapsulation, inject} from '@angular/core'; import {AnalyticsService} from './shared/analytics/analytics'; import {NavigationFocusService} from './shared/navigation-focus/navigation-focus.service'; import {Subscription} from 'rxjs'; -import {map, pairwise, startWith} from 'rxjs/operators'; -import {RouterOutlet} from '@angular/router'; +import {filter, map, pairwise, startWith} from 'rxjs/operators'; +import {NavigationEnd, Router, RouterOutlet} from '@angular/router'; import {NavBar} from './shared/navbar/navbar'; import {CookiePopup} from './shared/cookie-popup/cookie-popup'; +import {HeaderTagManager} from './shared/header-tag-manager'; @Component({ selector: 'material-docs-app', @@ -29,10 +30,12 @@ import {CookiePopup} from './shared/cookie-popup/cookie-popup'; }) export class MaterialDocsApp implements OnDestroy { private _subscriptions = new Subscription(); + private _headerTagManager = inject(HeaderTagManager); constructor() { const analytics = inject(AnalyticsService); const navigationFocusService = inject(NavigationFocusService); + const router = inject(Router); this._subscriptions.add( navigationFocusService.navigationEndEvents @@ -50,11 +53,24 @@ export class MaterialDocsApp implements OnDestroy { analytics.locationChanged(toUrl); }), ); + + router.events + .pipe( + filter((e): e is NavigationEnd => e instanceof NavigationEnd), + map(event => event.urlAfterRedirects), + ) + .subscribe(url => { + this._updateCanonicalLink(url); + }); } ngOnDestroy() { this._subscriptions.unsubscribe(); } + + private _updateCanonicalLink(absoluteUrl: string) { + this._headerTagManager.setCanonical(absoluteUrl); + } } function resetScrollPosition() { diff --git a/docs/src/app/shared/header-tag-manager.ts b/docs/src/app/shared/header-tag-manager.ts new file mode 100644 index 000000000000..3dc54fed7a37 --- /dev/null +++ b/docs/src/app/shared/header-tag-manager.ts @@ -0,0 +1,40 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {DOCUMENT} from '@angular/common'; +import {Injectable, inject} from '@angular/core'; + +const MAT_ANGULAR_DEV = 'https://material.angular.dev'; + +/** + * Information about the deployment of this application. + */ +@Injectable({providedIn: 'root'}) +export class HeaderTagManager { + private readonly _document = inject(DOCUMENT); + + /** + * Sets the canonical link in the header. + * It supposes the header link is already present in the index.html + * + * The function behave invariably and will always point to angular.dev, + * no matter if it's a specific version build + */ + setCanonical(absolutePath: string): void { + const pathWithoutFragment = this._normalizePath(absolutePath).split('#')[0]; + const fullPath = `${MAT_ANGULAR_DEV}/${pathWithoutFragment}`; + this._document.querySelector('link[rel=canonical]')?.setAttribute('href', fullPath); + } + + private _normalizePath(path: string): string { + if (path[0] === '/') { + return path.substring(1); + } + return path; + } +}