@redpanda-data/docs-extensions-and-macros 3.2.4 → 3.2.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.
package/README.adoc CHANGED
@@ -180,6 +180,18 @@ antora:
180
180
  - require: '@redpanda-data/docs-extensions-and-macros/extensions/add-global-attributes'
181
181
  ```
182
182
 
183
+ === Produce redirects (customization of core Antora)
184
+
185
+ This extension replaces the default https://gitlab.com/antora/antora/-/tree/v3.1.x/packages/redirect-producer[`produceRedirects()` function] in Antora to handle redirect loops caused by https://docs.antora.org/antora/latest/page/page-aliases/[page aliases]. Normally, page aliases in Antora are used to resolve outdated links without causing issues. However, with https://docs.antora.org/antora/latest/playbook/urls-html-extension-style/#html-extension-style-key[`indexify`], the same URL may inadvertently be used for both the source and target of a redirect, leading to loops. This problem is https://antora.zulipchat.com/#narrow/stream/282400-users/topic/Redirect.20Loop.20Issue.20with.20Page.20Renaming.20and.20Indexify/near/433691700[recognized as a bug] in core Antora. For example, creating a page alias for `modules/manage/security/authorization.adoc` to point to `modules/manage/security/authorization/index.adoc' can lead to a redirect loop where `manage/security/authorization/` points to `manage/security/authorization/`. Furthermore, omitting the alias would lead to `xref not found` errors because Antora relies on the alias to resolve the old xrefs. This extension is necessary until such behaviors are natively supported or fixed in Antora core.
186
+
187
+ ==== Registration example
188
+
189
+ ```yaml
190
+ antora:
191
+ extensions:
192
+ - '@redpanda-data/docs-extensions-and-macros/extensions/modify-redirects'
193
+ ```
194
+
183
195
  === Replace attributes in attachments
184
196
 
185
197
  This extension replaces AsciiDoc attribute placeholders with their respective values in attachment files, such as CSS, HTML, and YAML.
@@ -0,0 +1,173 @@
1
+ 'use strict';
2
+
3
+ const File = require('vinyl')
4
+ const { posix: path } = require('path')
5
+
6
+ const ENCODED_SPACE_RX = /%20/g
7
+
8
+ module.exports.register = function () {
9
+ this.once('contextStarted', () => {
10
+ const { publishFiles: produceRedirectsDelegate } = this.getFunctions()
11
+ this.replaceFunctions({
12
+ produceRedirects (playbook, aliases) {
13
+ if ('findBy' in aliases) aliases = aliases.findBy({ family: 'alias' }) // @deprecated remove in Antora 4
14
+ if (!aliases.length) return []
15
+ let siteUrl = playbook.site.url
16
+ if (siteUrl) siteUrl = stripTrailingSlash(siteUrl, '')
17
+ const directoryRedirects = (playbook.urls.htmlExtensionStyle || 'default') !== 'default'
18
+ switch (playbook.urls.redirectFacility) {
19
+ case 'gitlab':
20
+ return createNetlifyRedirects(aliases, extractUrlPath(siteUrl), !directoryRedirects, false)
21
+ case 'httpd':
22
+ return createHttpdHtaccess(aliases, extractUrlPath(siteUrl), directoryRedirects)
23
+ case 'netlify':
24
+ return createNetlifyRedirects(aliases, extractUrlPath(siteUrl), !directoryRedirects)
25
+ case 'nginx':
26
+ return createNginxRewriteConf(aliases, extractUrlPath(siteUrl))
27
+ case 'static':
28
+ return populateStaticRedirectFiles(
29
+ aliases.filter((it) => it.out),
30
+ siteUrl
31
+ )
32
+ default:
33
+ return unpublish(aliases)
34
+ }
35
+ return produceRedirectsDelegate.call(this, playbook, aliases)
36
+ }
37
+ })
38
+ })
39
+ }
40
+
41
+ function createStaticRedirectContents (file, siteUrl) {
42
+ const targetUrl = file.rel.pub.url
43
+ let linkTag
44
+ let to = targetUrl.charAt() === '/' ? computeRelativeUrlPath(file.pub.url, targetUrl) : undefined
45
+ let toText = to
46
+ if (to) {
47
+ if (siteUrl && siteUrl.charAt() !== '/') {
48
+ linkTag = `<link rel="canonical" href="${(toText = siteUrl + targetUrl)}">\n`
49
+ }
50
+ } else {
51
+ linkTag = `<link rel="canonical" href="${(toText = to = targetUrl)}">\n`
52
+ }
53
+ return `<!DOCTYPE html>
54
+ <meta charset="utf-8">
55
+ ${linkTag || ''}<script>location="${to}"</script>
56
+ <meta http-equiv="refresh" content="0; url=${to}">
57
+ <meta name="robots" content="noindex">
58
+ <title>Redirect Notice</title>
59
+ <h1>Redirect Notice</h1>
60
+ <p>The page you requested has been relocated to <a href="${to}">${toText}</a>.</p>`
61
+ }
62
+
63
+ function extractUrlPath (url) {
64
+ if (url) {
65
+ if (url.charAt() === '/') return url
66
+ const urlPath = new URL(url).pathname
67
+ return urlPath === '/' ? '' : urlPath
68
+ } else {
69
+ return ''
70
+ }
71
+ }
72
+
73
+ function createHttpdHtaccess (files, urlPath, directoryRedirects = false) {
74
+ const rules = files.reduce((accum, file) => {
75
+ if (isRedirectLoop(file)) return accum
76
+ delete file.out
77
+ let fromUrl = file.pub.url
78
+ fromUrl = ~fromUrl.indexOf('%20') ? `'${urlPath}${fromUrl.replace(ENCODED_SPACE_RX, ' ')}'` : urlPath + fromUrl
79
+ let toUrl = file.rel.pub.url
80
+ toUrl = ~toUrl.indexOf('%20') ? `'${urlPath}${toUrl.replace(ENCODED_SPACE_RX, ' ')}'` : urlPath + toUrl
81
+ // see https://httpd.apache.org/docs/current/en/mod/mod_alias.html#redirect
82
+ // NOTE: redirect rule for directory prefix does not require trailing slash
83
+ if (file.pub.splat) {
84
+ accum.push(`Redirect 302 ${fromUrl} ${stripTrailingSlash(toUrl)}`)
85
+ } else if (directoryRedirects) {
86
+ accum.push(`RedirectMatch 301 ^${regexpEscape(fromUrl)}$ ${stripTrailingSlash(toUrl)}`)
87
+ } else {
88
+ accum.push(`Redirect 301 ${fromUrl} ${toUrl}`)
89
+ }
90
+ return accum
91
+ }, [])
92
+ return [new File({ contents: Buffer.from(rules.join('\n') + '\n'), out: { path: '.htaccess' } })]
93
+ }
94
+
95
+ // NOTE: a trailing slash on the pathname will be ignored
96
+ // see https://docs.netlify.com/routing/redirects/redirect-options/#trailing-slash
97
+ // however, we keep it when generating the rules for clarity
98
+ function createNetlifyRedirects (files, urlPath, addDirectoryRedirects = false, useForceFlag = true) {
99
+ const rules = files.reduce((accum, file) => {
100
+ if (isRedirectLoop(file)) return accum
101
+ delete file.out
102
+ const fromUrl = urlPath + file.pub.url
103
+ const toUrl = urlPath + file.rel.pub.url
104
+ const forceFlag = useForceFlag ? '!' : ''
105
+ if (file.pub.splat) {
106
+ accum.push(`${fromUrl}/* ${ensureTrailingSlash(toUrl)}:splat 302${forceFlag}`)
107
+ } else {
108
+ accum.push(`${fromUrl} ${toUrl} 301${forceFlag}`)
109
+ if (addDirectoryRedirects && fromUrl.endsWith('/index.html')) {
110
+ accum.push(`${fromUrl.substr(0, fromUrl.length - 10)} ${toUrl} 301${forceFlag}`)
111
+ }
112
+ }
113
+ return accum
114
+ }, [])
115
+ return [new File({ contents: Buffer.from(rules.join('\n') + '\n'), out: { path: '_redirects' } })]
116
+ }
117
+
118
+ function createNginxRewriteConf (files, urlPath) {
119
+ const rules = files.map((file) => {
120
+ if (isRedirectLoop(file)) return ''
121
+ delete file.out
122
+ let fromUrl = file.pub.url
123
+ fromUrl = ~fromUrl.indexOf('%20') ? `'${urlPath}${fromUrl.replace(ENCODED_SPACE_RX, ' ')}'` : urlPath + fromUrl
124
+ let toUrl = file.rel.pub.url
125
+ toUrl = ~toUrl.indexOf('%20') ? `'${urlPath}${toUrl.replace(ENCODED_SPACE_RX, ' ')}'` : urlPath + toUrl
126
+ if (file.pub.splat) {
127
+ const toUrlWithTrailingSlash = ensureTrailingSlash(toUrl)
128
+ return `location ^~ ${fromUrl}/ { rewrite ^${regexpEscape(fromUrl)}/(.*)$ ${toUrlWithTrailingSlash}$1 redirect; }`
129
+ } else {
130
+ return `location = ${fromUrl} { return 301 ${toUrl}; }`
131
+ }
132
+ })
133
+ return [new File({ contents: Buffer.from(rules.join('\n').trim() + '\n'), out: { path: '.etc/nginx/rewrite.conf' } })]
134
+ }
135
+
136
+ function populateStaticRedirectFiles(files, siteUrl) {
137
+ for (const file of files) {
138
+ if (isRedirectLoop(file)) continue
139
+ const content = createStaticRedirectContents(file, siteUrl) + '\n';
140
+ file.contents = Buffer.from(content);
141
+ }
142
+ return []
143
+ }
144
+
145
+ function unpublish (files) {
146
+ files.forEach((file) => delete file.out)
147
+ return []
148
+ }
149
+
150
+ function computeRelativeUrlPath (from, to) {
151
+ if (to === from) return to.charAt(to.length - 1) === '/' ? './' : path.basename(to)
152
+ return (path.relative(path.dirname(from + '.'), to) || '.') + (to.charAt(to.length - 1) === '/' ? '/' : '')
153
+ }
154
+
155
+ function ensureTrailingSlash (str) {
156
+ return str.charAt(str.length - 1) === '/' ? str : str + '/'
157
+ }
158
+
159
+ function stripTrailingSlash (str, root = '/') {
160
+ if (str === '/') return root
161
+ const lastIdx = str.length - 1
162
+ return str.charAt(lastIdx) === '/' ? str.substr(0, lastIdx) : str
163
+ }
164
+
165
+ function regexpEscape (str) {
166
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // don't escape "-" since it's meaningless in a literal
167
+ }
168
+
169
+ function isRedirectLoop (file) {
170
+ if (file.pub.url === file.rel.pub.url) return true
171
+ return false
172
+ }
173
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "3.2.4",
3
+ "version": "3.2.6",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -35,6 +35,7 @@
35
35
  "./extensions/validate-attributes": "./extensions/validate-attributes.js",
36
36
  "./extensions/find-related-docs": "./extensions/find-related-docs.js",
37
37
  "./extensions/find-related-labs": "./extensions/find-related-labs.js",
38
+ "./extensions/modify-redirects": "./extensions/produce-redirects.js",
38
39
  "./extensions/algolia-indexer/index": "./extensions/algolia-indexer/index.js",
39
40
  "./extensions/aggregate-terms": "./extensions/aggregate-terms.js",
40
41
  "./macros/glossary": "./macros/glossary.js",