| 1 | # Auditing `metadata.yaml` links to Learn |
| 2 | |
| 3 | Use this when a generated integration page contains links to |
| 4 | `learn.netdata.cloud` and one of them drifts from the current Learn route. |
| 5 | |
| 6 | ## Why this matters |
| 7 | |
| 8 | `metadata.yaml` is rendered into integration pages on Learn, the website, and |
| 9 | the in-app integrations catalog (`../SKILL.md:37`). A broken Learn URL in |
| 10 | metadata therefore becomes a user-visible broken link on multiple surfaces. |
| 11 | |
| 12 | Learn routes are not derived from source filenames. They are derived from |
| 13 | `docs/.map/map.yaml` labels and hierarchy, while source-relative `/docs/... .md` |
| 14 | links can be rewritten by Learn ingest (`../../learn-site-structure/mapping.md:219`). |
| 15 | For example, `docs/network-flows/visualization/summary-sankey.md` is published |
| 16 | as `/docs/network-flows/visualization/sankey-and-table` because the map label is |
| 17 | `Sankey and Table` (`docs/.map/map.yaml:515`). |
| 18 | |
| 19 | ## Audit command |
| 20 | |
| 21 | Extract unique absolute Learn URLs from all metadata files and check their |
| 22 | published response: |
| 23 | |
| 24 | ```bash |
| 25 | rg -No "https://learn\\.netdata\\.cloud/docs[^)\\]\\s,\"']+" --glob 'metadata.yaml' . \ |
| 26 | | sed 's/^.*https:/https:/' \ |
| 27 | | sort -u \ |
| 28 | | while read -r url; do |
| 29 | printf '%s\t' "$url" |
| 30 | curl -sL -o /dev/null -w '%{http_code}\t%{url_effective}\n' "$url" |
| 31 | done |
| 32 | ``` |
| 33 | |
| 34 | For URLs with fragments, also confirm the target anchor exists in the rendered |
| 35 | HTML: |
| 36 | |
| 37 | ```bash |
| 38 | curl -A 'Mozilla/5.0' -sL 'https://learn.netdata.cloud/docs/netdata-agent/configuration' \ |
| 39 | | rg 'id="locate-your-config-directory"' |
| 40 | ``` |
| 41 | |
| 42 | Validate source-relative metadata links locally: |
| 43 | |
| 44 | ```bash |
| 45 | python3 - <<'PY' |
| 46 | import pathlib, re, sys |
| 47 | |
| 48 | root = pathlib.Path('.') |
| 49 | pat = re.compile(r'\[[^\]]+\]\(([^)]+)\)') |
| 50 | problems = [] |
| 51 | |
| 52 | for path in sorted(root.rglob('metadata.yaml')): |
| 53 | text = path.read_text(errors='replace') |
| 54 | for match in pat.finditer(text): |
| 55 | target = match.group(1).strip() |
| 56 | if target.startswith('/docs/'): |
| 57 | file = root / target.split('#', 1)[0].lstrip('/') |
| 58 | elif target.startswith('../') or target.startswith('./'): |
| 59 | file = (path.parent / target.split('#', 1)[0]).resolve() |
| 60 | else: |
| 61 | continue |
| 62 | |
| 63 | if not file.is_file(): |
| 64 | line = text.count('\n', 0, match.start()) + 1 |
| 65 | problems.append((str(path), line, target)) |
| 66 | |
| 67 | if problems: |
| 68 | for path, line, target in problems: |
| 69 | print(f'{path}:{line}: missing linked source file: {target}') |
| 70 | sys.exit(1) |
| 71 | |
| 72 | print('OK: all metadata.yaml /docs and relative markdown links resolve to source files') |
| 73 | PY |
| 74 | ``` |
| 75 | |
| 76 | ## Repair rule |
| 77 | |
| 78 | - If the link is Markdown text and points to a Netdata source doc, prefer the |
| 79 | source-relative `/docs/... .md` form when the consuming surface supports Learn |
| 80 | ingest rewriting. |
| 81 | - If the same metadata is consumed by non-Learn surfaces that do not rewrite |
| 82 | source-relative links, keep an absolute `https://learn.netdata.cloud/docs/...` |
| 83 | URL, but derive the slug from `docs/.map/map.yaml` labels and verify it with |
| 84 | `curl`. |
| 85 | - Do not infer slugs from filenames. Check the map label first, then validate |
| 86 | the published URL. |
| 87 | |
| 88 | ## How I figured this out |
| 89 | |
| 90 | Files read: |
| 91 | |
| 92 | - `../SKILL.md` |
| 93 | - `../../learn-site-structure/mapping.md` |
| 94 | - `docs/.map/map.yaml` |
| 95 | |
| 96 | Commands run: |
| 97 | |
| 98 | - `rg -No 'https://learn\.netdata\.cloud/docs...' --glob 'metadata.yaml' .` |
| 99 | - `curl -sL -o /dev/null -w '%{http_code}\t%{url_effective}\n' <url>` |
| 100 | - local source-relative metadata link validation script above. |