Skip to content

API#

Core#

Bases: BasePlugin[RelatedContentPluginConfig]


              flowchart TD
              mkdocs_related_content.plugin.RelatedContentPlugin[RelatedContentPlugin]

              

              click mkdocs_related_content.plugin.RelatedContentPlugin href "" "mkdocs_related_content.plugin.RelatedContentPlugin"
            

Main class for the Related Content Mkdocs plugin.

Computes, for every tagged page, a list of related pages based on shared tags, and exposes it to the Jinja context so a theme override (e.g. overrides/partials/related.html) can render it.

on_files - read every page's frontmatter straight from disk to build a full-site tags index. This has to happen before any page is rendered, and Page.meta isn't populated yet at this point - see util.Util.build_tags_index. on_nav - precompute every page's related pages in one pass, now that the index (built from disk, not from the render pipeline) is guaranteed complete. on_page_context - expose the precomputed result for the current page. on_post_build - optionally write our own tags.json-shaped export, only when Material's tags plugin isn't already doing so.

Instantiation.

First event called on build, right after config is loaded.

See: https://www.mkdocs.org/user-guide/plugins/#on_config

Parameters:

Name Type Description Default
config MkDocsConfig

global configuration object.

required

Returns:

Type Description
MkDocsConfig

The (possibly unmodified) global configuration object.

Called after the global Files collection is populated.

This is the step that makes the whole plugin possible: every File is known at this point, so we can read every page's YAML frontmatter straight from disk and know every tag on the site - something Page.meta can't give us yet, page by page.

See: https://www.mkdocs.org/user-guide/plugins/#on_files

Parameters:

Name Type Description Default
files Files

global files collection.

required
config MkDocsConfig

global configuration object.

required

Returns:

Type Description
Files

The (unmodified) global files collection.

Called after the site navigation is created.

Precomputes every page's related pages in a single pass. Safe to do here - rather than lazily on the first on_page_context call - because self.tags_index no longer depends on the page render order: it was built from raw frontmatter in on_files.

See: https://www.mkdocs.org/user-guide/plugins/#on_nav

Parameters:

Name Type Description Default
nav Navigation

global navigation object.

required
config MkDocsConfig

global configuration object.

required
files Files

global files collection.

required

Returns:

Type Description
Navigation

The (unmodified) global navigation object.

Called after the page context is created, before rendering.

Exposes related_pages, the configured section title and CSS class to Jinja.

See: https://www.mkdocs.org/user-guide/plugins/#on_page_context

Parameters:

Name Type Description Default
context dict

template context for the current page.

required
page Page

mkdocs.structure.pages.Page instance.

required
config MkDocsConfig

global configuration object.

required
nav Navigation

global navigation object.

required

Returns:

Type Description
dict

The enriched template context.

on_post_build(config) #

Called once the whole site has been built.

Writes our own tags.json-shaped export, but only as a fallback: if Material's tags plugin is active and already exports its own JSON, we skip this to avoid a redundant, possibly inconsistent file.

See: https://www.mkdocs.org/user-guide/plugins/#on_post_build

Parameters:

Name Type Description Default
config MkDocsConfig

global configuration object.

required

Runs once at the very beginning of an mkdocs invocation.

See: https://www.mkdocs.org/user-guide/plugins/#on_startup

Parameters:

Name Type Description Default
command Literal['build', 'gh-deploy', 'serve']

the command Mkdocs was invoked with.

required
dirty bool

whether --dirty flag was passed.

required

Bases: Config


              flowchart TD
              mkdocs_related_content.config.RelatedContentPluginConfig[RelatedContentPluginConfig]

              

              click mkdocs_related_content.config.RelatedContentPluginConfig href "" "mkdocs_related_content.config.RelatedContentPluginConfig"
            

Configuration for the Related Content plugin for Mkdocs.


Models#

Lightweight, timing-safe representation of one page's tags.

Built directly from a page's YAML frontmatter during on_files - i.e. straight from disk, before Mkdocs has read/rendered a single page. This is what makes it possible to know every page's tags before computing relatedness for the very first one.

title starts as a best-effort guess (frontmatter title, else first Markdown heading, else a humanized filename) and gets superseded by the real Page.title wherever that's already available - see util.Util.resolve_related_pages.

This page's own related_content.links frontmatter, in the order they were listed - see ManualLink.

One entry exposed to the Jinja template for a single related page.

True for a page listed in the current page's own related_content.links frontmatter, rather than computed from tag similarity.

Relative to the page it's attached to (see Util.resolve_related_pages), ready to use as-is in an href - unlike PageTagsEntry.url, which is root-relative. Exception: an external manual link's url is used exactly as written in the frontmatter, unchanged.

Integrations#

Shared base to detect whether the active theme is Material (or MaterialX).

Same pattern as mkdocs-rss-plugin's own IntegrationMaterialThemeBase: https://github.com/Guts/mkdocs-rss-plugin

Integration instantiation.

Parameters:

Name Type Description Default
mkdocs_config MkDocsConfig

Mkdocs website configuration object.

required

Check if the theme set in mkdocs.yml is material or not.

Parameters:

Name Type Description Default
mkdocs_config MkDocsConfig | None

Mkdocs website configuration object.

None

Returns:

Type Description
bool

True if the theme's name is 'material' or 'materialx'.

Bases: IntegrationMaterialThemeBase


              flowchart TD
              mkdocs_related_content.integrations.theme_material_tags.IntegrationMaterialTags[IntegrationMaterialTags]
              mkdocs_related_content.integrations.theme_material_base.IntegrationMaterialThemeBase[IntegrationMaterialThemeBase]

                              mkdocs_related_content.integrations.theme_material_base.IntegrationMaterialThemeBase --> mkdocs_related_content.integrations.theme_material_tags.IntegrationMaterialTags
                


              click mkdocs_related_content.integrations.theme_material_tags.IntegrationMaterialTags href "" "mkdocs_related_content.integrations.theme_material_tags.IntegrationMaterialTags"
              click mkdocs_related_content.integrations.theme_material_base.IntegrationMaterialThemeBase href "" "mkdocs_related_content.integrations.theme_material_base.IntegrationMaterialThemeBase"
            

Detects Material's built-in tags plugin and exposes its configuration.

Material's tags plugin builds its own page/tag mapping incrementally, page by page, exactly like our own plugin would if it used the same hooks. That mapping - and the tags.json file it can export - is only complete once every page has been processed, i.e. at on_post_build. That's after every page's Jinja context (and therefore its rendered HTML) has already been produced.

Consequence: we can't read tags.json, nor the tags plugin's internal mapping, to fill related_pages for a given page - that data doesn't exist yet at that point of the build (see plugin.py's on_files for how we work around this for our own index).

What is safely available at any point of the build is the tags plugin's static configuration (e.g. tags_allowed). We use it so our own, independently-computed tag index stays consistent with what readers see on Material's tags index page.

Integration instantiation.

Parameters:

Name Type Description Default
mkdocs_config MkDocsConfig

Mkdocs website configuration object.

required
switch_force bool

set to False to disable this integration even if Material's tags plugin is enabled. Defaults to True.

True

Allow-list configured on Material's tags plugin (tags_allowed), if any.

tags_allowed isn't a plain list of strings: Material's TagSet config option validates it into a set of its own Tag objects (material.plugins.tags.structure.tag.Tag). That class overrides __eq__ to only compare against other Tag instances - comparing one to a plain str raises AssertionError instead of returning False. Tag.__str__ does return the plain tag name, so we normalize to strings here rather than leaking Tag objects into util.build_tags_index, which compares against plain frontmatter strings.

Returns:

Type Description
set[str] | None

None when no restriction is configured (every tag found in a

set[str] | None

page's frontmatter is considered valid), otherwise the set of

set[str] | None

allowed tag names, as plain strings.

Best-effort check of whether Material's tags plugin will export its own tags.json for this build.

This inspects a couple of plausible, undocumented attribute names that have been used across Material versions and is deliberately conservative: if none match, it assumes export is active (Material's default), so our own fallback export in plugin.on_post_build stays opt-in rather than risking a silent duplicate file.

Returns:

Type Description
bool

True if Material's tags plugin appears to already export JSON.

Utils#

Standalone helpers for the Related Content plugin.

Kept free of any Mkdocs event wiring so the scoring logic can be unit tested on its own - same separation of concerns as mkdocs-rss-plugin's own Util class.

Build a {src_uri: PageTagsEntry} index by reading YAML frontmatter directly from disk.

This intentionally does not rely on Page.meta: at on_files time, no page has been read by Mkdocs yet, so this is the only way to know every page's tags before rendering the first one.

Parameters:

Name Type Description Default
files Files

Mkdocs' global Files collection.

required
allowed_tags set[str] | None

optional allow-list (typically Material's tags plugin tags_allowed setting) used to discard unknown tags. None means no filtering.

None
match_path_pattern Pattern[str] | None

optional compiled regex (see the plugin's match_path option) matched against each page's src_uri. A page that doesn't match is skipped entirely - it never appears as a related page for another page, and never gets related pages of its own, since it's absent from the returned index either way. None means every page matches.

None

A page can also opt itself out via its own YAML frontmatter, regardless of match_path:

related_content:
  exclude_from_scoring: true

This has the exact same effect as failing match_path - the page is entirely absent from the returned index - but is a per-page author decision rather than a site-wide, path-based one. Unlike the hide: [related_content] frontmatter convention (a purely template-level check - see docs/index.md), this is read and enforced by the plugin itself, before any candidate scoring runs.

A page can also pin its own hand-picked suggestions, which take priority over automatically-computed ones (see resolve_related_pages):

related_content:
  links:
    - some-page.md
    - Custom label: some-other-page.md
    - https://example.org/some-external-resource/

Same nav-like syntax as MaterialX/Material's blog plugin links property - a bare string, or a single-key {label: target} mapping for an explicit title (always used as-is, no auto-resolution) - see _parse_manual_links. A target may be internal (src_uri-style) or an external URL. A page with links but no tags is still indexed (it just never gets automatic suggestions of its own, nor is it ever suggested to others via scoring).

Returns:

Type Description
dict[str, PageTagsEntry]

The index, keyed by File.src_uri. A page is omitted only if

dict[str, PageTagsEntry]

it has neither a (valid) tag nor any related_content.links.

Precompute every page's related pages.

Parameters:

Name Type Description Default
tags_index dict[str, PageTagsEntry]

output of build_tags_index.

required
min_score float

minimum score for a page to be considered related.

required
max_related int

maximum number of related pages kept per page - shared between automatic and manual suggestions together, see resolve_related_pages.

required
tag_weights dict[str, float] | None

optional per-tag weight passed to jaccard_score (see compute_tag_weights). None (the default) scores every tag equally.

None

A page's own related_content.links targets (see build_tags_index) are kept out of its automatic candidates here, before max_related caps the list - a page already pinned manually never wastes a slot that a fresh automatic candidate could fill instead.

Returns:

Type Description
dict[str, list[tuple[float, str]]]

{src_uri: [(score, related_src_uri), ...]}, sorted by descending

dict[str, list[tuple[float, str]]]

score and capped to max_related.

Weight each tag by the inverse of how many pages use it.

A tag shared by only 2 pages out of 500 is a much stronger signal of relatedness than a tag half the site uses - this gives the former a weight of 0.5 and the latter 1/250 = 0.004, so it counts for much less in jaccard_score.

Parameters:

Name Type Description Default
tags_index dict[str, PageTagsEntry]

output of build_tags_index.

required

Returns:

Type Description
dict[str, float]

{tag: weight}, weight = 1 / number of pages using that tag.

Similarity between two tag sets: |intersection| / |union|.

Computes the union via inclusion-exclusion (|a| + |b| - |a & b|, or the weighted equivalent below) instead of building the union set itself (a | b) - same result, one fewer set allocation per call, which matters here since this runs inside compute_related_pages's O(N^2) loop.

When tag_weights is given (see compute_tag_weights), each tag contributes its weight instead of a flat 1 to both the intersection and union sums - rare, shared tags count for more than common ones. The result stays between 0 and 1 either way, since the intersection is always a subset of the union.

Parameters:

Name Type Description Default
tags_a set[str]

tags of the first page.

required
tags_b set[str]

tags of the second page.

required
tag_weights dict[str, float] | None

optional per-tag weight, e.g. from compute_tag_weights. A tag missing from this mapping falls back to a weight of 1. None (the default) is equivalent to every tag weighing 1 - the plain, unweighted Jaccard score.

None

Returns:

Type Description
float

A score between 0 (no shared tag) and 1 (identical tag sets).

Turn automatic candidates and the page's own manual links into template-ready RelatedPage objects, manual ones first.

Prefers the real, fully-resolved Page.title whenever Mkdocs has already produced it (i.e. the related page was processed earlier in the build), and falls back to the frontmatter/heading guess from tags_index otherwise. In practice the two are almost always identical. A manual link's own label (see ManualLink), when given, always wins over both - it's an explicit author choice.

RelatedPage.url is computed relative to current_page_url (via Mkdocs' own get_relative_url) rather than left root-relative like tags_index[...].url is. Root-relative URLs (e.g. sub/page-b/) only resolve correctly from the site root - used directly as href="{{ r.url }}" in a template, they produce a wrong link for any page that isn't at the site root itself. Resolving them here means every consumer gets a correct, ready-to-use href without having to remember to apply Mkdocs' | url Jinja filter themselves. An external manual link (see _is_external_link) is the one exception - its url is used exactly as written, since there's no internal file to resolve it against.

Manual links (related_content.links, see build_tags_index) are resolved first, capped to max_manual_related, and always shown regardless of min_score - an explicit author choice overrides the automatic threshold. Remaining slots, up to max_related in total, go to automatic candidates - compute_related_pages already keeps those free of anything also listed manually, so no duplicate work and no wasted slot.

Parameters:

Name Type Description Default
related list[tuple[float, str]]

(score, src_uri) pairs, typically from compute_related_pages.

required
tags_index dict[str, PageTagsEntry]

output of build_tags_index.

required
current_tags set[str]

tags of the page these related pages are for.

required
files Files

Mkdocs' global Files collection, used to reach the real Page object when it's already available.

required
current_page_url str

root-relative URL of the page these related pages are for (page.url), used to make each RelatedPage.url relative to it.

required
manual_links tuple[ManualLink, ...]

this page's own related_content.links, in frontmatter order - typically tags_index[this_page].manual_links.

()
max_related int

total number of related pages shown, manual and automatic combined.

5
max_manual_related int

maximum number of manual links honored within that total.

5

Returns:

Type Description
list[RelatedPage]

Ready-to-render related pages: manual ones first (in the

list[RelatedPage]

author's own order), then automatic ones by descending score.

Write a tags.json-shaped export, as a fallback when Material's tags plugin isn't available to produce one itself.

Meant to be called from on_post_build: by then every page has been processed, so titles/URLs are fully resolved - unlike the index built in on_files, which only has best-effort titles.

Parameters:

Name Type Description Default
tags_index dict[str, PageTagsEntry]

output of build_tags_index.

required
files Files

Mkdocs' global Files collection.

required
site_dir str

build output directory (config.site_dir).

required
filename str

name of the JSON file to write, relative to site_dir.

required