|
| 1 | +--- |
| 2 | +title: link-notes |
| 3 | +--- |
| 4 | + |
| 5 | +# link-notes |
| 6 | + |
| 7 | +Automatically attaches a **note** (arbitrary markdown) to outbound links that match a |
| 8 | +rule, so that: |
| 9 | + |
| 10 | +- hovering the link shows the note as a **tooltip**, and |
| 11 | +- the note is rendered **once at the bottom of the page** (the footer), |
| 12 | + |
| 13 | +without you having to hand-add a footnote to every post. It is a first-party Netdocs plugin |
| 14 | +— there is no equivalent MkDocs plugin — and is fully opt-in and data-driven. |
| 15 | + |
| 16 | +!!! info "Formerly `affiliate-links`" |
| 17 | + This plugin was renamed from `affiliate-links` to the neutral `link-notes` (attaching a |
| 18 | + disclosure to affiliate links is just one use-case). The old name still works as an |
| 19 | + **alias**, and the legacy `programs` / `disclosure` config keys are still accepted — no |
| 20 | + changes are required to existing configs. |
| 21 | + |
| 22 | +## A common use-case: affiliate disclosures |
| 23 | + |
| 24 | +Attaching an affiliate-disclosure note to eBay Partner Network / tagged Amazon links means |
| 25 | +the disclosure appears on hover **and** once in the page footer, satisfying the |
| 26 | +once-per-page disclosure requirement automatically: |
| 27 | + |
| 28 | +```json |
| 29 | +{ |
| 30 | + "name": "link-notes", |
| 31 | + "options": { |
| 32 | + "rules": [ |
| 33 | + { |
| 34 | + "name": "ebay", |
| 35 | + "label": "Affiliate links", |
| 36 | + "domains": [ "ebay.us" ], |
| 37 | + "note": "This is an eBay Partner Network affiliate link. It costs you nothing extra, and purchases made through it help support this site." |
| 38 | + } |
| 39 | + ] |
| 40 | + } |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +A link such as `[HBA card](https://ebay.us/abc123)` becomes, in the rendered page, a link |
| 45 | +with a small footnote marker; hovering it reveals the note, and the same text appears in the |
| 46 | +page's footnote list. |
| 47 | + |
| 48 | +!!! tip "Just write a normal link" |
| 49 | + You do **not** need a snippet or macro. Prefer an ordinary Markdown link — |
| 50 | + `[used APC PDU](https://ebay.us/bSAxHF)` — and let this plugin add the note. That keeps |
| 51 | + posts readable and avoids per-link markup such as |
| 52 | + `--8<-- "ebay.html" text="..." url="..."`, which is noisier and easy to get wrong. |
| 53 | + |
| 54 | +## How it works |
| 55 | + |
| 56 | +The plugin runs as a Markdown preprocessor (order `30`, after |
| 57 | +[snippets](snippets.md), [table-reader](table-reader.md) and [macros](macros.md), so links |
| 58 | +those plugins generate are also covered). For every configured *rule* it scans the page for |
| 59 | +links that match by **domain** and/or **regular expression** and appends a footnote |
| 60 | +reference carrying the note text. Because the tooltip and the footer note both come from the |
| 61 | +same footnote, enabling the Material |
| 62 | +[`content.footnote.tooltips`](../reference/theme.md) feature gives you the hover behavior for |
| 63 | +free. |
| 64 | + |
| 65 | +## Matching by domain and query parameter |
| 66 | + |
| 67 | +Some links only qualify when they carry a specific query parameter — for example a raw |
| 68 | +`amazon.com` URL is only an affiliate link when it has a `tag=` parameter, while `amzn.to` |
| 69 | +short links always are. A domain entry can therefore be either a plain string or an object |
| 70 | +with its own `query_contains` marker: |
| 71 | + |
| 72 | +```json |
| 73 | +{ |
| 74 | + "name": "amazon", |
| 75 | + "label": "Affiliate links", |
| 76 | + "domains": [ |
| 77 | + "amzn.to", |
| 78 | + { "domain": "amazon.com", "query_contains": "tag=" } |
| 79 | + ], |
| 80 | + "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." |
| 81 | +} |
| 82 | +``` |
| 83 | + |
| 84 | +A rule-level `query_contains` may also be set; it applies to every plain-string domain that |
| 85 | +doesn't override it. Subdomains of a configured domain match automatically. |
| 86 | + |
| 87 | +## Matching by regular expression |
| 88 | + |
| 89 | +For anything a domain rule can't express, add a `patterns` list. Each entry is a regular |
| 90 | +expression matched **case-insensitively against the full URL**; a link matches the rule if |
| 91 | +**any** pattern (or any domain rule) matches: |
| 92 | + |
| 93 | +```json |
| 94 | +{ |
| 95 | + "name": "sponsored", |
| 96 | + "label": "Sponsored", |
| 97 | + "patterns": [ |
| 98 | + "https?://[^/]*/go/", |
| 99 | + "utm_source=sponsor" |
| 100 | + ], |
| 101 | + "note": "This is a sponsored link." |
| 102 | +} |
| 103 | +``` |
| 104 | + |
| 105 | +Invalid patterns are logged and skipped rather than aborting the build. A rule needs at |
| 106 | +least one `domains` entry or one `patterns` entry; a rule with neither is dropped with a |
| 107 | +warning. |
| 108 | + |
| 109 | +## What is left untouched |
| 110 | + |
| 111 | +To keep output valid, the plugin does **not** inject a footnote reference when: |
| 112 | + |
| 113 | +- the link **already carries a footnote** (e.g. a hand-authored `[^ebay]`), so it coexists |
| 114 | + with existing content during migration; |
| 115 | +- the link is inside a **fenced code block**; |
| 116 | +- the link is inside a **pipe-table cell** (Markdig can't reliably parse footnote references |
| 117 | + there); or |
| 118 | +- the link is **glued directly to an adjacent link** (`[a](x)[b](y)`), where a wedged |
| 119 | + footnote would render ambiguously. |
| 120 | + |
| 121 | +In the table-cell and adjacent-link cases the footer note is still guaranteed: the rule |
| 122 | +emits a standalone `!!! info "<label>"` admonition at the bottom of the page instead of a |
| 123 | +footnote. |
| 124 | + |
| 125 | +## Options |
| 126 | + |
| 127 | +| Option | Type | Default | Description | |
| 128 | +|---|---|---|---| |
| 129 | +| `rules` | array | — | The link rules to detect (see below). Legacy alias: `programs`. | |
| 130 | + |
| 131 | +Each **rule** object: |
| 132 | + |
| 133 | +| Field | Type | Required | Description | |
| 134 | +|---|---|---|---| |
| 135 | +| `name` | string | yes | Rule id; used to build the footnote label (`linknote-<name>`). | |
| 136 | +| `note` | string | yes | Markdown shown as the tooltip and footer note. Legacy alias: `disclosure`. | |
| 137 | +| `domains` | array | no* | Hosts that identify the link. Each entry is a domain string or `{ "domain": "...", "query_contains": "..." }`. Subdomains match automatically. | |
| 138 | +| `patterns` | array | no* | Regular expressions matched (case-insensitively) against the full URL. | |
| 139 | +| `query_contains` | string | no | Default substring a matching URL must contain (per-domain values override this). | |
| 140 | +| `label` | string | no | Title for the standalone fallback admonition (table-only links). Default `Links`. | |
| 141 | + |
| 142 | +\* A rule must provide at least one of `domains` or `patterns`. |
| 143 | + |
| 144 | +!!! tip |
| 145 | + Enable [`content.footnote.tooltips`](../reference/theme.md) in your theme `features` |
| 146 | + so the notes appear as hover tooltips as well as in the footer. |
0 commit comments