@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.
Files changed (87) hide show
  1. package/bin/doc-tools-mcp.js +4 -5
  2. package/bin/doc-tools.js +354 -9
  3. package/bin/mcp-tools/generated-docs-review.js +26 -9
  4. package/bin/mcp-tools/overrides-audit.js +1 -1
  5. package/bin/mcp-tools/rpcn-docs.js +4 -14
  6. package/cli-utils/docs-links-to-xrefs.js +174 -0
  7. package/cli-utils/format-helm-spec.js +4 -3
  8. package/cli-utils/generated-marker.js +43 -0
  9. package/cli-utils/helm-commented-values.js +125 -21
  10. package/cli-utils/self-managed-docs-branch.js +36 -30
  11. package/extension-utils/html-utils.js +27 -0
  12. package/extension-utils/llms-utils.js +6 -2
  13. package/extension-utils/metadata-candidates.js +304 -0
  14. package/extension-utils/metadata-check.js +174 -0
  15. package/extension-utils/metadata-suggest.js +573 -0
  16. package/extension-utils/taxonomy-categories.js +106 -0
  17. package/extension-utils/taxonomy.js +566 -0
  18. package/extensions/REFERENCE.adoc +110 -8
  19. package/extensions/add-git-dates.js +13 -1
  20. package/extensions/aggregate-terms.js +34 -20
  21. package/extensions/algolia-indexer/generate-index.js +13 -1
  22. package/extensions/algolia-indexer/index.js +4 -2
  23. package/extensions/convert-sitemap-to-markdown.js +47 -12
  24. package/extensions/convert-to-markdown.js +176 -0
  25. package/extensions/generate-fields-only-pages.js +14 -0
  26. package/extensions/modify-connect-tag-playbook.js +181 -3
  27. package/extensions/resolve-xrefs-in-attachments.js +1 -12
  28. package/extensions/url-to-xref.js +92 -22
  29. package/extensions/util/connect-docs-asset.js +6 -5
  30. package/extensions/validate-attributes.js +10 -1
  31. package/extensions/version-fetcher/fetch-latest-docker-tag.js +35 -13
  32. package/extensions/version-fetcher/get-latest-redpanda-helm-version-from-operator.js +64 -40
  33. package/extensions/version-fetcher/list-connect-releases.js +3 -3
  34. package/extensions/version-fetcher/release-lines.js +64 -0
  35. package/extensions/version-fetcher/set-latest-version.js +73 -3
  36. package/macros/README.adoc +1 -0
  37. package/macros/REFERENCE.adoc +99 -0
  38. package/macros/badge.js +5 -8
  39. package/macros/enterprise.js +2 -5
  40. package/macros/glossary.js +4 -14
  41. package/macros/iceberg-explorer.js +242 -0
  42. package/macros/prop.js +3 -13
  43. package/mcp/USER_GUIDE.adoc +25 -135
  44. package/package.json +5 -2
  45. package/tools/check-metadata/index.js +523 -0
  46. package/tools/cloud-regions/cloud-regions-table-adoc.hbs +1 -2
  47. package/tools/cloud-regions/cloud-regions-table-md.hbs +2 -2
  48. package/tools/cloud-regions/render-cloud-regions.js +3 -0
  49. package/tools/dispatch-graph/check-dispatch-graph.js +790 -0
  50. package/tools/dispatch-graph/classify-runs.js +128 -0
  51. package/tools/dispatch-graph/parse-workflows.js +499 -0
  52. package/tools/doc-impact-recording/index.js +190 -0
  53. package/tools/enterprise-features/detect-gating-changes.js +393 -0
  54. package/tools/enterprise-features/verify.js +2 -1
  55. package/tools/gen-rpk-ascii.py +7 -1
  56. package/tools/lint-strings/fixtures/connect/lint_connect.go +8 -0
  57. package/tools/lint-strings/fixtures/crd/lint_types.go +4 -1
  58. package/tools/lint-strings/fixtures/rpk-pr461/delete.base.go +42 -0
  59. package/tools/lint-strings/fixtures/rpk-pr461/delete.head.go +45 -0
  60. package/tools/lint-strings/fixtures/rpk-pr461/reviewer-comments.json +1 -0
  61. package/tools/lint-strings/index.js +133 -3
  62. package/tools/lint-strings/rules/verbatim-asciidoc.js +166 -1
  63. package/tools/lint-strings/surfaces/connect.js +72 -12
  64. package/tools/lint-strings/surfaces/crd.js +10 -6
  65. package/tools/metrics/metrics.py +11 -0
  66. package/tools/overrides-audit/README.adoc +1 -2
  67. package/tools/overrides-audit/adapters/properties.js +1 -1
  68. package/tools/overrides-audit/index.js +3 -4
  69. package/tools/property-extractor/generate-handlebars-docs.js +9 -1
  70. package/tools/property-extractor/helpers/applyPropertyLinks.js +35 -0
  71. package/tools/redpanda-connect/AUTOMATION.md +3 -36
  72. package/tools/redpanda-connect/README.adoc +5 -33
  73. package/tools/redpanda-connect/generate-rpcn-connector-docs.js +32 -11
  74. package/tools/redpanda-connect/pr-summary-formatter.js +6 -4
  75. package/tools/redpanda-connect/report-delta.js +6 -3
  76. package/tools/redpanda-connect/rpcn-connector-docs-handler.js +42 -25
  77. package/tools/redpanda-connect/templates/bloblang-function.hbs +1 -1
  78. package/tools/redpanda-connect/templates/descriptions-partials.hbs +1 -1
  79. package/tools/redpanda-connect/templates/examples-partials.hbs +1 -1
  80. package/tools/redpanda-connect/templates/fields-partials.hbs +2 -2
  81. package/tools/redpanda-connect/templates/metadata-partials.hbs +1 -1
  82. package/tools/redpanda-release-notes/generate-release-notes.js +116 -1
  83. package/tools/rpk-docs/generate-rpk-docs.js +2 -0
  84. package/tools/rpk-docs/generate-x-env-partial.js +7 -4
  85. package/tools/rpk-docs/helpers/index.js +4 -1
  86. package/tools/rpk-docs/templates/command.hbs +2 -2
  87. 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 and `modules/components/examples/**` as examples 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.
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
- == Global attributes
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
- - Includes page metadata (modified dates, priority)
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
- - [Get Started](https://docs.redpanda.com/current/get-started/) (modified: 2024-03-29)
1428
- - [Deploy](https://docs.redpanda.com/current/deploy/) (modified: 2024-03-28)
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
- const components = contentCatalog.getComponents();
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
- components.forEach(({ versions }) => {
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
- if (glossaryPage) {
139
- asciidoc.attributes['glossary-page'] = 'reference:glossary.adoc';
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 algolia = generateIndex(playbook, contentCatalog, { indexLatestOnly, excludes, logger })
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
- markdown += `- [${extractPageTitle(loc)}](${loc})`
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 += ` (${metadata.join(', ')})`
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 `&lt;name&gt;` 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
  */