## Overview

zudo-doc includes a built-in link checker (`scripts/check-links.js`) that runs two types of validation after the site is built.

## What It Checks

### Mode 1: Built HTML scan

Scans all `.html` files in `dist/` for internal `<a href="...">` links and verifies each target exists on disk. It handles:

- Base path stripping (e.g., `/my-docs/docs/foo` → `docs/foo`)
- Trailing slash resolution (`docs/foo/` → `docs/foo/index.html`)
- Extension resolution (`docs/foo` → `docs/foo/index.html` or `docs/foo.html`)
- Query string and fragment stripping before resolution
- Relative link resolution against the file's directory
- Caching resolved paths for performance

Links that are skipped (not checked):

- External URLs (`https://`, `http://`)
- Anchor-only links (`#section`)
- `mailto:`, `javascript:`, `data:`, `tel:` URIs
- Versioned docs links (`/v/*/`) — version content may be incomplete

### Mode 2: MDX source scan

Scans all `.mdx` and `.md` files in the configured content directories for absolute links that bypass the base path. This catches links like:

```mdx
<!-- These are flagged — they bypass the base path -->
[guide](/docs/guides/foo)
<a href="/ja/docs/guides/foo">link</a>

<!-- These are fine — they use relative paths -->
[guide](./foo.mdx)
[guide](../other/bar.mdx)
```

When a base path is configured (e.g., `/my-docs/`), absolute links starting with `/docs/` or `/ja/docs/` will break because they're missing the base path prefix. The MDX source scan catches these before they become broken links in production.

Links inside fenced code blocks are ignored.

## Image Checker

zudo-doc also includes a strict post-build image and asset checker (`zudo-doc check images`). It scans every rendered `.html` file under the build output and checks local references supplied by these elements and attributes:

- HTML `img[src]` and each valid `img[srcset]` candidate
- HTML `source[src]` and each valid `source[srcset]` candidate (including `<picture>` sources)
- HTML `video[src]` and `video[poster]`
- HTML `audio[src]`, `track[src]`, and `input[type="image"][src]`
- SVG `image[href]` and `image[xlink:href]`

Both site-absolute URLs (`/images/diagram.png`) and relative URLs (`./diagram.png`, `../images/diagram.png`, or `images/diagram.png`) are resolved with URL semantics from the page that contains them. The configured `base` path and the page's `<base href>` are honored. Query strings and fragments do not affect the file lookup. A reference is valid only when it resolves to a file inside the build output; directories and symlinks that point outside the output are reported.

The checker skips external URLs (including URLs with a scheme such as `http:` or `https:`), `data:` URLs, protocol-relative URLs (`//cdn.example/...`), and fragment-only references (`#diagram`).

### Build-time plugin and strict CLI

The package's build-time `img-src-check` plugin runs after the build and uses the existing `onBrokenMarkdownLinks` setting. Its default is `warn`, which reports broken local references without failing the build; `error` fails the build, and `ignore` disables this scan. This setting also controls broken Markdown links, so use the CLI below when the image check itself must be a strict, independent gate.

The strict CLI exits nonzero when any broken local reference remains after allowlisting:

```bash
pnpm build && pnpm check:images
```

Its options are:

- `--dist <dir>` — build output directory (default: `dist`)
- `--base <path>` — public URL base (default: `/`)
- `--allowlist <file>` — exact broken-reference exceptions
- `--help` — show the current command help

For example, a site deployed below `/docs/` can pass its base explicitly:

```bash
pnpm check:images -- --dist dist --base /docs/ --allowlist=.check-images-allowlist
```

The allowlist contains one exact `<page>:<url>` key per line, where `<page>` is the path relative to the build output and `<url>` is the original URL value. Blank lines and lines beginning with `#` are ignored:

```text
# A generated page still points at an intentionally absent asset
docs/generated/index.html:/images/legacy.png
```

Keep exceptions narrow and remove each line when its underlying asset is fixed. A generated project should add the strict check to CI after its build (and after any link check), so broken local assets cannot be published:

```yaml
- name: Build site
  run: pnpm build
- name: Check images
  run: pnpm run check:images
```

## Running the Link Checker

### Standalone

Build the site first, then run the checker:

```bash
pnpm build && pnpm check:links
```

### Failure category flags

By default, the checker exits with code 0 even when issues are found. Select the categories that should fail the command with the explicit `--strict-broken`, `--strict-absolute`, and `--strict-trailing` flags. The b4push and CI workflows fail on broken links and absolute MDX links while leaving trailing-slash findings informational:

```bash
pnpm check:links -- --strict-broken --strict-absolute --allowlist=.check-links-allowlist
```

Run `pnpm check:links -- --help` to see the current CLI options.

### Allowlist

`--allowlist=PATH` excludes exact `<file>:<line>:<href>` entries from the strict-mode failure counts — for issues that cannot be fixed at the source, such as a JA page that intentionally links to an EN-only sibling, or an absolute href that resolves to a runtime-generated route with no MDX file. The repo ships a real allowlist at `.check-links-allowlist`: one `<file>:<line>:<href>` entry per line, with `#` comments and blank lines ignored. Each entry must match the printed report verbatim — file path relative to the repo root, 1-based line number, and href as it appeared in the source or built HTML. Delete an entry once its underlying issue is fixed, so the strict gate catches future regressions of the same shape.

### As part of b4push

The link checker runs as step 30 of 34 in the pre-push validation (`pnpm b4push`). The strict image checker follows it as step 31, before HTML validation:

```
28. Build
29. Content-fallback check
30. **Link check**
31. **Image check**
32. HTML validation
33. Preview smoke
34. Manual smoke
```

The canonical, authoritative b4push step list lives in the header comment of `scripts/run-b4push.sh` — treat that script as the single source of truth for the full 34-step sequence.

### In CI

The link checker runs in the `build-site` job of the PR checks workflow (`pr-checks.yml`), immediately after `pnpm build`; the strict image checker runs immediately after the link checker. The production and preview deployment workflows run the same image gate before archiving their build output.

## Output

When no issues are found:

```
Checking links (base: /my-docs/)...

✓ No broken links or absolute path issues found
```

When issues are found:

```
Checking links (base: /my-docs/)...

=== Broken Links in Built HTML ===
  dist/docs/page/index.html:42  /my-docs/docs/missing-page

=== Absolute Links Bypassing Base Path (MDX Source) ===
  src/content/docs/guides/test.mdx:15  /docs/guides/foo

✗ Found 1 broken link and 1 absolute path warning
```

Each entry shows the file path, line number, and the problematic href.

## Configuration

The link checker reads its configuration from the resolved config (the fields you pass to `zudoDoc({...})` in `zfb.config.ts`):

- **`base`** — The site's base path, used to strip prefixes when resolving links
- **`docsDir`** — Primary content directory for MDX source scanning
- **`locales`** — Locale configurations with `dir` fields pointing to additional content directories for MDX source scanning (e.g., `locales: { ja: { dir: "src/content/docs-ja" } }`)