Commit 53e2cca2 authored by Sebastiaan Deckers's avatar Sebastiaan Deckers 馃悜

feat: custom HTTP redirects

parent 79ac3096
......@@ -43,7 +43,6 @@ The path to the base directory containing static files to serve.
```js
module.exports = {
hosts: [{
domain: 'example.net',
root: './website'
}]
}
......@@ -66,15 +65,22 @@ Fallbacks are supported for:
- `200` to serve a missing file as a success response.
- `404` to serve a missing file as an error response.
Example:
Example for client-side routing (e.g. SPA or PWA):
```js
module.exports = {
hosts: [{
domain: 'example.net',
fallback: {
200: './index.html'
}
fallback: { 200: './app.html' }
}]
}
```
Example for "Not Found" error pages:
```js
module.exports = {
hosts: [{
fallback: { 404: './page-not-found.html' }
}]
}
```
......@@ -110,7 +116,6 @@ Sets custom HTTP response headers. The `headers` array contains header objects.
```js
module.exports = {
hosts: [{
domain: 'example.net',
headers: [
{
fields: {
......@@ -145,7 +150,7 @@ module.exports = {
}
```
## `hosts[].headers[].fields`
### `hosts[].headers[].fields`
`fields` is a required object whose property names and values are the HTTP headers to set on the response.
......@@ -160,9 +165,9 @@ Values may contain named template segments in braces. The values are substituted
Values may also be arrays of strings to set multiple headers with the same name.
## `hosts[].headers[].uri`
### `hosts[].headers[].uri`
`uri` is an optional string as a [URI Template](). The header fields are applied only if the request pathname matches the route.
`uri` is an optional string as a [URI Template](https://tools.ietf.org/html/rfc6570). The header fields are applied only if the request pathname matches the route.
If no `uri` property is set, the `fields` headers are applied to all responses.
......@@ -180,7 +185,6 @@ Example: Inline manifest
```js
module.exports = {
hosts: [{
domain: 'example.net',
manifest: [
// Example:
{
......@@ -211,3 +215,54 @@ module.exports = {
}]
}
```
## `hosts[].redirects[]`
Create redirects *from* gone pages *to* new pages.
Custom redirects help preserve inbound links and maintain search engine rankings & optimisation (SEO). As a website's content changes, pages are often moved around. Redirects are used to avoid leaving broken links from external sites to your old pages.
Tip: Use a fallback page (See: `hosts[].fallback`) to capture inbound links to no-longer-existing pages. With proper analytics reporting you can build a list of the most frequently visited broken links, and redirect them somewhere more useful.
Examples:
```js
module.exports = {
hosts: [{
redirects: [
// Old page to new page
{ from: '/old/page', to: '/new/page' },
// Old page to a different website
{ from: '/somewhere', to: 'https://new.example.net/other/site' },
// Override the HTTP status code to use a temporary redirect
{ from: '/promotion', to: '/summer-special', status: 307 },
// Use URI Template pattern matching to preserve variables
{ from: '/shop{?sort,product}',
to: '/store/item{/product}{?sort}' }
]
}]
}
```
### `hosts[].redirects[].from`
`from` is a required string as a [URI Template](https://tools.ietf.org/html/rfc6570).
Only paths on the current site can be matched.
### `hosts[].redirects[].to`
`to` is a required string as a [URI Template](https://tools.ietf.org/html/rfc6570).
This path may be relative to the current origin (a.k.a. domain name) or an absolute URL pointing to a different origin.
### `hosts[].redirects[].status`
`status` is an optional integer from 300 through 399.
The default status code is 308 which means Permanent Redirect. Use 307 to generate a Temporary Redirect.
See the IANA list of assigned HTTP status codes: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
......@@ -4,6 +4,7 @@ const parseUrl = require('parseurl')
const { getHost } = require('../helpers/getHost')
const { NotFound } = require('http-errors')
const accepts = require('accepts')
const UriTemplate = require('uri-templates')
const PERMANENT_REDIRECT = 308
......@@ -38,6 +39,19 @@ module.exports.resolveRequest = () => {
return
}
for (const { from, to, status } of request.options.redirects) {
const predicate = new UriTemplate(from)
const parts = predicate.fromUri(pathname)
if (parts !== undefined) {
const destination = new UriTemplate(to).fill(parts)
const base = `https://${getHost(request)}${request.url}`
const location = new URL(destination, base)
response.writeHead(status, { location })
response.end()
return
}
}
const fallback = request.options.fallback['200']
if (fileIndex.absolute.has(fallback)) {
if (
......
......@@ -22,6 +22,15 @@ class Response {
async arrayBuffer () {
return this.body
}
get status () {
return this.headers.get(':status')
}
get ok () {
const { status } = this
return status >= 200 && status <= 400
}
}
function receiveHttp2 (url, options = {}) {
......
const test = require('blue-tape')
const { Master } = require('..')
const { join } = require('path')
const { h2 } = require('./helpers/receive')
let master
test('start server', async (t) => {
const cwd = join(__dirname, 'fixtures')
const options = {
hosts: [{
redirects: [
{ from: '/relative', to: '/expected' },
{ from: '/relative-dot', to: './expected' },
{ from: '/remote-absolute', to: 'https://example.net/expected' },
{ from: '/default-status', to: '/expected' },
{ from: '/custom-status', to: '/expected', status: 302 },
{ from: '/substitution{/pattern}', to: '/expected{/pattern}' }
]
}]
}
master = new Master({ cwd, options })
await master.listen()
})
test('redirect relative', async (t) => {
const url = 'https://localhost:8443/relative'
const response = await h2(url)
const actual = response.headers.get('location')
const expected = 'https://localhost:8443/expected'
t.is(actual, expected)
})
test('redirect relative', async (t) => {
const url = 'https://localhost:8443/relative-dot'
const response = await h2(url)
const actual = response.headers.get('location')
const expected = 'https://localhost:8443/expected'
t.is(actual, expected)
})
test('redirect remote absolute', async (t) => {
const url = 'https://localhost:8443/remote-absolute'
const response = await h2(url)
const actual = response.headers.get('location')
const expected = 'https://example.net/expected'
t.is(actual, expected)
})
test('redirect default status', async (t) => {
const url = 'https://localhost:8443/default-status'
const response = await h2(url)
t.is(response.status, 308)
})
test('redirect custom status', async (t) => {
const url = 'https://localhost:8443/custom-status'
const response = await h2(url)
t.is(response.status, 302)
})
test('redirect substitution pattern', async (t) => {
const url = 'https://localhost:8443/substitution/given'
const response = await h2(url)
const actual = response.headers.get('location')
const expected = 'https://localhost:8443/expected/given'
t.is(actual, expected)
})
test('stop server', async (t) => master.close())
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment