From 9fda3cc87ea5f95b5ac547a0e8169c37445864d6 Mon Sep 17 00:00:00 2001 From: Bret Comnes Date: Sat, 18 Apr 2026 11:48:08 -0700 Subject: [PATCH 1/4] Document redirect pages template pattern Adds a Redirect Pages section to the Templates docs showing how to use the object array template type to generate meta-refresh HTML pages for URL migrations. Includes an XSS-safe escapeXml helper, a note on SEO implications of meta-refresh, and an example of generating a Netlify _redirects file instead. Closes #237 --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/README.md b/README.md index 008f8fb..e1d7b08 100644 --- a/README.md +++ b/README.md @@ -1284,6 +1284,69 @@ const layout: LayoutFunction<{site: string}, VDOMNode, string> = ({ children }) } ``` +### Redirect Pages + +Sites migrating from another platform often need redirect pages for old URLs that no longer exist. DomStack has no built-in redirect mechanism, but the object array template type makes it straightforward to generate as many HTML meta-refresh redirect pages as you need from a single template file. + +```js +// src/redirects.template.js +// Generates one index.html per redirect entry using the meta-refresh pattern. + +const redirects = [ + { from: '2020/old-slug', to: '/2020/new-slug/' }, + { from: '2021/another-old', to: '/2021/another-new/' }, +] + +export default function redirectsTemplate () { + return redirects.map(({ from, to }) => ({ + outputName: `${from}/index.html`, + content: ` + + + + + + Redirecting... + + +

Redirecting to ${escapeXml(to)}

+ +`, + })) +} + +function escapeXml (str) { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} +``` + +The `outputName` field controls the output path. Using `${from}/index.html` creates a directory-style URL at the old path. The `escapeXml` helper prevents XSS if any redirect target contains special characters. + +**SEO note:** Meta-refresh is a client-side redirect. Search engines may not treat it as a permanent 301 redirect. For static hosting platforms that support server-side redirects, you can instead generate a `_redirects` file (Netlify, Cloudflare Pages) or `vercel.json` (Vercel) using the same template approach with a single string return: + +```js +// src/redirects-netlify.txt.template.js +// Generates a _redirects file for Netlify / Cloudflare Pages. + +const redirects = [ + { from: '/2020/old-slug/', to: '/2020/new-slug/' }, +] + +export default function () { + return { + outputName: '_redirects', + content: redirects.map(({ from, to }) => `${from} ${to} 301`).join('\n'), + } +} +``` + +Both approaches can coexist. Using `--copy` to include a hand-crafted `_redirects` file is also an option when you prefer to manage redirects outside the build. + ## Design Goals - Convention over configuration. All configuration should be optional, and at most it should be minimal. From b6cfeabd67763c71d62779d7aabe4aa0d33dad13 Mon Sep 17 00:00:00 2001 From: Bret Comnes Date: Sat, 18 Apr 2026 12:12:59 -0700 Subject: [PATCH 2/4] docs: move Redirect Pages section into Templates, fix examples Move the Redirect Pages section from under TypeScript Support to its correct location at the end of the Templates section. Fix the prose to accurately describe escapeXml scope (HTML injection only, not javascript: scheme), warn about leading / in from values resolving outside the build dir, and use correct template type terminology (single-object template type). Co-Authored-By: Claude Sonnet 4.6 --- README.md | 126 +++++++++++++++++++++++++++--------------------------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index e1d7b08..ac6916e 100644 --- a/README.md +++ b/README.md @@ -951,6 +951,69 @@ const feedsTemplate: TemplateAsyncIterator = async function * ({ export default feedsTemplate ``` +### Redirect Pages + +Sites migrating from another platform often need redirect pages for old URLs that no longer exist. DomStack has no built-in redirect mechanism, but the object array template type makes it straightforward to generate as many HTML meta-refresh redirect pages as you need from a single template file. + +```js +// src/redirects.template.js +// Generates one index.html per redirect entry using the meta-refresh pattern. + +const redirects = [ + { from: '2020/old-slug', to: '/2020/new-slug/' }, + { from: '2021/another-old', to: '/2021/another-new/' }, +] + +export default function redirectsTemplate () { + return redirects.map(({ from, to }) => ({ + outputName: `${from}/index.html`, + content: ` + + + + + + Redirecting... + + +

Redirecting to ${escapeXml(to)}

+ +`, + })) +} + +function escapeXml (str) { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} +``` + +The `outputName` field controls the output path. Using `${from}/index.html` creates a directory-style URL at the old path. The `escapeXml` helper prevents HTML injection in attribute values. Note that `escapeXml` alone does not block dangerous URL schemes like `javascript:` — keep redirect targets to known-safe URL patterns (relative paths or verified external URLs). Also avoid leading `/` in `from` values: because `outputName` is resolved with `path.resolve`, a leading `/` would resolve as an absolute filesystem path and write the file outside the build directory. + +**SEO note:** Meta-refresh is a client-side redirect. Search engines may not treat it as a permanent 301 redirect. For static hosting platforms that support server-side redirects, you can instead generate a `_redirects` file (Netlify, Cloudflare Pages) or `vercel.json` (Vercel) using the single-object template type: + +```js +// src/redirects-netlify.txt.template.js +// Generates a _redirects file for Netlify / Cloudflare Pages. + +const redirects = [ + { from: '/2020/old-slug/', to: '/2020/new-slug/' }, +] + +export default function () { + return { + outputName: '_redirects', + content: redirects.map(({ from, to }) => `${from} ${to} 301`).join('\n'), + } +} +``` + +Both approaches can coexist. Using `--copy` to include a hand-crafted `_redirects` file is also an option when you prefer to manage redirects outside the build. + ## Global Assets There are a few important (and optional) global assets that live anywhere in the `src` directory. If duplicate named files that match the global asset file name pattern are found, a build error will occur until the duplicate file error is resolved. @@ -1284,69 +1347,6 @@ const layout: LayoutFunction<{site: string}, VDOMNode, string> = ({ children }) } ``` -### Redirect Pages - -Sites migrating from another platform often need redirect pages for old URLs that no longer exist. DomStack has no built-in redirect mechanism, but the object array template type makes it straightforward to generate as many HTML meta-refresh redirect pages as you need from a single template file. - -```js -// src/redirects.template.js -// Generates one index.html per redirect entry using the meta-refresh pattern. - -const redirects = [ - { from: '2020/old-slug', to: '/2020/new-slug/' }, - { from: '2021/another-old', to: '/2021/another-new/' }, -] - -export default function redirectsTemplate () { - return redirects.map(({ from, to }) => ({ - outputName: `${from}/index.html`, - content: ` - - - - - - Redirecting... - - -

Redirecting to ${escapeXml(to)}

- -`, - })) -} - -function escapeXml (str) { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') -} -``` - -The `outputName` field controls the output path. Using `${from}/index.html` creates a directory-style URL at the old path. The `escapeXml` helper prevents XSS if any redirect target contains special characters. - -**SEO note:** Meta-refresh is a client-side redirect. Search engines may not treat it as a permanent 301 redirect. For static hosting platforms that support server-side redirects, you can instead generate a `_redirects` file (Netlify, Cloudflare Pages) or `vercel.json` (Vercel) using the same template approach with a single string return: - -```js -// src/redirects-netlify.txt.template.js -// Generates a _redirects file for Netlify / Cloudflare Pages. - -const redirects = [ - { from: '/2020/old-slug/', to: '/2020/new-slug/' }, -] - -export default function () { - return { - outputName: '_redirects', - content: redirects.map(({ from, to }) => `${from} ${to} 301`).join('\n'), - } -} -``` - -Both approaches can coexist. Using `--copy` to include a hand-crafted `_redirects` file is also an option when you prefer to manage redirects outside the build. - ## Design Goals - Convention over configuration. All configuration should be optional, and at most it should be minimal. From df0162e7697f457ac741a7a60fddddac028b1875 Mon Sep 17 00:00:00 2001 From: Bret Comnes Date: Sat, 18 Apr 2026 12:54:38 -0700 Subject: [PATCH 3/4] Fix redirect docs: wording, security warning, section placement, terminology --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ac6916e..12738eb 100644 --- a/README.md +++ b/README.md @@ -992,9 +992,9 @@ function escapeXml (str) { } ``` -The `outputName` field controls the output path. Using `${from}/index.html` creates a directory-style URL at the old path. The `escapeXml` helper prevents HTML injection in attribute values. Note that `escapeXml` alone does not block dangerous URL schemes like `javascript:` — keep redirect targets to known-safe URL patterns (relative paths or verified external URLs). Also avoid leading `/` in `from` values: because `outputName` is resolved with `path.resolve`, a leading `/` would resolve as an absolute filesystem path and write the file outside the build directory. +The `outputName` field controls the output path. Using `${from}/index.html` creates a directory-style URL at the old path. The `escapeXml` helper prevents HTML injection in attribute values. Note that `escapeXml` alone does not block dangerous URL schemes like `javascript:` — keep redirect targets to known-safe URL patterns (relative paths or verified external URLs). Be careful with `from` values used in `outputName`: a leading `/` makes `path.resolve` treat it as an absolute filesystem path, writing the file outside the build directory. Path segments containing `..` can also escape the build directory. If `from` values come from any external source, sanitize them before use — for example, strip any leading slashes and reject entries that contain `..`. -**SEO note:** Meta-refresh is a client-side redirect. Search engines may not treat it as a permanent 301 redirect. For static hosting platforms that support server-side redirects, you can instead generate a `_redirects` file (Netlify, Cloudflare Pages) or `vercel.json` (Vercel) using the single-object template type: +**SEO note:** Meta-refresh is a client-side redirect. Search engines may not treat it as a permanent 301 redirect. For static hosting platforms that support server-side redirects, you can instead generate a `_redirects` file (Netlify, Cloudflare Pages) or `vercel.json` (Vercel) using the object template type: ```js // src/redirects-netlify.txt.template.js From 3078c5c40cbab9f055f7414e6010c57981bbbbf4 Mon Sep 17 00:00:00 2001 From: Bret Comnes Date: Sat, 18 Apr 2026 19:50:10 -0700 Subject: [PATCH 4/4] Rephrase --copy sentence to say copy a directory containing _redirects Co-Authored-By: Claude Sonnet 4.6 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 12738eb..edfee06 100644 --- a/README.md +++ b/README.md @@ -1012,7 +1012,7 @@ export default function () { } ``` -Both approaches can coexist. Using `--copy` to include a hand-crafted `_redirects` file is also an option when you prefer to manage redirects outside the build. +Both approaches can coexist. Copying a directory that contains a hand-crafted `_redirects` file via `--copy` is also an option when you prefer to manage redirects outside the build. ## Global Assets