]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
server: Add 404 support
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 13 Sep 2022 09:33:42 +0000 (11:33 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 14 Sep 2022 12:25:33 +0000 (14:25 +0200)
commands/commands_test.go
commands/server.go
commands/server_test.go
config/commonConfig.go
docs/content/en/getting-started/configuration.md

index 97d81ec6ee97d82ffe09b6168cb2c5592299d6bb..d37ec1f0d6ce19e65e42d75d62f1895dc74e4092 100644 (file)
@@ -368,6 +368,11 @@ Content
 
 Single: {{ .Title }}
 
+`)
+
+       writeFile(t, filepath.Join(dir, "layouts", "404.html"), `
+404: {{ .Title }}|Not Found.
+
 `)
 
        writeFile(t, filepath.Join(dir, "layouts", "_default", "list.html"), `
index f082164cee93bbd3cf30217e620227b0933c8956..7689f03dbc4528c1eb0d718b865609c174f1516b 100644 (file)
@@ -412,12 +412,18 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
                                // See https://docs.netlify.com/routing/redirects/rewrites-proxies/
                                if !redirect.Force {
                                        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 fi.IsDir() {
                                                        // There will be overlapping directories, so we
                                                        // 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
                                                } else {
                                                        doRedirect = false
@@ -426,15 +432,28 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
                                }
 
                                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 {
                                                        requestURI = redirect.To
                                                        r = r2
                                                }
-                                       } else {
+                                               fallthrough
+                                       default:
                                                w.Header().Set("Content-Type", "")
                                                http.Redirect(w, r, redirect.To, redirect.Status)
                                                return
+
                                        }
                                }
 
index 56d3949ee83ce818c4e291148942ca3ad6747555..47cc8b9e9bf8255e99eb32bfb4a1f52d8c480b4c 100644 (file)
@@ -41,12 +41,30 @@ func TestServerPanicOnConfigError(t *testing.T) {
 linenos='table'
 `
 
-       r := runServerTest(c, 0, config)
+       r := runServerTest(c,
+               serverTestOptions{
+                       config: config,
+               },
+       )
 
        c.Assert(r.err, qt.IsNotNil)
        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) {
        c := qt.New(t)
 
@@ -81,7 +99,13 @@ baseURL="https://example.org"
                                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)
 
@@ -140,7 +164,16 @@ baseURL="https://example.org"
                        if 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)
 
                })
@@ -152,11 +185,19 @@ baseURL="https://example.org"
 type serverTestResult struct {
        err            error
        homesContent   []string
+       content404     string
        publicDirnames map[string]bool
 }
 
-func runServerTest(c *qt.C, getNumHomes int, config string, args ...string) (result serverTestResult) {
-       dir := createSimpleTestSite(c, testSiteConfig{configTOML: config})
+type serverTestOptions struct {
+       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()
        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)
 
        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)
 
        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
        })
 
-       if getNumHomes > 0 {
+       if opts.getNumHomes > 0 {
                // Esp. on slow CI machines, we need to wait a little before the web
                // server is ready.
                time.Sleep(567 * time.Millisecond)
-               result.homesContent = make([]string, getNumHomes)
-               for i := 0; i < getNumHomes; i++ {
+               result.homesContent = make([]string, opts.getNumHomes)
+               for i := 0; i < opts.getNumHomes; i++ {
                        func() {
                                resp, err := http.Get(fmt.Sprintf("http://localhost:%d/", port+i))
                                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)
 
        select {
index efaa589d18e439fe3af015d6dd278b7b0d01a022..31705841ef2b06e02771e17ad04662f47c2a7487 100644 (file)
@@ -180,10 +180,15 @@ type Headers struct {
 }
 
 type Redirect struct {
-       From   string
-       To     string
+       From string
+       To   string
+
+       // HTTP status code to use for the redirect.
+       // A status code of 200 will trigger a URL rewrite.
        Status int
-       Force  bool
+
+       // Forcode redirect, even if original request path exists.
+       Force bool
 }
 
 func (r Redirect) IsZero() bool {
@@ -200,16 +205,31 @@ func DecodeServer(cfg Provider) (*Server, error) {
        _ = mapstructure.WeakDecode(m, s)
 
        for i, redir := range s.Redirects {
-               // Get it in line with the Hugo server.
-               redir.To = strings.TrimSuffix(redir.To, "index.html")
-               if !strings.HasPrefix(redir.To, "https") && !strings.HasSuffix(redir.To, "/") {
-                       // There are some tricky infinite loop situations when dealing
-                       // 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)
+               // Get it in line with the Hugo server for OK responses.
+               // We currently treat the 404 as a special case, they are always "ugly", so keep them as is.
+               if redir.Status != 404 {
+                       redir.To = strings.TrimSuffix(redir.To, "index.html")
+                       if !strings.HasPrefix(redir.To, "https") && !strings.HasSuffix(redir.To, "/") {
+                               // There are some tricky infinite loop situations when dealing
+                               // 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
        }
 
+       if len(s.Redirects) == 0 {
+               // Set up a default redirect for 404s.
+               s.Redirects = []Redirect{
+                       {
+                               From:   "**",
+                               To:     "/404.html",
+                               Status: 404,
+                       },
+               }
+
+       }
+
        return s, nil
 }
index 91279712a5d8f13fddc6484d831efe09ee80584b..4f1efefb3d39ce9928651a2cf99f9ed15861f7d2 100644 (file)
@@ -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.
 
+## 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
 
 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).