URI: 
       server: Add 404 support - hugo - [fork] hugo port for 9front
  HTML git clone git@git.drkhsh.at/hugo.git
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
   DIR README
   DIR LICENSE
       ---
   DIR commit a5cda5ca4dc9ced8179eb6bcccb1bbdc567afe17
   DIR parent 5e2b28d6e64cfb5d45ad557e1482b63e4ec84292
  HTML Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
       Date:   Tue, 13 Sep 2022 11:33:42 +0200
       
       server: Add 404 support
       
       Diffstat:
         M commands/commands_test.go           |       5 +++++
         M commands/server.go                  |      27 +++++++++++++++++++++++----
         M commands/server_test.go             |      69 +++++++++++++++++++++++++++----
         M config/commonConfig.go              |      40 +++++++++++++++++++++++--------
         M docs/content/en/getting-started/co… |      13 +++++++++++++
       
       5 files changed, 131 insertions(+), 23 deletions(-)
       ---
   DIR diff --git a/commands/commands_test.go b/commands/commands_test.go
       @@ -370,6 +370,11 @@ Single: {{ .Title }}
        
        `)
        
       +        writeFile(t, filepath.Join(dir, "layouts", "404.html"), `
       +404: {{ .Title }}|Not Found.
       +
       +`)
       +
                writeFile(t, filepath.Join(dir, "layouts", "_default", "list.html"), `
        
        List: {{ .Title }}
   DIR diff --git a/commands/server.go b/commands/server.go
       @@ -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
       +
                                                }
                                        }
        
   DIR diff --git a/commands/server_test.go b/commands/server_test.go
       @@ -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 {
   DIR diff --git a/config/commonConfig.go b/config/commonConfig.go
       @@ -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
        }
   DIR diff --git a/docs/content/en/getting-started/configuration.md b/docs/content/en/getting-started/configuration.md
       @@ -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).