@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 +12 -0
- package/extensions/produce-redirects.js +173 -0
- package/package.json +2 -1
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.
|
|
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",
|