getzola/zola · error

Relative link not found.

Error message

Relative link {} not found.

What it means

resolve_internal_link looks up a cleaned, percent-decoded internal link path in the site's permalinks map (built from all known pages/sections/assets). If the path is not found — meaning no content file corresponds to the link — this error is thrown. It is the root cause of 'broken internal link' failures across the library.

Solutions

  1. Fix the link path in the source markdown to point at an existing file (check spelling, case-sensitivity, and extension)
  2. Create the missing target file/asset the link references
  3. Run zola check to list all unresolvable links and fix each one
  4. If the file exists but isn't linked, verify it's inside the content/ or the page's colocated asset directory so it registers a permalink

Example fix

// before (content/posts/a.md)
[other post](./oher-post.md)
// after
[other post](./other-post.md)
Defensive patterns

Strategy: validation

Validate before calling

// Verify a link target exists before writing it:
// test -f content/posts/other-post.md && echo OK
// or run: zola check  (reports every unresolvable internal link)

Try / catch

// Rust (library consumers, e.g. helpers calling fix_link)
match resolve_internal_link(link, &permalinks, config_path) {
    Ok(resolved) => resolved.permalink,
    Err(_) => {
        log::warn!("dead internal link dropped: {link}");
        String::new()
    }
}

Prevention

When it happens

Trigger: Any internal link (@/ page/section reference, markdown relative link, colocated asset reference) whose target path does not exist in the permalinks map: missing file, wrong relative path, mistyped filename/extension, or incorrect percent-encoding.

Common situations: Renaming or deleting a page that other pages link to; linking before creating the target content file; typos in relative paths like ./ ../ or @/ prefixes; linking to assets not actually colocated with the page.

Related errors


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

Appendix: source

Thrown at components/utils/src/site.rs:33

    pub anchor: Option<String>,
}

/// Resolves an internal link (of the `@/posts/something.md#hey` sort) to its absolute link and
/// returns the path + anchor as well
pub fn resolve_internal_link(
    link: &str,
    permalinks: &HashMap<String, String>,
) -> Result<ResolvedInternalLink> {
    // First we remove the @/ since that's zola specific
    let clean_link = link.replacen("@/", "", 1);
    // Then we remove any potential anchor
    // parts[0] will be the file path and parts[1] the anchor if present
    let parts = clean_link.split('#').collect::<Vec<_>>();
    // If we have slugification turned off, we might end up with some escaped characters so we need
    // to decode them first
    let decoded = percent_decode(parts[0].as_bytes()).decode_utf8_lossy().to_string();
    let target =
        permalinks.get(&decoded).ok_or_else(|| anyhow!("Relative link {} not found.", link))?;
    if parts.len() > 1 {
        Ok(ResolvedInternalLink {
            permalink: format!("{}#{}", target, parts[1]),
            md_path: decoded,
            anchor: Some(parts[1].to_string()),
        })
    } else {
        Ok(ResolvedInternalLink { permalink: target.to_string(), md_path: decoded, anchor: None })
    }
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use super::resolve_internal_link;

    #[test]

View on GitHub (pinned to 61d3082821)