skip to content

In Angular's router, how would you write a custom TitleStrategy that appends the site name to every route's title?

level: middleimportance: nice to knowfreq 30%

answer

  1. an abstract class you provide
  2. override updateTitle
  3. buildTitle walks primary children
  4. routes without a title

basics

~10 s

Extend TitleStrategy, override updateTitle(snapshot), call buildTitle(snapshot) for the deepest primary route's title, and set it with the Title service plus a suffix. Provide it with {provide: TitleStrategy, useClass: AppTitleStrategy}.

solid answer

~40 s

`TitleStrategy` is an abstract class in `@angular/router` with one abstract method, `updateTitle(snapshot: RouterStateSnapshot)`, which the router calls right after a navigation ends. Its helper `buildTitle(snapshot)` walks down the primary outlet and returns the title of the deepest route that has one, including titles produced by a resolver. A custom strategy extends the class, injects `Title` from `@angular/platform-browser`, and sets `` `${title} | Shop` ``, falling back to the bare site name when no route defines a title. You register it with `{provide: TitleStrategy, useClass: AppTitleStrategy}` beside `provideRouter`. The fallback matters: `DefaultTitleStrategy` only writes when a title exists, so a route without one silently keeps the previous page's title.

code

ts · 16 lines
ts
import {Injectable, inject} from '@angular/core';
import {Title} from '@angular/platform-browser';
import {RouterStateSnapshot, TitleStrategy} from '@angular/router';

@Injectable()
export class AppTitleStrategy extends TitleStrategy {
  private readonly title = inject(Title);

  override updateTitle(snapshot: RouterStateSnapshot): void {
    const pageTitle = this.buildTitle(snapshot);
    this.title.setTitle(pageTitle ? `${pageTitle} | Shop` : 'Shop');
  }
}

// app.config.ts
// providers: [provideRouter(routes), {provide: TitleStrategy, useClass: AppTitleStrategy}]

go deeper

for a junior

Know that a route's title property sets the tab title and that a TitleStrategy decides how it is written.

for a middle

Explain extending TitleStrategy, overriding updateTitle, using buildTitle, and providing the class in place of the default.

for a senior

Catch the edge cases: missing titles keep the old tab title, named outlets are ignored, and the title changes only after NavigationEnd.

for a principal

Centralise page-identity concerns such as titles, meta tags and screen-reader announcements in one strategy the whole team can rely on.

## Where route titles come from A route can declare `title`, either as a string or as a resolver function whose result arrives in the route's resolved data. Declaring titles is ordinary route-definition work; **what the router does with them** is decided by a `TitleStrategy`. The router injects one and calls it after every successful navigation. ## The TitleStrategy API `TitleStrategy` is an abstract class exported by `@angular/router`: | Member | Kind | Behaviour | |---|---|---| | `updateTitle(snapshot)` | abstract | called by the router after a navigation completes; you decide what to write | | `buildTitle(snapshot)` | helper | walks from the root down the **primary** outlet and returns the title of the deepest route that has one, or `undefined` | | `getResolvedTitleForRoute(route)` | helper | returns one route's final title (static or resolved) as `string` or `undefined` | The default is **`DefaultTitleStrategy`**. It calls `buildTitle()` and, **only if the result is not `undefined`**, passes it to the `Title` service's `setTitle()`. Two consequences follow: - A route with **no title** leaves the previous page's title in the tab, which is a common and easily missed bug. - **Named outlets** such as a popup never contribute a title, because `buildTitle()` only follows primary children. ## Writing a custom strategy 1. Create an injectable class that extends `TitleStrategy`. 2. Inject `Title` from `@angular/platform-browser`. 3. Override `updateTitle(snapshot)`: call `this.buildTitle(snapshot)`, add the suffix, and use a fallback when the result is `undefined`. 4. Provide it: `{provide: TitleStrategy, useClass: AppTitleStrategy}` in the application providers next to `provideRouter(routes)`. In an NgModule app the same provider goes in the root module. Common extras that belong in the same class: - **Translation**: treat the route title as a key and look it up in an i18n service before writing it. - **Breadcrumb titles**: instead of `buildTitle()`, walk the snapshot yourself and join the titles of every level ("Order 42 | Orders | Shop"). - **Metadata**: update a matching `og:title` through the `Meta` service so shared links stay in step. - **Accessibility**: announce the new title to screen readers, because the document title is the main page-change cue in a single-page app. ## When it runs The router calls `updateTitle()` **immediately after emitting `NavigationEnd`**. A navigation that is cancelled or fails never reaches it, so the title always reflects a completed navigation. A side effect: code subscribed to `NavigationEnd` that synchronously reads `document.title` sees the **old** title. Analytics that need the new title should read it on a later tick or compute it from the snapshot themselves. ## Testing the strategy Because the strategy is a plain injectable, a router test can provide it, navigate, and check the result: 1. Configure the testing module with `provideRouter(testRoutes)` and `{provide: TitleStrategy, useClass: AppTitleStrategy}`. 2. Navigate with the router or a test harness to a route with a title, then to one without. 3. Assert `inject(Title).getTitle()` after each navigation: "Orders | Shop", then the fallback "Shop". Testing the fallback path is the important part, since that is the behaviour the default strategy gets wrong for most apps. ## Why a strategy instead of setting titles in components Setting `document.title` in each component's constructor spreads the rule across the app, runs before the navigation is known to have succeeded, and breaks when a component is reused for a new parameter. A strategy puts the rule in **one place**, works for every route including lazy-loaded ones, and keeps route definitions declarative. It is also easy to test: build a snapshot through a navigation in a router test and assert what `Title.getTitle()` returns.

  • Why might an analytics call made on NavigationEnd report the previous page's document title?
    The router emits `NavigationEnd` first and only then calls `TitleStrategy.updateTitle()`. A subscriber that reads `document.title` synchronously inside its `NavigationEnd` handler runs before the new title is written, so it records the old one. Defer the read or derive the title from the router snapshot.
  • A lazy-loaded feature wants its own title format. Can one app-wide TitleStrategy handle it?
    Yes. The strategy receives the whole `RouterStateSnapshot`, so it can walk the tree and read each route's `data` or title, for example a flag the feature sets on its parent route, and choose a format. There is still one `TitleStrategy` for the router; the rule lives in it rather than in the feature.

saying these in an interview costs you the question

  • Route titles only work if each component sets document.title
  • The default strategy clears the title for routes without one
  • buildTitle() joins the titles of all outlets, named ones included
  • updateTitle() runs before guards so the title changes early
  • A custom TitleStrategy must be provided per lazy-loaded route