agno docs

Navigation

Menus, section ordering, previous/next links, pagination, tables of contents and active states.

4 min read 691 words

agno gives you the raw navigation data; the markup is yours. This chapter covers every piece the templates receive and the patterns this site uses.

Menu items come from agno.toml and arrive as site.menu:

[[menu]]
title = "Docs"
url = "/docs/"

[[menu]]
title = "CLI"
url = "/docs/cli/"
<nav>
  {% for item in site.menu %}
  <a href="{{ item.url }}"
     {% if item.url|starts_with("/") and page.url|starts_with(item.url) %}class="active"{% endif %}>
    {{ item.title }}
  </a>
  {% endfor %}
</nav>

starts_with keeps external links from ever matching. For the docs entry, /docs/ matches /docs/cli/ too, which is usually the behaviour you want for a top-level section.

Sections

site.sections lists top-level sections with a title, URL and page count:

<ul>
  {% for section in site.sections %}
  <li><a href="{{ section.url }}">{{ section.title }}</a> <span>{{ section.count }}</span></li>
  {% endfor %}
</ul>

Nested sections are not included. For a full sidebar you either hardcode the chapter list, as this site does in _sidebar.html, or generate it from a section listing page.

Ordering with weight

Documentation reads best in a deliberate order. Put sort_by = "weight" in the section’s _index.md and give each page a weight:

+++
title = "Installation"
weight = 10
+++
+++
title = "Quickstart"
weight = 20
+++

Lower weights come first. Equal weights fall back to date, then title. Pages without a weight count as 0. The same weights drive page.prev and page.next.

Numbering

Leaving gaps (10, 20, 30) makes it easy to insert a chapter later without renumbering everything.

Previous and next

Inside a section, each page receives its neighbours in that section’s order:

<nav class="pager">
  {% if page.prev %}
  <a rel="prev" href="{{ page.prev.url }}">
    <span>← Previous</span>
    <strong>{{ page.prev.title }}</strong>
  </a>
  {% endif %}
  {% if page.next %}
  <a rel="next" href="{{ page.next.url }}">
    <span>Next →</span>
    <strong>{{ page.next.title }}</strong>
  </a>
  {% endif %}
</nav>

Each neighbour is { title, url, date_display }. Both are null at the ends of the section. Changing the order re-renders the affected neighbours on the next build.

Pagination

Sections can split their listing:

+++
title = "Blog"
paginate_by = 10
+++
  • Page 1 stays at /blog/.
  • Later pages live at /blog/page/2/, /blog/page/3/.
  • page.paginator provides current, total, page_size, item_count, prev_url, next_url, first_url, last_url and links[].
{% if page.paginator %}
<nav class="pagination">
  {% if page.paginator.prev_url %}
  <a rel="prev" href="{{ page.paginator.prev_url }}">← Newer</a>
  {% endif %}

  {% for link in page.paginator.links %}
  <a href="{{ link.url }}"{% if link.current %} aria-current="page"{% endif %}>{{ link.number }}</a>
  {% endfor %}

  {% if page.paginator.next_url %}
  <a rel="next" href="{{ page.paginator.next_url }}">Older →</a>
  {% endif %}
</nav>
{% endif %}

Pagination is a section feature. The home page accepts sort_by but does not paginate.

Tables of contents

page.toc is empty when a page has fewer than two headings. When it is populated, page.toc_html is ready to print:

{% if page.toc|length > 1 %}
<aside class="toc">
  <h2>On this page</h2>
  {{ page.toc_html|safe }}
</aside>
{% endif %}

For a sticky sidebar TOC, wrap it in a positioned element. To highlight the section currently in view, attach a scroll listener or IntersectionObserver to the heading IDs; the headings already have slugified id attributes.

There is no built-in breadcrumb object, but page.section and page.url are enough:

<nav aria-label="Breadcrumb">
  <a href="/docs/">Documentation</a>
  <span>/</span>
  <span>{{ page.title }}</span>
</nav>

For multi-level paths, split page.section on / in the template or keep a lookup of labels.

Exact matching is the simplest reliable approach:

<a href="/docs/installation/"
   {% if page.url == "/docs/installation/" %}class="active" aria-current="page"{% endif %}>
  Installation
</a>

For a generated sidebar, compare page.url with item.url, or use starts_with when a parent entry should stay active for child pages.

RSS, sitemap and 404

These are not navigation data but they belong in the shell:

<link rel="alternate" type="application/rss+xml" href="/rss.xml">
<link rel="canonical" href="{{ site.base_url }}{{ page.url }}">

404.html is generated at the output root. Most static hosts serve it automatically; on others, point the host’s not-found rule at /404.html.

Next

For client-side search, continue with Search.

Found a mistake? Open an issue or send a pull request.