├── .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 |
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 |
308 |
--------------------------------------------------------------------------------