@flyewi/eleventy-plugin-redirects 0.1.2 → 0.3.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.
package/README.md CHANGED
@@ -37,9 +37,11 @@ By default this writes an `_redirects` file into your output directory after eve
37
37
  | `defaultStatus` | `301` | Status code used when an entry doesn't specify one. |
38
38
  | `netlify` | `true` | Write a Netlify `_redirects` file. |
39
39
  | `netlifyFile` | `"_redirects"` | Filename for the Netlify output, relative to the output directory. |
40
- | `apache` | `false` | Write an Apache `.htaccess` with `Redirect` directives. |
40
+ | `apache` | `false` | Write an Apache `.htaccess` with exact-match `RedirectMatch` directives. |
41
41
  | `apacheFile` | `".htaccess"` | Filename for the Apache output. |
42
42
  | `html` | `false` | Also write a static HTML fallback page at each `from` URL (meta-refresh + `rel=canonical`), for hosts (e.g. GitHub Pages) that can't act on `_redirects`/`.htaccess`. |
43
+ | `htmlLang` | `"en"` | `lang` attribute of the HTML fallback pages. |
44
+ | `htmlMessage` | `"This page has moved to"` | Text shown (followed by the link) on the HTML fallback pages, for the moment before the redirect or when it doesn't fire. |
43
45
  | `log` | `true` | Log a one-line summary after each build. |
44
46
 
45
47
  ## Netlify output
@@ -54,10 +56,16 @@ If your output directory already contains an `_redirects` file (e.g. copied thro
54
56
  ## Apache output
55
57
 
56
58
  ```
57
- Redirect 301 /old-path/ /new-path/
58
- Redirect 302 /legacy-page/ /new-page/
59
+ RedirectMatch 301 ^/old-path/$ /new-path/
60
+ RedirectMatch 302 ^/legacy-page/$ /new-page/
59
61
  ```
60
62
 
63
+ Each rule matches its path **exactly**, the same as in Netlify's `_redirects`: `/old-path/` redirects, `/old-path/sub/` does not. Regex characters in the path (e.g. the `.` in `/old.html`) are escaped, and paths containing spaces are quoted. The query string is passed on to the target unchanged.
64
+
65
+ > **Changed in 0.3.0:** earlier versions wrote `Redirect` directives, which Apache treats as a *prefix* match (`/old-path/` also redirected `/old-path/anything` to `/new-path/anything`). If you relied on that, add the deeper paths as separate entries.
66
+
67
+ Requires `mod_alias`, which is enabled on practically every Apache host, and `.htaccess` overrides (`AllowOverride FileInfo`) — on shared hosting this is usually the case.
68
+
61
69
  ## HTML fallback pages
62
70
 
63
71
  With `html: true`, each `from` path also gets a real HTML file at that URL (e.g. `/old-path/` → `old-path/index.html` in the output directory) containing:
@@ -65,8 +73,11 @@ With `html: true`, each `from` path also gets a real HTML file at that URL (e.g.
65
73
  ```html
66
74
  <meta http-equiv="refresh" content="0; url=/new-path/">
67
75
  <link rel="canonical" href="/new-path/">
76
+ <script>location.replace("/new-path/" + location.hash);</script>
68
77
  ```
69
78
 
79
+ The script redirects immediately and keeps the URL fragment (`/old-path/#section` → `/new-path/#section`), like a server-side redirect would; the meta refresh is the fallback when JavaScript is off. If the target already contains a `#fragment`, that one is kept instead.
80
+
70
81
  This is a static fallback for platforms without server-side redirect support — a real `_redirects`/`.htaccess` (or your host/CDN's redirect rules) is always the better option when available, since the HTML approach is a moment slower and depends on the browser executing the refresh.
71
82
 
72
83
  ## License
package/lib/formats.js CHANGED
@@ -4,8 +4,28 @@ function toNetlify(list) {
4
4
  return list.map(({ from, to, status }) => `${from} ${to} ${status}`).join("\n") + "\n";
5
5
  }
6
6
 
7
+ function escapeRegex(str) {
8
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9
+ }
10
+
11
+ // Apache splits directive arguments on whitespace; quote those that contain it.
12
+ function apacheArg(str) {
13
+ return /[\s"]/.test(str) ? `"${str.replace(/"/g, '\\"')}"` : str;
14
+ }
15
+
16
+ // RedirectMatch with an anchored, escaped pattern matches the path exactly,
17
+ // like Netlify's _redirects. Plain `Redirect` would be a prefix match
18
+ // (/old/ would also catch /old/anything), so the same config would behave
19
+ // differently depending on the host.
7
20
  function toApache(list) {
8
- return list.map(({ from, to, status }) => `Redirect ${status} ${from} ${to}`).join("\n") + "\n";
21
+ return (
22
+ list
23
+ .map(({ from, to, status }) => {
24
+ const pattern = apacheArg(`^${escapeRegex(from)}$`);
25
+ return `RedirectMatch ${status} ${pattern} ${apacheArg(to)}`;
26
+ })
27
+ .join("\n") + "\n"
28
+ );
9
29
  }
10
30
 
11
31
  function escapeAttr(str) {
@@ -16,21 +36,32 @@ function escapeAttr(str) {
16
36
  .replace(/>/g, "&gt;");
17
37
  }
18
38
 
39
+ // JSON.stringify yields a valid JS string literal; escaping "<" additionally
40
+ // keeps a target containing "</script>" from closing the inline script early.
41
+ function toScriptString(str) {
42
+ return JSON.stringify(String(str)).replace(/</g, "\\u003c");
43
+ }
44
+
19
45
  // A static fallback for hosts that can't act on _redirects/.htaccess (e.g.
20
46
  // GitHub Pages): an actual HTML file at the old URL that both redirects
21
47
  // browsers instantly and points crawlers at the new canonical URL.
22
- function toHtmlPage(to) {
48
+ // The script runs before the meta refresh fires and carries the URL fragment
49
+ // (#section) over, as a server-side 301 would; the meta refresh remains as the
50
+ // no-JS fallback. A target that already has its own fragment keeps it.
51
+ function toHtmlPage(to, { lang = "en", message = "This page has moved to" } = {}) {
23
52
  const target = escapeAttr(to);
53
+ const hash = String(to).includes("#") ? "" : " + location.hash";
24
54
  return `<!doctype html>
25
- <html lang="en">
55
+ <html lang="${escapeAttr(lang)}">
26
56
  <head>
27
57
  <meta charset="utf-8">
28
- <title>Redirecting…</title>
58
+ <title>${escapeAttr(message)} ${target}</title>
29
59
  <meta http-equiv="refresh" content="0; url=${target}">
30
60
  <link rel="canonical" href="${target}">
61
+ <script>location.replace(${toScriptString(to)}${hash});</script>
31
62
  </head>
32
63
  <body>
33
- <p>This page has moved to <a href="${target}">${target}</a>.</p>
64
+ <p>${escapeAttr(message)} <a href="${target}">${target}</a>.</p>
34
65
  </body>
35
66
  </html>
36
67
  `;
package/lib/redirects.js CHANGED
@@ -10,6 +10,8 @@ const DEFAULTS = {
10
10
  netlify: true,
11
11
  apache: false,
12
12
  html: false,
13
+ htmlLang: "en",
14
+ htmlMessage: "This page has moved to",
13
15
  netlifyFile: "_redirects",
14
16
  apacheFile: ".htaccess",
15
17
  log: true,
@@ -50,7 +52,7 @@ module.exports = function redirectsPlugin(eleventyConfig, userOptions = {}) {
50
52
  // Preserve (and prepend ahead of) any existing .htaccess content, e.g.
51
53
  // hand-maintained directives (ErrorDocument, mod_deflate, mod_expires)
52
54
  // brought in via a static passthrough copy — this plugin only owns the
53
- // Redirect lines, not the whole file.
55
+ // RedirectMatch lines, not the whole file.
54
56
  const existing = readExisting(apachePath);
55
57
  fs.writeFileSync(apachePath, toApache(list) + existing);
56
58
  }
@@ -59,7 +61,10 @@ module.exports = function redirectsPlugin(eleventyConfig, userOptions = {}) {
59
61
  for (const { from, to } of list) {
60
62
  const outPath = path.join(outputDir, toOutputPath(from));
61
63
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
62
- fs.writeFileSync(outPath, toHtmlPage(to));
64
+ fs.writeFileSync(
65
+ outPath,
66
+ toHtmlPage(to, { lang: options.htmlLang, message: options.htmlMessage })
67
+ );
63
68
  }
64
69
  }
65
70
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flyewi/eleventy-plugin-redirects",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "Eleventy plugin that generates Netlify _redirects, Apache .htaccess, and static HTML fallback pages from a single redirects config.",
5
5
  "main": "lib/redirects.js",
6
6
  "type": "commonjs",