Skip to content

feat(platform-browser): add Link service for managing <link> elements #68220

@andrei-shpileuski

Description

@andrei-shpileuski

Prior art and why now

This request was previously raised as #34605 (2020) and PR #34606, both closed as not_planned. The team's reasoning at the time was that they did not want to expand Angular's API surface further.

Several things have changed since that decision:

  • NgOptimizedImage already manages <link> elements internally — the directive emits <link rel="preload"> hints for priority images. The Angular team has therefore already accepted programmatic <link> management as a legitimate concern inside the framework; a shared, public service would make the same capability available to application code.
  • Angular SSR (@angular/ssr) has become a first-class citizen. Managing canonical URLs, preconnect hints, and Web App Manifest links is now a standard SSR task that developers encounter on virtually every production Angular application.
  • Incremental hydration and partial rendering (Angular v17+) increase the importance of <link rel="modulepreload"> and <link rel="preload"> management in application code.
  • A reference implementation is available as @grandgular/link, demonstrating a stable, well-tested API design that mirrors Meta and Title.

Description

Angular ships Meta and Title as first-class services for managing document metadata, yet there is no equivalent for <link> elements. Developers who need to manage canonical URLs, preload hints, stylesheets, or Web App Manifest links currently have to either:

  • Manipulate the DOM directly (breaks Angular SSR)
  • Reach for third-party libraries
  • Write their own DOCUMENT + Renderer2 wrapper

A Link service would complete Angular's "document-head management" story.


Describe the solution you'd like

LinkDefinition

A type alias covering every standardised attribute of the HTML <link> element, with an index signature for non-standard attributes. Deprecated HTML5 attributes (charset, rev, target) are kept for legacy compatibility, matching the approach taken by MetaDefinition which also retains deprecated fields.

/**
 * Represents the attributes of an HTML `<link>` element.
 *
 * @see [HTML link element](https://developer.mozilla.org/docs/Web/HTML/Element/link)
 * @see {@link Link}
 *
 * @publicApi
 */
export type LinkDefinition = {
  as?: string;
  blocking?: string;
  charset?: string;           // deprecated in HTML5, retained for legacy
  crossorigin?: string;
  disabled?: string;
  fetchpriority?: string;
  href?: string;
  hreflang?: string;
  imagesizes?: string;
  imagesrcset?: string;
  integrity?: string;
  media?: string;
  nonce?: string;
  referrerpolicy?: string;
  rel?: string;
  rev?: string;               // deprecated in HTML5, retained for legacy
  sizes?: string;
  target?: string;            // deprecated in HTML5, retained for legacy
  title?: string;
  type?: string;
} & {
  [prop: string]: string;
};

Link service

/**
 * A service for managing HTML `<link>` tags.
 *
 * Properties of the `LinkDefinition` object match the attributes of the
 * HTML `<link>` tag. These tags are used for canonical URLs, stylesheets,
 * preload/prefetch hints, Web App Manifest references, and more.
 *
 * To identify specific `<link>` tags in a document, use an attribute selection
 * string in the format `"tag_attribute='value string'"`.
 * For example, an `attrSelector` value of `"rel='canonical'"` matches a tag
 * whose `rel` attribute has the value `"canonical"`.
 * Selectors are used with the `querySelector()` Document method,
 * in the format `link[{attrSelector}]`.
 *
 * @see [HTML link element](https://developer.mozilla.org/docs/Web/HTML/Element/link)
 * @see [Document.querySelector()](https://developer.mozilla.org/docs/Web/API/Document/querySelector)
 *
 * @publicApi
 */
@Injectable({ providedIn: 'root' })
export class Link {
  /**
   * Retrieves or creates a specific `<link>` tag element in the current HTML document.
   * @param tag The definition of a `<link>` element to match or create.
   * @param forceCreation True to create a new element without checking whether one already exists.
   */
  addTag(tag: LinkDefinition, forceCreation?: boolean): HTMLLinkElement | null;

  /**
   * Retrieves or creates a set of `<link>` tag elements in the current HTML document.
   */
  addTags(tags: LinkDefinition[], forceCreation?: boolean): HTMLLinkElement[];

  /**
   * Retrieves a `<link>` tag element in the current HTML document.
   * @param attrSelector The tag attribute and value to match against, in the format
   * `"tag_attribute='value string'"`.
   */
  getTag(attrSelector: string): HTMLLinkElement | null;

  /**
   * Retrieves a set of `<link>` tag elements in the current HTML document.
   */
  getTags(attrSelector: string): HTMLLinkElement[];

  /**
   * Modifies an existing `<link>` tag element in the current HTML document,
   * or creates a new one if not found.
   */
  updateTag(tag: LinkDefinition, selector?: string): HTMLLinkElement | null;

  /**
   * Removes an existing `<link>` tag element from the current HTML document.
   */
  removeTag(attrSelector: string): void;

  /**
   * Removes a specific `<link>` element from the DOM.
   */
  removeTagElement(link: HTMLLinkElement): void;
}

Usage examples

readonly link = inject(Link);

// Canonical URL
this.link.addTag({ rel: 'canonical', href: 'https://example.com/page' });

// Preload a critical font
this.link.addTag({
  rel: 'preload',
  as: 'font',
  type: 'font/woff2',
  href: '/fonts/brand.woff2',
  crossorigin: 'anonymous',
});

// Update canonical — no duplicate is created
this.link.updateTag({ rel: 'canonical', href: 'https://example.com/new-page' });

// Remove a stylesheet
this.link.removeTag('rel="stylesheet"');

Placement

The service fits naturally inside the existing platform-browser package with no new entry points and no new peer dependencies:

packages/platform-browser/
  src/
    browser/
      meta.ts                ← existing
      title.ts               ← existing
      link.ts                ← NEW
    platform-browser.ts      ← add: export {Link, LinkDefinition} from './browser/link';
  public_api.ts              ← already re-exports everything from platform-browser.ts
  goldens/public-api/
    platform-browser_api.d.ts  ← needs to be updated (yarn bazel run //tools/public_api_guard:platform-browser_api.accept)

SSR compatibility

The proposed implementation uses Renderer2 (via RendererFactory2) and the DOCUMENT token exclusively. This is a deliberate improvement over the existing Meta service which relies on ɵDomAdapter (internal, unstable API): Renderer2 is public, integrates with custom/test renderers, and is consistent with how third-party Angular libraries are expected to interact with the DOM.


Alternatives considered

  • Extending NgOptimizedImage: too narrow in scope; canonical URLs and manifest links have nothing to do with images.
  • Keeping it as a community library: the same argument could have been made against Meta and Title. Document-head management is a sufficiently common concern to warrant first-class framework support.
  • Using @angular/platform-browser's DomAdapter directly: DomAdapter is marked ɵ (internal) and is not recommended for application or library use.

Breaking changes

None. This is a purely additive change.


Prior art / proof of concept

A reference implementation is published as @grandgular/link (source: https://github.com/Grandgular/link). It has been used in production applications and its test suite covers all public API methods, SSR rendering, deduplication, and edge cases.


Open questions for the Angular team

  • Confirm placement inside platform-browser vs. a new platform-browser/link secondary entry point
  • Confirm naming: LinkDefinition (parallel to MetaDefinition) vs. LinkAttributes
  • Confirm whether Renderer2-based implementation is preferred over ɵDomAdapter for a new service
  • Discuss angular.dev documentation page scope

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreIssues related to the framework runtimefeatureLabel used to distinguish feature request from other issues

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions