diff --git a/caddyhttp/httpserver/context.go b/caddyhttp/httpserver/context.go index 46a73a69..0535a012 100644 --- a/caddyhttp/httpserver/context.go +++ b/caddyhttp/httpserver/context.go @@ -29,6 +29,20 @@ type Context struct { Req *http.Request URL *url.URL Args []interface{} // defined by arguments to .Include + + // just used for adding preload links for server push + responseHeader http.Header +} + +// NewContextWithHeader creates a context with given response header. +// +// To plugin developer: +// The returned context's exported fileds remain empty, +// you should then initialize them if you want. +func NewContextWithHeader(rh http.Header) Context { + return Context{ + responseHeader: rh, + } } // Include returns the contents of filename relative to the site root. @@ -410,6 +424,15 @@ func (c Context) RandomString(minLen, maxLen int) string { return string(result) } +// Push adds a preload link in response header for server push +func (c Context) Push(link string) string { + if c.responseHeader == nil { + return "" + } + c.responseHeader.Add("Link", "<"+link+">; rel=preload") + return "" +} + // buffer pool for .Include context actions var includeBufs = sync.Pool{ New: func() interface{} { diff --git a/caddyhttp/httpserver/context_test.go b/caddyhttp/httpserver/context_test.go index 91d6cfd7..01ca55bc 100644 --- a/caddyhttp/httpserver/context_test.go +++ b/caddyhttp/httpserver/context_test.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "net" "net/http" + "net/http/httptest" "net/url" "os" "path/filepath" @@ -731,8 +732,9 @@ func initTestContext() (Context, error) { if err != nil { return Context{}, err } + res := httptest.NewRecorder() - return Context{Root: http.Dir(os.TempDir()), Req: request}, nil + return Context{Root: http.Dir(os.TempDir()), responseHeader: res.Header(), Req: request}, nil } func getContextOrFail(t *testing.T) Context { @@ -874,3 +876,35 @@ func TestFiles(t *testing.T) { } } } + +func TestPush(t *testing.T) { + for name, c := range map[string]struct { + input string + expectLinks []string + }{ + "oneLink": { + input: `{{.Push "/test.css"}}`, + expectLinks: []string{"; rel=preload"}, + }, + "multipleLinks": { + input: `{{.Push "/test1.css"}} {{.Push "/test2.css"}}`, + expectLinks: []string{"; rel=preload", "; rel=preload"}, + }, + } { + c := c + t.Run(name, func(t *testing.T) { + ctx := getContextOrFail(t) + tmpl, err := template.New("").Parse(c.input) + if err != nil { + t.Fatal(err) + } + err = tmpl.Execute(ioutil.Discard, ctx) + if err != nil { + t.Fatal(err) + } + if got := ctx.responseHeader["Link"]; !reflect.DeepEqual(got, c.expectLinks) { + t.Errorf("Result not match: expect %v, but got %v", c.expectLinks, got) + } + }) + } +} diff --git a/caddyhttp/markdown/markdown.go b/caddyhttp/markdown/markdown.go index efb94501..27e60b03 100644 --- a/caddyhttp/markdown/markdown.go +++ b/caddyhttp/markdown/markdown.go @@ -133,11 +133,10 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error } lastModTime = latest(lastModTime, fs.ModTime()) - ctx := httpserver.Context{ - Root: md.FileSys, - Req: r, - URL: r.URL, - } + ctx := httpserver.NewContextWithHeader(w.Header()) + ctx.Root = md.FileSys + ctx.Req = r + ctx.URL = r.URL html, err := cfg.Markdown(title(fpath), f, dirents, ctx) if err != nil { return http.StatusInternalServerError, err diff --git a/caddyhttp/templates/templates.go b/caddyhttp/templates/templates.go index 36388fba..0b547f4b 100644 --- a/caddyhttp/templates/templates.go +++ b/caddyhttp/templates/templates.go @@ -34,7 +34,10 @@ func (t Templates) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error for _, ext := range rule.Extensions { if reqExt == ext { // Create execution context - ctx := httpserver.Context{Root: t.FileSys, Req: r, URL: r.URL} + ctx := httpserver.NewContextWithHeader(w.Header()) + ctx.Root = t.FileSys + ctx.Req = r + ctx.URL = r.URL // New template templateName := filepath.Base(fpath)