Skip to content

Commit 09598cc

Browse files
NetdocsCopilot
andcommitted
Rename affiliate-links plugin to link-notes + add regex matching
The plugin is generic (attach a note/footnote to matching outbound links); "affiliate links" is just one use-case, and the affiliate framing is unfriendly in FOSS contexts. Rename AffiliateLinksPlugin -> LinkNotesPlugin (Name "link-notes") and register "affiliate-links" as a backward-compatible alias. The legacy config keys stay supported: "programs" -> "rules", "disclosure" -> "note". New capabilities: - patterns: optional list of regular expressions matched case-insensitively against the full URL, in addition to (or instead of) domain rules. - label: title for the standalone fallback admonition used for table-only links (default "Links"), so the affiliate use-case can keep its wording. Footnote label prefix changed affiliate-<id> -> linknote-<id> (invisible in rendered output). Tests renamed and extended (regex, regex+domain, legacy keys, invalid-regex, custom label). Docs: affiliate-links.md -> link-notes.md with the new options, cross-links and nav updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b703638 commit 09598cc

10 files changed

Lines changed: 668 additions & 500 deletions

File tree

docs-site/appsettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
"children": [
6767
{ "path": "plugins/index.md" },
6868
{ "path": "plugins/abbreviations.md" },
69-
{ "path": "plugins/affiliate-links.md" },
69+
{ "path": "plugins/link-notes.md" },
7070
{ "path": "plugins/arithmatex.md" },
7171
{ "path": "plugins/b64.md" },
7272
{ "path": "plugins/blog.md" },

docs-site/docs/plugins/affiliate-links.md

Lines changed: 0 additions & 107 deletions
This file was deleted.

docs-site/docs/plugins/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ content that uses them (for example [table-reader](table-reader.md) is a no-op o
4545
```
4646

4747
Add [`blog`](blog.md) if you publish posts, [`file-filter`](file-filter.md) for
48-
environment-driven content gating, and [`affiliate-links`](affiliate-links.md) if you use
49-
affiliate links. A few plugins are intentionally left out of the "enable everything" default:
48+
environment-driven content gating, and [`link-notes`](link-notes.md) if you want to
49+
annotate outbound links (e.g. affiliate disclosures). A few plugins are intentionally left out of the "enable everything" default:
5050

5151
| Plugin | Why it's opt-in |
5252
|---|---|
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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.

docs-site/docs/plugins/table-reader.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ This plugin has no options — enable it by name:
6262

6363
It runs as a Markdown preprocessor (order `20`), so directives are expanded before the
6464
Markdown is parsed and before later preprocessors (such as
65-
[affiliate-links](affiliate-links.md)) inspect the resulting table.
65+
[link-notes](link-notes.md)) inspect the resulting table.
6666

6767
## Attribution
6868

src/Netdocs.Cli/CliApp.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ private static async Task<int> ImportAsync(string[] args)
230230
.Register<SocialPlugin>("social")
231231
.Register<TypesetPlugin>("typeset")
232232
.Register<TableReaderPlugin>("table-reader")
233-
.Register<AffiliateLinksPlugin>("affiliate-links")
233+
.Register<LinkNotesPlugin>("link-notes", "affiliate-links")
234234
.Register<Base64EmbedPlugin>("b64", "pymdownx.b64")
235235
.Register<ArithmatexPlugin>("arithmatex", "pymdownx.arithmatex")
236236
.Register<MacrosPlugin>("macros");

0 commit comments

Comments
 (0)