server: Add 404 support
This commit is contained in:
parent
5e2b28d6e6
commit
a5cda5ca4d
5 changed files with 131 additions and 23 deletions
|
@ -368,6 +368,11 @@ Content
|
||||||
|
|
||||||
Single: {{ .Title }}
|
Single: {{ .Title }}
|
||||||
|
|
||||||
|
`)
|
||||||
|
|
||||||
|
writeFile(t, filepath.Join(dir, "layouts", "404.html"), `
|
||||||
|
404: {{ .Title }}|Not Found.
|
||||||
|
|
||||||
`)
|
`)
|
||||||
|
|
||||||
writeFile(t, filepath.Join(dir, "layouts", "_default", "list.html"), `
|
writeFile(t, filepath.Join(dir, "layouts", "_default", "list.html"), `
|
||||||
|
|
|
@ -412,12 +412,18 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||||
// See https://docs.netlify.com/routing/redirects/rewrites-proxies/
|
// See https://docs.netlify.com/routing/redirects/rewrites-proxies/
|
||||||
if !redirect.Force {
|
if !redirect.Force {
|
||||||
path := filepath.Clean(strings.TrimPrefix(requestURI, u.Path))
|
path := filepath.Clean(strings.TrimPrefix(requestURI, u.Path))
|
||||||
fi, err := f.c.hugo().BaseFs.PublishFs.Stat(path)
|
if root != "" {
|
||||||
|
path = filepath.Join(root, path)
|
||||||
|
}
|
||||||
|
fs := f.c.publishDirServerFs
|
||||||
|
|
||||||
|
fi, err := fs.Stat(path)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if fi.IsDir() {
|
if fi.IsDir() {
|
||||||
// There will be overlapping directories, so we
|
// There will be overlapping directories, so we
|
||||||
// need to check for a file.
|
// need to check for a file.
|
||||||
_, err = f.c.hugo().BaseFs.PublishFs.Stat(filepath.Join(path, "index.html"))
|
_, err = fs.Stat(filepath.Join(path, "index.html"))
|
||||||
doRedirect = err != nil
|
doRedirect = err != nil
|
||||||
} else {
|
} else {
|
||||||
doRedirect = false
|
doRedirect = false
|
||||||
|
@ -426,15 +432,28 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
|
||||||
}
|
}
|
||||||
|
|
||||||
if doRedirect {
|
if doRedirect {
|
||||||
if redirect.Status == 200 {
|
switch redirect.Status {
|
||||||
|
case 404:
|
||||||
|
w.WriteHeader(404)
|
||||||
|
file, err := fs.Open(filepath.FromSlash(strings.TrimPrefix(redirect.To, u.Path)))
|
||||||
|
if err == nil {
|
||||||
|
defer file.Close()
|
||||||
|
io.Copy(w, file)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(w, "<h1>Page Not Found</h1>")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case 200:
|
||||||
if r2 := f.rewriteRequest(r, strings.TrimPrefix(redirect.To, u.Path)); r2 != nil {
|
if r2 := f.rewriteRequest(r, strings.TrimPrefix(redirect.To, u.Path)); r2 != nil {
|
||||||
requestURI = redirect.To
|
requestURI = redirect.To
|
||||||
r = r2
|
r = r2
|
||||||
}
|
}
|
||||||
} else {
|
fallthrough
|
||||||
|
default:
|
||||||
w.Header().Set("Content-Type", "")
|
w.Header().Set("Content-Type", "")
|
||||||
http.Redirect(w, r, redirect.To, redirect.Status)
|
http.Redirect(w, r, redirect.To, redirect.Status)
|
||||||
return
|
return
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,12 +41,30 @@ func TestServerPanicOnConfigError(t *testing.T) {
|
||||||
linenos='table'
|
linenos='table'
|
||||||
`
|
`
|
||||||
|
|
||||||
r := runServerTest(c, 0, config)
|
r := runServerTest(c,
|
||||||
|
serverTestOptions{
|
||||||
|
config: config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
c.Assert(r.err, qt.IsNotNil)
|
c.Assert(r.err, qt.IsNotNil)
|
||||||
c.Assert(r.err.Error(), qt.Contains, "cannot parse 'Highlight.LineNos' as bool:")
|
c.Assert(r.err.Error(), qt.Contains, "cannot parse 'Highlight.LineNos' as bool:")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServer404(t *testing.T) {
|
||||||
|
c := qt.New(t)
|
||||||
|
|
||||||
|
r := runServerTest(c,
|
||||||
|
serverTestOptions{
|
||||||
|
test404: true,
|
||||||
|
getNumHomes: 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
c.Assert(r.err, qt.IsNil)
|
||||||
|
c.Assert(r.content404, qt.Contains, "404: 404 Page not found|Not Found.")
|
||||||
|
}
|
||||||
|
|
||||||
func TestServerFlags(t *testing.T) {
|
func TestServerFlags(t *testing.T) {
|
||||||
c := qt.New(t)
|
c := qt.New(t)
|
||||||
|
|
||||||
|
@ -81,7 +99,13 @@ baseURL="https://example.org"
|
||||||
args = strings.Split(test.flag, "=")
|
args = strings.Split(test.flag, "=")
|
||||||
}
|
}
|
||||||
|
|
||||||
r := runServerTest(c, 1, config, args...)
|
opts := serverTestOptions{
|
||||||
|
config: config,
|
||||||
|
args: args,
|
||||||
|
getNumHomes: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := runServerTest(c, opts)
|
||||||
|
|
||||||
test.assert(c, r)
|
test.assert(c, r)
|
||||||
|
|
||||||
|
@ -140,7 +164,16 @@ baseURL="https://example.org"
|
||||||
if test.flag != "" {
|
if test.flag != "" {
|
||||||
args = strings.Split(test.flag, "=")
|
args = strings.Split(test.flag, "=")
|
||||||
}
|
}
|
||||||
r := runServerTest(c, test.numservers, test.config, args...)
|
|
||||||
|
opts := serverTestOptions{
|
||||||
|
config: test.config,
|
||||||
|
getNumHomes: test.numservers,
|
||||||
|
test404: true,
|
||||||
|
args: args,
|
||||||
|
}
|
||||||
|
|
||||||
|
r := runServerTest(c, opts)
|
||||||
|
c.Assert(r.content404, qt.Contains, "404: 404 Page not found|Not Found.")
|
||||||
test.assert(c, r)
|
test.assert(c, r)
|
||||||
|
|
||||||
})
|
})
|
||||||
|
@ -152,11 +185,19 @@ baseURL="https://example.org"
|
||||||
type serverTestResult struct {
|
type serverTestResult struct {
|
||||||
err error
|
err error
|
||||||
homesContent []string
|
homesContent []string
|
||||||
|
content404 string
|
||||||
publicDirnames map[string]bool
|
publicDirnames map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func runServerTest(c *qt.C, getNumHomes int, config string, args ...string) (result serverTestResult) {
|
type serverTestOptions struct {
|
||||||
dir := createSimpleTestSite(c, testSiteConfig{configTOML: config})
|
getNumHomes int
|
||||||
|
test404 bool
|
||||||
|
config string
|
||||||
|
args []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func runServerTest(c *qt.C, opts serverTestOptions) (result serverTestResult) {
|
||||||
|
dir := createSimpleTestSite(c, testSiteConfig{configTOML: opts.config})
|
||||||
|
|
||||||
sp, err := helpers.FindAvailablePort()
|
sp, err := helpers.FindAvailablePort()
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
|
@ -172,7 +213,7 @@ func runServerTest(c *qt.C, getNumHomes int, config string, args ...string) (res
|
||||||
scmd := b.newServerCmdSignaled(stop)
|
scmd := b.newServerCmdSignaled(stop)
|
||||||
|
|
||||||
cmd := scmd.getCommand()
|
cmd := scmd.getCommand()
|
||||||
args = append([]string{"-s=" + dir, fmt.Sprintf("-p=%d", port)}, args...)
|
args := append([]string{"-s=" + dir, fmt.Sprintf("-p=%d", port)}, opts.args...)
|
||||||
cmd.SetArgs(args)
|
cmd.SetArgs(args)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
@ -184,12 +225,12 @@ func runServerTest(c *qt.C, getNumHomes int, config string, args ...string) (res
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
|
|
||||||
if getNumHomes > 0 {
|
if opts.getNumHomes > 0 {
|
||||||
// Esp. on slow CI machines, we need to wait a little before the web
|
// Esp. on slow CI machines, we need to wait a little before the web
|
||||||
// server is ready.
|
// server is ready.
|
||||||
time.Sleep(567 * time.Millisecond)
|
time.Sleep(567 * time.Millisecond)
|
||||||
result.homesContent = make([]string, getNumHomes)
|
result.homesContent = make([]string, opts.getNumHomes)
|
||||||
for i := 0; i < getNumHomes; i++ {
|
for i := 0; i < opts.getNumHomes; i++ {
|
||||||
func() {
|
func() {
|
||||||
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/", port+i))
|
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/", port+i))
|
||||||
c.Check(err, qt.IsNil)
|
c.Check(err, qt.IsNil)
|
||||||
|
@ -202,6 +243,16 @@ func runServerTest(c *qt.C, getNumHomes int, config string, args ...string) (res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.test404 {
|
||||||
|
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/this-page-does-not-exist", port))
|
||||||
|
c.Check(err, qt.IsNil)
|
||||||
|
c.Check(resp.StatusCode, qt.Equals, http.StatusNotFound)
|
||||||
|
if err == nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
result.content404 = helpers.ReaderToString(resp.Body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|
|
@ -180,10 +180,15 @@ type Headers struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Redirect struct {
|
type Redirect struct {
|
||||||
From string
|
From string
|
||||||
To string
|
To string
|
||||||
|
|
||||||
|
// HTTP status code to use for the redirect.
|
||||||
|
// A status code of 200 will trigger a URL rewrite.
|
||||||
Status int
|
Status int
|
||||||
Force bool
|
|
||||||
|
// Forcode redirect, even if original request path exists.
|
||||||
|
Force bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r Redirect) IsZero() bool {
|
func (r Redirect) IsZero() bool {
|
||||||
|
@ -200,16 +205,31 @@ func DecodeServer(cfg Provider) (*Server, error) {
|
||||||
_ = mapstructure.WeakDecode(m, s)
|
_ = mapstructure.WeakDecode(m, s)
|
||||||
|
|
||||||
for i, redir := range s.Redirects {
|
for i, redir := range s.Redirects {
|
||||||
// Get it in line with the Hugo server.
|
// Get it in line with the Hugo server for OK responses.
|
||||||
redir.To = strings.TrimSuffix(redir.To, "index.html")
|
// We currently treat the 404 as a special case, they are always "ugly", so keep them as is.
|
||||||
if !strings.HasPrefix(redir.To, "https") && !strings.HasSuffix(redir.To, "/") {
|
if redir.Status != 404 {
|
||||||
// There are some tricky infinite loop situations when dealing
|
redir.To = strings.TrimSuffix(redir.To, "index.html")
|
||||||
// when the target does not have a trailing slash.
|
if !strings.HasPrefix(redir.To, "https") && !strings.HasSuffix(redir.To, "/") {
|
||||||
// This can certainly be handled better, but not time for that now.
|
// There are some tricky infinite loop situations when dealing
|
||||||
return nil, fmt.Errorf("unsupported redirect to value %q in server config; currently this must be either a remote destination or a local folder, e.g. \"/blog/\" or \"/blog/index.html\"", redir.To)
|
// when the target does not have a trailing slash.
|
||||||
|
// This can certainly be handled better, but not time for that now.
|
||||||
|
return nil, fmt.Errorf("unsupported redirect to value %q in server config; currently this must be either a remote destination or a local folder, e.g. \"/blog/\" or \"/blog/index.html\"", redir.To)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
s.Redirects[i] = redir
|
s.Redirects[i] = redir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(s.Redirects) == 0 {
|
||||||
|
// Set up a default redirect for 404s.
|
||||||
|
s.Redirects = []Redirect{
|
||||||
|
{
|
||||||
|
From: "**",
|
||||||
|
To: "/404.html",
|
||||||
|
Status: 404,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -550,6 +550,19 @@ force = false
|
||||||
|
|
||||||
{{< new-in "0.76.0" >}} Setting `force=true` will make a redirect even if there is existing content in the path. Note that before Hugo 0.76 `force` was the default behaviour, but this is inline with how Netlify does it.
|
{{< new-in "0.76.0" >}} Setting `force=true` will make a redirect even if there is existing content in the path. Note that before Hugo 0.76 `force` was the default behaviour, but this is inline with how Netlify does it.
|
||||||
|
|
||||||
|
## 404 Server Error Page
|
||||||
|
|
||||||
|
{{< new-in "0.103.0" >}}
|
||||||
|
|
||||||
|
Hugo will, by default, render all 404 errors when running `hugo server` with the `404.html` template. Note that if you have already added one or more redirects to your [Server Config](#server-config), you need to add the 404 redirect explicitly, e.g:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[redirects]]
|
||||||
|
from = "/**"
|
||||||
|
to = "/404.html"
|
||||||
|
status = 404
|
||||||
|
```
|
||||||
|
|
||||||
## Configure Title Case
|
## Configure Title Case
|
||||||
|
|
||||||
Set `titleCaseStyle` to specify the title style used by the [title](/functions/title/) template function and the automatic section titles in Hugo. It defaults to [AP Stylebook](https://www.apstylebook.com/) for title casing, but you can also set it to `Chicago` or `Go` (every word starts with a capital letter).
|
Set `titleCaseStyle` to specify the title style used by the [title](/functions/title/) template function and the automatic section titles in Hugo. It defaults to [AP Stylebook](https://www.apstylebook.com/) for title casing, but you can also set it to `Chicago` or `Go` (every word starts with a capital letter).
|
||||||
|
|
Loading…
Add table
Reference in a new issue