├── .gitignore ├── static ├── css │ ├── input.css │ └── output.css └── svg │ └── logo.svg ├── tmp ├── main └── build-errors.log ├── internal ├── generate │ └── generate.go ├── component │ ├── component_templ.txt │ ├── component.templ │ └── component_templ.go ├── template │ ├── template_templ.txt │ ├── template.templ │ └── template_templ.go ├── view │ └── view.go └── middleware │ └── middleware.go ├── go.mod ├── tailwind.config.js ├── go.sum ├── main.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | ./tmp 3 | -------------------------------------------------------------------------------- /static/css/input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /tmp/main: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phillip-England/templ-quickstart/main/tmp/main -------------------------------------------------------------------------------- /internal/generate/generate.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | func GenerateMain() error { 4 | return nil 5 | } 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module xerus 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/a-h/templ v0.2.598 7 | github.com/joho/godotenv v1.5.1 8 | ) 9 | -------------------------------------------------------------------------------- /tmp/build-errors.log: -------------------------------------------------------------------------------- 1 | exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 -------------------------------------------------------------------------------- /internal/component/component_templ.txt: -------------------------------------------------------------------------------- 1 |
2 |

3 |

4 |

5 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./internal/**/*.{go,js,templ,html}" 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | } -------------------------------------------------------------------------------- /internal/template/template_templ.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | 6 | -------------------------------------------------------------------------------- /internal/component/component.templ: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | templ Banner() { 4 |
5 | 6 |
7 | } 8 | 9 | templ TextAndTitle(title string, text string) { 10 |
11 |

{title}

12 |

{text}

13 |
14 | } -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/a-h/templ v0.2.598 h1:6jMIHv6wQZvdPxTuv87erW4RqN/FPU0wk7ZHN5wVuuo= 2 | github.com/a-h/templ v0.2.598/go.mod h1:SA7mtYwVEajbIXFRh3vKdYm/4FYyLQAtPH1+KxzGPA8= 3 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 4 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 5 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 6 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 7 | -------------------------------------------------------------------------------- /internal/view/view.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "net/http" 5 | "path/filepath" 6 | ) 7 | 8 | func ServeFavicon(w http.ResponseWriter, r *http.Request) { 9 | filePath := "favicon.ico" 10 | fullPath := filepath.Join(".", "static", filePath) 11 | http.ServeFile(w, r, fullPath) 12 | } 13 | 14 | func ServeStaticFiles(w http.ResponseWriter, r *http.Request) { 15 | filePath := r.URL.Path[len("/static/"):] 16 | fullPath := filepath.Join(".", "static", filePath) 17 | http.ServeFile(w, r, fullPath) 18 | } 19 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "xerus/internal/generate" 8 | "xerus/internal/middleware" 9 | "xerus/internal/template" 10 | "xerus/internal/view" 11 | 12 | "github.com/joho/godotenv" 13 | ) 14 | 15 | func main() { 16 | 17 | err := generate.GenerateMain() 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | _ = godotenv.Load() 23 | mux := http.NewServeMux() 24 | 25 | mux.HandleFunc("GET /favicon.ico", view.ServeFavicon) 26 | mux.HandleFunc("GET /static/", view.ServeStaticFiles) 27 | 28 | mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { 29 | if r.URL.Path != "/" { 30 | http.NotFound(w, r) 31 | return 32 | } 33 | middleware.Chain(w, r, template.Home("Templ Quickstart")) 34 | }) 35 | 36 | fmt.Printf("server is running on port %s\n", os.Getenv("PORT")) 37 | err = http.ListenAndServe(":"+os.Getenv("PORT"), mux) 38 | if err != nil { 39 | fmt.Println(err) 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /internal/template/template.templ: -------------------------------------------------------------------------------- 1 | package template 2 | 3 | import "xerus/internal/component" 4 | 5 | 6 | templ Base(title string) { 7 | 8 | 9 | 10 | 11 | 12 | 13 | {title} 14 | 15 | @component.Banner() 16 | 17 |
18 | { children... } 19 |
20 | 21 | 22 | } 23 | 24 | 25 | 26 | // route!("GET /") 27 | // middleware!(ParseForm) 28 | templ Home(title string) { 29 | @Base(title) { 30 | @component.TextAndTitle("I'm a Component!", "I am included as a content item in the Base Template!") 31 | @component.TextAndTitle("I'm another Component!", "I am also included in the Base Template!") 32 | } 33 | } -------------------------------------------------------------------------------- /internal/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/a-h/templ" 10 | ) 11 | 12 | type CustomContext struct { 13 | context.Context 14 | StartTime time.Time 15 | } 16 | 17 | type CustomHandler func(ctx *CustomContext, w http.ResponseWriter, r *http.Request) 18 | type CustomMiddleware func(ctx *CustomContext, w http.ResponseWriter, r *http.Request) error 19 | 20 | func Chain(w http.ResponseWriter, r *http.Request, template templ.Component, middleware ...CustomMiddleware) { 21 | customContext := &CustomContext{ 22 | Context: context.Background(), 23 | StartTime: time.Now(), 24 | } 25 | for _, mw := range middleware { 26 | err := mw(customContext, w, r) 27 | if err != nil { 28 | return 29 | } 30 | } 31 | template.Render(customContext, w) 32 | Log(customContext, w, r) 33 | } 34 | 35 | func Log(ctx *CustomContext, w http.ResponseWriter, r *http.Request) error { 36 | elapsedTime := time.Since(ctx.StartTime) 37 | formattedTime := time.Now().Format("2006-01-02 15:04:05") 38 | fmt.Printf("[%s] [%s] [%s] [%s]\n", formattedTime, r.Method, r.URL.Path, elapsedTime) 39 | return nil 40 | } 41 | 42 | func ParseForm(ctx *CustomContext, w http.ResponseWriter, r *http.Request) error { 43 | r.ParseForm() 44 | return nil 45 | } 46 | 47 | func ParseMultipartForm(ctx *CustomContext, w http.ResponseWriter, r *http.Request) error { 48 | r.ParseMultipartForm(10 << 20) 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /internal/component/component_templ.go: -------------------------------------------------------------------------------- 1 | // Code generated by templ - DO NOT EDIT. 2 | 3 | // templ: version: v0.2.707 4 | package component 5 | 6 | //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 | 8 | import "github.com/a-h/templ" 9 | import "context" 10 | import "io" 11 | import "bytes" 12 | 13 | func Banner() templ.Component { 14 | return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { 15 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) 16 | if !templ_7745c5c3_IsBuffer { 17 | templ_7745c5c3_Buffer = templ.GetBuffer() 18 | defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) 19 | } 20 | ctx = templ.InitializeContext(ctx) 21 | templ_7745c5c3_Var1 := templ.GetChildren(ctx) 22 | if templ_7745c5c3_Var1 == nil { 23 | templ_7745c5c3_Var1 = templ.NopComponent 24 | } 25 | ctx = templ.ClearChildren(ctx) 26 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) 27 | if templ_7745c5c3_Err != nil { 28 | return templ_7745c5c3_Err 29 | } 30 | if !templ_7745c5c3_IsBuffer { 31 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) 32 | } 33 | return templ_7745c5c3_Err 34 | }) 35 | } 36 | 37 | func TextAndTitle(title string, text string) templ.Component { 38 | return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { 39 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) 40 | if !templ_7745c5c3_IsBuffer { 41 | templ_7745c5c3_Buffer = templ.GetBuffer() 42 | defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) 43 | } 44 | ctx = templ.InitializeContext(ctx) 45 | templ_7745c5c3_Var2 := templ.GetChildren(ctx) 46 | if templ_7745c5c3_Var2 == nil { 47 | templ_7745c5c3_Var2 = templ.NopComponent 48 | } 49 | ctx = templ.ClearChildren(ctx) 50 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) 51 | if templ_7745c5c3_Err != nil { 52 | return templ_7745c5c3_Err 53 | } 54 | var templ_7745c5c3_Var3 string 55 | templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(title) 56 | if templ_7745c5c3_Err != nil { 57 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/component/component.templ`, Line: 11, Col: 44} 58 | } 59 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 60 | if templ_7745c5c3_Err != nil { 61 | return templ_7745c5c3_Err 62 | } 63 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 3) 64 | if templ_7745c5c3_Err != nil { 65 | return templ_7745c5c3_Err 66 | } 67 | var templ_7745c5c3_Var4 string 68 | templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(text) 69 | if templ_7745c5c3_Err != nil { 70 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/component/component.templ`, Line: 12, Col: 32} 71 | } 72 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 73 | if templ_7745c5c3_Err != nil { 74 | return templ_7745c5c3_Err 75 | } 76 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 4) 77 | if templ_7745c5c3_Err != nil { 78 | return templ_7745c5c3_Err 79 | } 80 | if !templ_7745c5c3_IsBuffer { 81 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) 82 | } 83 | return templ_7745c5c3_Err 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /internal/template/template_templ.go: -------------------------------------------------------------------------------- 1 | // Code generated by templ - DO NOT EDIT. 2 | 3 | // templ: version: v0.2.707 4 | package template 5 | 6 | //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 | 8 | import "github.com/a-h/templ" 9 | import "context" 10 | import "io" 11 | import "bytes" 12 | 13 | import "xerus/internal/component" 14 | 15 | func Base(title string) templ.Component { 16 | return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { 17 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) 18 | if !templ_7745c5c3_IsBuffer { 19 | templ_7745c5c3_Buffer = templ.GetBuffer() 20 | defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) 21 | } 22 | ctx = templ.InitializeContext(ctx) 23 | templ_7745c5c3_Var1 := templ.GetChildren(ctx) 24 | if templ_7745c5c3_Var1 == nil { 25 | templ_7745c5c3_Var1 = templ.NopComponent 26 | } 27 | ctx = templ.ClearChildren(ctx) 28 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) 29 | if templ_7745c5c3_Err != nil { 30 | return templ_7745c5c3_Err 31 | } 32 | var templ_7745c5c3_Var2 string 33 | templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) 34 | if templ_7745c5c3_Err != nil { 35 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/template/template.templ`, Line: 13, Col: 25} 36 | } 37 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 38 | if templ_7745c5c3_Err != nil { 39 | return templ_7745c5c3_Err 40 | } 41 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) 42 | if templ_7745c5c3_Err != nil { 43 | return templ_7745c5c3_Err 44 | } 45 | templ_7745c5c3_Err = component.Banner().Render(ctx, templ_7745c5c3_Buffer) 46 | if templ_7745c5c3_Err != nil { 47 | return templ_7745c5c3_Err 48 | } 49 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 3) 50 | if templ_7745c5c3_Err != nil { 51 | return templ_7745c5c3_Err 52 | } 53 | templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer) 54 | if templ_7745c5c3_Err != nil { 55 | return templ_7745c5c3_Err 56 | } 57 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 4) 58 | if templ_7745c5c3_Err != nil { 59 | return templ_7745c5c3_Err 60 | } 61 | if !templ_7745c5c3_IsBuffer { 62 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) 63 | } 64 | return templ_7745c5c3_Err 65 | }) 66 | } 67 | 68 | // route!("GET /") 69 | // middleware!(ParseForm) 70 | func Home(title string) templ.Component { 71 | return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { 72 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) 73 | if !templ_7745c5c3_IsBuffer { 74 | templ_7745c5c3_Buffer = templ.GetBuffer() 75 | defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) 76 | } 77 | ctx = templ.InitializeContext(ctx) 78 | templ_7745c5c3_Var3 := templ.GetChildren(ctx) 79 | if templ_7745c5c3_Var3 == nil { 80 | templ_7745c5c3_Var3 = templ.NopComponent 81 | } 82 | ctx = templ.ClearChildren(ctx) 83 | templ_7745c5c3_Var4 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { 84 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) 85 | if !templ_7745c5c3_IsBuffer { 86 | templ_7745c5c3_Buffer = templ.GetBuffer() 87 | defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) 88 | } 89 | templ_7745c5c3_Err = component.TextAndTitle("I'm a Component!", "I am included as a content item in the Base Template!").Render(ctx, templ_7745c5c3_Buffer) 90 | if templ_7745c5c3_Err != nil { 91 | return templ_7745c5c3_Err 92 | } 93 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 5) 94 | if templ_7745c5c3_Err != nil { 95 | return templ_7745c5c3_Err 96 | } 97 | templ_7745c5c3_Err = component.TextAndTitle("I'm another Component!", "I am also included in the Base Template!").Render(ctx, templ_7745c5c3_Buffer) 98 | if templ_7745c5c3_Err != nil { 99 | return templ_7745c5c3_Err 100 | } 101 | if !templ_7745c5c3_IsBuffer { 102 | _, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer) 103 | } 104 | return templ_7745c5c3_Err 105 | }) 106 | templ_7745c5c3_Err = Base(title).Render(templ.WithChildren(ctx, templ_7745c5c3_Var4), templ_7745c5c3_Buffer) 107 | if templ_7745c5c3_Err != nil { 108 | return templ_7745c5c3_Err 109 | } 110 | if !templ_7745c5c3_IsBuffer { 111 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) 112 | } 113 | return templ_7745c5c3_Err 114 | }) 115 | } 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # templ-quickstart 2 | 3 | ## Introduction 4 | 5 | templ-quickstart provides a quick and easy way to scaffold an Go http server. The tech stack included in this repo includes Go, HTMX, Templ, and Tailwind. 6 | 7 | ## Core Technologies 8 | 9 | As mentioned above, this project depends on some awesome technologies. Let me start by giving credit where credit is due: 10 | 11 | - [Go](https://go.dev/) - Version 1.22.0 or greater required 12 | - [Templ](https://templ.guide/) 13 | - [Air](https://github.com/cosmtrek/air) 14 | - [Htmx](https://htmx.org/) 15 | - [Tailwindcss](https://tailwindcss.com/) 16 | 17 | ## Installation 18 | 19 | ### Clone the Repository 20 | 21 | ```bash 22 | git clone https://github.com/phillip-england/templ-quickstart 23 | ``` 24 | 25 | ```bash 26 | cd 27 | ``` 28 | 29 | ### Install Dependencies 30 | 31 | ```bash 32 | go mod tidy 33 | ``` 34 | 35 | ### Create a .env file and include a PORT variable 36 | 37 | ```bash 38 | touch .env; 39 | ``` 40 | 41 | ```bash 42 | echo "PORT=8080" > .env 43 | ``` 44 | 45 | ## Build Steps and Serving 46 | 47 | This project requires a build step. The following are commands needed to build your html and css output. 48 | 49 | ### Templ HTML Generation 50 | 51 | With templ installed and the binary somewhere on your PATH, run the following to generate your HTML components and templates (remove --watch to simply build and not hot reload) 52 | 53 | ```bash 54 | templ generate --watch 55 | ``` 56 | 57 | ### CSS File Generation 58 | 59 | With the [Tailwind Binary](https://tailwindcss.com/blog/standalone-cli) installed and moved somewhere on your PATH, run the following to generate your CSS output for your tailwind classes (remove --watch to simply build and not hot reload) 60 | 61 | ```bash 62 | tailwindcss -i ./static/css/input.css -o ./static/css/output.css --watch 63 | ``` 64 | 65 | ### Serving with Air 66 | 67 | With the [Air Binary](https://github.com/cosmtrek/air) installed and moved somewhere on your PATH, run the following to serve and hot reload the application: 68 | 69 | ```bash 70 | air 71 | ``` 72 | 73 | To configure air, you can modify .air.toml in the root of the project. (it will be auto-generated after the first time you run air in your repo) 74 | 75 | ## Project Overview 76 | 77 | This project has a few core concepts to help you get going, let's start with ./main.go 78 | 79 | ### Main - ./main.go 80 | 81 | This is our applications entry-point and does a few things: 82 | 83 | 1. Here, we load in our .env file and then we initialize our mux server. 84 | 85 | ```go 86 | _ = godotenv.Load() 87 | mux := http.NewServeMux() 88 | ``` 89 | 90 | 2. We define a few basic routes for our server. I will go into these routes in more depth later. In short, these routes enable you to use static files in your project, to use a favicon.ico, and sets up a view found at "/". 91 | 92 | ```go 93 | mux.HandleFunc("GET /favicon.ico", view.ServeFavicon) 94 | mux.HandleFunc("GET /static/", view.ServeStaticFiles) 95 | mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { 96 | middleware.Chain(w, r, view.Home) 97 | }) 98 | ``` 99 | 100 | Please take note of this line here as it will be important in the next section when we discuss middleware: 101 | 102 | ```go 103 | middleware.Chain(w, r, view.Home) 104 | ``` 105 | 106 | 3. We serve our application on the PORT defined at ./.env 107 | 108 | ```go 109 | fmt.Println(fmt.Sprintf("server is running on port %s", os.Getenv("PORT"))) 110 | err := http.ListenAndServe(":"+os.Getenv("PORT"), mux) 111 | if err != nil { 112 | fmt.Println(err) 113 | } 114 | ``` 115 | ### Middleware - ./internal/middleware/middleware.go 116 | 117 | Custom middleware can be implemented with ease in this project. Lets first start with our middleware chain. 118 | 119 | This function enables you to tack on middleware at the end of a handler instead of having to deeply-nest middleware components (which is what you would usually expect). 120 | 121 | ```go 122 | 123 | type CustomContext struct { 124 | context.Context 125 | StartTime time.Time 126 | } 127 | 128 | type CustomHandler func(ctx *CustomContext, w http.ResponseWriter, r *http.Request) 129 | 130 | type CustomMiddleware func(ctx *CustomContext, w http.ResponseWriter, r *http.Request) error 131 | 132 | func Chain(w http.ResponseWriter, r *http.Request, handler CustomHandler, middleware ...CustomMiddleware) { 133 | customContext := &CustomContext{ 134 | Context: context.Background(), 135 | StartTime: time.Now(), 136 | } 137 | for _, mw := range middleware { 138 | err := mw(customContext, w, r) 139 | if err != nil { 140 | return 141 | } 142 | } 143 | handler(customContext, w, r) 144 | Log(customContext, w, r) 145 | } 146 | ``` 147 | 148 | You'll notice we are using a few custom types here. In short, this function works by initializing a custom context, iterating through our middleware, and then finally calling our handler and logger. The custom context is passed through each middleware, enabling you to store and access context values throughout the chain. If a middleware returns an error, the chain will stop executing. This enables you to allow your middleware to write responses early and avoid calling the handler in case of an error. 149 | 150 | ### Creating Custom Middleware 151 | 152 | Let's say you want to create custom middleware. Here is how to do so: 153 | 154 | 1. If this middleware requires some context, add the context value to the CustomContext type. 155 | 156 | ```go 157 | type CustomContext struct { 158 | context.Context 159 | StartTime time.Time 160 | NewContextValue string 161 | } 162 | ``` 163 | 164 | 2. Define your new middleware functions (remember middleware must match the CustomMiddleware type definition). 165 | 166 | ```go 167 | // this middleware will be placed early in the chain 168 | func EarlyMiddleware(ctx *CustomContext, w http.ResponseWriter, r *http.Request) error { 169 | ctx.NewContextValue = "I was set early in the chain" // set your new context value 170 | return nil 171 | } 172 | 173 | // this middleware will be place late in the chain 174 | func LateMiddleware(ctx *CustomContext, w http.ResponseWriter, r *http.Request) error { 175 | fmt.Println(ctx.NewContextValue) // outputs "I was set early in the chain" 176 | return nil 177 | } 178 | ``` 179 | 180 | 3. Include the middleware in your Chain func in your routes. 181 | 182 | ```go 183 | // modified version of ./main.go 184 | mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { 185 | middleware.Chain(w, r, view.Home, middleware.EarlyMiddleware, middleware.LateMiddleware) 186 | }) 187 | ``` 188 | 189 | That's it! Easily create custom middleware without the need to deeply nest your routes. 190 | 191 | ### Views - ./internal/view/view.go 192 | 193 | Our views are straightforward and rely on templ to generate html content. Here is an example of the Home view found at ./internal/view/view.go 194 | 195 | ```go 196 | func Home(ctx *middleware.CustomContext, w http.ResponseWriter, r *http.Request) { 197 | if r.URL.Path != "/" { // catches 404s, only needed in the '/' route for entire app 198 | http.NotFound(w, r) 199 | return 200 | 201 | } 202 | template.Home("Templ Quickstart").Render(ctx, w) 203 | } 204 | ``` 205 | 206 | ### Templates - ./internal/template/template.templ 207 | 208 | Our templates are included in this file. Here is the Base template discussed in the previous section. This function simply takes in a title and an array of templ.Component. For more info on templ syntax, please visit [Templ.guide](templ.guide) 209 | 210 | To put very simple, Base is a 'base-level template' that can take in children. Then, we reuse base in our home template. Please note the sytax for passing children to @Base. Normally you'd expect to pass children as parameters, but with templ, you place children inside brackets. 211 | 212 | ```html 213 | templ Base(title string) { 214 | 215 | 216 | 217 | 218 | 219 | 220 | {title} 221 | 222 | @component.Banner() 223 | 224 |
225 | { children... } 226 |
227 | 228 | 229 | } 230 | 231 | templ Home(title string) { 232 | @Base(title) { 233 | @component.TextAndTitle("I'm a Component!", "I am included as a content item in the Base Template!") 234 | @component.TextAndTitle("I'm another Component!", "I am also included in the Base Template!") 235 | } 236 | } 237 | ``` 238 | 239 | Also note, htmx and your tailwind output are included in the head of this template: 240 | 241 | ```html 242 | 243 | 244 | ``` 245 | 246 | ### Components - ./internal/component/component.templ 247 | 248 | Comonents are very similar to templates. Here is an example of the TextAndTitle component used in ./internal/view/view.go 249 | 250 | ```html 251 | templ TextAndTitle(title string, text string) { 252 |
253 |

{title}

254 |

{text}

255 |
256 | } 257 | ``` -------------------------------------------------------------------------------- /static/css/output.css: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com 3 | */ 4 | 5 | /* 6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 8 | */ 9 | 10 | *, 11 | ::before, 12 | ::after { 13 | box-sizing: border-box; 14 | /* 1 */ 15 | border-width: 0; 16 | /* 2 */ 17 | border-style: solid; 18 | /* 2 */ 19 | border-color: #e5e7eb; 20 | /* 2 */ 21 | } 22 | 23 | ::before, 24 | ::after { 25 | --tw-content: ''; 26 | } 27 | 28 | /* 29 | 1. Use a consistent sensible line-height in all browsers. 30 | 2. Prevent adjustments of font size after orientation changes in iOS. 31 | 3. Use a more readable tab size. 32 | 4. Use the user's configured `sans` font-family by default. 33 | 5. Use the user's configured `sans` font-feature-settings by default. 34 | 6. Use the user's configured `sans` font-variation-settings by default. 35 | 7. Disable tap highlights on iOS 36 | */ 37 | 38 | html, 39 | :host { 40 | line-height: 1.5; 41 | /* 1 */ 42 | -webkit-text-size-adjust: 100%; 43 | /* 2 */ 44 | -moz-tab-size: 4; 45 | /* 3 */ 46 | -o-tab-size: 4; 47 | tab-size: 4; 48 | /* 3 */ 49 | font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 50 | /* 4 */ 51 | font-feature-settings: normal; 52 | /* 5 */ 53 | font-variation-settings: normal; 54 | /* 6 */ 55 | -webkit-tap-highlight-color: transparent; 56 | /* 7 */ 57 | } 58 | 59 | /* 60 | 1. Remove the margin in all browsers. 61 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 62 | */ 63 | 64 | body { 65 | margin: 0; 66 | /* 1 */ 67 | line-height: inherit; 68 | /* 2 */ 69 | } 70 | 71 | /* 72 | 1. Add the correct height in Firefox. 73 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 74 | 3. Ensure horizontal rules are visible by default. 75 | */ 76 | 77 | hr { 78 | height: 0; 79 | /* 1 */ 80 | color: inherit; 81 | /* 2 */ 82 | border-top-width: 1px; 83 | /* 3 */ 84 | } 85 | 86 | /* 87 | Add the correct text decoration in Chrome, Edge, and Safari. 88 | */ 89 | 90 | abbr:where([title]) { 91 | -webkit-text-decoration: underline dotted; 92 | text-decoration: underline dotted; 93 | } 94 | 95 | /* 96 | Remove the default font size and weight for headings. 97 | */ 98 | 99 | h1, 100 | h2, 101 | h3, 102 | h4, 103 | h5, 104 | h6 { 105 | font-size: inherit; 106 | font-weight: inherit; 107 | } 108 | 109 | /* 110 | Reset links to optimize for opt-in styling instead of opt-out. 111 | */ 112 | 113 | a { 114 | color: inherit; 115 | text-decoration: inherit; 116 | } 117 | 118 | /* 119 | Add the correct font weight in Edge and Safari. 120 | */ 121 | 122 | b, 123 | strong { 124 | font-weight: bolder; 125 | } 126 | 127 | /* 128 | 1. Use the user's configured `mono` font-family by default. 129 | 2. Use the user's configured `mono` font-feature-settings by default. 130 | 3. Use the user's configured `mono` font-variation-settings by default. 131 | 4. Correct the odd `em` font sizing in all browsers. 132 | */ 133 | 134 | code, 135 | kbd, 136 | samp, 137 | pre { 138 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 139 | /* 1 */ 140 | font-feature-settings: normal; 141 | /* 2 */ 142 | font-variation-settings: normal; 143 | /* 3 */ 144 | font-size: 1em; 145 | /* 4 */ 146 | } 147 | 148 | /* 149 | Add the correct font size in all browsers. 150 | */ 151 | 152 | small { 153 | font-size: 80%; 154 | } 155 | 156 | /* 157 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 158 | */ 159 | 160 | sub, 161 | sup { 162 | font-size: 75%; 163 | line-height: 0; 164 | position: relative; 165 | vertical-align: baseline; 166 | } 167 | 168 | sub { 169 | bottom: -0.25em; 170 | } 171 | 172 | sup { 173 | top: -0.5em; 174 | } 175 | 176 | /* 177 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 178 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 179 | 3. Remove gaps between table borders by default. 180 | */ 181 | 182 | table { 183 | text-indent: 0; 184 | /* 1 */ 185 | border-color: inherit; 186 | /* 2 */ 187 | border-collapse: collapse; 188 | /* 3 */ 189 | } 190 | 191 | /* 192 | 1. Change the font styles in all browsers. 193 | 2. Remove the margin in Firefox and Safari. 194 | 3. Remove default padding in all browsers. 195 | */ 196 | 197 | button, 198 | input, 199 | optgroup, 200 | select, 201 | textarea { 202 | font-family: inherit; 203 | /* 1 */ 204 | font-feature-settings: inherit; 205 | /* 1 */ 206 | font-variation-settings: inherit; 207 | /* 1 */ 208 | font-size: 100%; 209 | /* 1 */ 210 | font-weight: inherit; 211 | /* 1 */ 212 | line-height: inherit; 213 | /* 1 */ 214 | letter-spacing: inherit; 215 | /* 1 */ 216 | color: inherit; 217 | /* 1 */ 218 | margin: 0; 219 | /* 2 */ 220 | padding: 0; 221 | /* 3 */ 222 | } 223 | 224 | /* 225 | Remove the inheritance of text transform in Edge and Firefox. 226 | */ 227 | 228 | button, 229 | select { 230 | text-transform: none; 231 | } 232 | 233 | /* 234 | 1. Correct the inability to style clickable types in iOS and Safari. 235 | 2. Remove default button styles. 236 | */ 237 | 238 | button, 239 | input:where([type='button']), 240 | input:where([type='reset']), 241 | input:where([type='submit']) { 242 | -webkit-appearance: button; 243 | /* 1 */ 244 | background-color: transparent; 245 | /* 2 */ 246 | background-image: none; 247 | /* 2 */ 248 | } 249 | 250 | /* 251 | Use the modern Firefox focus style for all focusable elements. 252 | */ 253 | 254 | :-moz-focusring { 255 | outline: auto; 256 | } 257 | 258 | /* 259 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 260 | */ 261 | 262 | :-moz-ui-invalid { 263 | box-shadow: none; 264 | } 265 | 266 | /* 267 | Add the correct vertical alignment in Chrome and Firefox. 268 | */ 269 | 270 | progress { 271 | vertical-align: baseline; 272 | } 273 | 274 | /* 275 | Correct the cursor style of increment and decrement buttons in Safari. 276 | */ 277 | 278 | ::-webkit-inner-spin-button, 279 | ::-webkit-outer-spin-button { 280 | height: auto; 281 | } 282 | 283 | /* 284 | 1. Correct the odd appearance in Chrome and Safari. 285 | 2. Correct the outline style in Safari. 286 | */ 287 | 288 | [type='search'] { 289 | -webkit-appearance: textfield; 290 | /* 1 */ 291 | outline-offset: -2px; 292 | /* 2 */ 293 | } 294 | 295 | /* 296 | Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | ::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /* 304 | 1. Correct the inability to style clickable types in iOS and Safari. 305 | 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; 310 | /* 1 */ 311 | font: inherit; 312 | /* 2 */ 313 | } 314 | 315 | /* 316 | Add the correct display in Chrome and Safari. 317 | */ 318 | 319 | summary { 320 | display: list-item; 321 | } 322 | 323 | /* 324 | Removes the default spacing and border for appropriate elements. 325 | */ 326 | 327 | blockquote, 328 | dl, 329 | dd, 330 | h1, 331 | h2, 332 | h3, 333 | h4, 334 | h5, 335 | h6, 336 | hr, 337 | figure, 338 | p, 339 | pre { 340 | margin: 0; 341 | } 342 | 343 | fieldset { 344 | margin: 0; 345 | padding: 0; 346 | } 347 | 348 | legend { 349 | padding: 0; 350 | } 351 | 352 | ol, 353 | ul, 354 | menu { 355 | list-style: none; 356 | margin: 0; 357 | padding: 0; 358 | } 359 | 360 | /* 361 | Reset default styling for dialogs. 362 | */ 363 | 364 | dialog { 365 | padding: 0; 366 | } 367 | 368 | /* 369 | Prevent resizing textareas horizontally by default. 370 | */ 371 | 372 | textarea { 373 | resize: vertical; 374 | } 375 | 376 | /* 377 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 378 | 2. Set the default placeholder color to the user's configured gray 400 color. 379 | */ 380 | 381 | input::-moz-placeholder, textarea::-moz-placeholder { 382 | opacity: 1; 383 | /* 1 */ 384 | color: #9ca3af; 385 | /* 2 */ 386 | } 387 | 388 | input::placeholder, 389 | textarea::placeholder { 390 | opacity: 1; 391 | /* 1 */ 392 | color: #9ca3af; 393 | /* 2 */ 394 | } 395 | 396 | /* 397 | Set the default cursor for buttons. 398 | */ 399 | 400 | button, 401 | [role="button"] { 402 | cursor: pointer; 403 | } 404 | 405 | /* 406 | Make sure disabled buttons don't get the pointer cursor. 407 | */ 408 | 409 | :disabled { 410 | cursor: default; 411 | } 412 | 413 | /* 414 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 415 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 416 | This can trigger a poorly considered lint error in some tools but is included by design. 417 | */ 418 | 419 | img, 420 | svg, 421 | video, 422 | canvas, 423 | audio, 424 | iframe, 425 | embed, 426 | object { 427 | display: block; 428 | /* 1 */ 429 | vertical-align: middle; 430 | /* 2 */ 431 | } 432 | 433 | /* 434 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 435 | */ 436 | 437 | img, 438 | video { 439 | max-width: 100%; 440 | height: auto; 441 | } 442 | 443 | /* Make elements with the HTML hidden attribute stay hidden by default */ 444 | 445 | [hidden] { 446 | display: none; 447 | } 448 | 449 | *, ::before, ::after { 450 | --tw-border-spacing-x: 0; 451 | --tw-border-spacing-y: 0; 452 | --tw-translate-x: 0; 453 | --tw-translate-y: 0; 454 | --tw-rotate: 0; 455 | --tw-skew-x: 0; 456 | --tw-skew-y: 0; 457 | --tw-scale-x: 1; 458 | --tw-scale-y: 1; 459 | --tw-pan-x: ; 460 | --tw-pan-y: ; 461 | --tw-pinch-zoom: ; 462 | --tw-scroll-snap-strictness: proximity; 463 | --tw-gradient-from-position: ; 464 | --tw-gradient-via-position: ; 465 | --tw-gradient-to-position: ; 466 | --tw-ordinal: ; 467 | --tw-slashed-zero: ; 468 | --tw-numeric-figure: ; 469 | --tw-numeric-spacing: ; 470 | --tw-numeric-fraction: ; 471 | --tw-ring-inset: ; 472 | --tw-ring-offset-width: 0px; 473 | --tw-ring-offset-color: #fff; 474 | --tw-ring-color: rgb(59 130 246 / 0.5); 475 | --tw-ring-offset-shadow: 0 0 #0000; 476 | --tw-ring-shadow: 0 0 #0000; 477 | --tw-shadow: 0 0 #0000; 478 | --tw-shadow-colored: 0 0 #0000; 479 | --tw-blur: ; 480 | --tw-brightness: ; 481 | --tw-contrast: ; 482 | --tw-grayscale: ; 483 | --tw-hue-rotate: ; 484 | --tw-invert: ; 485 | --tw-saturate: ; 486 | --tw-sepia: ; 487 | --tw-drop-shadow: ; 488 | --tw-backdrop-blur: ; 489 | --tw-backdrop-brightness: ; 490 | --tw-backdrop-contrast: ; 491 | --tw-backdrop-grayscale: ; 492 | --tw-backdrop-hue-rotate: ; 493 | --tw-backdrop-invert: ; 494 | --tw-backdrop-opacity: ; 495 | --tw-backdrop-saturate: ; 496 | --tw-backdrop-sepia: ; 497 | --tw-contain-size: ; 498 | --tw-contain-layout: ; 499 | --tw-contain-paint: ; 500 | --tw-contain-style: ; 501 | } 502 | 503 | ::backdrop { 504 | --tw-border-spacing-x: 0; 505 | --tw-border-spacing-y: 0; 506 | --tw-translate-x: 0; 507 | --tw-translate-y: 0; 508 | --tw-rotate: 0; 509 | --tw-skew-x: 0; 510 | --tw-skew-y: 0; 511 | --tw-scale-x: 1; 512 | --tw-scale-y: 1; 513 | --tw-pan-x: ; 514 | --tw-pan-y: ; 515 | --tw-pinch-zoom: ; 516 | --tw-scroll-snap-strictness: proximity; 517 | --tw-gradient-from-position: ; 518 | --tw-gradient-via-position: ; 519 | --tw-gradient-to-position: ; 520 | --tw-ordinal: ; 521 | --tw-slashed-zero: ; 522 | --tw-numeric-figure: ; 523 | --tw-numeric-spacing: ; 524 | --tw-numeric-fraction: ; 525 | --tw-ring-inset: ; 526 | --tw-ring-offset-width: 0px; 527 | --tw-ring-offset-color: #fff; 528 | --tw-ring-color: rgb(59 130 246 / 0.5); 529 | --tw-ring-offset-shadow: 0 0 #0000; 530 | --tw-ring-shadow: 0 0 #0000; 531 | --tw-shadow: 0 0 #0000; 532 | --tw-shadow-colored: 0 0 #0000; 533 | --tw-blur: ; 534 | --tw-brightness: ; 535 | --tw-contrast: ; 536 | --tw-grayscale: ; 537 | --tw-hue-rotate: ; 538 | --tw-invert: ; 539 | --tw-saturate: ; 540 | --tw-sepia: ; 541 | --tw-drop-shadow: ; 542 | --tw-backdrop-blur: ; 543 | --tw-backdrop-brightness: ; 544 | --tw-backdrop-contrast: ; 545 | --tw-backdrop-grayscale: ; 546 | --tw-backdrop-hue-rotate: ; 547 | --tw-backdrop-invert: ; 548 | --tw-backdrop-opacity: ; 549 | --tw-backdrop-saturate: ; 550 | --tw-backdrop-sepia: ; 551 | --tw-contain-size: ; 552 | --tw-contain-layout: ; 553 | --tw-contain-paint: ; 554 | --tw-contain-style: ; 555 | } 556 | 557 | .static { 558 | position: static; 559 | } 560 | 561 | .flex { 562 | display: flex; 563 | } 564 | 565 | .grid { 566 | display: grid; 567 | } 568 | 569 | .h-20 { 570 | height: 5rem; 571 | } 572 | 573 | .w-20 { 574 | width: 5rem; 575 | } 576 | 577 | .flex-col { 578 | flex-direction: column; 579 | } 580 | 581 | .justify-between { 582 | justify-content: space-between; 583 | } 584 | 585 | .gap-4 { 586 | gap: 1rem; 587 | } 588 | 589 | .p-6 { 590 | padding: 1.5rem; 591 | } 592 | 593 | .text-lg { 594 | font-size: 1.125rem; 595 | line-height: 1.75rem; 596 | } 597 | 598 | .text-sm { 599 | font-size: 0.875rem; 600 | line-height: 1.25rem; 601 | } 602 | 603 | .font-bold { 604 | font-weight: 700; 605 | } -------------------------------------------------------------------------------- /static/svg/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 11 | 13 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 57 | 60 | 63 | 66 | 69 | 72 | 75 | 78 | 81 | 84 | 87 | 90 | 93 | 96 | 99 | 102 | 105 | 108 | 111 | 114 | 117 | 120 | 123 | 126 | 129 | 132 | 135 | 138 | 141 | 144 | 147 | 150 | 153 | 156 | 159 | 162 | 165 | 168 | 171 | 174 | 177 | 180 | 183 | 186 | 189 | 192 | 195 | 198 | 201 | 204 | 207 | 210 | 213 | 216 | 219 | 222 | 225 | 228 | 231 | 234 | 237 | 240 | 243 | 246 | 249 | 252 | 255 | 258 | 261 | 264 | 267 | 270 | 273 | 276 | 279 | 282 | 285 | 288 | 291 | 294 | 297 | 300 | 303 | 306 | 307 | 308 | --------------------------------------------------------------------------------