pyssg is pre-1.0 and under active development - APIs, config, and themes may change.
The linking model

2 min read

The linking model

pyssg treats links as first-class data, not just text in the output. That is what makes wikilinks, transclusion, backlinks, and safe internal links work together.

A normal Markdown link to a local .md file is rewritten to the target page's resolved URL, and a reverse LINK connection is recorded so backlinks work:

See [getting started](getting-started.md).

Resolution happens during evaluate_collections, after every document is parsed and every page URL is known. The rewrite always starts from the document's pre-resolution HTML, so if a target is moved or renamed, the link updates on the next finalize - even though the linking document was not itself re-edited. The rewritten content is re-hashed, which is precisely what makes the render sweep re-render a page whose link targets changed (keeping incremental == full).

The wikilink plugin adds Obsidian-style ... links, resolved by target title or filename:

Read about Core concepts next.

Wikilink runs early in the finalize_content waterfall (stage 100), before plain internal link resolution (stage 200), so by the time other plugins see the HTML, wikilinks are already ordinary hrefs.

Transclusion

missing: ...
embeds another document's finalized content into the current page, rather than just linking to it:

missing: shared/disclaimer

Transclusion runs in expand_content, after finalize_content, because it needs the embedded document's finalized HTML (links already resolved) to splice in.

Because every resolved link is stored as a reverse edge in the graph, pyssg can show, on any page, which other pages link to it - with no extra configuration. The backlink list is derived from the same LINK connections the resolver records, so it is always consistent with the actual links in your content.

Since links are resolved against the real graph of documents, a link whose target does not exist is detectable rather than silently producing a dead href. This is the other half of treating links as data: you find out at build time, not when a reader clicks.

Links with a URL scheme (http:, mailto:, ...) are left alone by the internal resolver. The optional external_links contrib plugin can rewrite off-site anchors to open in a new tab with rel="noopener noreferrer"; it taps finalize_content at stage 300, after wikilink and internal resolution, so it only ever sees final hrefs.