---
title: "Links — Angular"
description: "Make the links in a PDF work: go to a page, open a website, and change how link areas look."
framework: "Angular"
source: "https://www.embedpdf.com/docs/headless/angular/viewing/links"
---

# Links

Most PDFs have links: a table of contents that jumps to a chapter, a footnote to a website. Register
`withLink()` and put `<epdf-link-layer>` in each page, and they work:

**`link-layer.ts`**

```typescript
import { Component } from '@angular/core';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfLinkLayer, withLink } from '@embedpdf/angular/link';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { provideEmbedPdf } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { engine } from './pdf';

@Component({
  selector: 'app-reader',
  imports: [EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfLinkLayer],
  providers: [provideEmbedPdf({ engine }, withStage(), withRender(), withInteraction(), withLink())],
  template: `
    <epdf-stage>
      <ng-template epdfPage>
        <epdf-render-layer />
        <epdf-link-layer />
      </ng-template>
    </epdf-stage>
  `,
})
export class Reader {}
```

**`basic.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  effect,
  inject,
  untracked,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput, PageRef } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnnotation, withAnnotation } from '@embedpdf/angular/annotation';
import { EpdfLinkLayer, withLink } from '@embedpdf/angular/link';
import { localEngine } from '@embedpdf/engine';

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// Where the two links go on the first page, in page coordinates.
const TO_PAGE_3 = { x: 72, y: 24, width: 160, height: 28 };
const TO_WEBSITE = { x: 250, y: 24, width: 190, height: 28 };

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfLinkLayer],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      // The annotation plugin is only here to make the links below.
      withAnnotation(),
      withLink(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './basic.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <p class="hint">
        Two links at the top of the first page. Click one, or press Tab to reach it and Enter to
        follow it.
      </p>
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-link-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  private readonly annotation = inject(EpdfAnnotation);
  private readonly document = inject(EpdfDocument);
  private added = false;

  constructor() {
    // Made on load, since the document has none: a label the page shows, and a link over it.
    effect(() => {
      const [first, , third] = this.document.pages();
      if (!first || !third || this.added) return;
      this.added = true;
      untracked(() => void this.addLinks(first.ref, third.ref));
    });
  }

  private async addLinks(first: PageRef, third: PageRef) {
    const label = (text: string, box: typeof TO_PAGE_3) =>
      this.annotation.create(first, {
        subtype: 'free-text',
        box,
        contents: text,
        fontSize: 13,
        fontColor: '#054fb3',
        interiorColor: '#e8f1ff',
        color: '#7db6ff',
        strokeWidth: 1,
      });
    await label('Go to page 3 →', TO_PAGE_3);
    await label('Open embedpdf.com ↗', TO_WEBSITE);
    await this.annotation.create(first, {
      subtype: 'link',
      rect: TO_PAGE_3,
      target: { kind: 'goto', destination: { kind: 'fit', page: third } },
    });
    await this.annotation.create(first, {
      subtype: 'link',
      rect: TO_WEBSITE,
      target: { kind: 'uri', uri: 'https://www.embedpdf.com' },
    });
  }
}
```

**`basic.css`**

```css
/* Colors */
:root {
  --muted: #5a6b92;
  --line: #e6eaf2;
  --canvas: #f7f9fc;
  --shadow: rgb(7 32 76 / 0.04);
}

/* What to try, above the pages */
.hint {
  margin: 0 0 12px;
  font-size: 13px;
  color: var(--muted);
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

- **A link to a page** goes there, to the exact spot and zoom the link names.
- **A link to a website** opens in a new tab, after a safety check (see [Good to know](#good-to-know)).
- **A link with a PDF action**, such as a button that resets a form, runs the action when the
  [actions plugin](../documents/actions) is registered.

The pointer turns into a hand over a link, and a link can be reached with the Tab key and followed
with Enter.

## Change how links look

By default a link area is invisible, as in other PDF apps. `<epdf-link-layer><ng-template epdfLink>` lets you draw it
yourself, for example with an underline on hover:

**`custom-links.ts`**

```typescript
import { NgTemplateOutlet } from '@angular/common';
import { Component } from '@angular/core';
import { EpdfLinkLayer, EpdfLinkTemplate } from '@embedpdf/angular/link';

@Component({
  selector: 'app-links',
  imports: [EpdfLinkLayer, EpdfLinkTemplate, NgTemplateOutlet],
  template: `
    <epdf-link-layer>
      <ng-template epdfLink let-link let-native="native">
        <span class="pdf-link" [attr.title]="link.target.kind === 'uri' ? link.target.uri : null">
          <ng-container [ngTemplateOutlet]="native" />
        </span>
      </ng-template>
    </epdf-link-layer>
  `,
})
export class Links {}
```

**`custom.ts`**

```typescript
import { NgTemplateOutlet } from '@angular/common';
import {
  ChangeDetectionStrategy,
  Component,
  effect,
  inject,
  signal,
  untracked,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput, PageRef } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnnotation, withAnnotation } from '@embedpdf/angular/annotation';
import { EpdfLinkLayer, EpdfLinkTemplate, withLink } from '@embedpdf/angular/link';
import { localEngine } from '@embedpdf/engine';

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// Where the two links go on the first page, in page coordinates.
const TO_PAGE_3 = { x: 72, y: 24, width: 160, height: 28 };
const TO_WEBSITE = { x: 250, y: 24, width: 190, height: 28 };

@Component({
  selector: 'demo-root',
  imports: [
    NgTemplateOutlet,
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfLinkLayer,
    EpdfLinkTemplate,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      // The annotation plugin is only here to make the links below.
      withAnnotation(),
      withLink(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './custom.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <div class="toolbar">
        <div class="segmented" role="group" aria-label="Link areas">
          <button type="button" [attr.aria-pressed]="shown()" (click)="shown.set(true)">
            Show link areas
          </button>
          <button type="button" [attr.aria-pressed]="!shown()" (click)="shown.set(false)">
            As the PDF has them
          </button>
        </div>
      </div>
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-link-layer>
            <ng-template epdfLink let-link let-native="native">
              @if (shown()) {
                <!-- Wrapping the layer's own anchor keeps what a click does. -->
                <span class="pdf-link" [attr.data-kind]="link.target.kind">
                  <ng-container [ngTemplateOutlet]="native" />
                </span>
              } @else {
                <!-- The layer's own, invisible area. -->
                <ng-container [ngTemplateOutlet]="native" />
              }
            </ng-template>
          </epdf-link-layer>
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  private readonly annotation = inject(EpdfAnnotation);
  private readonly document = inject(EpdfDocument);
  private added = false;
  protected readonly shown = signal(true);

  constructor() {
    // Made on load, since the document has none: a label the page shows, and a link over it.
    effect(() => {
      const [first, , third] = this.document.pages();
      if (!first || !third || this.added) return;
      this.added = true;
      untracked(() => void this.addLinks(first.ref, third.ref));
    });
  }

  private async addLinks(first: PageRef, third: PageRef) {
    const label = (text: string, box: typeof TO_PAGE_3) =>
      this.annotation.create(first, {
        subtype: 'free-text',
        box,
        contents: text,
        fontSize: 13,
        fontColor: '#054fb3',
        interiorColor: '#e8f1ff',
        color: '#7db6ff',
        strokeWidth: 1,
      });
    await label('Go to page 3 →', TO_PAGE_3);
    await label('Open embedpdf.com ↗', TO_WEBSITE);
    await this.annotation.create(first, {
      subtype: 'link',
      rect: TO_PAGE_3,
      target: { kind: 'goto', destination: { kind: 'fit', page: third } },
    });
    await this.annotation.create(first, {
      subtype: 'link',
      rect: TO_WEBSITE,
      target: { kind: 'uri', uri: 'https://www.embedpdf.com' },
    });
  }
}
```

**`custom.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
  --mark: #0876fd;
}

/* Show or hide the link areas */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

/* One choice of a few: the pressed button is the current one */
.segmented {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 2px;
  padding: 3px;
  background: var(--tint);
  border: 1px solid var(--line);
  border-radius: 11px;
}

.segmented button {
  font: inherit;
  font-size: 12.5px;
  font-weight: 550;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 8px;
  padding: 6px 12px;
  cursor: pointer;
  white-space: nowrap;
}

.segmented button:hover {
  color: var(--ink);
}

.segmented button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--surface);
  box-shadow: 0 1px 2px rgb(7 32 76 / 0.1);
}

.segmented button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* Your look for a link: a tint, and an underline that grows on hover */
.pdf-link a {
  border-radius: 4px;
  background: rgb(8 118 253 / 0.08);
  box-shadow: inset 0 -2px 0 rgb(8 118 253 / 0.55);
  transition:
    background-color 0.15s ease,
    box-shadow 0.15s ease;
}

.pdf-link a:hover {
  background: rgb(8 118 253 / 0.16);
  box-shadow: inset 0 -3px 0 var(--mark);
}

/* A link to a website gets a different color */
.pdf-link[data-kind='uri'] a {
  background: rgb(22 163 74 / 0.08);
  box-shadow: inset 0 -2px 0 rgb(22 163 74 / 0.6);
}

.pdf-link[data-kind='uri'] a:hover {
  background: rgb(22 163 74 / 0.16);
}

.pdf-link a:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

`native` is the layer's own clickable area: wrap it to keep what a click does.
For a link you don't want to change, show `native` alone.

## Read the links on a page

```ts
link.listLinks(page.ref); // every link on the page
link.getLinkAt(page.ref, { x: 120, y: 80 }); // the link at a point, or null
```

Each link has an `id`, its `bounds` on the page, and a `target`: where it goes.

| `target.kind`               | Goes to                                                                           |
| :-------------------------- | :-------------------------------------------------------------------------------- |
| `'goto'`                    | A page of this document: `target.destination`                                     |
| `'uri'`                     | A website or an email address: `target.uri`                                       |
| `'named'`                   | A viewer action, such as the next page                                            |
| `'javascript'`              | Runs the link's script, when the [actions plugin](../documents/actions) allows it |
| `'goto-remote'`, `'launch'` | Nowhere: see [Which links never run](#which-links-never-run)                      |

## Follow a link from code

`activate()` does what a click does:

```ts
const { outcome } = link.activate(someLink);
```

It returns what happened right away, so a website can still open in a new tab: `'revealed'` (the
view went to a page), `'uri'` (a website opened), or `'dispatched'` (the actions plugin runs the
link's action). A link it won't follow, such as one to another file, is `'reported'`.

**`list.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  signal,
  untracked,
  ViewEncapsulation,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfDocument, EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput, PageRef } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnnotation, withAnnotation } from '@embedpdf/angular/annotation';
import { EpdfLink, EpdfLinkLayer, withLink } from '@embedpdf/angular/link';
import { localEngine } from '@embedpdf/engine';

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// Where the links go on the first page, in page coordinates.
const TO_PAGE_3 = { x: 72, y: 24, width: 160, height: 28 };
const TO_WEBSITE = { x: 250, y: 24, width: 190, height: 28 };
const TO_SCRIPT = { x: 458, y: 24, width: 82, height: 28 };

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfLinkLayer],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withInteraction(),
      // The annotation plugin is only here to make the links below.
      withAnnotation(),
      withLink(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './list.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <div class="layout">
        <!-- The first page's links, read from the plugin, each with a button that follows it. -->
        <aside class="panel">
          <h3 class="heading">Links on page 1</h3>
          <ul class="links">
            @for (item of links(); track item.id) {
              <li class="link">
                <span class="kind">{{ item.target.kind }}</span>
                <span class="label">{{ link.getLabel(item) }}</span>
                <span class="where">at {{ round(item.bounds.x) }}, {{ round(item.bounds.y) }}</span>
                <button type="button" class="button" (click)="link.activate(item)">Follow</button>
              </li>
            }
          </ul>
          <p class="last">
            <code>activated$</code> {{ last() ?? 'not yet' }}
          </p>
        </aside>
        <epdf-stage class="stage">
          <ng-template epdfPage>
            <epdf-render-layer />
            <epdf-link-layer />
          </ng-template>
        </epdf-stage>
      </div>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  protected readonly link = inject(EpdfLink);
  private readonly annotation = inject(EpdfAnnotation);
  private readonly document = inject(EpdfDocument);
  private added = false;
  protected readonly round = Math.round;

  /** The list reads the links once they're made, and again each time a page's links are read. */
  private readonly ready = signal(false);
  private readonly reads = signal(0);
  protected readonly links = computed(() => {
    this.reads();
    return this.ready() ? this.link.listLinks(0) : [];
  });
  protected readonly last = signal<string | null>(null);

  constructor() {
    // Every way of following a link ends up here: a click, a key, or code.
    this.link.activated$
      .pipe(takeUntilDestroyed())
      .subscribe(({ target, activation }) => this.last.set(`${target.kind} → ${activation.outcome}`));
    this.link.loaded$.pipe(takeUntilDestroyed()).subscribe(() => this.reads.update((n) => n + 1));

    // Made on load, since the document has none: a label the page shows, and a link over it.
    // The third one is a `javascript:` address, which a link never opens.
    effect(() => {
      const [first, , third] = this.document.pages();
      if (!first || !third || this.added) return;
      this.added = true;
      untracked(() => void this.addLinks(first.ref, third.ref).then(() => this.ready.set(true)));
    });
  }

  private async addLinks(first: PageRef, third: PageRef) {
    const label = (text: string, box: typeof TO_PAGE_3) =>
      this.annotation.create(first, {
        subtype: 'free-text',
        box,
        contents: text,
        fontSize: 13,
        fontColor: '#054fb3',
        interiorColor: '#e8f1ff',
        color: '#7db6ff',
        strokeWidth: 1,
      });
    await label('Go to page 3 →', TO_PAGE_3);
    await label('Open embedpdf.com ↗', TO_WEBSITE);
    await label('A script', TO_SCRIPT);
    await this.annotation.create(first, {
      subtype: 'link',
      rect: TO_PAGE_3,
      target: { kind: 'goto', destination: { kind: 'fit', page: third } },
    });
    await this.annotation.create(first, {
      subtype: 'link',
      rect: TO_WEBSITE,
      target: { kind: 'uri', uri: 'https://www.embedpdf.com' },
    });
    await this.annotation.create(first, {
      subtype: 'link',
      rect: TO_SCRIPT,
      target: { kind: 'uri', uri: 'javascript:alert(1)' },
    });
  }
}
```

**`list.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The list beside the pages; above them on a narrow screen */
.layout {
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.panel {
  flex: 1 1 100%;
  min-width: 0;
}

.heading {
  margin: 0 0 8px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

.links {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* One link: what it is, where it goes, where it sits, and a button to follow it */
.link {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 2px 8px;
  align-items: center;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 8px 10px;
  box-shadow: 0 1px 2px var(--shadow);
}

.kind {
  font-family: ui-monospace, monospace;
  font-size: 11.5px;
  color: var(--accent);
}

.label {
  grid-column: 1;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  overflow-wrap: anywhere;
}

.where {
  grid-column: 1;
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.link .button {
  grid-column: 2;
  grid-row: 1 / span 3;
}

.last {
  margin: 10px 0 0;
  font-size: 12.5px;
  color: var(--ink);
}

.last code {
  margin-right: 6px;
  font-weight: 600;
  color: var(--accent);
}

.button {
  min-width: 38px;
  font: inherit;
  font-size: 13.5px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 11px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
  flex: 1 1 100%;
}

@container (min-width: 620px) {
  .panel {
    flex: 0 0 240px;
  }

  .stage {
    flex: 1 1 0;
    min-width: 0;
  }
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Methods

Everything `inject(EpdfLink)` gives you.

| Method                              | What it does                                                                                                                          |
| :---------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------ |
| `listLinks(page)`                   | Every link on a page, once the page's links are read. [Read the links on a page](#read-the-links-on-a-page)                           |
| `getLink(page, id)`                 | One link by its `id`, or `null`                                                                                                       |
| `getLinkAt(page, point)`            | The link at a point on the page, the smallest when links overlap, or `null`                                                           |
| `listAllLinks()`                    | Every link in the document, reading each page's links as needed                                                                       |
| `ensureLoaded(page)`                | Reads a page's links if they aren't read yet. `<epdf-link-layer>` does this for every page it shows.                                  |
| `isLoaded(page)`, `getStatus(page)` | Whether a page's links are read, and in detail: `'idle'`, `'loading'`, `'ready'`, `'error'` or `'forbidden'`                          |
| `activate(link)`                    | Follows a link, or a link's `target`, as a click does, and returns what happened. [Follow a link from code](#follow-a-link-from-code) |
| `activateAt(page, point)`           | Follows the link at a point on the page. Returns `null` when there's no link there.                                                   |
| `resolve(target)`                   | What following a target would do, without doing it: `{ kind }` is `'destination'`, `'uri'`, `'named'` or `'reported'`                 |
| `getLabel(link)`                    | A short label for a tooltip: the web address, or where the link goes                                                                  |

Every method that returns a promise also takes a `signal` in its options, to cancel it.

## Events

**`link-analytics.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfLink, EpdfLinkLayer } from '@embedpdf/angular/link';
import { EpdfRenderLayer } from '@embedpdf/angular/render';
import { EpdfPageTemplate, EpdfStage } from '@embedpdf/angular/stage';
import { analytics } from './analytics';

@Component({
  selector: 'app-reader',
  imports: [EpdfStage, EpdfPageTemplate, EpdfRenderLayer, EpdfLinkLayer],
  template: `
    <epdf-stage>
      <ng-template epdfPage>
        <epdf-render-layer />
        <epdf-link-layer />
      </ng-template>
    </epdf-stage>
  `,
})
export class Reader {
  constructor() {
    inject(EpdfLink)
      .activated$.pipe(takeUntilDestroyed())
      .subscribe(({ target }) => analytics.track('link_followed', { kind: target.kind }));
  }
}
```

| Event        | Fires when                                             | Carries                |
| :----------- | :----------------------------------------------------- | :--------------------- |
| `activated$` | Someone followed a link, by click, key or code         | `target`, `activation` |
| `loaded$`    | A page's links were read, or read again after a change | `page`                 |

## Good to know

### Which links never run

A website link opens only with `http`, `https`, `mailto` or `tel`. An address without one opens on
`https` when it starts with `//` or `www.`; any other, such as `/help` or `page2.html`, is relative
to the document and opens nothing, so a link can never open a page of your own app. Links that
would start a program or open another file on the user's computer are never followed, and a link's
JavaScript runs only when the [actions plugin](../documents/actions) has JavaScript turned on.

### Links while editing annotations

Links work while the `pointer` or `pan` tool is active. With the annotation plugin's `link` tool,
the layer steps aside and links become annotations you can move, resize and retarget; see
[Annotations](../annotations).

### Making links

Links are annotations: create them with the annotation plugin, or with the engine; see
[Links](https://www.embedpdf.com/docs/engine/annotations/links).
