From e41ff2b897be24a894e75b850f1c06652cc034be Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Sat, 14 May 2022 17:02:30 -0600 Subject: Implement index handler This involved re-arranging how templates are being parsed, slightly. --- srv/src/api/api.go | 21 +++--- srv/src/api/middleware.go | 9 ++- srv/src/api/posts.go | 91 ------------------------- srv/src/api/render.go | 161 +++++++++++++++++++++++++++++++++++++++++++++ srv/src/api/tpl/index.html | 20 ++++++ srv/src/post/post.go | 5 +- 6 files changed, 197 insertions(+), 110 deletions(-) delete mode 100644 srv/src/api/posts.go create mode 100644 srv/src/api/render.go create mode 100644 srv/src/api/tpl/index.html diff --git a/srv/src/api/api.go b/srv/src/api/api.go index 92771a1..79979be 100644 --- a/srv/src/api/api.go +++ b/srv/src/api/api.go @@ -3,10 +3,8 @@ package api import ( "context" - "embed" "errors" "fmt" - "html/template" "net" "net/http" "net/http/httputil" @@ -22,11 +20,6 @@ import ( "github.com/mediocregopher/mediocre-go-lib/v2/mlog" ) -//go:embed tpl -var fs embed.FS - -var tpls = template.Must(template.ParseFS(fs, "tpl/*")) - // Params are used to instantiate a new API instance. All fields are required // unless otherwise noted. type Params struct { @@ -184,10 +177,9 @@ func (a *api) handler() http.Handler { ))) var apiHandler http.Handler = apiMux - apiHandler = postOnlyMiddleware(apiHandler) // TODO probably should be last? apiHandler = checkCSRFMiddleware(apiHandler) - apiHandler = logMiddleware(a.params.Logger, apiHandler) - apiHandler = annotateMiddleware(apiHandler) + apiHandler = postOnlyMiddleware(apiHandler) + apiHandler = logReqMiddleware(apiHandler) apiHandler = addResponseHeaders(map[string]string{ "Cache-Control": "no-store, max-age=0", "Pragma": "no-cache", @@ -196,7 +188,12 @@ func (a *api) handler() http.Handler { mux.Handle("/api/", http.StripPrefix("/api", apiHandler)) - mux.Handle("/v2/posts/", a.postHandler()) + // TODO need to setCSRFMiddleware on all these rendering endpoints + mux.Handle("/v2/posts/", a.renderPostHandler()) + mux.Handle("/v2/", a.renderIndexHandler()) + + var globalHandler http.Handler = mux + globalHandler = setLoggerMiddleware(a.params.Logger, globalHandler) - return mux + return globalHandler } diff --git a/srv/src/api/middleware.go b/srv/src/api/middleware.go index 0b3eec7..fcd29b3 100644 --- a/srv/src/api/middleware.go +++ b/srv/src/api/middleware.go @@ -19,7 +19,7 @@ func addResponseHeaders(headers map[string]string, h http.Handler) http.Handler }) } -func annotateMiddleware(h http.Handler) http.Handler { +func setLoggerMiddleware(logger *mlog.Logger, h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { type reqInfoKey string @@ -34,6 +34,7 @@ func annotateMiddleware(h http.Handler) http.Handler { ) r = r.WithContext(ctx) + r = apiutil.SetRequestLogger(r, logger) h.ServeHTTP(rw, r) }) } @@ -58,11 +59,9 @@ func (lrw *logResponseWriter) WriteHeader(statusCode int) { lrw.ResponseWriter.WriteHeader(statusCode) } -func logMiddleware(logger *mlog.Logger, h http.Handler) http.Handler { +func logReqMiddleware(h http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - r = apiutil.SetRequestLogger(r, logger) - lrw := newLogResponseWriter(rw) started := time.Now() @@ -77,7 +76,7 @@ func logMiddleware(logger *mlog.Logger, h http.Handler) http.Handler { logCtxKey("response_code"), lrw.statusCode, ) - logger.Info(ctx, "handled HTTP request") + apiutil.GetRequestLogger(r).Info(ctx, "handled HTTP request") }) } diff --git a/srv/src/api/posts.go b/srv/src/api/posts.go deleted file mode 100644 index cc7a176..0000000 --- a/srv/src/api/posts.go +++ /dev/null @@ -1,91 +0,0 @@ -package api - -import ( - "errors" - "fmt" - "html/template" - "net/http" - "path/filepath" - "strings" - - "github.com/gomarkdown/markdown" - "github.com/gomarkdown/markdown/html" - "github.com/gomarkdown/markdown/parser" - "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutil" - "github.com/mediocregopher/blog.mediocregopher.com/srv/post" -) - -func (a *api) postHandler() http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - - id := strings.TrimSuffix(filepath.Base(r.URL.Path), ".html") - - storedPost, err := a.params.PostStore.GetByID(id) - - if errors.Is(err, post.ErrPostNotFound) { - http.Error(rw, "Post not found", 404) - return - } else if err != nil { - apiutil.InternalServerError( - rw, r, fmt.Errorf("fetching post with id %q: %w", id, err), - ) - return - } - - parserExt := parser.CommonExtensions | parser.AutoHeadingIDs - parser := parser.NewWithExtensions(parserExt) - - htmlFlags := html.CommonFlags | html.HrefTargetBlank - htmlRenderer := html.NewRenderer(html.RendererOptions{Flags: htmlFlags}) - - renderedBody := markdown.ToHTML([]byte(storedPost.Body), parser, htmlRenderer) - - tplData := struct { - post.StoredPost - SeriesPrevious, SeriesNext *post.StoredPost - Body template.HTML - }{ - StoredPost: storedPost, - Body: template.HTML(renderedBody), - } - - if series := storedPost.Series; series != "" { - - seriesPosts, err := a.params.PostStore.GetBySeries(series) - if err != nil { - apiutil.InternalServerError( - rw, r, - fmt.Errorf("fetching posts for series %q: %w", series, err), - ) - return - } - - var foundThis bool - - for i := range seriesPosts { - - seriesPost := seriesPosts[i] - - if seriesPost.ID == storedPost.ID { - foundThis = true - continue - } - - if !foundThis { - tplData.SeriesPrevious = &seriesPost - continue - } - - tplData.SeriesNext = &seriesPost - break - } - } - - if err := tpls.ExecuteTemplate(rw, "post.html", tplData); err != nil { - apiutil.InternalServerError( - rw, r, fmt.Errorf("rendering post with id %q: %w", id, err), - ) - return - } - }) -} diff --git a/srv/src/api/render.go b/srv/src/api/render.go new file mode 100644 index 0000000..b2ca3c8 --- /dev/null +++ b/srv/src/api/render.go @@ -0,0 +1,161 @@ +package api + +import ( + "embed" + "errors" + "fmt" + "html/template" + "io/fs" + "net/http" + "path/filepath" + "strings" + + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" + "github.com/mediocregopher/blog.mediocregopher.com/srv/api/apiutil" + "github.com/mediocregopher/blog.mediocregopher.com/srv/post" +) + +//go:embed tpl +var tplFS embed.FS + +func mustParseTpl(name string) *template.Template { + + mustRead := func(fileName string) string { + path := filepath.Join("tpl", fileName) + + b, err := fs.ReadFile(tplFS, path) + if err != nil { + panic(fmt.Errorf("reading file %q from tplFS: %w", path, err)) + } + + return string(b) + } + + tpl := template.Must(template.New("").Parse(mustRead(name))) + tpl = template.Must(tpl.New("base.html").Parse(mustRead("base.html"))) + + return tpl +} + +func (a *api) renderIndexHandler() http.Handler { + + tpl := mustParseTpl("index.html") + const pageCount = 20 + + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + + if path := r.URL.Path; !strings.HasSuffix(path, "/") && filepath.Base(path) != "index.html" { + http.Error(rw, "Page not found", 404) + return + } + + page, err := apiutil.StrToInt(r.FormValue("p"), 0) + if err != nil { + apiutil.BadRequest( + rw, r, fmt.Errorf("invalid page number: %w", err), + ) + return + } + + posts, _, err := a.params.PostStore.Get(page, pageCount) + if err != nil { + apiutil.InternalServerError( + rw, r, fmt.Errorf("fetching page %d of posts: %w", page, err), + ) + return + } + + tplData := struct { + Posts []post.StoredPost + }{ + Posts: posts, + } + + if err := tpl.Execute(rw, tplData); err != nil { + apiutil.InternalServerError( + rw, r, fmt.Errorf("rendering index: %w", err), + ) + return + } + }) +} + +func (a *api) renderPostHandler() http.Handler { + + tpl := mustParseTpl("post.html") + + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + + id := strings.TrimSuffix(filepath.Base(r.URL.Path), ".html") + + storedPost, err := a.params.PostStore.GetByID(id) + + if errors.Is(err, post.ErrPostNotFound) { + http.Error(rw, "Post not found", 404) + return + } else if err != nil { + apiutil.InternalServerError( + rw, r, fmt.Errorf("fetching post with id %q: %w", id, err), + ) + return + } + + parserExt := parser.CommonExtensions | parser.AutoHeadingIDs + parser := parser.NewWithExtensions(parserExt) + + htmlFlags := html.CommonFlags | html.HrefTargetBlank + htmlRenderer := html.NewRenderer(html.RendererOptions{Flags: htmlFlags}) + + renderedBody := markdown.ToHTML([]byte(storedPost.Body), parser, htmlRenderer) + + tplData := struct { + post.StoredPost + SeriesPrevious, SeriesNext *post.StoredPost + Body template.HTML + }{ + StoredPost: storedPost, + Body: template.HTML(renderedBody), + } + + if series := storedPost.Series; series != "" { + + seriesPosts, err := a.params.PostStore.GetBySeries(series) + if err != nil { + apiutil.InternalServerError( + rw, r, + fmt.Errorf("fetching posts for series %q: %w", series, err), + ) + return + } + + var foundThis bool + + for i := range seriesPosts { + + seriesPost := seriesPosts[i] + + if seriesPost.ID == storedPost.ID { + foundThis = true + continue + } + + if !foundThis { + tplData.SeriesPrevious = &seriesPost + continue + } + + tplData.SeriesNext = &seriesPost + break + } + } + + if err := tpl.Execute(rw, tplData); err != nil { + apiutil.InternalServerError( + rw, r, fmt.Errorf("rendering post with id %q: %w", id, err), + ) + return + } + }) +} diff --git a/srv/src/api/tpl/index.html b/srv/src/api/tpl/index.html new file mode 100644 index 0000000..240df92 --- /dev/null +++ b/srv/src/api/tpl/index.html @@ -0,0 +1,20 @@ +{{ define "body" }} + +{{ end }} + +{{ template "base.html" . }} diff --git a/srv/src/post/post.go b/srv/src/post/post.go index 5835995..30ded15 100644 --- a/srv/src/post/post.go +++ b/srv/src/post/post.go @@ -58,8 +58,9 @@ type Store interface { // overwrites a previous Post with the same ID, if there was one. Set(post Post, now time.Time) error - // Get returns count StoredPosts, sorted time descending, offset by the given page - // number. The returned boolean indicates if there are more pages or not. + // Get returns count StoredPosts, sorted time descending, offset by the + // given page number. The returned boolean indicates if there are more pages + // or not. Get(page, count int) ([]StoredPost, bool, error) // GetByID will return the StoredPost with the given ID, or ErrPostNotFound. -- cgit v1.2.3