diff --git a/cmd/serve.go b/cmd/serve.go index 4f928bb..b1f7f87 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -12,8 +12,9 @@ var serveCmd = &cobra.Command{ Short: "Start the Almanac dev server", Run: func(cmd *cobra.Command, args []string) { server := devserver.NewServer(devserver.Config{ - Addr: must(cmd.Flags().GetString("addr")), - ContentDir: must(cmd.Flags().GetString("content-dir")), + Addr: must(cmd.Flags().GetString("addr")), + ContentDir: must(cmd.Flags().GetString("content-dir")), + UseBundledAssets: must(cmd.Flags().GetBool("use-bundled-assets")), }) err := server.Start() checkError(err, "failed to start dev server") @@ -24,4 +25,5 @@ func init() { rootCmd.AddCommand(serveCmd) serveCmd.Flags().String("addr", ":8080", "Address to listen on") + serveCmd.Flags().Bool("use-bundled-assets", true, "Whether to use bundled assets embedded in the binary") } diff --git a/go.mod b/go.mod index 1c71d38..0f796e9 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( require ( github.com/BurntSushi/toml v1.2.1 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/labstack/gommon v0.4.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -26,5 +27,6 @@ require ( golang.org/x/net v0.14.0 // indirect golang.org/x/sys v0.11.0 // indirect golang.org/x/text v0.12.0 // indirect + golang.org/x/time v0.3.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 8d73a3f..9b34a95 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= @@ -59,6 +61,8 @@ golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/devserver/server.go b/pkg/devserver/server.go index 6703545..9d552da 100644 --- a/pkg/devserver/server.go +++ b/pkg/devserver/server.go @@ -1,21 +1,27 @@ package devserver import ( + "embed" "errors" "fmt" + "html/template" + "io" "log/slog" "net/http" + "os" "path" "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" slogecho "github.com/samber/slog-echo" "github.com/fogo-sh/almanac/pkg/content" ) type Config struct { - Addr string - ContentDir string + Addr string + ContentDir string + UseBundledAssets bool } type Server struct { @@ -31,18 +37,101 @@ func (s *Server) Start() error { return nil } +type PageTemplate struct { + Title string + Content string +} + +var pageTemplateContent = ` + + + {{ .Title }} + + + +

{{ .Title }}

+ {{ .Content }} + +` + +var pageTemplate *template.Template + +type PageData struct { + Title string + Content template.HTML +} + +func init() { + t, err := template.New("page").Parse(pageTemplateContent) + if err != nil { + panic(err) + } + pageTemplate = t +} + +type Renderer struct{} + +func (r *Renderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error { + if name != "page" { + return fmt.Errorf("unknown template: %s", name) + } + + return pageTemplate.Execute(w, data) +} + +func serveNotFound(c echo.Context) error { + return c.Render(http.StatusNotFound, "page", PageData{ + Title: "Not found!", + Content: template.HTML("

Looks like this page doesn't exist yet

"), + }) +} + func (s *Server) servePage(c echo.Context) error { - file, err := content.ParseFile(path.Join(s.config.ContentDir, c.Param("page")+".md")) + page := c.Param("page") + contentPath := path.Join(s.config.ContentDir, page+".md") + + _, err := os.Stat(contentPath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return serveNotFound(c) + } + return fmt.Errorf("error checking file: %w", err) + } + + file, err := content.ParseFile(contentPath) if err != nil { return fmt.Errorf("error processing file: %w", err) } - return c.HTMLBlob(http.StatusOK, file.ParsedContent) + return c.Render(http.StatusOK, "page", PageData{ + Title: page, + Content: template.HTML(string(file.ParsedContent)), + }) } +//go:embed static +var staticFS embed.FS + func NewServer(config Config) *Server { echoInst := echo.New() + var configuredFrontendFS http.FileSystem + if config.UseBundledAssets { + configuredFrontendFS = http.FS(staticFS) + } else { + configuredFrontendFS = http.Dir("./pkg/devserver/static/") + } + + echoInst.Use(middleware.StaticWithConfig(middleware.StaticConfig{ + Root: ".", + Index: "index.html", + Browse: false, + HTML5: true, + Filesystem: configuredFrontendFS, + })) + + echoInst.Renderer = &Renderer{} + echoInst.Use(slogecho.New(slog.Default())) server := &Server{ @@ -52,5 +141,7 @@ func NewServer(config Config) *Server { echoInst.GET("/:page", server.servePage) + echoInst.GET("*", serveNotFound) + return server } diff --git a/pkg/devserver/static/assets/css/main.css b/pkg/devserver/static/assets/css/main.css new file mode 100644 index 0000000..4b3a624 --- /dev/null +++ b/pkg/devserver/static/assets/css/main.css @@ -0,0 +1,17 @@ +:root { + --bg: #f8f9fa; +} + +* { + box-sizing: border-box; + font-family: "Linux Libertine", "Georgia", "Times", serif; +} + +body { + background-color: var(--bg); + padding: 1rem; +} + +body > :first-child { + margin-top: 0; +}