getzola/zola · warning

Anchor `#{}` not found on page

Error message

Anchor `#{}` not found on page

What it means

The link checker, when validating in-page anchors, fetches the page body and searches for a matching id/name via `has_anchor_id`. If the fragment identifier after '#' is not found anywhere in the page, `check_page_for_anchor` returns this error, marking the internal link as broken.

Source

Thrown at components/link_checker/src/lib.rs:117

fn has_anchor(url: &str) -> bool {
    match url.find('#') {
        Some(index) => match url.get(index..=index + 1) {
            Some("#/") | Some("#!") | None => false,
            Some(_) => true,
        },
        None => false,
    }
}

fn check_page_for_anchor(url: &str, body: String) -> errors::Result<()> {
    let index = url.find('#').unwrap();
    let anchor = url.get(index + 1..).unwrap();

    if has_anchor_id(&body, anchor) {
        Ok(())
    } else {
        Err(anyhow!("Anchor `#{}` not found on page", anchor))
    }
}

#[cfg(test)]
mod tests {
    use super::{
        LINKS, LinkChecker, check_page_for_anchor, check_url, has_anchor, is_valid, message,
    };
    use reqwest::StatusCode;

    // NOTE: HTTP mock paths below are randomly generated to avoid name
    // collisions. Mocks with the same path can sometimes bleed between tests
    // and cause them to randomly pass/fail. Please make sure to use unique
    // paths when adding or modifying tests that use Mockito.

    #[test]
    fn can_validate_ok_links() {
        let mut server = mockito::Server::new();

View on GitHub (pinned to 61d3082821)

Solutions

  1. Open the target page and find the actual rendered id of the section; update the link's fragment to match
  2. Slugify the heading text the same way Zola does (lowercase, spaces to hyphens, punctuation stripped) and use that as the anchor
  3. If the target truly should not have an anchor, remove the '#fragment' from the link
  4. Add an explicit anchor/id to the target element

Example fix

// before
[docs](./guide.md#Quick-Start)
// after
[docs](./guide.md#quick-start)
Defensive patterns

Strategy: validation

Validate before calling

import fs from 'fs';
function anchorsInFile(path) {
  const html = fs.readFileSync(path, 'utf8');
  return new Set([...html.matchAll(/(?:id|name)="([^"]+)"/g)].map((m) => m[1]));
}
function linkIsValid(pageAnchors, url) {
  const i = url.indexOf('#');
  if (i === -1) return true;
  return pageAnchors.has(url.slice(i + 1));
}

Type guard

function hasAnchor(html, fragment) {
  return typeof fragment === 'string' &&
    new RegExp(`(?:id|name)=["']${CSS.escape(fragment)}["']`).test(html);
}

Try / catch

match check_internal_link(url) {
    Ok(()) => {},
    Err(e) if e.to_string().starts_with("Anchor `#") => {
        log::warn!("dead anchor, fix link: {} ({e})", url);
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: A markdown/HTML link like `[text](./page.md#section)` where page.md exists but no heading or element defines `id="section"` (or a matching `name` attribute) — detected by `check_page_for_anchor`, invoked from `check_url`.

Common situations: Renaming or deleting a heading without updating links to its anchor; anchors with special characters that Zola slugifies differently (e.g. 'C++' -> 'c'); case mismatches; anchor pointing at a heading rendered by shortcodes not present at check time.

Related errors


AI-assisted analysis of getzola/zola@61d3082821 (2026-09-03). Data as JSON: /api/errors/702b03ea08ace190. Report an issue: GitHub.