@redpanda-data/docs-extensions-and-macros 5.9.0 → 5.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/doc-tools.js +92 -57
- package/cli-utils/format-helm-spec.js +58 -0
- package/cli-utils/helm-commented-values.js +361 -0
- package/extensions/README.adoc +4 -0
- package/extensions/REFERENCE.adoc +91 -0
- package/extensions/external-link-checker.js +132 -0
- package/extensions/url-to-xref.js +515 -0
- package/extensions/util/scan-content-urls.js +121 -0
- package/package.json +7 -1
- package/tools/generate-cli-docs.js +1 -0
- package/tools/rpk-docs/generate-rpk-docs.js +62 -0
- package/tools/rpk-docs/generate-x-env-partial.js +284 -0
- package/tools/rpk-docs/rpk-docs-handler.js +11 -21
- package/tools/rpk-docs/validate-output.js +4 -3
- package/cli-utils/convert-doc-links.js +0 -58
|
@@ -485,6 +485,97 @@ antora:
|
|
|
485
485
|
- require: '@redpanda-data/docs-extensions-and-macros/extensions/set-available-attachment-versions'
|
|
486
486
|
----
|
|
487
487
|
|
|
488
|
+
== URL to xref
|
|
489
|
+
|
|
490
|
+
This extension converts absolute docs.redpanda.com URLs found in page and partial content into Antora xrefs, using the content catalog as the single source of truth. A URL is converted only when it maps to a page that is actually published in the current build, so the extension never guesses at URL structure. Converted links are validated by Antora's own xref resolution, and internal URLs that match no published page are logged as warnings — which makes this extension a broken-internal-link detector for the whole site.
|
|
491
|
+
|
|
492
|
+
Behavior details:
|
|
493
|
+
|
|
494
|
+
* Runs at `contentClassified`, before AsciiDoc conversion, so emitted xrefs flow through Antora's normal resolution and broken-xref logging.
|
|
495
|
+
* URLs inside listing, literal, fenced, and passthrough blocks, inline code spans, attribute entry lines, and macro attribute values (such as `link=` on an image macro) are left untouched.
|
|
496
|
+
* Legacy URL shapes (`/docs/...`, `/current/...`, `/vX.Y/...`, and pre-rename component slugs such as `/redpanda-connect/`) are rewritten to candidate paths, but a candidate is only used when it matches a published page.
|
|
497
|
+
* Fragments (`#anchor`) and link labels are preserved, including a label that a generator wrapped across a line break, whose line breaks are collapsed to single spaces.
|
|
498
|
+
* An unlabeled URL becomes `xref:...[]` so that Antora fills in the target page title. An unlabeled URL that carries a fragment is given link text explicitly, because Antora cannot resolve a section title and would render the raw resource ID as the link text: the extension uses the heading the fragment points at, falls back to the target page title, and leaves the URL as a raw link if it can read neither. Heading ids are also read from the partials a target page includes, because the reference pages that hold most anchor targets are thin wrappers around generated partials.
|
|
499
|
+
* Targets in the latest version of a component emit an unversioned xref; targets pinned to an older version emit `version@component:module:page`.
|
|
500
|
+
* A URL that points at a page's former path resolves through the `page-aliases` attribute that the page declares. Antora does not register those aliases as catalog files until it converts documents, which is after this extension runs, so the extension reads them from the page header. Only aliases in the same component and version as the page that declares them are mapped.
|
|
501
|
+
* A URL that carries a query string is left as a raw link, because an xref cannot express one and dropping it would change the link (for example `?platform=kubernetes`, which the UI reads to preselect a tab). These are reported at info level, not as warnings.
|
|
502
|
+
* Builds that publish the latest version under its real version number instead of a symbolic segment (such as this repository's preview build) still resolve `/current/...` URLs, so the same content works in both.
|
|
503
|
+
|
|
504
|
+
=== Environment variables
|
|
505
|
+
|
|
506
|
+
This extension does not require any environment variables.
|
|
507
|
+
|
|
508
|
+
=== Configuration options
|
|
509
|
+
|
|
510
|
+
hostnames (optional)::
|
|
511
|
+
List of hostnames treated as internal documentation URLs. Default: `['docs.redpanda.com']`.
|
|
512
|
+
|
|
513
|
+
ignore (optional)::
|
|
514
|
+
List of regular expressions matched against URL paths. Matching URLs are left untouched with no warning. Use this for paths on the docs domain that live outside the Antora catalog. Default: `['^/api/', '^/mcp(/|$)']` (the API reference is hosted by Bump.sh, and `/mcp` is the docs MCP server endpoint).
|
|
515
|
+
|
|
516
|
+
log_unconverted (optional)::
|
|
517
|
+
Whether to log a warning for each internal URL that matches no published page. Default: `true`.
|
|
518
|
+
|
|
519
|
+
=== Registration
|
|
520
|
+
|
|
521
|
+
Register the `url-to-xref` extension in the Antora playbook under the `antora.extensions` key like so:
|
|
522
|
+
|
|
523
|
+
[source,yaml]
|
|
524
|
+
----
|
|
525
|
+
antora:
|
|
526
|
+
extensions:
|
|
527
|
+
- require: '@redpanda-data/docs-extensions-and-macros/extensions/url-to-xref'
|
|
528
|
+
----
|
|
529
|
+
|
|
530
|
+
== External link checker
|
|
531
|
+
|
|
532
|
+
This extension collects every external http(s) URL referenced in page and partial content and verifies that it responds, reporting dead links in the build log. Internal links are covered by the url-to-xref extension plus Antora's own xref validation; this extension covers everything that points off-site.
|
|
533
|
+
|
|
534
|
+
Because it performs network requests, register it only in builds where that belongs (for example a scheduled nightly build), not necessarily on every preview build.
|
|
535
|
+
|
|
536
|
+
Responses are classified as follows:
|
|
537
|
+
|
|
538
|
+
* 2xx/3xx: ok
|
|
539
|
+
* 404/410: broken — logged as a warning, or at error level with `fail_on_broken`
|
|
540
|
+
* 401/403/429: unverifiable — logged at info level, because bot-walled or rate-limited sites are not broken
|
|
541
|
+
* Other statuses, network errors, and timeouts (after one retry): unverifiable — logged as a warning
|
|
542
|
+
|
|
543
|
+
=== Environment variables
|
|
544
|
+
|
|
545
|
+
This extension does not require any environment variables.
|
|
546
|
+
|
|
547
|
+
=== Configuration options
|
|
548
|
+
|
|
549
|
+
internal_hostnames (optional)::
|
|
550
|
+
Hostnames to skip because they are internal documentation URLs. Default: `['docs.redpanda.com']`.
|
|
551
|
+
|
|
552
|
+
include (optional)::
|
|
553
|
+
List of regular expressions. When set, only URLs matching at least one pattern are checked.
|
|
554
|
+
|
|
555
|
+
exclude (optional)::
|
|
556
|
+
List of regular expressions. URLs matching any pattern are skipped.
|
|
557
|
+
|
|
558
|
+
concurrency (optional)::
|
|
559
|
+
Number of simultaneous requests. Default: `10`.
|
|
560
|
+
|
|
561
|
+
timeout (optional)::
|
|
562
|
+
Per-request timeout in milliseconds. Default: `10000`.
|
|
563
|
+
|
|
564
|
+
fail_on_broken (optional)::
|
|
565
|
+
Log broken links at error level so a build run with `--log-failure-level=error` fails. Default: `false`.
|
|
566
|
+
|
|
567
|
+
=== Registration
|
|
568
|
+
|
|
569
|
+
Register the `external-link-checker` extension in the Antora playbook under the `antora.extensions` key like so:
|
|
570
|
+
|
|
571
|
+
[source,yaml]
|
|
572
|
+
----
|
|
573
|
+
antora:
|
|
574
|
+
extensions:
|
|
575
|
+
- require: '@redpanda-data/docs-extensions-and-macros/extensions/external-link-checker'
|
|
576
|
+
fail_on_broken: false
|
|
577
|
+
----
|
|
578
|
+
|
|
488
579
|
== Validate attributes
|
|
489
580
|
|
|
490
581
|
This extension ensures the consistency and validity of page attributes, focusing on validating page categories against a predefined list of valid categories and subcategories. It automatically adds missing parent categories for any specified subcategories and removes any specified categories that are invalid. Additionally, it processes specific environment attributes, setting corresponding page-level attributes when environment conditions are met.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { scanContentUrls } = require('./util/scan-content-urls')
|
|
4
|
+
const { raiseListenerLimit } = require('./util/raise-listener-limit')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* External Link Checker Extension
|
|
8
|
+
*
|
|
9
|
+
* Collects every external http(s) URL referenced in page and partial content
|
|
10
|
+
* and verifies it responds, reporting dead links in the build log. Internal
|
|
11
|
+
* links are covered by the url-to-xref extension plus Antora's own xref
|
|
12
|
+
* validation; this extension covers everything that points off-site.
|
|
13
|
+
*
|
|
14
|
+
* Because it performs network requests, register it only where that belongs
|
|
15
|
+
* (for example a nightly build), not necessarily on every preview build:
|
|
16
|
+
*
|
|
17
|
+
* antora:
|
|
18
|
+
* extensions:
|
|
19
|
+
* - require: '@redpanda-data/docs-extensions-and-macros/extensions/external-link-checker'
|
|
20
|
+
* # internal_hostnames: ['docs.redpanda.com'] hosts to skip (default)
|
|
21
|
+
* # include: [] regex allowlist; when set, only matching URLs are checked
|
|
22
|
+
* # exclude: [] regex denylist
|
|
23
|
+
* # concurrency: 10 simultaneous requests
|
|
24
|
+
* # timeout: 10000 per-request timeout in ms
|
|
25
|
+
* # fail_on_broken: false log broken links at error level so a build
|
|
26
|
+
* # run with --log-failure-level=error fails
|
|
27
|
+
*
|
|
28
|
+
* Classification:
|
|
29
|
+
* - 2xx/3xx: ok
|
|
30
|
+
* - 404/410: broken (warn, or error with fail_on_broken)
|
|
31
|
+
* - 401/403/429: unverifiable (info) — bot-walled or rate-limited, not broken
|
|
32
|
+
* - other statuses, network errors, timeouts (after one retry): unverifiable (warn)
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const USER_AGENT = 'redpanda-docs-link-checker'
|
|
36
|
+
|
|
37
|
+
module.exports.register = function ({ config = {} }) {
|
|
38
|
+
raiseListenerLimit(this)
|
|
39
|
+
const logger = this.getLogger('external-link-checker-extension')
|
|
40
|
+
const internalHostnames = new Set(config.internalHostnames || ['docs.redpanda.com'])
|
|
41
|
+
const include = (config.include || []).map((pattern) => new RegExp(pattern))
|
|
42
|
+
const exclude = (config.exclude || []).map((pattern) => new RegExp(pattern))
|
|
43
|
+
const concurrency = config.concurrency || 10
|
|
44
|
+
const timeout = config.timeout || 10000
|
|
45
|
+
const failOnBroken = Boolean(config.failOnBroken)
|
|
46
|
+
|
|
47
|
+
this.on('contentClassified', async ({ contentCatalog }) => {
|
|
48
|
+
const urlReferences = collectExternalUrls(contentCatalog, { internalHostnames, include, exclude })
|
|
49
|
+
if (!urlReferences.size) return
|
|
50
|
+
const counts = { ok: 0, broken: 0, unverifiable: 0 }
|
|
51
|
+
await runWithConcurrency([...urlReferences.keys()], concurrency, async (url) => {
|
|
52
|
+
const verdict = await checkUrl(url, { timeout })
|
|
53
|
+
counts[verdict.classification]++
|
|
54
|
+
const pages = [...urlReferences.get(url)].join(', ')
|
|
55
|
+
if (verdict.classification === 'broken') {
|
|
56
|
+
const message = `Broken external link ${url} (HTTP ${verdict.status}) referenced in: ${pages}`
|
|
57
|
+
failOnBroken ? logger.error(message) : logger.warn(message)
|
|
58
|
+
} else if (verdict.classification === 'unverifiable') {
|
|
59
|
+
const reason = verdict.status ? `HTTP ${verdict.status}` : verdict.reason
|
|
60
|
+
const log = verdict.status && [401, 403, 429].includes(verdict.status) ? 'info' : 'warn'
|
|
61
|
+
logger[log](`Could not verify external link ${url} (${reason}) referenced in: ${pages}`)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
logger.info(
|
|
65
|
+
`Checked ${urlReferences.size} external links: ${counts.ok} ok, ${counts.broken} broken, ` +
|
|
66
|
+
`${counts.unverifiable} unverifiable`
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function collectExternalUrls (contentCatalog, { internalHostnames, include, exclude }) {
|
|
72
|
+
const urlReferences = new Map()
|
|
73
|
+
const files = contentCatalog
|
|
74
|
+
.getPages((page) => page.out)
|
|
75
|
+
.concat(contentCatalog.findBy({ family: 'partial' }))
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
if (!file.contents) continue
|
|
78
|
+
for (const match of scanContentUrls(file.contents.toString())) {
|
|
79
|
+
let url
|
|
80
|
+
try {
|
|
81
|
+
url = new URL(match.url)
|
|
82
|
+
} catch {
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
if (internalHostnames.has(url.hostname)) continue
|
|
86
|
+
if (include.length && !include.some((pattern) => pattern.test(match.url))) continue
|
|
87
|
+
if (exclude.some((pattern) => pattern.test(match.url))) continue
|
|
88
|
+
if (!urlReferences.has(match.url)) urlReferences.set(match.url, new Set())
|
|
89
|
+
urlReferences.get(match.url).add(file.path)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return urlReferences
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function checkUrl (url, { timeout, fetchFn = module.exports._fetch }) {
|
|
96
|
+
let lastError
|
|
97
|
+
for (const attempt of [{ method: 'HEAD' }, { method: 'GET' }, { method: 'GET', retry: true }]) {
|
|
98
|
+
try {
|
|
99
|
+
const response = await fetchFn(url, {
|
|
100
|
+
method: attempt.method,
|
|
101
|
+
redirect: 'follow',
|
|
102
|
+
headers: { 'user-agent': USER_AGENT },
|
|
103
|
+
signal: AbortSignal.timeout(timeout),
|
|
104
|
+
})
|
|
105
|
+
const status = response.status
|
|
106
|
+
// HEAD rejected or unsupported: many CDNs and bot walls refuse HEAD
|
|
107
|
+
// (403, 404, ...) but answer GET normally, so confirm any HEAD failure
|
|
108
|
+
// with GET before classifying
|
|
109
|
+
if (attempt.method === 'HEAD' && status >= 400) continue
|
|
110
|
+
if (status < 400) return { classification: 'ok', status }
|
|
111
|
+
if (status === 404 || status === 410) return { classification: 'broken', status }
|
|
112
|
+
return { classification: 'unverifiable', status }
|
|
113
|
+
} catch (error) {
|
|
114
|
+
lastError = error
|
|
115
|
+
if (attempt.retry) break
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { classification: 'unverifiable', status: null, reason: lastError ? lastError.message : 'request failed' }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function runWithConcurrency (items, limit, task) {
|
|
122
|
+
const queue = [...items]
|
|
123
|
+
const workers = Array.from({ length: Math.min(limit, queue.length) }, async () => {
|
|
124
|
+
while (queue.length) await task(queue.shift())
|
|
125
|
+
})
|
|
126
|
+
await Promise.all(workers)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Indirection so tests can stub the HTTP layer without a network.
|
|
130
|
+
module.exports._fetch = (...args) => fetch(...args)
|
|
131
|
+
module.exports.checkUrl = checkUrl
|
|
132
|
+
module.exports.collectExternalUrls = collectExternalUrls
|