@redpanda-data/docs-extensions-and-macros 4.15.4 → 4.15.6

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.
@@ -5,27 +5,65 @@
5
5
  *
6
6
  * USAGE:
7
7
  * :page-faq-1-question: How do I install Redpanda?
8
- * :page-faq-1-answer: Download from redpanda.com and run the installer.
8
+ * :page-faq-1-answer: Download from redpanda.com and run the installer. See our xref:get-started:intro.adoc[quickstart guide] for details.
9
9
  * :page-faq-1-anchor: #installation (optional - links to section)
10
10
  *
11
11
  * :page-faq-2-question: What are the system requirements?
12
- * :page-faq-2-answer: You need at least 2GB RAM and 2 CPU cores.
12
+ * :page-faq-2-answer: You need at least 2GB RAM and 2 CPU cores for production. For development, xref:deploy:docker-compose.adoc[use Docker Compose].
13
13
  * :page-faq-2-anchor: #requirements
14
14
  *
15
15
  * The extension:
16
16
  * - Generates schema.org FAQPage JSON-LD in <head>
17
17
  * - Supports multiple FAQs numbered sequentially (1, 2, 3...)
18
18
  * - Anchor is optional and adds URL to the FAQ question
19
- * - Writers can reference existing page content in answers
19
+ * - Writers can use AsciiDoc xrefs in answers - they're resolved to full URLs
20
+ * - Xrefs are converted to "link text (URL)" format in JSON-LD
20
21
  */
21
22
 
23
+ /**
24
+ * Resolve xrefs in text to full URLs
25
+ * @param {string} text - Text containing xref macros
26
+ * @param {Object} currentPage - Current page context
27
+ * @param {Object} contentCatalog - Antora content catalog
28
+ * @param {string} siteUrl - Base site URL
29
+ * @param {Object} logger - Logger instance
30
+ * @returns {string} Text with xrefs resolved to plain text + URLs
31
+ */
32
+ function resolveXrefs(text, currentPage, contentCatalog, siteUrl, logger) {
33
+ // Match xref:target[link text] pattern
34
+ const xrefPattern = /xref:([^\[]+)\[([^\]]+)\]/g
35
+
36
+ return text.replace(xrefPattern, (match, target, linkText) => {
37
+ try {
38
+ // Resolve the resource using Antora's content catalog
39
+ // This uses Antora's standard API for resolving page references
40
+ const resource = contentCatalog.resolveResource(target, currentPage.src, 'page')
41
+
42
+ if (resource && resource.pub && resource.pub.url) {
43
+ const fullUrl = siteUrl ? `${siteUrl}${resource.pub.url}` : `https://docs.redpanda.com${resource.pub.url}`
44
+ return `${linkText} (${fullUrl})`
45
+ } else {
46
+ // Xref couldn't be resolved (page doesn't exist or isn't loaded yet)
47
+ // Fall back to just the link text - this is expected for cross-component refs in local builds
48
+ return linkText
49
+ }
50
+ } catch (error) {
51
+ logger.warn(`FAQ xref resolution error for ${target}: ${error.message}`)
52
+ return linkText // Fallback to just the link text
53
+ }
54
+ })
55
+ }
56
+
22
57
  /**
23
58
  * Extract FAQ entries from page attributes
24
59
  * @param {Object} attributes - Page attributes object
60
+ * @param {Object} page - Current page object
61
+ * @param {Object} contentCatalog - Antora content catalog
62
+ * @param {string} siteUrl - Base site URL
25
63
  * @param {Object} logger - Logger instance
26
64
  * @returns {Array<{question: string, answer: string, anchor?: string}>}
27
65
  */
28
- function extractFaqs(attributes, logger) {
66
+ function extractFaqs(attributes, page, contentCatalog, siteUrl, logger) {
29
67
  const faqs = []
30
68
  const faqNumbers = new Set()
31
69
 
@@ -58,9 +96,12 @@ function extractFaqs(attributes, logger) {
58
96
  return
59
97
  }
60
98
 
99
+ // Resolve any xrefs in the answer text
100
+ const resolvedAnswer = resolveXrefs(answer.trim(), page, contentCatalog, siteUrl, logger)
101
+
61
102
  const faq = {
62
103
  question: question.trim(),
63
- answer: answer.trim()
104
+ answer: resolvedAnswer
64
105
  }
65
106
 
66
107
  if (anchor) {
@@ -115,35 +156,26 @@ module.exports.register = function () {
115
156
 
116
157
  this.on('documentsConverted', ({ contentCatalog }) => {
117
158
  const pages = contentCatalog.getPages()
159
+ const siteUrl = playbook?.site?.url || ''
118
160
  let processedCount = 0
119
161
  let totalFaqs = 0
120
- const siteUrl = playbook?.site?.url || 'https://docs.redpanda.com'
121
162
 
122
163
  pages.forEach(page => {
123
164
  const attributes = page.asciidoc?.attributes
124
165
  if (!attributes) return
125
166
 
126
- // Extract FAQs from attributes
127
- const faqs = extractFaqs(attributes, logger)
167
+ // Extract FAQs from attributes and resolve any xrefs
168
+ const faqs = extractFaqs(attributes, page, contentCatalog, siteUrl, logger)
128
169
 
129
170
  if (faqs.length === 0) return
130
171
 
131
- // Generate base URL for the page
132
- let baseUrl = ''
133
- if (page.pub?.url) {
134
- baseUrl = `${siteUrl}${page.pub.url}`
135
- }
136
-
137
- // Generate FAQPage JSON-LD
138
- const faqJsonLd = generateFaqJsonLd(faqs, baseUrl)
139
-
140
- // Store as JSON string in page attribute for UI template
141
- attributes['page-faq-json-ld'] = JSON.stringify(faqJsonLd, null, 2)
172
+ // Store structured FAQ data as JSON for UI template to format
173
+ // Template will generate the JSON-LD structure using page.url
174
+ attributes['page-has-faqs'] = 'true'
175
+ attributes['page-faqs'] = JSON.stringify(faqs)
142
176
 
143
177
  processedCount++
144
178
  totalFaqs += faqs.length
145
-
146
- logger.debug(`Added ${faqs.length} FAQs to ${page.src.relative}`)
147
179
  })
148
180
 
149
181
  if (processedCount > 0) {
@@ -80,11 +80,18 @@ module.exports.register = function () {
80
80
  // The markdown converter applies smart typography that turns -- into — (em dash)
81
81
  // and inserts zero-width spaces (U+200B) and other invisible Unicode characters
82
82
  // This breaks URLs like deploy-preview-159--redpanda-documentation.netlify.app
83
+ // Fix URLs in parentheses (actual hrefs)
83
84
  content = content.replace(/\(https?:\/\/[^)]*[—\u200B-\u200D\uFEFF][^)]*\)/g, (match) => {
84
85
  return match
85
86
  .replace(/—/g, '--')
86
87
  .replace(/[\u200B-\u200D\uFEFF]/g, '');
87
88
  });
89
+ // Fix URLs in square brackets (link text)
90
+ content = content.replace(/\[https?:\/\/[^\]]*[—\u200B-\u200D\uFEFF][^\]]*\]/g, (match) => {
91
+ return match
92
+ .replace(/—/g, '--')
93
+ .replace(/[\u200B-\u200D\uFEFF]/g, '');
94
+ });
88
95
  logger.debug('Fixed em dashes and invisible characters in URLs');
89
96
 
90
97
  // Unpublish the HTML page FIRST (following unpublish-pages pattern)
@@ -490,17 +490,27 @@ module.exports.register = function () {
490
490
  logger.debug(`Generated frontmatter for ${page.src?.path}`)
491
491
  }
492
492
 
493
- // Prepend frontmatter first, then source reference and AI-friendly note
493
+ // Extract H1 heading if present (only at document start)
494
+ const h1Match = markdown.match(/^(#\s+.+?)(\n|$)/)
495
+ let h1Heading = ''
496
+ let restOfMarkdown = markdown
497
+
498
+ if (h1Match) {
499
+ h1Heading = h1Match[0]
500
+ restOfMarkdown = markdown.substring(h1Match[0].length).trimStart()
501
+ }
502
+
503
+ // Add frontmatter AFTER H1 heading, then source reference and AI-friendly note
494
504
  if (canonicalUrl) {
495
505
  const componentName = page.src?.component || '';
496
506
  const urlHint = componentName
497
507
  ? `<!-- Note for AI: This is a Markdown export. For aggregated content, see /llms.txt (curated overview), /${componentName}-full.txt (this component only), or /llms-full.txt (complete documentation). -->`
498
508
  : `<!-- Note for AI: This is a Markdown export. For aggregated content, see /llms.txt (curated overview) or /llms-full.txt (complete documentation). -->`;
499
509
 
500
- markdown = `${frontmatter}<!-- Source: ${canonicalUrl} -->\n${urlHint}\n\n${markdown}`
510
+ markdown = `${h1Heading}\n${frontmatter}<!-- Source: ${canonicalUrl} -->\n${urlHint}\n\n${restOfMarkdown}`
501
511
  } else if (frontmatter) {
502
- // If no canonical URL but we have frontmatter, still add it
503
- markdown = `${frontmatter}${markdown}`
512
+ // If no canonical URL but we have frontmatter, still add it after H1
513
+ markdown = `${h1Heading}\n${frontmatter}${restOfMarkdown}`
504
514
  }
505
515
 
506
516
  // Clean up unnecessary whitespace
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "4.15.4",
3
+ "version": "4.15.6",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",