@redpanda-data/docs-extensions-and-macros 3.2.5 → 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 +2 -11
- package/extensions/produce-redirects.js +173 -0
- package/package.json +2 -2
- package/extensions/modify-redirects.js +0 -36
package/README.adoc
CHANGED
|
@@ -180,18 +180,9 @@ antora:
|
|
|
180
180
|
- require: '@redpanda-data/docs-extensions-and-macros/extensions/add-global-attributes'
|
|
181
181
|
```
|
|
182
182
|
|
|
183
|
-
===
|
|
183
|
+
=== Produce redirects (customization of core Antora)
|
|
184
184
|
|
|
185
|
-
This extension
|
|
186
|
-
|
|
187
|
-
The issue is https://antora.zulipchat.com/#narrow/stream/282400-users/topic/Redirect.20Loop.20Issue.20with.20Page.20Renaming.20and.20Indexify/near/433691700[recognized as a bug] within Antora's redirect producer, which does not currently
|
|
188
|
-
check if the source and target URLs are the same before creating a redirect.
|
|
189
|
-
|
|
190
|
-
The purpose of this script is to scan the `_redirects` file and remove any entries that point
|
|
191
|
-
a URL to itself, which not only prevents redirect loops but also optimizes the redirect process
|
|
192
|
-
by eliminating unnecessary entries. This cleanup helps ensure that the redirects file only contains valid and useful redirection rules.
|
|
193
|
-
|
|
194
|
-
By integrating this script into the Antora pipeline, we ensure that each build's output is optimized and free from potential issues related to improper redirects, enhancing both site performance and user experience.
|
|
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.
|
|
195
186
|
|
|
196
187
|
==== Registration example
|
|
197
188
|
|
|
@@ -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,7 +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/
|
|
38
|
+
"./extensions/modify-redirects": "./extensions/produce-redirects.js",
|
|
39
39
|
"./extensions/algolia-indexer/index": "./extensions/algolia-indexer/index.js",
|
|
40
40
|
"./extensions/aggregate-terms": "./extensions/aggregate-terms.js",
|
|
41
41
|
"./macros/glossary": "./macros/glossary.js",
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
|
|
6
|
-
function redirectModifier(files, outputDir, logger) {
|
|
7
|
-
files.forEach((file) => {
|
|
8
|
-
const filePath = path.join(outputDir, file);
|
|
9
|
-
if (!fs.existsSync(filePath)) return
|
|
10
|
-
let content = fs.readFileSync(filePath, 'utf8');
|
|
11
|
-
const lines = content.split('\n');
|
|
12
|
-
|
|
13
|
-
// Filter out redirects that point to themselves
|
|
14
|
-
const modifiedLines = lines.filter((line) => {
|
|
15
|
-
const parts = line.split(' ');
|
|
16
|
-
if (parts[0] == parts[1]) logger.info(`Removed redirect that points to itself: ${line}`)
|
|
17
|
-
return parts[0] !== parts[1]; // Ensure the source and target are not the same
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
// Join the array back into a string and write it back to the file
|
|
21
|
-
const modifiedContent = modifiedLines.join('\n');
|
|
22
|
-
fs.writeFileSync(filePath, modifiedContent, 'utf8');
|
|
23
|
-
logger.info(`Processed and updated redirects in ${filePath}`);
|
|
24
|
-
})
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
module.exports.register = function ({ config }) {
|
|
28
|
-
const logger = this.getLogger('redirects-produced');
|
|
29
|
-
this.on('sitePublished', async ({ publications }) => {
|
|
30
|
-
publications.forEach(publication => {
|
|
31
|
-
const outputDir = publication.resolvedPath;
|
|
32
|
-
const redirectFile = ['_redirects'];
|
|
33
|
-
redirectModifier(redirectFile, outputDir, logger);
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
};
|