@redpanda-data/docs-extensions-and-macros 5.54.0 → 5.56.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-mcp.js +4 -5
- package/bin/doc-tools.js +354 -9
- package/bin/mcp-tools/generated-docs-review.js +26 -9
- package/bin/mcp-tools/overrides-audit.js +1 -1
- package/bin/mcp-tools/rpcn-docs.js +4 -14
- package/cli-utils/docs-links-to-xrefs.js +174 -0
- package/cli-utils/format-helm-spec.js +4 -3
- package/cli-utils/generated-marker.js +43 -0
- package/cli-utils/helm-commented-values.js +125 -21
- package/cli-utils/self-managed-docs-branch.js +36 -30
- package/extension-utils/html-utils.js +27 -0
- package/extension-utils/llms-utils.js +6 -2
- package/extension-utils/metadata-candidates.js +304 -0
- package/extension-utils/metadata-check.js +174 -0
- package/extension-utils/metadata-suggest.js +573 -0
- package/extension-utils/taxonomy-categories.js +106 -0
- package/extension-utils/taxonomy.js +566 -0
- package/extensions/REFERENCE.adoc +110 -8
- package/extensions/add-git-dates.js +13 -1
- package/extensions/aggregate-terms.js +34 -20
- package/extensions/algolia-indexer/generate-index.js +13 -1
- package/extensions/algolia-indexer/index.js +4 -2
- package/extensions/convert-sitemap-to-markdown.js +47 -12
- package/extensions/convert-to-markdown.js +176 -0
- package/extensions/generate-fields-only-pages.js +14 -0
- package/extensions/modify-connect-tag-playbook.js +181 -3
- package/extensions/resolve-xrefs-in-attachments.js +1 -12
- package/extensions/url-to-xref.js +92 -22
- package/extensions/util/connect-docs-asset.js +6 -5
- package/extensions/validate-attributes.js +10 -1
- package/extensions/version-fetcher/fetch-latest-docker-tag.js +35 -13
- package/extensions/version-fetcher/get-latest-redpanda-helm-version-from-operator.js +64 -40
- package/extensions/version-fetcher/list-connect-releases.js +3 -3
- package/extensions/version-fetcher/release-lines.js +64 -0
- package/extensions/version-fetcher/set-latest-version.js +73 -3
- package/macros/README.adoc +1 -0
- package/macros/REFERENCE.adoc +99 -0
- package/macros/badge.js +5 -8
- package/macros/enterprise.js +2 -5
- package/macros/glossary.js +4 -14
- package/macros/iceberg-explorer.js +242 -0
- package/macros/prop.js +3 -13
- package/mcp/USER_GUIDE.adoc +25 -135
- package/package.json +5 -2
- package/tools/check-metadata/index.js +523 -0
- package/tools/cloud-regions/cloud-regions-table-adoc.hbs +1 -2
- package/tools/cloud-regions/cloud-regions-table-md.hbs +2 -2
- package/tools/cloud-regions/render-cloud-regions.js +3 -0
- package/tools/dispatch-graph/check-dispatch-graph.js +790 -0
- package/tools/dispatch-graph/classify-runs.js +128 -0
- package/tools/dispatch-graph/parse-workflows.js +499 -0
- package/tools/doc-impact-recording/index.js +190 -0
- package/tools/enterprise-features/detect-gating-changes.js +393 -0
- package/tools/enterprise-features/verify.js +2 -1
- package/tools/gen-rpk-ascii.py +7 -1
- package/tools/lint-strings/fixtures/connect/lint_connect.go +8 -0
- package/tools/lint-strings/fixtures/crd/lint_types.go +4 -1
- package/tools/lint-strings/fixtures/rpk-pr461/delete.base.go +42 -0
- package/tools/lint-strings/fixtures/rpk-pr461/delete.head.go +45 -0
- package/tools/lint-strings/fixtures/rpk-pr461/reviewer-comments.json +1 -0
- package/tools/lint-strings/index.js +133 -3
- package/tools/lint-strings/rules/verbatim-asciidoc.js +166 -1
- package/tools/lint-strings/surfaces/connect.js +72 -12
- package/tools/lint-strings/surfaces/crd.js +10 -6
- package/tools/metrics/metrics.py +11 -0
- package/tools/overrides-audit/README.adoc +1 -2
- package/tools/overrides-audit/adapters/properties.js +1 -1
- package/tools/overrides-audit/index.js +3 -4
- package/tools/property-extractor/generate-handlebars-docs.js +9 -1
- package/tools/property-extractor/helpers/applyPropertyLinks.js +35 -0
- package/tools/redpanda-connect/AUTOMATION.md +3 -36
- package/tools/redpanda-connect/README.adoc +5 -33
- package/tools/redpanda-connect/generate-rpcn-connector-docs.js +32 -11
- package/tools/redpanda-connect/pr-summary-formatter.js +6 -4
- package/tools/redpanda-connect/report-delta.js +6 -3
- package/tools/redpanda-connect/rpcn-connector-docs-handler.js +42 -25
- package/tools/redpanda-connect/templates/bloblang-function.hbs +1 -1
- package/tools/redpanda-connect/templates/descriptions-partials.hbs +1 -1
- package/tools/redpanda-connect/templates/examples-partials.hbs +1 -1
- package/tools/redpanda-connect/templates/fields-partials.hbs +2 -2
- package/tools/redpanda-connect/templates/metadata-partials.hbs +1 -1
- package/tools/redpanda-release-notes/generate-release-notes.js +116 -1
- package/tools/rpk-docs/generate-rpk-docs.js +2 -0
- package/tools/rpk-docs/generate-x-env-partial.js +7 -4
- package/tools/rpk-docs/helpers/index.js +4 -1
- package/tools/rpk-docs/templates/command.hbs +2 -2
- package/tools/overrides-audit/adapters/connect.js +0 -144
|
@@ -100,6 +100,29 @@ This extension generates an Algolia index for each version of each component. Th
|
|
|
100
100
|
|
|
101
101
|
NOTE: Only pages that include an `<article>` element with the `doc` class are indexed.
|
|
102
102
|
|
|
103
|
+
When the build has the docs taxonomy (see <<Docs taxonomy>>), every Product Docs record also carries these attributes, next to `categories`:
|
|
104
|
+
|
|
105
|
+
[cols="1,3"]
|
|
106
|
+
|===
|
|
107
|
+
|Attribute |Value
|
|
108
|
+
|
|
109
|
+
|`topics`, `topicIds`
|
|
110
|
+
|The topic concepts of the page's authored `page-categories`, with the broader concepts they imply (Replication implies High Availability). Labels and IDs.
|
|
111
|
+
|
|
112
|
+
|`useCases`, `useCaseIds`
|
|
113
|
+
|The use-case concepts of the page's categories.
|
|
114
|
+
|
|
115
|
+
|`technologies`, `technologyIds`
|
|
116
|
+
|The technology concepts of the page's categories, those a topic implies (Iceberg Topics implies Apache Iceberg), and the technology whose `component` is the page's component (a page in `connect` is about Redpanda Connect).
|
|
117
|
+
|
|
118
|
+
|`deployment`, `deploymentIds`
|
|
119
|
+
|The deployment concept of the page's `env-*` attributes, and its broader concept.
|
|
120
|
+
|===
|
|
121
|
+
|
|
122
|
+
`categories` is unchanged: it is still the legacy list that validate-attributes writes, because labs-home filters on it. Labs records get none of the new attributes, and keep their string `deployment`.
|
|
123
|
+
|
|
124
|
+
The Algolia index settings are not in any repository. To filter or facet on the new attributes, add them to `attributesForFaceting` in the index settings.
|
|
125
|
+
|
|
103
126
|
=== Environment variables
|
|
104
127
|
|
|
105
128
|
- `ALGOLIA_ADMIN_API_KEY` (required)
|
|
@@ -412,7 +435,8 @@ At build time, when the playbook has the `connect` component, the extension:
|
|
|
412
435
|
|
|
413
436
|
. Picks the release: the latest stable `vX.Y.Z` release by default, so the site never shows fields from unreleased code on connect's `main` branch. It asks the GitHub API for the latest release and falls back to the highest stable `vX.Y.Z` tag from `git ls-remote`. If neither works, the build stops with an error. The `tag` option picks a specific release instead.
|
|
414
437
|
. Downloads that release's `redpanda-connect-docs.tar.gz` asset. With a GitHub token it downloads through the releases API, which keeps working if the connect repository becomes private. Without one, or if GitHub rejects the token, it uses the public download URL.
|
|
415
|
-
. Adds `modules/components/partials/**` as partials
|
|
438
|
+
. Adds `modules/components/partials/**` as partials, `modules/components/examples/**` as examples, and `modules/components/attachments/**` (the release's `connect-<version>.json`, read by the Bloblang playground) as attachments of the `components` module, in every version of the `connect` component. A file that another source, such as rp-connect-docs, already provides is skipped and counted in the log, so rp-connect-docs can keep committing its own copies until it stops.
|
|
439
|
+
. When the playbook also registers `add-git-dates`, dates each page by the Connect release that last changed its reference. For every generated partial or example a page includes, directly or through another generated partial such as `bloblang/methods.adoc`, the extension compares the release with older releases that have the asset, newest first, and takes the publish date of the newest release that changed the file. A page that single-sources such a connect page, such as a Cloud stub, gets the same date. `add-git-dates` uses the date as `page-git-modified-date` when it is later than the page's last commit. The walk stops at v4.113.0, the first release with the asset, and after at most 10 older releases. A file that is unchanged for the whole walk keeps the page's git date. If the releases listing or an older release's download fails, the extension logs a warning and pages keep their git dates, so the build never stops for a date. Without `add-git-dates` in the playbook, the extension skips this step and downloads only the release it uses.
|
|
416
440
|
|
|
417
441
|
A release from before the asset existed has none, and the download returns 404. A new release can also be missing it for a while: Connect publishes the release first and attaches the asset 15 to 20 minutes later in a separate job, which can fail.
|
|
418
442
|
|
|
@@ -490,8 +514,8 @@ The following attributes are available to all versions of all Antora components:
|
|
|
490
514
|
`console-beta-version`:: The latest beta version of Redpanda Console.
|
|
491
515
|
`operator-beta-version`:: The latest beta version of the Redpanda Operator.
|
|
492
516
|
`helm-beta-version`:: The latest beta version of the Redpanda Helm chart.
|
|
493
|
-
`latest-operator-version`:: The latest release version of the Redpanda Operator. This value keeps its `v` prefix (for example, `v25.1.3`) because pages pass it straight to `helm --version`.
|
|
494
|
-
`latest-redpanda-helm-chart-version`:: The latest release version of the Redpanda Helm chart.
|
|
517
|
+
`latest-operator-version`:: The latest release version of the Redpanda Operator for the component version's release line (see <<operator-and-chart-per-version>>). This value keeps its `v` prefix (for example, `v25.1.3`) because pages pass it straight to `helm --version`.
|
|
518
|
+
`latest-redpanda-helm-chart-version`:: The latest release version of the Redpanda Helm chart for the component version's release line (see <<operator-and-chart-per-version>>). This value has no `v` prefix (for example, `25.3.11`).
|
|
495
519
|
|
|
496
520
|
The following attributes are available to the latest version of each Antora component:
|
|
497
521
|
|
|
@@ -511,6 +535,19 @@ ever moves forwards.
|
|
|
511
535
|
|
|
512
536
|
A short attribute is only set when a `major.minor` pair can be derived. Release channels that are not versions, such as `nightly` or `latest`, get a `-version` and a `-tag` but no `-version-short`, so a page never renders a fabricated release line.
|
|
513
537
|
|
|
538
|
+
[[operator-and-chart-per-version]]
|
|
539
|
+
==== Operator and Helm chart versions per component version
|
|
540
|
+
|
|
541
|
+
An operator or Helm chart release supports its own Redpanda version and one minor version above and below it, so each component version gets the release of its own line:
|
|
542
|
+
|
|
543
|
+
* A component version named `<major>.<minor>` that is older than the component's latest version gets the newest stable release in that line. For example, version `25.3` gets the newest `v25.3.x` operator and the newest `25.3.x` chart, even when a `26.2.x` release exists.
|
|
544
|
+
* The component's latest version, any prerelease version ahead of it, and unversioned components get the newest stable release overall.
|
|
545
|
+
* If an older line has no stable operator release (for example, lines that shipped only legacy `v2.x.y-<redpanda-version>` operator tags), the newest release does not support it, so both attributes keep the values the component version's `antora.yml` sets. If the line has an operator release but no chart release, only the chart attribute keeps its `antora.yml` value. An attribute that `antora.yml` leaves unset or empty falls back to the newest release overall. Either way, the build logs a warning that names the line.
|
|
546
|
+
|
|
547
|
+
The release lines come from the fetched operator tags, so no version map needs updating when a line ships. The extension reads up to three pages (300 tags) of the operator's Docker Hub tags, newest pushes first.
|
|
548
|
+
|
|
549
|
+
The chart version of a line is the newest stable `charts/redpanda/v<major>.<minor>.*` tag in the redpanda-operator repository, which marks a published chart (what `helm search repo` reports). A release branch's `Chart.yaml` can be ahead of the published chart, so the extension reads it only when the line has no chart tag, trying each path the chart has used in order: `charts/redpanda/chart/Chart.yaml`, then `charts/redpanda/Chart.yaml` (the layout of `release/v25.1.x`).
|
|
550
|
+
|
|
514
551
|
==== Seed a fallback in antora.yml
|
|
515
552
|
|
|
516
553
|
Every attribute here depends on a successful GitHub lookup at build time. If a lookup fails, for example because of a rate limit or a missing token, the attribute is never set and any page that references it renders the attribute name in braces.
|
|
@@ -716,6 +753,17 @@ This extension does not require any environment variables.
|
|
|
716
753
|
|
|
717
754
|
There are no configurable options for this extension. It operates based on site attributes defined in `add-global-attributes.js` to determine valid categories and subcategories.
|
|
718
755
|
|
|
756
|
+
=== With the docs taxonomy
|
|
757
|
+
|
|
758
|
+
When the build has the docs taxonomy (see <<Docs taxonomy>>), each `page-categories` value is also resolved through it:
|
|
759
|
+
|
|
760
|
+
* A value from `valid-categories.yml` passes through unchanged, so `page-categories` is exactly what it was without the taxonomy. When the value is a deprecated concept, the build logs one warning per value, with the number of pages, a sample, and the replacement. Labs and solutions pages are left to the legacy check: labs is retiring, and solutions-catalog resolves solution categories itself.
|
|
761
|
+
* A value only the taxonomy knows, such as a new label like `Iceberg Topics`, is replaced by the legacy category of the concept it resolves to, or dropped when there is none. It is not reported as invalid.
|
|
762
|
+
* A value neither knows warns per page and is dropped, as before.
|
|
763
|
+
* A legacy value the taxonomy does not resolve warns as a gap in `taxonomy.yml`.
|
|
764
|
+
|
|
765
|
+
Nothing here fails the build. The authored list is kept in `page-categories-authored` for the extensions that resolve concepts from it.
|
|
766
|
+
|
|
719
767
|
=== Registration
|
|
720
768
|
|
|
721
769
|
Register the `validate-attributes` extension in the Antora playbook under the `antora.extensions` key like so:
|
|
@@ -773,7 +821,26 @@ antora:
|
|
|
773
821
|
- require: '@redpanda-data/docs-extensions-and-macros/extensions/related-labs-extension.js'
|
|
774
822
|
----
|
|
775
823
|
|
|
776
|
-
==
|
|
824
|
+
== Docs taxonomy
|
|
825
|
+
|
|
826
|
+
`extension-utils/taxonomy.js` loads the docs taxonomy, the controlled vocabulary in the docs repository at `shared/modules/ROOT/attachments/taxonomy.yml`, and validates it against `taxonomy.schema.json` next to it. It is not an extension to register: validate-attributes and the Algolia indexer load it on first use and share it on `siteCatalog.taxonomy`.
|
|
827
|
+
|
|
828
|
+
The file is read from the content catalog as an attachment of the `shared` component, so it is never merged into page attributes. <<Global attributes>> only reads `modules/ROOT/partials/`.
|
|
829
|
+
|
|
830
|
+
The file has seven concept schemes (topic, task, content-type, use-case, industry, technology, deployment). Every concept has an `id`, a `prefLabel`, `altLabels`, a `definition`, a `broader` concept, `related` concepts, and `deprecated` with `replacedBy`. Every old category is either the label of a live concept or a deprecated concept whose `replacedBy` names its replacement.
|
|
831
|
+
|
|
832
|
+
At load, the build fails when the file does not match the schema, when an ID repeats, when a label or altLabel repeats in any scheme (case-insensitive), or when a `broader`, `related`, `implies`, or `replacedBy` ID does not exist. When the file is absent, the build logs one warning and every consumer keeps its behavior without the taxonomy, so the docs repository and this package can deploy in either order. When the schema is absent, only the structural checks run.
|
|
833
|
+
|
|
834
|
+
`Taxonomy#resolve(value)` returns the concept a label, altLabel, or ID names, and for a deprecated concept its `replacedBy` IDs. `Taxonomy#expand(ids)` adds the broader concepts and `implies` targets as implied. `pageFacets` turns a page's authored categories, deployment type, and component into the search facets.
|
|
835
|
+
|
|
836
|
+
Outside Antora, `loadTaxonomyFrom({ source, root })` loads the same file from a path or URL, or else from the first that exists of the docs checkout at `root`, the docs checkout at `$REDPANDA_DOCS_PATH`, and the published copy at https://docs.redpanda.com/shared/_attachments/taxonomy.yml. It validates against the schema beside whichever it used.
|
|
837
|
+
|
|
838
|
+
The metadata rules live in `extension-utils/metadata-check.js` (`checkPageMetadata`). validate-attributes runs them on Product Docs pages, solutions-catalog on solution overviews, and `doc-tools check-metadata` on page headers in a pull request, so the build and the PR check cannot disagree. See `check-metadata` in the CLI reference for the rules and their severities, and `.github/workflows/check-metadata.yml` for the reusable PR check.
|
|
839
|
+
|
|
840
|
+
Suggestions live in `extension-utils/metadata-suggest.js` (`suggestForPage`), which `doc-tools check-metadata --suggest` runs. It matches every live concept's labels against a page's title, description, path, headings, and body, deterministically and with no model, and returns up to a few concepts per facet that the page does not carry yet, each with a score and the evidence for it. It honors the facet cardinalities in the taxonomy, `implies`, and `broader`, and never suggests a deprecated concept. Another tool that tags content against the taxonomy should call it rather than match labels itself, so every tagger agrees.
|
|
841
|
+
|
|
842
|
+
`extension-utils/metadata-candidates.js` finds the other side: frequent, distinctive terms on a page that no concept's label matches (`pageCandidates`), and the same across a repository with the pages each is on (`aggregateCandidates`). `doc-tools check-metadata --suggest` lists them per page and `--candidates-only` aggregates them. They are evidence for proposing a new concept or altLabel, deterministic like the suggestions.
|
|
843
|
+
|
|
777
844
|
|
|
778
845
|
This extension collects Asciidoc attributes from the https://github.com/redpanda-data/docs-site[`shared` component] or a local YAML file and makes them available to all component versions. Having global attributes is useful for consistent configuration of local and production builds.
|
|
779
846
|
|
|
@@ -878,6 +945,8 @@ The extension accepts the following configuration options:
|
|
|
878
945
|
|
|
879
946
|
termspath (optional):: Specifies the path to a local directory containing term files (in `.adoc` format). If this path is provided, the extension will attempt to load terms from this directory first. If this path is not provided or no valid terms are found in the specified directory, the extension will fall back to loading terms from the `shared` component.
|
|
880
947
|
|
|
948
|
+
glossarypage (optional):: The resource ID of one page that holds the glossary for the whole site, such as `home:ROOT:glossary.adoc`. The extension appends every term to that page and sets every component's `glossary-page` attribute to it, so `glossterm` links from any component point at the one glossary. Any `reference:glossary.adoc` pages that components still have are filled too, so they stay complete until they are removed. If the page does not exist, the extension logs a warning and links each component to its own `reference:glossary.adoc`, as without this option. When the page exists, it takes precedence over a `glossary-page` attribute set in a component's `antora.yml`, so a per-component value only applies if this page is missing.
|
|
949
|
+
|
|
881
950
|
Term files should follow the following structure:
|
|
882
951
|
|
|
883
952
|
```asciidoc
|
|
@@ -1113,7 +1182,7 @@ This extension adds accurate git-based created and modified dates to documentati
|
|
|
1113
1182
|
The extension adds the following attributes to each page:
|
|
1114
1183
|
|
|
1115
1184
|
- `page-git-created-date`: The date when the file was first committed (YYYY-MM-DD format)
|
|
1116
|
-
- `page-git-modified-date`: The date when the file was last modified (YYYY-MM-DD format)
|
|
1185
|
+
- `page-git-modified-date`: The date when the file was last modified (YYYY-MM-DD format). For a page with Connect reference content, this is the date of the most recent Connect release that changed that content, when it is later than the page's last commit (see <<Redpanda Connect tag modifier>>).
|
|
1117
1186
|
|
|
1118
1187
|
These attributes are available for use in UI templates (for metadata, display banners, etc.) and are exported to markdown files for AI consumption.
|
|
1119
1188
|
|
|
@@ -1284,8 +1353,38 @@ The extension exports these attributes to YAML frontmatter:
|
|
|
1284
1353
|
- `page-categories`: Categories
|
|
1285
1354
|
- `page-topic-type`: Topic type (tutorial, how-to, concept, reference)
|
|
1286
1355
|
- `page-personas`: Target audience
|
|
1356
|
+
- `page-generated-content`: Always present: `none`, `partial` (the page includes generated files) or `all` (the page itself was generated)
|
|
1357
|
+
- `page-autogenerated`, `page-generated-by`, `page-generated-from`: Set when the page itself was written by a generator: the tool that wrote it, and the source it names (`page-generated-from` only when the marker names one)
|
|
1358
|
+
- `page-generated-sources`: One entry per include whose file was written by a generator: the include target, the generator, its version when the marker carries one, and the source it names
|
|
1359
|
+
- `page-introduced-in`, `page-introduced-in-product`: Release that introduced the page's feature, as a bare `X.Y.Z`, and the product it belongs to (generated Connect reference pages, self-managed only)
|
|
1287
1360
|
- Custom attributes from allowlist
|
|
1288
1361
|
|
|
1362
|
+
=== Generated content provenance
|
|
1363
|
+
|
|
1364
|
+
Every doc-tools generator writes one comment line near the top of each file it produces, built by `cli-utils/generated-marker.js`:
|
|
1365
|
+
|
|
1366
|
+
----
|
|
1367
|
+
// This content is autogenerated. Do not edit manually. Generated by doc-tools (<generator>) from <source>. <how to change it>
|
|
1368
|
+
----
|
|
1369
|
+
|
|
1370
|
+
The marker names no doc-tools version. Generated files are committed, so a version would rewrite the first line of every generated file on each release even when nothing else changed. A marker that does carry a token after `doc-tools` (`doc-tools 5.55.0 (rpk-docs)`) is still read, and the token is reported as the version when it is one. Pages generated during the site build, which are never committed, record the version of the package that built them.
|
|
1371
|
+
|
|
1372
|
+
While the catalog still holds AsciiDoc (`contentClassified`), the extension reads that marker on every page and on every file the page includes, then records what it found as page attributes (`documentsConverted`) so both the Markdown twin and UI templates can use it. Every twin states `page-generated-content`, so `none` is a statement and not an absence. A page that is generated end to end gets `all`, `page-autogenerated: true`, `page-generated-by` and `page-generated-from`; a hand-written page that includes generated partials gets `partial` and `page-generated-sources`, for example:
|
|
1373
|
+
|
|
1374
|
+
[,yaml]
|
|
1375
|
+
----
|
|
1376
|
+
page-generated-content: partial
|
|
1377
|
+
page-generated-sources:
|
|
1378
|
+
- include: connect:components:partial$descriptions/inputs/postgres_cdc.adoc
|
|
1379
|
+
generator: rpcn-connector-docs
|
|
1380
|
+
version: null
|
|
1381
|
+
source: the component spec in redpanda-data/connect
|
|
1382
|
+
----
|
|
1383
|
+
|
|
1384
|
+
Older files that carry only `// This content is autogenerated. Do not edit manually.` are still recognized; they report a null generator and source, and the twin has no `page-generated-from`. A source is read only right after `doc-tools (<generator>)` or after `autogenerated`, so a later "from" in a legacy banner's prose is not taken for one. A comment counts as a marker only when it opens with `This content is autogenerated`, `Autogenerated`, `AUTO-GENERATED`, or `Generated by doc-tools`, so a hand-written comment such as `// Generated by running rpk cluster info` does not. The partials that ship in the Connect release asset carry connect's own banner, which names the file that registers the component as `<repository>[@<version>]:<path>` (for example `// This content is autogenerated from the component spec in redpanda-data/benthos@v4.84.0:internal/impl/pure/processor_mapping.go. Do not edit manually.`), or the repository when the generator could not read the name; either reports that source and a null generator. An extension that creates pages at build time sets `page.generatedProvenance` on the file itself, which this scan honors.
|
|
1385
|
+
|
|
1386
|
+
Include detection is best effort. Each `include::` target at the start of a line is resolved as a partial in the page's context (falling back to pages and examples), so a page-relative include such as `include::shared.adoc[]` is looked up as `partial$shared.adoc`. Includes inside comment blocks or inactive `ifdef` branches are counted, targets with an attribute reference are skipped, and nested includes (a hand-written partial that includes a generated one) are not followed.
|
|
1387
|
+
|
|
1289
1388
|
=== Example output
|
|
1290
1389
|
|
|
1291
1390
|
[,yaml]
|
|
@@ -1380,7 +1479,8 @@ The `convert-sitemap-to-markdown` extension generates human-readable markdown ve
|
|
|
1380
1479
|
- Generates individual `.md` files for each sitemap
|
|
1381
1480
|
- Creates master `sitemap-all.md` combining all pages
|
|
1382
1481
|
- Organizes URLs by component/version
|
|
1383
|
-
-
|
|
1482
|
+
- Uses each page's document title and `:description:`, falling back to a title from the URL for files that aren't Antora pages
|
|
1483
|
+
- Includes page metadata (modified dates, priority) on a nested line under each entry, so the entry line keeps the `- [Title](url): description` form
|
|
1384
1484
|
- Uses Antora content catalog (no filesystem operations)
|
|
1385
1485
|
|
|
1386
1486
|
=== Configuration
|
|
@@ -1424,8 +1524,10 @@ Total pages: 4,126
|
|
|
1424
1524
|
|
|
1425
1525
|
### Current
|
|
1426
1526
|
|
|
1427
|
-
- [
|
|
1428
|
-
-
|
|
1527
|
+
- [Introduction to Redpanda](https://docs.redpanda.com/current/get-started/): Learn about Redpanda and its features.
|
|
1528
|
+
- modified: 2024-03-29
|
|
1529
|
+
- [Deploy](https://docs.redpanda.com/current/deploy/)
|
|
1530
|
+
- modified: 2024-03-28
|
|
1429
1531
|
----
|
|
1430
1532
|
|
|
1431
1533
|
=== Use cases
|
|
@@ -180,6 +180,16 @@ async function getTreeFiles (git, gitdir, oid, prefix, cache, treeCache) {
|
|
|
180
180
|
return files
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// The later of two YYYY-MM-DD dates. modify-connect-tag-playbook sets
|
|
184
|
+
// page.connectReferenceModified to the date of the Connect release that last
|
|
185
|
+
// changed a page's generated reference, which lives in a release asset and so
|
|
186
|
+
// in no git commit of the page's repo.
|
|
187
|
+
function laterDate (a, b) {
|
|
188
|
+
if (!b) return a
|
|
189
|
+
if (!a) return b
|
|
190
|
+
return b > a ? b : a
|
|
191
|
+
}
|
|
192
|
+
|
|
183
193
|
module.exports.register = function () {
|
|
184
194
|
raiseListenerLimit(this)
|
|
185
195
|
const logger = this.getLogger('add-git-dates-extension')
|
|
@@ -271,7 +281,7 @@ module.exports.register = function () {
|
|
|
271
281
|
const dates = fileDateMap.get(relativeFilePath)
|
|
272
282
|
if (dates) {
|
|
273
283
|
page.asciidoc.attributes['page-git-created-date'] = dates.created
|
|
274
|
-
page.asciidoc.attributes['page-git-modified-date'] = dates.modified
|
|
284
|
+
page.asciidoc.attributes['page-git-modified-date'] = laterDate(dates.modified, page.connectReferenceModified)
|
|
275
285
|
processedCount++
|
|
276
286
|
}
|
|
277
287
|
}
|
|
@@ -288,3 +298,5 @@ module.exports.register = function () {
|
|
|
288
298
|
logger.info(`Git dates added: processed=${processedCount}, skipped=${skippedCount}, duration=${duration}ms (${perPage}ms/page)`)
|
|
289
299
|
})
|
|
290
300
|
}
|
|
301
|
+
|
|
302
|
+
module.exports._internal = Object.assign(module.exports._internal || {}, { laterDate })
|
|
@@ -127,32 +127,46 @@ module.exports.register = function ({ config }) {
|
|
|
127
127
|
})
|
|
128
128
|
|
|
129
129
|
.on('contentClassified', ({ siteCatalog, contentCatalog }) => {
|
|
130
|
-
|
|
130
|
+
// Appends every term, by category, to a glossary page.
|
|
131
|
+
function mergeTerms (glossaryPage) {
|
|
132
|
+
let glossaryContent = glossaryPage.contents.toString('utf8');
|
|
133
|
+
Object.keys(siteCatalog.termsByCategory).sort().forEach(category => {
|
|
134
|
+
let categoryContent = `\n\n== ${category}\n`;
|
|
135
|
+
siteCatalog.termsByCategory[category].sort((a, b) => a.name.localeCompare(b.name)).forEach(term => {
|
|
136
|
+
categoryContent += `\n\n${processTermContent(term.content)}`;
|
|
137
|
+
});
|
|
138
|
+
glossaryContent += categoryContent;
|
|
139
|
+
});
|
|
140
|
+
glossaryPage.contents = Buffer.from(glossaryContent, 'utf8');
|
|
141
|
+
}
|
|
142
|
+
|
|
131
143
|
try {
|
|
132
|
-
|
|
144
|
+
// With `glossarypage`, one page holds the glossary for the whole site,
|
|
145
|
+
// and every component's glossterm links point at it. Glossary pages
|
|
146
|
+
// components still have are filled too, so they stay complete until
|
|
147
|
+
// they are removed.
|
|
148
|
+
let sitePage = null;
|
|
149
|
+
if (config.glossarypage) {
|
|
150
|
+
sitePage = contentCatalog.resolvePage(config.glossarypage);
|
|
151
|
+
if (sitePage) {
|
|
152
|
+
mergeTerms(sitePage);
|
|
153
|
+
logger.info(`Merged terms into the site glossary ${config.glossarypage}.`);
|
|
154
|
+
} else {
|
|
155
|
+
logger.warn(`The glossarypage ${config.glossarypage} does not exist, so each component links to its own reference:glossary.adoc instead.`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
contentCatalog.getComponents().forEach(({ versions }) => {
|
|
133
160
|
versions.forEach(({ name: component, version, asciidoc, title }) => {
|
|
134
161
|
if (component == 'shared') return;
|
|
162
|
+
if (sitePage) asciidoc.attributes['glossary-page'] = config.glossarypage;
|
|
135
163
|
|
|
136
164
|
const glossaryPage = contentCatalog.resolvePage(`${version ? version + '@' : ''}${component}:reference:glossary.adoc`);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
let glossaryContent = glossaryPage.contents.toString('utf8');
|
|
141
|
-
|
|
142
|
-
Object.keys(siteCatalog.termsByCategory).sort().forEach(category => {
|
|
143
|
-
let categoryContent = `\n\n== ${category}\n`;
|
|
144
|
-
|
|
145
|
-
siteCatalog.termsByCategory[category].sort((a, b) => a.name.localeCompare(b.name)).forEach(term => {
|
|
146
|
-
let processedContent = processTermContent(term.content);
|
|
147
|
-
categoryContent += `\n\n${processedContent}`;
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
glossaryContent += categoryContent;
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
glossaryPage.contents = Buffer.from(glossaryContent, 'utf8');
|
|
165
|
+
if (glossaryPage && glossaryPage !== sitePage) {
|
|
166
|
+
if (!sitePage) asciidoc.attributes['glossary-page'] = 'reference:glossary.adoc';
|
|
167
|
+
mergeTerms(glossaryPage);
|
|
154
168
|
logger.info(`Merged terms into glossary for ${component} component${version ? ' version ' + version : ''}.`);
|
|
155
|
-
} else {
|
|
169
|
+
} else if (!glossaryPage && !sitePage) {
|
|
156
170
|
logger.info(`Skipping ${title} ${version ? ' version ' + version : ''} - No glossary page (reference:glossary.adoc) found`);
|
|
157
171
|
}
|
|
158
172
|
});
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { parse } = require('node-html-parser')
|
|
4
4
|
const { decode } = require('html-entities')
|
|
5
5
|
const path = require('path')
|
|
6
|
+
const { pageFacets, authoredCategories } = require('../../extension-utils/taxonomy')
|
|
6
7
|
|
|
7
8
|
// Create encoder once at module scope for efficiency
|
|
8
9
|
const textEncoder = new TextEncoder()
|
|
@@ -20,10 +21,12 @@ const textEncoder = new TextEncoder()
|
|
|
20
21
|
* @param {Boolean} config.indexLatestOnly - If true, only index the latest version of any given page.
|
|
21
22
|
* @param {Array} config.excludes - CSS selectors for elements to exclude from indexing.
|
|
22
23
|
* @param {Object} config.logger - Logger to use
|
|
24
|
+
* @param {Object} [config.taxonomy] - The site taxonomy (extension-utils/taxonomy.js). When
|
|
25
|
+
* set, Product Docs records also carry topics, useCases, technologies, and deployment.
|
|
23
26
|
* @typedef {Object} SearchIndexData
|
|
24
27
|
* @returns {SearchIndexData} A data object that contains the Algolia index
|
|
25
28
|
*/
|
|
26
|
-
function generateIndex (playbook, contentCatalog, { indexLatestOnly = false, excludes = [], logger } = {}) {
|
|
29
|
+
function generateIndex (playbook, contentCatalog, { indexLatestOnly = false, excludes = [], logger, taxonomy } = {}) {
|
|
27
30
|
// Use provided logger or create a no-op logger for tests
|
|
28
31
|
if (!logger) {
|
|
29
32
|
logger = process.env.NODE_ENV === 'test'
|
|
@@ -288,6 +291,10 @@ function generateIndex (playbook, contentCatalog, { indexLatestOnly = false, exc
|
|
|
288
291
|
? page.asciidoc.attributes['page-categories'].split(',').map(category => category.trim())
|
|
289
292
|
: []
|
|
290
293
|
|
|
294
|
+
const facets = taxonomy && component.name !== 'labs'
|
|
295
|
+
? pageFacets(taxonomy, { categories: authoredCategories(page.asciidoc?.attributes), deployment, component: component.name })
|
|
296
|
+
: null
|
|
297
|
+
|
|
291
298
|
const commercialNames = page.asciidoc?.attributes['page-commercial-names']
|
|
292
299
|
? page.asciidoc.attributes['page-commercial-names'].split(',').map(name => name.trim())
|
|
293
300
|
: []
|
|
@@ -332,6 +339,11 @@ function generateIndex (playbook, contentCatalog, { indexLatestOnly = false, exc
|
|
|
332
339
|
indexItem.breadcrumbs = breadcrumbs
|
|
333
340
|
indexItem.type = 'Doc'
|
|
334
341
|
indexItem._tags = Array.isArray(tag) ? tag : [tag]
|
|
342
|
+
// Taxonomy facets, next to `categories` rather than instead of it:
|
|
343
|
+
// labs-home filters on `categories`, so it stays the legacy list until
|
|
344
|
+
// every consumer has moved. Arrays of prefLabels for display and
|
|
345
|
+
// faceting, plus the concept IDs, which do not change when a label does.
|
|
346
|
+
if (taxonomy) Object.assign(indexItem, facets)
|
|
335
347
|
} else {
|
|
336
348
|
indexItem.deployment = deployment
|
|
337
349
|
indexItem.type = 'Lab'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const generateIndex = require('./generate-index')
|
|
4
|
+
const { getTaxonomy } = require('../../extension-utils/taxonomy')
|
|
4
5
|
const { raiseListenerLimit } = require('../util/raise-listener-limit')
|
|
5
6
|
const algoliasearch = require('algoliasearch')
|
|
6
7
|
const http = require('http')
|
|
@@ -51,8 +52,9 @@ function register ({
|
|
|
51
52
|
})
|
|
52
53
|
const index = client.initIndex(process.env.ALGOLIA_INDEX_NAME)
|
|
53
54
|
|
|
54
|
-
this.on('beforePublish', async ({ playbook, contentCatalog }) => {
|
|
55
|
-
const
|
|
55
|
+
this.on('beforePublish', async ({ playbook, contentCatalog, siteCatalog }) => {
|
|
56
|
+
const taxonomy = getTaxonomy({ contentCatalog, siteCatalog, logger })
|
|
57
|
+
const algolia = generateIndex(playbook, contentCatalog, { indexLatestOnly, excludes, logger, taxonomy })
|
|
56
58
|
|
|
57
59
|
if (!algolia || Object.keys(algolia).length === 0) {
|
|
58
60
|
logger.warn('No content to index for Algolia')
|
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
|
|
15
15
|
const path = require('path')
|
|
16
16
|
const { parseStringPromise } = require('xml2js')
|
|
17
|
+
const { decode } = require('html-entities')
|
|
18
|
+
const { toMarkdownUrl } = require('../extension-utils/url-utils')
|
|
17
19
|
|
|
18
20
|
module.exports.register = function ({ config }) {
|
|
19
21
|
const logger = this.getLogger('convert-sitemap-to-markdown')
|
|
20
22
|
|
|
21
23
|
this
|
|
22
|
-
.on('beforePublish', async ({ playbook, siteCatalog }) => {
|
|
24
|
+
.on('beforePublish', async ({ playbook, siteCatalog, contentCatalog }) => {
|
|
23
25
|
const startTime = Date.now()
|
|
24
26
|
|
|
25
27
|
logger.info('Sitemap to markdown converter starting...')
|
|
@@ -37,10 +39,12 @@ module.exports.register = function ({ config }) {
|
|
|
37
39
|
|
|
38
40
|
logger.info(`Found ${sitemapFiles.length} sitemap file(s)`)
|
|
39
41
|
|
|
42
|
+
const pageInfo = collectPageInfo(contentCatalog)
|
|
43
|
+
|
|
40
44
|
// Convert each sitemap and collect all URLs
|
|
41
45
|
const allUrls = []
|
|
42
46
|
for (const sitemapFile of sitemapFiles) {
|
|
43
|
-
const urls = await convertSitemapToMarkdown(sitemapFile, siteCatalog, logger)
|
|
47
|
+
const urls = await convertSitemapToMarkdown(sitemapFile, siteCatalog, logger, pageInfo)
|
|
44
48
|
if (urls) {
|
|
45
49
|
allUrls.push(...urls)
|
|
46
50
|
}
|
|
@@ -48,7 +52,7 @@ module.exports.register = function ({ config }) {
|
|
|
48
52
|
|
|
49
53
|
// Create combined master sitemap if we have multiple sitemaps
|
|
50
54
|
if (sitemapFiles.length > 1 && allUrls.length > 0) {
|
|
51
|
-
await createMasterSitemap(sitemapFiles, allUrls, siteCatalog, logger)
|
|
55
|
+
await createMasterSitemap(sitemapFiles, allUrls, siteCatalog, logger, pageInfo)
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
const duration = ((Date.now() - startTime) / 1000).toFixed(2)
|
|
@@ -63,7 +67,7 @@ module.exports.register = function ({ config }) {
|
|
|
63
67
|
/**
|
|
64
68
|
* Create a master combined sitemap from all individual sitemaps
|
|
65
69
|
*/
|
|
66
|
-
async function createMasterSitemap(sitemapFiles, allUrls, siteCatalog, logger) {
|
|
70
|
+
async function createMasterSitemap(sitemapFiles, allUrls, siteCatalog, logger, pageInfo) {
|
|
67
71
|
const sitemapCount = sitemapFiles.length
|
|
68
72
|
const sitemapWord = sitemapCount === 1 ? 'sitemap' : 'sitemaps'
|
|
69
73
|
|
|
@@ -81,7 +85,7 @@ async function createMasterSitemap(sitemapFiles, allUrls, siteCatalog, logger) {
|
|
|
81
85
|
|
|
82
86
|
// Add all URLs grouped by component
|
|
83
87
|
if (allUrls.length > 0) {
|
|
84
|
-
markdown += convertUrlsetToMarkdown(allUrls)
|
|
88
|
+
markdown += convertUrlsetToMarkdown(allUrls, pageInfo)
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
// Add to site catalog
|
|
@@ -97,7 +101,7 @@ async function createMasterSitemap(sitemapFiles, allUrls, siteCatalog, logger) {
|
|
|
97
101
|
* Convert a sitemap file to markdown
|
|
98
102
|
* Returns the URLs found in this sitemap for aggregation
|
|
99
103
|
*/
|
|
100
|
-
async function convertSitemapToMarkdown(sitemapFile, siteCatalog, logger) {
|
|
104
|
+
async function convertSitemapToMarkdown(sitemapFile, siteCatalog, logger, pageInfo) {
|
|
101
105
|
const xmlContent = sitemapFile.contents.toString('utf8')
|
|
102
106
|
const basename = path.basename(sitemapFile.out.path)
|
|
103
107
|
const outputPath = basename.replace(/\.xml$/, '.md')
|
|
@@ -113,7 +117,7 @@ async function convertSitemapToMarkdown(sitemapFile, siteCatalog, logger) {
|
|
|
113
117
|
// Handle standard sitemap
|
|
114
118
|
if (parsed.urlset && parsed.urlset.url) {
|
|
115
119
|
urls = parsed.urlset.url
|
|
116
|
-
markdown += convertUrlsetToMarkdown(urls)
|
|
120
|
+
markdown += convertUrlsetToMarkdown(urls, pageInfo)
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
// Handle sitemap index
|
|
@@ -139,7 +143,7 @@ async function convertSitemapToMarkdown(sitemapFile, siteCatalog, logger) {
|
|
|
139
143
|
/**
|
|
140
144
|
* Convert URL entries to markdown
|
|
141
145
|
*/
|
|
142
|
-
function convertUrlsetToMarkdown(urls) {
|
|
146
|
+
function convertUrlsetToMarkdown(urls, pageInfo = new Map()) {
|
|
143
147
|
let markdown = '## Pages\n\n'
|
|
144
148
|
markdown += `Total pages: ${urls.length.toLocaleString()}\n\n`
|
|
145
149
|
|
|
@@ -156,18 +160,23 @@ function convertUrlsetToMarkdown(urls) {
|
|
|
156
160
|
const priority = url.priority ? url.priority[0] : ''
|
|
157
161
|
|
|
158
162
|
if (loc) {
|
|
159
|
-
|
|
163
|
+
const page = pageInfo.get(new URL(loc).pathname)
|
|
164
|
+
const title = (page && page.title) || extractPageTitle(loc)
|
|
165
|
+
markdown += `- [${title}](${loc})`
|
|
166
|
+
if (page && page.description) markdown += `: ${page.description}`
|
|
167
|
+
markdown += '\n'
|
|
160
168
|
|
|
169
|
+
// Metadata goes on a nested line. Readers of the llms.txt link form
|
|
170
|
+
// take everything after `: ` as the description, so metadata on the
|
|
171
|
+
// entry line would become part of it.
|
|
161
172
|
const metadata = []
|
|
162
173
|
if (lastmod) metadata.push(`modified: ${lastmod}`)
|
|
163
174
|
if (changefreq) metadata.push(`frequency: ${changefreq}`)
|
|
164
175
|
if (priority) metadata.push(`priority: ${priority}`)
|
|
165
176
|
|
|
166
177
|
if (metadata.length > 0) {
|
|
167
|
-
markdown += `
|
|
178
|
+
markdown += ` - ${metadata.join(', ')}\n`
|
|
168
179
|
}
|
|
169
|
-
|
|
170
|
-
markdown += '\n'
|
|
171
180
|
}
|
|
172
181
|
}
|
|
173
182
|
|
|
@@ -177,6 +186,32 @@ function convertUrlsetToMarkdown(urls) {
|
|
|
177
186
|
return markdown
|
|
178
187
|
}
|
|
179
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Map each published page's HTML and Markdown paths to its document title and
|
|
191
|
+
* description, so sitemap entries carry the same text as the page itself
|
|
192
|
+
* rather than a title guessed from the URL.
|
|
193
|
+
*/
|
|
194
|
+
function collectPageInfo(contentCatalog) {
|
|
195
|
+
const info = new Map()
|
|
196
|
+
if (!contentCatalog) return info
|
|
197
|
+
for (const page of contentCatalog.getPages((p) => p.pub && p.pub.url && p.out)) {
|
|
198
|
+
const asciidoc = page.asciidoc || {}
|
|
199
|
+
const title = plainText(asciidoc.doctitle || asciidoc.navtitle)
|
|
200
|
+
const description = plainText(asciidoc.attributes && asciidoc.attributes.description)
|
|
201
|
+
if (!title && !description) continue
|
|
202
|
+
const entry = { title: title.replace(/([[\]])/g, '\\$1'), description }
|
|
203
|
+
info.set(page.pub.url, entry)
|
|
204
|
+
info.set(toMarkdownUrl(page.pub.url), entry)
|
|
205
|
+
}
|
|
206
|
+
return info
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Converted titles and descriptions are HTML. Strip tags before decoding so an
|
|
210
|
+
// escaped placeholder such as `<name>` survives as `<name>`.
|
|
211
|
+
function plainText(html) {
|
|
212
|
+
return decode(String(html || '').replace(/<[^>]+>/g, '')).replace(/\s+/g, ' ').trim()
|
|
213
|
+
}
|
|
214
|
+
|
|
180
215
|
/**
|
|
181
216
|
* Convert sitemap index to markdown
|
|
182
217
|
*/
|