Mudar o título da página de acordo com a rota - Angular

O título (<title>) da sua página é utilizado em vários lugares, porém todos tme um único propósito: dar uma boa ideia sobre o conteúdo abordado.

Para te dar uma ideia, pessoas com deficiência visual utilizam programas que fazem a leitura da tela. Ao focar em uma aba de um navegador, normalmente o título da página é lido e assim a pessoa pode decidir se aquele conteúdo é relevante ou não para sua pesquisa.

Uma técnica comum de navegação para usuários de tecnologias assistivas é ler o título da página a fim de deduzir o que ela contém. Isso ocorre porque navegar pela página para determinar o conteúdo dela pode ser confuso e consumir muito tempo.

fonte: https://developer.mozilla.org/pt-BR/docs/Web/HTML/Element/title#specifications

Além disso, podemos comentar sobre a importância do seu título para os mecanismos de busca.

A figura abaixo mostra o navegador renderizando o conteúdo do <title> na aba:

O conteúdo do <title> é mostrado no título da aba do seu navegador

Agora vamos focar no mundo Angular! Se você está começando com Angular provavelmente já percebeu que existe um arquivo chamado index.html e que o <title> da sua aplicação está lá:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
    
  <!-- **************************** -->
  <title>ConsolelogTitleService</title>
  <!-- **************************** -->  
    
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
</body>
</html>
index.html

Quando começamos um projeto novo não existe um arquivo ou configuração pronta para alterar o título de acordo com a rota. Por exemplo, considere uma aplicação com as seguintes rotas:

  • /home
  • /sobre
  • /contato

Conforme o usuário navega entre as rotas o título permanece o mesmo, veja abaixo  ao lado direito (<title>Olá</title>):

O título da página não se altera conforme o usuário navega entre as rotas

Gerenciando o título com a classe Title

O Angular fornece uma classe chamada Title. Esta classe nos permite alterar o título da aplicação. Abaixo há um exemplo bem direto sobre isto:

import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  constructor(private titleService: Title) {
    this.titleService.setTitle('Olá');
  }
}
app.component.ts
Título definido através da classe Title

Sabendo que o Title nos permite trocar o título, podemos alterar o título dentro de cada componente vinculado a uma rota:

/* ************************************** */
/* home.component.ts                      */
/* rota: /home                            */
/* ************************************** */

/* ... */
export class HomeComponent {
  constructor(private titleService: Title) {
    this.titleService.setTitle('Home');
  }
}



/* ************************************** */
/* sobre.component.ts                     */
/* rota: /sobre                           */
/* ************************************** */

/* ... */
export class SobreComponent {
  constructor(private titleService: Title) {
    this.titleService.setTitle('Sobre');
  }
}



/* ************************************** */
/* Contato.component.ts                   */
/* rota: /contato                         */
/* ************************************** */

/* ... */
export class ContetoComponent {
  constructor(private titleService: Title) {
    this.titleService.setTitle('Contato');
  }
}

Apesar de atingirmos o objetivo deste texto, alterar o título de acordo com a rota, podemos observar que esta estratégia irá funcionar bem para pequenas aplicações, mas conforme as rotas forem crescendo ficaria inviável essa excessiva duplicação de código.

Vamos avançar um pouco mais com o objetivo de criar a lógica acima em um único lugar - veja a seguir.

Router - identificando a rota ativa

Vamos fazer um rápido "parênteses" antes de voltarmos a falar sobre as rotas e seus títulos.

Inicialmente nossa aplicação carrega o AppComponent. Ele é o componente root que o Angular cria e insere no index.html.

Quando trabalhamos com rotas, dentro de seu template (app.component.html) encontraremos o elemento <router-outlet> que é onde os componentes são carregados de acordo com a rota ativa. A figura abaixo ilustra este cenário:

Esquema visual do router-outlet

Aqui é importante perceber que o AppComponent continua ativo independente da rota.


Quem vêm do ASP.NET pode fazer uma pequena analogia à MasterPage, mas lembre-se de que o mecanismo é totalmente diferente.


Então coisas que são comuns a toda a aplicação normalmente serão encontradas no template do AppComponent, como menu, footer, headers, etc. Lembre-se de que isto não é uma regra, como eu disse: normalmente.

Se o AppComponent é o root de nossa aplicação, significa que ele sempre está ativo independente da rota que estamos. Então independente da rota, no nosso cenário, o AppComponent está sempre lá. Sendo assim, podemos utilizar a classe Router para monitorar qual a rota que está ativa e assim determinar o valor do título:

import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { NavigationEnd, Router } from '@angular/router';
import { filter } from 'rxjs';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  constructor(router: Router, titleService: Title) {
    router.events
    
      // ****************************************
      // Filtramos apenas os eventos do tipo
      // NavigationEnd que ocorre quando a
      // navegação da rota é efetuada com sucesso.
      // Para saber mais sobre sobre este eventos:
      // https://angular.io/guide/router-reference#router-events
      // ****************************************
      .pipe(filter((event) => event instanceof NavigationEnd))
      .subscribe((event: NavigationEnd | any) => {
        console.log('URL ativa: ', event.url);

        switch (event.url) {
          case '/home':
            titleService.setTitle('Home');
            break;

          case '/sobre':
            titleService.setTitle('Sobre');
            break;

          case '/contato':
            titleService.setTitle('Contato');
            break;

          default:
            titleService.setTitle('Olá');
            break;
        }
      });
  }
}
app.component.ts
O título é alterado de acordo com a rota

Perceba que nosso código funciona bem. Conseguimos adicionar um código para gerenciar os títulos dentro de um único lugar (AppComponent), porém novamente temos o problema do crescimento das rotas. Conforme a aplicação for crescendo teremos que adicionar vários case: neste switch.

Pensando em melhorar um pouco mais, vamos focar no arquivo app-routing.module.

Route.data - adicionando informações às rotas

No arquivo app-routing.module.ts onde normalmente declaramos as rotas, é possível notar que as rotas são do tipo Routes (const routes: Routes = [ /* ... */ ]). Se olharmos melhor a definição do Routes temos o seguinte:

export declare type Routes = Route[];

Então cada rota que declaramos é do tipo Route e o conjunto de rotas define o Route[] que é a mesma coisa que Routes. Se novamente observarmos melhor, Route é uma interface com vários campos:

export declare interface Route {
    path?: string;
    pathMatch?: string;
    matcher?: UrlMatcher;
    component?: Type<any>;
    redirectTo?: string;
    outlet?: string;
    canActivate?: any[];
    canActivateChild?: any[];
    canDeactivate?: any[];
    canLoad?: any[];
    
    /**
     * Additional developer-defined data provided to the component via
     * `ActivatedRoute`. By default, no additional data is passed.
     */
    data?: Data;
    
    resolve?: ResolveData;
    children?: Routes;
    loadChildren?: LoadChildren;
    runGuardsAndResolvers?: RunGuardsAndResolvers;
}

Veja que há um campo opcional chamado data com a tipagem Data:

export declare type Data = {
    [name: string]: any;
};

Podemos utilizar este campo para atribuir quaisquer valores que julguemos necessário.

The data property in the third route is a place to store arbitrary data associated with this specific route. The data property is accessible within each activated route. Use it to store items such as page titles, breadcrumb text, and other read-only, static data.

fonte: https://angular.io/guide/router-reference

Por exemplo, podemos adicionar um valor de título na configuração de cada rota para posteriormente efetuar sua leitura quando a rota estiver ativa. Veja abaixo:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ContatoComponent } from './contato/contato.component';
import { HomeComponent } from './home/home.component';
import { SobreComponent } from './sobre/sobre.component';

const routes: Routes = [
  {
    path: 'home',
    component: HomeComponent,
    data: { titulo: 'Home' },
  },
  {
    path: 'contato',
    component: ContatoComponent,
    data: { titulo: 'Contato' },
  },
  {
    path: 'sobre',
    component: SobreComponent,
    data: { titulo: 'Sobre' },
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}
app-routing.module.ts

Para efetuar a leitura do conteúdo que está no data podemos recorrer ao ActivatedRoute:

import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { filter, map, switchMap } from 'rxjs';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  constructor(
    activatedRoute: ActivatedRoute,
    router: Router,
    titleService: Title
  ) {
    router.events
      // Sempre que a navegação (rota) for finalizada...
      .pipe(filter((event) => event instanceof NavigationEnd))
      
      // ...retornamos o activatedRoute...
      .pipe(map(() => activatedRoute))
      
      // ...e buscamos na árvore de rotas
      // a rota que está ativa.
      //
      // Lembre-se: quando trabalhamos com rotas
      // filhas (children) podemos ter várias
      // rotas ativas ao mesmo tempo. O map
      // abaixo trata este cenário.
      .pipe(
        map((route) => {
          while (route.firstChild) route = route.firstChild;
          return route;
        })
      )
      
      // Pega o "data" da rota, que está lá
      // no arquivo app-routing.module...
      .pipe(switchMap((route) => route.data))
      
      // Finalmente define o título
      .subscribe((event) => titleService.setTitle(event['titulo']));
  }
}
app.component.ts
<div>
  <h1>Header</h1>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit. Quas ducimus facere
  porro amet possimus fugiat totam labore a odio quae.
</div>
<div>
  <a href="#" [routerLink]="['/home']">Home</a> |
  <a href="#" [routerLink]="['/contato']">Contato</a> |
  <a href="#" [routerLink]="['/sobre']">Sobre</a> |
</div>
<div style="border: 1px solid red; padding: 16px; margin: 16px auto">
  <router-outlet></router-outlet>
</div>
<div>
  <h1>Footer</h1>
  Lorem ipsum, dolor sit amet consectetur adipisicing elit. Ad, ducimus?
</div>
app.component.html

Agora chegamos no cenário desejado, atualizar o título de acordo com a rota ativa mantendo esta gestão em um único lugar (AppComponent):

O título é alterado de acordo com a rota

TranslateService - tradução dos títulos

Caso esteja utilizando o TranslateService para trabalhar com vários idiomas, podemos seguir a mesma lógica, porém ao invés de adicionarmos o título no app-routing.module.ts vamos adicionar apenas a chave de tradução:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ContatoComponent } from './contato/contato.component';
import { HomeComponent } from './home/home.component';
import { SobreComponent } from './sobre/sobre.component';

const routes: Routes = [
  {
    path: 'home',
    component: HomeComponent,
    data: { titulo: 'home' },    // <-- chave de tradução
  {
    path: 'contato',
    component: ContatoComponent,
    data: { titulo: 'contato' }, // <-- chave de tradução
  },
  {
    path: 'sobre',
    component: SobreComponent,
    data: { titulo: 'sobre' },   // <-- chave de tradução
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}
app-routing.module.ts
{
    "home": "Inicial",
    "sobre": "Sobre",
    "contato": "Contato"
}
pt.json
{
    "home": "Home",
    "sobre": "About",
    "contato": "Contact"
}
en.json
<div>
  <h1>Header</h1>
  Lorem ipsum dolor sit, amet consectetur adipisicing elit.
  Quas ducimus facere porro amet possimus fugiat totam
  labore a odio quae.
</div>
<div>
  <a href="#" [routerLink]="['/home']">Home</a> |
  <a href="#" [routerLink]="['/contato']">Contato</a> |
  <a href="#" [routerLink]="['/sobre']">Sobre</a> |
  <button (click)="trocarIdioma('pt')">Português</button>
  <button (click)="trocarIdioma('en')">Inglês</button>
</div>
<div
  style="
    border: 1px solid red;
    padding: 16px;
    margin: 16px auto;
  "
>
  <router-outlet></router-outlet>
</div>
<div>
  <h1>Footer</h1>
  Lorem ipsum, dolor sit amet consectetur adipisicing elit.
  Ad, ducimus?
</div>
app.component.html

Também é necessário modificar o AppComponent para sabermos se o usuário alterou o idioma. Sempre que alterar o idioma temos que traduzir o título:

import { Component } from '@angular/core';
import { Title } from '@angular/platform-browser';
import {
  ActivatedRoute,
  NavigationEnd,
  Router,
} from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { filter, map, switchMap } from 'rxjs';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  chaveTitulo: string = '';

  constructor(
    activatedRoute: ActivatedRoute,
    router: Router,
    titleService: Title,
    private translate: TranslateService
  ) {
    translate.addLangs(['pt', 'en']);
    translate.use('pt');

    router.events
      .pipe(
        filter((event) => event instanceof NavigationEnd)
      )
      .pipe(map(() => activatedRoute))
      .pipe(
        map((route) => {
          while (route.firstChild) route = route.firstChild;
          return route;
        })
      )
      .pipe(switchMap((route) => route.data))
      .subscribe((event) => {
        // Sempre que a rota for alterada, 
        // guardamos a chave do título em uma
        // variável
        this.chaveTitulo = event['titulo'];
        const tituloTraduzido = this.translate.instant(
          this.chaveTitulo
        );

        titleService.setTitle(tituloTraduzido);
      });

    // Quando o usuário trocar de idioma
    // o titulo do site deve ser atualizado
    // para o idioma selecionado:
    this.translate.onLangChange.subscribe(() => {
      const tituloTraduzido = this.translate.instant(
        this.chaveTitulo
      );

      titleService.setTitle(tituloTraduzido);
    });
  }

  trocarIdioma(idioma: 'pt' | 'en') {
    this.translate.use(idioma);
  }
}
app.component.ts
O Angular altera e traduz o título de acordo com a rota e idioma

Considerações

A partir do Angular 14 a gestão do título ficará um pouco diferente. Vou deixar um link onde li sobre este assunto e em breve abordarei novamente o tema:

Setting Page Titles Natively With The Angular Router ?
When building applications with Angular, one common thing you should do is have the page title update...

Links interessantes: