Linking Guide

Complete guide to creating links between pages, headings, and external resources

9 min read 1702 words

Bengal provides multiple ways to create links between pages, headings, and external resources. Choose the method that best fits your use case.

Tip

Try it out: This guide includes a test target anchor that demonstrates arbitrary reference targets. Scroll down to see it in action!

Quick Reference

Method Use Case Syntax
Markdown Links Standard links, external URLs [text](url)
Cross-References Internal page links with auto-title [[path]]
External References Link to external docs (Python, NumPy, etc.) [[ext:project:target]]
Template Functions Dynamic links in templates {{ ref('path') }}
Anchor Links Link to headings [[#heading]]or#heading
Target Directives Arbitrary anchors mid-page :::{target} id
Target References Link to target directives [[!target-id]]

Use standard Markdown link syntax for external URLs and simple internal links.

[Visit GitHub](https://github.com)
[Email us](mailto:hello@example.com)

Absolute paths (from site root):

[Get Started](/docs/get-started/)
[API Reference](/docs/reference/api/)

Relative paths (from current page):

<!-- From docs/get-started/installation.md -->
[Quickstart](./quickstart.md)
[Configuration](../reference/config.md)
[Home](../../_index.md)

Path Resolution Rules:

  • .mdextension is optional — Bengal resolves bothpage.mdandpage
  • Relative paths resolve from the current page's directory
  • ./refers to current directory,../goes up one level
  • Paths are normalized automatically (trailing slashes, etc.)
[Link text](https://example.com "Optional title")

Cross-references provide intelligent linking with automatic title resolution and O(1) lookup performance. Use them for internal page links.

Basic Cross-Reference

[[docs/getting-started]]

This automatically:

  • Resolves to the page atdocs/getting-started.md
  • Uses the page's title as link text
  • Handles path normalization automatically
[[docs/getting-started|Get Started Guide]]

The|separator lets you specify custom link text while still using intelligent path resolution.

If a page has a customidin frontmatter:

---
id: install-guide
title: Installation Guide
---

Link to it by ID:

[[id:install-guide]]
[[id:install-guide|Installation]]

Benefits of ID-based links:

  • Stable even if page path changes
  • Shorter syntax
  • Works across site restructures

Link to any heading in your site:

[[#installation]]
[[#configuration]]

This finds the heading text (case-insensitive) and links to it. If multiple pages have the same heading, it uses the first match.

Link to heading in specific page:

[[docs/getting-started#installation]]

Path Resolution

Cross-references support multiple path formats:

[[docs/getting-started]]        # Path without extension
[[docs/getting-started.md]]     # Path with extension (also works)
[[getting-started]]             # Slug (if unique)
[[id:install-guide]]            # Custom ID

Resolution order:

  1. Custom ID (id:xxx)
  2. Path lookup (docs/page)
  3. Slug lookup (page-name)

Broken References

If a cross-reference can't be resolved, Bengal shows a broken reference indicator:

[[nonexistent-page]]

Renders as:[nonexistent-page]with a visual indicator for debugging.

Link to headings within the same page or across pages.

Same-Page Anchors

[Installation](#installation)
[Configuration](#configuration)

Headings automatically get anchor IDs based on their text. For example:

## Installation

This heading gets ID: `installation`

Custom Anchor IDs

Use{#custom-id}syntax for custom anchor IDs on headings:

## Installation {#install-guide}

Link to it: [[#install-guide]]

Arbitrary Reference Targets

Create anchor targets anywhere in content (not just on headings) using the{target}directive. This is similar to RST's.. _label:syntax.

Syntax:

:::{target} my-anchor-id
:::

Example: Target Directive Usage

:::{target} important-caveat
:::

:::{warning}
This caveat is critical for production use.
:::

See [[!important-caveat|the caveat]] for details.

Reference Syntax:

Use[[!target-id]]to explicitly reference target directives:

[[!test-target]]              # Link with auto-generated text
[[!test-target|Custom Text]]   # Link with custom text

Why!instead of#?

The!prefix distinguishes target directive references from heading anchor references:

  • [[#heading]]- References heading anchors (auto-generated or custom{#id})
  • [[!target-id]]- References target directives (explicit:::{target})

This eliminates collisions and makes your intent explicit.

Use cases:

  • Anchor before a note/warning that users should link to
  • Stable anchor that survives heading text changes
  • Anchor in middle of content (not tied to heading)
  • Migration from Sphinx (.. _label:) or RST reference targets

Anchor ID requirements:

  • Must start with a letter (a-z, A-Z)
  • May contain letters, numbers, hyphens, underscores
  • Case-sensitive in output, case-insensitive for resolution

Note: The target renders as an invisible anchor element. Any content inside the directive is ignored (targets are point anchors, not containers).

Try it: This page has a test target below. Jump to it: Test Target

This is a test target anchor. You can link to it using[[!test-target]]from anywhere in your site.

Cross-Page Anchors

[Installation](/docs/getting-started/#installation)
[[docs/getting-started#installation]]

Both syntaxes work. Cross-references are preferred for internal links.

Template Functions

Use template functions in Kida templates for dynamic link generation.

ref(path, text=None)

Generate a cross-reference link:

{{ ref('docs/getting-started') }}
{{ ref('docs/getting-started', 'Get Started') }}
{{ ref('id:install-guide') }}

Returns: Safe HTML link (<a href="...">...</a>)

Use cases:

  • Dynamic navigation menus
  • Related pages sections
  • Breadcrumbs

doc(path)

Get a page object for custom link generation:

{% let page = doc('docs/getting-started') %}
{% if page %}
  <a href="{{ page.url }}">{{ page.title }}</a>
  <p>{{ page.description }}</p>
{% end %}

Returns:Pageobject orNone

Use cases:

  • Custom link formatting
  • Accessing page metadata
  • Conditional rendering

anchor(heading, page_path=None)

Link to a heading:

{{ anchor('Installation') }}
{{ anchor('Configuration', 'docs/getting-started') }}

Parameters:

  • heading: Heading text to find (case-insensitive)
  • page_path: Optional page path to restrict search

Returns: Safe HTML link with anchor fragment

Use cases:

  • Table of contents
  • "Jump to" links
  • Cross-page heading references

relref(path)

Get relative URL without generating a link:

<a href="{{ relref('docs/api') }}" class="btn">API Docs</a>

{% let api_url = relref('docs/api') %}
{% if api_url %}
  <link rel="preload" href="{{ api_url }}" as="document">
{% end %}

Returns: URL string or empty string if not found

Use cases:

  • Custom link HTML
  • Meta tags
  • Preload/prefetch links

Path Resolution

Understanding how Bengal resolves paths helps you write reliable links.

Path Formats

Bengal accepts multiple path formats:

[[docs/getting-started]]        # Recommended: path without extension
[[docs/getting-started.md]]     # Also works: path with extension
[[getting-started]]             # Slug (if unique across site)
[[id:install-guide]]            # Custom ID

Resolution Order

When resolving a cross-reference:

  1. Custom ID (id:xxx) — Checked first, fastest lookup
  2. Path lookup (docs/page) — Most explicit, recommended
  3. Slug lookup (page-name) — Fallback, may be ambiguous

Relative Path Resolution

Relative paths resolve from the current page's directory:

<!-- File: docs/getting-started/installation.md -->

[[./quickstart]]              # docs/getting-started/quickstart.md
[[../reference/config]]       # docs/reference/config.md
[[../../_index]]              # _index.md (site root)

Rules:

  • .mdextension is optional
  • ./refers to current directory
  • ../goes up one level
  • Paths are normalized (trailing slashes, etc.)

Best Practices

When to Use Each Method

Use Markdown Links ([text](url)) when:

  • Linking to external URLs
  • You need full control over link text
  • Simple relative paths within a section

Use Cross-References ([[path]]) when:

  • Linking to internal pages
  • You want automatic title resolution
  • You want stable links that survive path changes (with IDs)
  • You're writing documentation

Use Template Functions (ref(),doc(), etc.) when:

  • Generating links dynamically in templates
  • Building navigation menus
  • Creating related pages sections
  • Conditional link rendering

Use Anchor Links (#heading) when:

  • Linking to specific sections
  • Creating table of contents
  • Cross-referencing specific content

Use Target Directives (:::{target}) when:

  • Creating anchors not tied to headings
  • Need stable anchors that survive content restructuring
  • Migrating from RST/Sphinx (.. _label:)
  • Anchoring before notes/warnings for direct linking

Reference with[[!target-id]]:

  • Explicit syntax avoids collisions with heading anchors
  • Makes intent clear (target directive vs heading)
  • Required for target directive references

Most Stable (survives path changes):

  • Custom IDs:[[id:my-page]]
  • Template functions with IDs:{{ ref('id:my-page') }}

Moderately Stable (survives minor changes):

  • Path-based cross-references:[[docs/page]]
  • Absolute markdown links:[text](/docs/page/)

Least Stable (breaks on path changes):

  • Relative markdown links:[text](../page.md)
  • Slug-based references (if slugs change)

Performance

All linking methods use O(1) lookups from pre-built indexes:

  • Cross-references: Built during discovery phase
  • Template functions: Use same index
  • Markdown links: Resolved during rendering

No performance difference between methods — choose based on use case.

Examples

<nav>
  {% for item in site.menus.main %}
    <a href="{{ item.url }}">{{ item.name }}</a>
  {% endfor %}
</nav>
## Related Pages

- [[docs/tutorials/getting-started|Getting Started]]
- [[docs/reference/api|API Reference]]
- [[docs/tutorials/examples|Examples]]
{% let related = site.pages | where('tags', page.tags, 'in') | limit(5) %}
{% if related %}
  <h2>Related Pages</h2>
  <ul>
    {% for page in related %}
      <li>{{ ref(page.path) }}</li>
    {% endfor %}
  </ul>
{% end %}

Table of Contents

## Table of Contents

- [[#introduction|Introduction]]
- [[#installation|Installation]]
- [[#configuration|Configuration]]
- [[#usage|Usage]]

Cross-Reference in Content

For detailed setup instructions, see [[docs/getting-started/installation|the installation guide]].

Once installed, configure your site as described in [[docs/reference/config#site-settings|Site Settings]].

Troubleshooting

If a link doesn't resolve:

  1. Check path spelling — Paths are case-sensitive
  2. Verify page exists — Usebengal validateto check
  3. Check path format — Use path without.mdextension
  4. Try absolute path — Use/docs/pageinstead of relative

Bengal validates links during build:

bengal validate

This checks:

  • Cross-references resolve
  • Markdown links point to existing pages
  • Anchor links target valid headings

Debugging Broken References

Broken cross-references show visual indicators:

<span class="broken-ref" data-ref="nonexistent-page"
      title="Page not found: nonexistent-page">
  [nonexistent-page]
</span>

Use browser dev tools to inspect broken references and see what path was attempted.

See Also