Implement index handler

This involved re-arranging how templates are being parsed, slightly.
This commit is contained in:
Brian Picciano 2022-05-14 17:02:30 -06:00
parent 4c04177c05
commit e41ff2b897
5 changed files with 108 additions and 21 deletions

View File

@ -3,10 +3,8 @@ package api
import ( import (
"context" "context"
"embed"
"errors" "errors"
"fmt" "fmt"
"html/template"
"net" "net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
@ -22,11 +20,6 @@ import (
"github.com/mediocregopher/mediocre-go-lib/v2/mlog" "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 // Params are used to instantiate a new API instance. All fields are required
// unless otherwise noted. // unless otherwise noted.
type Params struct { type Params struct {
@ -184,10 +177,9 @@ func (a *api) handler() http.Handler {
))) )))
var apiHandler http.Handler = apiMux var apiHandler http.Handler = apiMux
apiHandler = postOnlyMiddleware(apiHandler) // TODO probably should be last?
apiHandler = checkCSRFMiddleware(apiHandler) apiHandler = checkCSRFMiddleware(apiHandler)
apiHandler = logMiddleware(a.params.Logger, apiHandler) apiHandler = postOnlyMiddleware(apiHandler)
apiHandler = annotateMiddleware(apiHandler) apiHandler = logReqMiddleware(apiHandler)
apiHandler = addResponseHeaders(map[string]string{ apiHandler = addResponseHeaders(map[string]string{
"Cache-Control": "no-store, max-age=0", "Cache-Control": "no-store, max-age=0",
"Pragma": "no-cache", "Pragma": "no-cache",
@ -196,7 +188,12 @@ func (a *api) handler() http.Handler {
mux.Handle("/api/", http.StripPrefix("/api", apiHandler)) 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())
return mux var globalHandler http.Handler = mux
globalHandler = setLoggerMiddleware(a.params.Logger, globalHandler)
return globalHandler
} }

View File

@ -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) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
type reqInfoKey string type reqInfoKey string
@ -34,6 +34,7 @@ func annotateMiddleware(h http.Handler) http.Handler {
) )
r = r.WithContext(ctx) r = r.WithContext(ctx)
r = apiutil.SetRequestLogger(r, logger)
h.ServeHTTP(rw, r) h.ServeHTTP(rw, r)
}) })
} }
@ -58,11 +59,9 @@ func (lrw *logResponseWriter) WriteHeader(statusCode int) {
lrw.ResponseWriter.WriteHeader(statusCode) 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) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
r = apiutil.SetRequestLogger(r, logger)
lrw := newLogResponseWriter(rw) lrw := newLogResponseWriter(rw)
started := time.Now() started := time.Now()
@ -77,7 +76,7 @@ func logMiddleware(logger *mlog.Logger, h http.Handler) http.Handler {
logCtxKey("response_code"), lrw.statusCode, logCtxKey("response_code"), lrw.statusCode,
) )
logger.Info(ctx, "handled HTTP request") apiutil.GetRequestLogger(r).Info(ctx, "handled HTTP request")
}) })
} }

View File

@ -1,9 +1,11 @@
package api package api
import ( import (
"embed"
"errors" "errors"
"fmt" "fmt"
"html/template" "html/template"
"io/fs"
"net/http" "net/http"
"path/filepath" "path/filepath"
"strings" "strings"
@ -15,7 +17,75 @@ import (
"github.com/mediocregopher/blog.mediocregopher.com/srv/post" "github.com/mediocregopher/blog.mediocregopher.com/srv/post"
) )
func (a *api) postHandler() http.Handler { //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) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
id := strings.TrimSuffix(filepath.Base(r.URL.Path), ".html") id := strings.TrimSuffix(filepath.Base(r.URL.Path), ".html")
@ -81,7 +151,7 @@ func (a *api) postHandler() http.Handler {
} }
} }
if err := tpls.ExecuteTemplate(rw, "post.html", tplData); err != nil { if err := tpl.Execute(rw, tplData); err != nil {
apiutil.InternalServerError( apiutil.InternalServerError(
rw, r, fmt.Errorf("rendering post with id %q: %w", id, err), rw, r, fmt.Errorf("rendering post with id %q: %w", id, err),
) )

View File

@ -0,0 +1,20 @@
{{ define "body" }}
<ul id="posts-list">
{{ range .Posts }}
<li>
<h2>
<a href="{{ .HTTPPath }}">{{ .Title }}</a>
</h2>
<span>{{ .PublishedAt.Format "2006-01-02" }}</span>
{{ if not .LastUpdatedAt.IsZero }}
<span>(Updated {{ .LastUpdatedAt.Format "2006-01-02" }})</span>
{{ end }}
<p>{{ .Description }}</p>
</li>
{{ end }}
</ul>
{{ end }}
{{ template "base.html" . }}

View File

@ -58,8 +58,9 @@ type Store interface {
// overwrites a previous Post with the same ID, if there was one. // overwrites a previous Post with the same ID, if there was one.
Set(post Post, now time.Time) error Set(post Post, now time.Time) error
// Get returns count StoredPosts, sorted time descending, offset by the given page // Get returns count StoredPosts, sorted time descending, offset by the
// number. The returned boolean indicates if there are more pages or not. // given page number. The returned boolean indicates if there are more pages
// or not.
Get(page, count int) ([]StoredPost, bool, error) Get(page, count int) ([]StoredPost, bool, error)
// GetByID will return the StoredPost with the given ID, or ErrPostNotFound. // GetByID will return the StoredPost with the given ID, or ErrPostNotFound.