link-notes

Automatically attaches a note (arbitrary markdown) to outbound links that match a rule, so that:

  • hovering the link shows the note as a tooltip, and
  • the note is rendered once at the bottom of the page (the footer),

without you having to hand-add a footnote to every post. It is a first-party Netdocs plugin — there is no equivalent MkDocs plugin — and is fully opt-in and data-driven.

Formerly `affiliate-links`

This plugin was renamed from affiliate-links to the neutral link-notes (attaching a disclosure to affiliate links is just one use-case). The old name still works as an alias, and the legacy programs / disclosure config keys are still accepted — no changes are required to existing configs.

Two render modes: footnote or hover popup

Each rule renders its note in one of two ways:

  • Footnote mode (default) — the link gets a small footnote marker; the note appears both as a hover tooltip (with Material's content.footnote.tooltips) and once in the page's footnote list.
  • Tooltip mode (link_snippet set) — each matching link is replaced inline with a snippet rendered as a template (a hover popup), and the rule's disclosure box is emitted once per page instead of per-link footnotes. See Pretty hover popups below.

A common use-case: affiliate disclosures

Attaching an affiliate-disclosure note to eBay Partner Network / tagged Amazon links means the disclosure appears on hover and once in the page footer, satisfying the once-per-page disclosure requirement automatically:

{
  "name": "link-notes",
  "options": {
    "rules": [
      {
        "name": "ebay",
        "label": "Affiliate links",
        "domains": [ "ebay.us" ],
        "note": "This is an eBay Partner Network affiliate link. It costs you nothing extra, and purchases made through it help support this site."
      }
    ]
  }
}

A link such as [HBA card](https://ebay.us/abc123) becomes, in the rendered page, a link with a small footnote marker; hovering it reveals the note, and the same text appears in the page's footnote list.

Just write a normal link

You do not need a snippet or macro. Prefer an ordinary Markdown link — [used APC PDU](https://ebay.us/bSAxHF) — and let this plugin add the note. That keeps posts readable and avoids per-link markup such as --8<-- "ebay.html" text="..." url="...", which is noisier and easy to get wrong.

Reusing a snippet for the note

Instead of typing the note text inline in your config, a rule can point at a Markdown snippet file with note_snippet. This keeps a single source of truth for a disclosure that you may also include manually elsewhere (via snippets), so the wording stays consistent everywhere:

{
  "name": "ebay",
  "domains": [ "ebay.us" ],
  "note_snippet": "snippets/ebay-affiliate.md"
}

The path is resolved against the project root and the docs/ directory, each with a conventional snippets subdirectory — so snippets/ebay-affiliate.md, docs/snippets/ebay-affiliate.md, or a bare ebay-affiliate.md (found in docs/snippets) all work. If a referenced snippet cannot be found, the build fails with a clear error (even without --strict) — a mistyped path can never silently drop an affiliate disclosure.

When the snippet is a single admonition — the usual pretty affiliate box:

!!! info "E-Bay Affiliate Links Used"
    This post **DOES** include eBay affiliate links. ...

    You will pay the same amount as normal ...

…the plugin is admonition-aware:

  • its title (E-Bay Affiliate Links Used) becomes the standalone fallback box's header (unless the rule sets an explicit label), and its admonition kind (info) is reused;
  • its body becomes the tooltip / footer-note text. (An admonition can't render inside a footnote, so the body is used directly there; the pretty box is reproduced for table-only links via the standalone admonition.)

Pretty hover popups (tooltip mode)

Footnotes are great for a plain disclosure, but sometimes you want a nicer per-link popup — a styled card that appears on hover — instead of a superscript number and a footnote list. Point a rule at a link_snippet to switch it into tooltip mode:

{
  "name": "ebay",
  "domains": [ "ebay.us" ],
  "link_snippet": "snippets/ebay-link.html",
  "note_snippet": "snippets/ebay-affiliate.md"
}

In this mode, every matching link is replaced inline with link_snippet rendered as a template, and the note_snippet disclosure box is emitted once at the bottom of the page (no per-link footnotes at all). The snippet receives the matched link as template parameters, using the same ${key} convention as parameterized snippets includes:

Placeholder Value
${url} The matched link URL (HTML-escaped).
${text} The link's display text (HTML-escaped).
${domain} The URL host, e.g. ebay.us (HTML-escaped).

A link_snippet is typically a small HTML fragment that wraps the link with a CSS-styled tooltip:

<span class="buylink-wrap"><a href="${url}" target="_blank" rel="nofollow sponsored noopener"
class="buylink-a">${text}</a><span class="buylink-pop"><span class="buylink-pop-h">eBay
Affiliate Link</span><span class="buylink-pop-b">This is an eBay affiliate link…</span></span></span>

Because the replacement is inline HTML (not a footnote), tooltip mode also works inside pipe-table cells — where footnote references can't go — so links generated from CSVs by the table-reader get the same pretty popup. A referenced-but-missing link_snippet fails the build, exactly like note_snippet.

Avoid ad-blocker-triggering class names

Don't put the word affiliate (or sponsor/ad/promo) in the class names. Ad-blockers ship cosmetic filters such as [class*="affiliate"] { display: none }, which will hide the whole link for a large share of readers while it still renders fine in a clean browser. Use a neutral prefix (the example above uses buylink-*).

Style it once

Put the tooltip CSS (.buylink-wrap / .buylink-pop etc.) in your extra_css and reuse the same classes across every snippet so all popups look consistent.

How it works

The plugin runs as a Markdown preprocessor (order 30, after snippets, table-reader and macros, so links those plugins generate are also covered). For every configured rule it scans the page for links that match by domain and/or regular expression and appends a footnote reference carrying the note text. Because the tooltip and the footer note both come from the same footnote, enabling the Material content.footnote.tooltips feature gives you the hover behavior for free.

Matching by domain and query parameter

Some links only qualify when they carry a specific query parameter — for example a raw amazon.com URL is only an affiliate link when it has a tag= parameter, while amzn.to short links always are. A domain entry can therefore be either a plain string or an object with its own query_contains marker:

{
  "name": "amazon",
  "label": "Affiliate links",
  "domains": [
    "amzn.to",
    { "domain": "amazon.com", "query_contains": "tag=" }
  ],
  "note": "This is an Amazon affiliate link. As an Amazon Associate I earn from qualifying purchases at no additional cost to you; it helps support this site."
}

A rule-level query_contains may also be set; it applies to every plain-string domain that doesn't override it. Subdomains of a configured domain match automatically.

Matching by regular expression

For anything a domain rule can't express, add a patterns list. Each entry is a regular expression matched case-insensitively against the full URL; a link matches the rule if any pattern (or any domain rule) matches:

{
  "name": "sponsored",
  "label": "Sponsored",
  "patterns": [
    "https?://[^/]*/go/",
    "utm_source=sponsor"
  ],
  "note": "This is a sponsored link."
}

Invalid patterns are logged and skipped rather than aborting the build. A rule needs at least one domains entry or one patterns entry; a rule with neither is dropped with a warning.

What is left untouched

To keep output valid, the plugin does not inject a footnote reference when:

  • the link already carries a footnote (e.g. a hand-authored [^ebay]), so it coexists with existing content during migration;
  • the link is inside a fenced code block;
  • the link is inside a pipe-table cell (Markdig can't reliably parse footnote references there); or
  • the link is glued directly to an adjacent link ([a](x)[b](y)), where a wedged footnote would render ambiguously.

In the table-cell and adjacent-link cases the footer note is still guaranteed: the rule emits a standalone !!! info "<label>" admonition at the bottom of the page instead of a footnote.

Options

Option Type Default Description
rules array The link rules to detect (see below). Legacy alias: programs.

Each rule object:

Field Type Required Description
name string yes Rule id; used to build the footnote label (linknote-<name>).
note string yes* Markdown shown as the tooltip and footer note. Legacy alias: disclosure.
note_snippet string yes* Path to a Markdown snippet whose content is used as the note (resolved against the project root / docs dir and their snippets subdirs). A single-admonition snippet contributes its title (→ label) and kind, and its body becomes the note. A referenced-but-missing snippet fails the build. Legacy alias: disclosure_snippet.
link_snippet string no Path to an HTML/Markdown snippet template. When set the rule switches to tooltip mode: each matching link is replaced inline with this snippet rendered with ${url}/${text}/${domain} substituted, and the note/note_snippet box is emitted once per page (no footnotes). A referenced-but-missing snippet fails the build.
domains array no† Hosts that identify the link. Each entry is a domain string or { "domain": "...", "query_contains": "..." }. Subdomains match automatically.
patterns array no† Regular expressions matched (case-insensitively) against the full URL.
query_contains string no Default substring a matching URL must contain (per-domain values override this).
label string no Title for the standalone fallback admonition (table-only links). Defaults to the snippet's admonition title, else Links.

* A rule must provide either a note (note or note_snippet) or a link_snippet. If note_snippet is set it takes precedence over note; any referenced snippet that cannot be found fails the build rather than falling back, so a mistyped path is caught instead of silently dropping the note.

† A rule must provide at least one of domains or patterns.

Tip

Enable content.footnote.tooltips in your theme features so the notes appear as hover tooltips as well as in the footer.