Repository: a-h/templ

Issue:
LSP warning of editing generated file
**Describe the bug**
Just today, the templ lsp has started warning me that I am editing a generated file, even though I have never opened the *_templ.go files.

E.g., I open layout.templ, go to insert mode, start typing. As soon as I type, I get the error: `LSP[templ][Warning] Warning: editing layout_templ.go, a generated file.`

**To Reproduce**
I have no idea if this is an lsp bug or if I've done something dumb, unsure how to reproduce.

**Expected behavior**
No warning as I'm not editing the generated file.

**Logs**
```bash
which go gopls templ
/opt/homebrew/bin/go
/opt/homebrew/bin/gopls
/Users/macpig/go/bin/templ
```

**`templ info` output**
```
(✓) os [ goos=darwin goarch=arm64 ]
(✓) go [ location=/opt/homebrew/bin/go version=go version go1.24.4 darwin/arm64 ]
(✓) gopls [ location=/opt/homebrew/bin/gopls version=golang.org/x/tools/gopls v0.19.1 ]
(✓) templ [ location=/Users/macpig/go/bin/templ version=v0.3.898 ]
```

**Desktop (please complete the following information):**
- OS: MacOS
- templ CLI version v0.3.898
- Go version go version go1.24.4 darwin/arm64
- `gopls` version golang.org/x/tools/gopls v0.19.1

**Additional context**
My neovim lsp & treesitter config below:

**LSP**
```lua
return {
	"mason-org/mason-lspconfig.nvim",
	opts = {
		ensure_installed = {
			"clangd",
			"cssls",
			"gopls",
			"html",
			"htmx",
			"lua_ls",
			"pyright",
			"templ",
			"ts_ls",
		},
	},
	dependencies = {
		{ "j-hui/fidget.nvim",    opts = { notification = { window = { winblend = 0 } } } },
		{ "mason-org/mason.nvim", opts = {} },
		"neovim/nvim-lspconfig",
	},
}
```

**Treesitter**
```lua
return {
	"nvim-treesitter/nvim-treesitter",
	branch = "master",
	lazy = false,
	build = ":TSUpdate",
	opts = {
		ensure_installed = {
			"c",
			"css",
			"go",
			"html",
			"javascript",
			"jsdoc",
			"lua",
			"markdown",
			"markdown_inline",
			"python",
			"query",
			"sql",
			"templ",
			"toml",
			"typescript",
			"vim",
			"vimdoc",
			"yaml",
		},
		sync_install = false,
		auto_install = true,
		highlight = { enable = true },
		indent = { enable = true },
		additional_vim_regex_highlighting = false,
	},
	config = function(_, opts)
		require("nvim-treesitter.install").prefer_git = true
		require("nvim-treesitter.configs").setup(opts)
		vim.diagnostic.config({ virtual_text = true })
	end
}
```

Discussion:
@joerdav: This seems to be due to a new feature in `gopls` you are on the latest version so have picked up this change: https://github.com/golang/go/issues/73959


We should look to supress this.@joerdav: Actually.. the warning itself is not new. They just changed the message...@dimmerz92: I see, glad I didn't do something dumb, then!

Do you know if there is a way to suppress this?@joerdav: Previously I think we have scrubbed out the generated comment before sending to gopls, to trick gopls in a way. That could be a solution.

Solution patch:
diff --git a/cmd/templ/lspcmd/proxy/client.go b/cmd/templ/lspcmd/proxy/client.go
index 1058853..cf1248b 100644
--- a/cmd/templ/lspcmd/proxy/client.go
+++ b/cmd/templ/lspcmd/proxy/client.go
@@ -38,6 +38,7 @@ func (p Client) Progress(ctx context.Context, params *lsp.ProgressParams) (err e
 	p.Log.Info("client <- server: Progress")
 	return p.Target.Progress(ctx, params)
 }
+
 func (p Client) WorkDoneProgressCreate(ctx context.Context, params *lsp.WorkDoneProgressCreateParams) (err error) {
 	p.Log.Info("client <- server: WorkDoneProgressCreate")
 	return p.Target.WorkDoneProgressCreate(ctx, params)
@@ -103,6 +104,9 @@ func (p Client) ShowMessage(ctx context.Context, params *lsp.ShowMessageParams)
 	if strings.HasPrefix(params.Message, "Do not edit this file!") {
 		return
 	}
+	if strings.HasPrefix(params.Message, "Warning: editing") {
+		return
+	}
 	return p.Target.ShowMessage(ctx, params)
 }
 
diff --git a/docs/docs/03-syntax-and-usage/19-fragments.md b/docs/docs/03-syntax-and-usage/19-fragments.md
new file mode 100644
index 0000000..78f9bd5
--- /dev/null
+++ b/docs/docs/03-syntax-and-usage/19-fragments.md
@@ -0,0 +1,167 @@
+# Fragments
+
+The `templ.Fragment` component can be used to render a subsection of a template, discarding all other output.
+
+Fragments work well as an optimisation for HTMX, as discussed in https://htmx.org/essays/template-fragments/
+
+## Define fragments
+
+Define a fragment with `@templ.Fragment("name")`, where `"name"` is the identifier for the fragment.
+
+```templ
+templ Page() {
+  <div>Page Header</div>
+  @templ.Fragment("name") {
+    <div>Content of the fragment</div>
+  }
+}
+```
+
+To avoid name clashes with other libraries, you can define a custom type for your package.
+
+```templ
+type nameFragmentKey struct {}
+var Name = nameFragmentKey{}
+
+templ Page() {
+  <div>Page Header</div>
+  @templ.Fragment(Name) {
+    <div>Content of the fragment</div>
+  }
+}
+```
+
+## Use with HTTP
+
+The most common use case for `Fragment` is to render only a specific part of the template to the HTML response, while discarding the rest of the output.
+
+To render only the "name" fragment from the `Page` template, use the `templ.WithFragments("name")` option when creating the HTTP handler:
+
+```go title="main.go"
+handler := templ.Handler(Page(), templ.WithFragments("name"))
+http.Handle("/", handler)
+```
+
+When the HTTP request is made, only the content of the specified fragment will be returned in the response:
+
+```html title="output.html"
+<div>Content of the fragment</div>
+```
+
+:::note
+The whole of the template is rendered, so any function calls or logic in the template will still be executed, but only the specified fragment's output is sent to the client.
+:::
+
+If the `templ.WithFragments("name")` option is omitted, the whole page is rendered as normal.
+
+```go title="main.go"
+handler := templ.Handler(Page())
+http.Handle("/", handler)
+```
+
+```html title="output.html"
+<div>Page Header</div>
+<div>Content of the fragment</div>
+```
+
+## Use outside of an HTTP handler
+
+To use outside of an HTTP handler, e.g. when generating static content, you can render fragments with the `templ.RenderFragments` function.
+
+```go
+w := new(bytes.Buffer)
+if err := templ.RenderFragments(context.Background(), w, fragmentPage, "name"); err != nil {
+  t.Fatalf("failed to render: %v", err)
+}
+
+// <div>Content of the fragment</div>
+html := w.String()
+```
+
+:::note
+All fragments with matching identifiers will be rendered. If the fragment identifier isn't matched, no output will be produced.
+:::
+
+## Nested fragments
+
+Fragments can be nested, allowing for complex structures to be defined and rendered selectively.
+
+Given this example templ file:
+
+```templ
+templ Page() {
+	@templ.Fragment("outer") {
+		<div>Outer Fragment Start</div>
+		@templ.Fragment("inner") {
+			<div>Inner Fragment Content</div>
+		}
+		<div>Outer Fragment End</div>
+	}
+}
+```
+
+If the `outer` fragment is selected for rendering, then the `inner` fragment is also rendered.
+
+## HTMX example
+
+```templ title="main.templ"
+package main
+
+import (
+  "fmt"
+  "net/http"
+  "strconv"
+)
+
+type PageState struct {
+  Counter int
+  Next    int
+}
+
+templ Page(state PageState) {
+  <html>
+    <head>
+       <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.min.js"></script>
+       <link rel="stylesheet" href="https://unpkg.com/missing.css@1.1.3/dist/missing.min.css"/>
+    </head>
+    <body>
+      @templ.Fragment("buttonOnly") {
+        <button hx-get={ fmt.Sprintf("/?counter=%d&template=buttonOnly", state.Next) } hx-swap="outerHTML">
+          This Button Has Been Clicked { state.Counter } Times
+        </button>
+      }
+    </body>
+  </html>
+}
+
+// handleRequest does the work to execute the template (or fragment) and serve the result.
+// It's mostly boilerplate, so don't get hung up on it.
+func handleRequest(w http.ResponseWriter, r *http.Request) {
+  // Collect state info to pass to the template.
+  var state PageState
+  state.Counter, _ = strconv.Atoi(r.URL.Query().Get("counter"))
+  state.Next = state.Counter + 1
+
+  // If the template querystring paramater is set, render the pecific fragment.
+  var opts []func(*templ.ComponentHandler)
+  if templateName := r.URL.Query().Get("template"); templateName != "" {
+    opts = append(opts, templ.WithFragments(templateName))
+  }
+
+  // Render the template or fragment and serve it.
+  templ.Handler(Page(state), opts...).ServeHTTP(w, r)
+}
+
+func main() {
+  // Handle the template.
+  http.HandleFunc("/", handleRequest)
+  
+  // Start the server.
+  fmt.Println("Server is running at http://localhost:8080")
+  http.ListenAndServe("localhost:8080", nil)
+}
+```
+
+:::note
+This was adapted from `benpate`'s Go stdlib example at https://gist.github.com/benpate/f92b77ea9b3a8503541eb4b9eb515d8a
+:::
diff --git a/examples/htmx-fragments/README.md b/examples/htmx-fragments/README.md
new file mode 100644
index 0000000..84819c5
--- /dev/null
+++ b/examples/htmx-fragments/README.md
@@ -0,0 +1,7 @@
+## Tasks
+
+### run
+
+```bash
+templ generate --watch --proxy="http://localhost:8080" --cmd="go run ."
+```
diff --git a/examples/htmx-fragments/go.mod b/examples/htmx-fragments/go.mod
new file mode 100644
index 0000000..76e71ab
--- /dev/null
+++ b/examples/htmx-fragments/go.mod
@@ -0,0 +1,9 @@
+module github.com/a-h/templ/examples/htmx-fragments
+
+go 1.23.0
+
+toolchain go1.23.6
+
+require github.com/a-h/templ v0.2.747
+
+replace github.com/a-h/templ => ../../
diff --git a/examples/htmx-fragments/go.sum b/examples/htmx-fragments/go.sum
new file mode 100644
index 0000000..5a8d551
--- /dev/null
+++ b/examples/htmx-fragments/go.sum
@@ -0,0 +1,2 @@
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
diff --git a/examples/htmx-fragments/main.templ b/examples/htmx-fragments/main.templ
new file mode 100644
index 0000000..d41aa65
--- /dev/null
+++ b/examples/htmx-fragments/main.templ
@@ -0,0 +1,71 @@
+package main
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+)
+
+type PageState struct {
+	Counter int
+	Next    int
+}
+
+templ Page(state PageState) {
+	<!DOCTYPE html>
+	<html>
+		<head>
+			<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.min.js"></script>
+			<link rel="stylesheet" href="https://unpkg.com/missing.css@1.1.3/dist/missing.min.css"/>
+			<title>Template Fragment Example</title>
+		</head>
+		<body>
+			<h1>Template Fragment Example</h1>
+			<p>
+				This page demonstrates how to create and serve 
+				<a href="https://htmx.org/essays/template-fragments/">template fragments</a>
+				using <a href="https://templ.guide">templ</a> in Go.
+			</p>
+			<p>
+				This is accomplished by using the "templ.Fragment" component, which lets you
+				select areas to include in the output.
+			</p>
+			<p>
+				Adapted from https://gist.github.com/benpate/f92b77ea9b3a8503541eb4b9eb515d8a
+			</p>
+			<!-- Here's the fragment.  We can target it by executing the "buttonOnly" template. -->
+			@templ.Fragment("buttonOnly") {
+				<button hx-get={ fmt.Sprintf("/?counter=%d&template=buttonOnly", state.Next) } hx-swap="outerHTML">
+					This Button Has Been Clicked { state.Counter } Times
+				</button>
+			}
+		</body>
+	</html>
+}
+
+// handleRequest does the work to execute the template (or fragment) and serve the result.
+// It's mostly boilerplate, so don't get hung up on it.
+func handleRequest(w http.ResponseWriter, r *http.Request) {
+	// Collect state info to pass to the template.
+	var state PageState
+	state.Counter, _ = strconv.Atoi(r.URL.Query().Get("counter"))
+	state.Next = state.Counter + 1
+
+	// If the template querystring paramater is set, render the pecific fragment.
+	var opts []func(*templ.ComponentHandler)
+	if templateName := r.URL.Query().Get("template"); templateName != "" {
+		opts = append(opts, templ.WithFragments(templateName))
+	}
+
+	// Render the template or fragment and serve it.
+	templ.Handler(Page(state), opts...).ServeHTTP(w, r)
+}
+
+func main() {
+	// Handle the template.
+	http.HandleFunc("/", handleRequest)
+
+	// Start the server.
+	fmt.Println("Server is running at http://localhost:8080")
+	http.ListenAndServe("localhost:8080", nil)
+}
diff --git a/examples/htmx-fragments/main_templ.go b/examples/htmx-fragments/main_templ.go
new file mode 100644
index 0000000..aa7f002
--- /dev/null
+++ b/examples/htmx-fragments/main_templ.go
@@ -0,0 +1,129 @@
+// Code generated by templ - DO NOT EDIT.
+
+package main
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+import (
+	"fmt"
+	"net/http"
+	"strconv"
+)
+
+type PageState struct {
+	Counter int
+	Next    int
+}
+
+func Page(state PageState) templ.Component {
+	return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+		templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+		if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+			return templ_7745c5c3_CtxErr
+		}
+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+		if !templ_7745c5c3_IsBuffer {
+			defer func() {
+				templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+				if templ_7745c5c3_Err == nil {
+					templ_7745c5c3_Err = templ_7745c5c3_BufErr
+				}
+			}()
+		}
+		ctx = templ.InitializeContext(ctx)
+		templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var1 == nil {
+			templ_7745c5c3_Var1 = templ.NopComponent
+		}
+		ctx = templ.ClearChildren(ctx)
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html><head><script src=\"https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.min.js\"></script><link rel=\"stylesheet\" href=\"https://unpkg.com/missing.css@1.1.3/dist/missing.min.css\"><title>Template Fragment Example</title></head><body><h1>Template Fragment Example</h1><p>This page demonstrates how to create and serve  <a href=\"https://htmx.org/essays/template-fragments/\">template fragments</a> using <a href=\"https://templ.guide\">templ</a> in Go.</p><p>This is accomplished by using the \"templ.Fragment\" component, which lets you select areas to include in the output.</p><p>Adapted from https://gist.github.com/benpate/f92b77ea9b3a8503541eb4b9eb515d8a</p><!-- Here's the fragment.  We can target it by executing the \"buttonOnly\" template. -->")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+			templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+			templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+			if !templ_7745c5c3_IsBuffer {
+				defer func() {
+					templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+					if templ_7745c5c3_Err == nil {
+						templ_7745c5c3_Err = templ_7745c5c3_BufErr
+					}
+				}()
+			}
+			ctx = templ.InitializeContext(ctx)
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<button hx-get=\"")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			var templ_7745c5c3_Var3 string
+			templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/?counter=%d&template=buttonOnly", state.Next))
+			if templ_7745c5c3_Err != nil {
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/htmx-fragments/main.templ`, Line: 38, Col: 80}
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" hx-swap=\"outerHTML\">This Button Has Been Clicked ")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			var templ_7745c5c3_Var4 string
+			templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(state.Counter)
+			if templ_7745c5c3_Err != nil {
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/htmx-fragments/main.templ`, Line: 39, Col: 49}
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " Times</button>")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			return nil
+		})
+		templ_7745c5c3_Err = templ.Fragment("buttonOnly").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</body></html>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		return nil
+	})
+}
+
+// handleRequest does the work to execute the template (or fragment) and serve the result.
+// It's mostly boilerplate, so don't get hung up on it.
+func handleRequest(w http.ResponseWriter, r *http.Request) {
+	// Collect state info to pass to the template.
+	var state PageState
+	state.Counter, _ = strconv.Atoi(r.URL.Query().Get("counter"))
+	state.Next = state.Counter + 1
+
+	// If the template querystring paramater is set, render the pecific fragment.
+	var opts []func(*templ.ComponentHandler)
+	if templateName := r.URL.Query().Get("template"); templateName != "" {
+		opts = append(opts, templ.WithFragments(templateName))
+	}
+
+	// Render the template or fragment and serve it.
+	templ.Handler(Page(state), opts...).ServeHTTP(w, r)
+}
+
+func main() {
+	// Handle the template.
+	http.HandleFunc("/", handleRequest)
+
+	// Start the server.
+	fmt.Println("Server is running at http://localhost:8080")
+	http.ListenAndServe("localhost:8080", nil)
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/fragment.go b/fragment.go
new file mode 100644
index 0000000..0b8624d
--- /dev/null
+++ b/fragment.go
@@ -0,0 +1,70 @@
+package templ
+
+import (
+	"context"
+	"io"
+	"slices"
+)
+
+// RenderFragments renders the specified fragments to w.
+func RenderFragments(ctx context.Context, w io.Writer, c Component, ids ...any) error {
+	ctx = context.WithValue(ctx, fragmentContextKey, &FragmentContext{
+		W:   w,
+		IDs: ids,
+	})
+	return c.Render(ctx, io.Discard)
+}
+
+type fragmentContextKeyType int
+
+const fragmentContextKey fragmentContextKeyType = iota
+
+// FragmentContext is used to control rendering of fragments within a template.
+type FragmentContext struct {
+	W      io.Writer
+	IDs    []any
+	Active bool
+}
+
+// Fragment defines a fragment within a template that can be rendered conditionally based on the id.
+// You can use it to render a specific part of a page, e.g. to reduce the amount of HTML returned from a HTMX-initiated request.
+// Any non-matching contents of the template are rendered, but discarded by the FramentWriter.
+func Fragment(id any) Component {
+	return &fragment{
+		ID: id,
+	}
+}
+
+type fragment struct {
+	ID any
+}
+
+func (f *fragment) Render(ctx context.Context, w io.Writer) (err error) {
+	// If not in a fragment context, if we're a child fragment, or in a mismatching fragment context, render children normally.
+	fragmentCtx := getFragmentContext(ctx)
+	if fragmentCtx == nil || fragmentCtx.Active || !slices.Contains(fragmentCtx.IDs, f.ID) {
+		return GetChildren(ctx).Render(ctx, w)
+	}
+
+	// Instruct child fragments to render their contents normally, because the writer
+	// passed to them is already the FragmentContext's writer.
+	fragmentCtx.Active = true
+	defer func() {
+		fragmentCtx.Active = false
+	}()
+	return GetChildren(ctx).Render(ctx, fragmentCtx.W)
+}
+
+// getFragmentContext retrieves the FragmentContext from the provided context. It returns nil if no
+// FragmentContext is found or if the context value is of an unexpected type.
+func getFragmentContext(ctx context.Context) *FragmentContext {
+	ctxValue := ctx.Value(fragmentContextKey)
+	if ctxValue == nil {
+		return nil
+	}
+	v, ok := ctxValue.(*FragmentContext)
+	if !ok {
+		return nil
+	}
+	return v
+}
diff --git a/generator/generator.go b/generator/generator.go
index ad30430..5d01c01 100644
--- a/generator/generator.go
+++ b/generator/generator.go
@@ -1344,7 +1344,7 @@ func (g *generator) writeExpressionAttribute(indentLevel int, elementName string
 	}
 	attrKey := html.EscapeString(attr.Key.String())
 	// Value.
-	if (elementName == "a" && attrKey == "href") || (elementName == "form" && attrKey == "action") {
+	if isExpressionAttributeValueURL(elementName, attrKey) {
 		if err := g.writeExpressionAttributeValueURL(indentLevel, attr); err != nil {
 			return err
 		}
@@ -1789,3 +1789,20 @@ func stripTypes(parameters string) string {
 	}
 	return strings.Join(variableNames, ", ")
 }
+
+func isExpressionAttributeValueURL(elementName, attrName string) bool {
+	var elementAttributeLookup = map[string]string{
+		"a":      "href",
+		"form":   "action",
+		"link":   "href",
+		"object": "data",
+	}
+	expectedAttribute, exists := elementAttributeLookup[elementName]
+	if !exists {
+		return false
+	}
+	if attrName != expectedAttribute {
+		return false
+	}
+	return true
+}
diff --git a/generator/test-fragment/complete.html b/generator/test-fragment/complete.html
new file mode 100644
index 0000000..e7462f8
--- /dev/null
+++ b/generator/test-fragment/complete.html
@@ -0,0 +1,7 @@
+<div>Page Header</div>
+<div>Fragment Content A</div>
+<div>Fragment Content B</div>
+<div>Outer Fragment Start</div>
+<div>Inner Fragment Content</div>
+<div>Outer Fragment End</div>
+<div>Page Footer</div>
\ No newline at end of file
diff --git a/generator/test-fragment/template.templ b/generator/test-fragment/template.templ
new file mode 100644
index 0000000..6e49e23
--- /dev/null
+++ b/generator/test-fragment/template.templ
@@ -0,0 +1,19 @@
+package testfragment
+
+templ Page() {
+	<div>Page Header</div>
+	@templ.Fragment("content-a") {
+		<div>Fragment Content A</div>
+	}
+	@templ.Fragment("content-b") {
+		<div>Fragment Content B</div>
+	}
+	@templ.Fragment("outer") {
+		<div>Outer Fragment Start</div>
+		@templ.Fragment("inner") {
+			<div>Inner Fragment Content</div>
+		}
+		<div>Outer Fragment End</div>
+	}
+	<div>Page Footer</div>
+}
diff --git a/handler.go b/handler.go
index a28d561..ae1552b 100644
--- a/handler.go
+++ b/handler.go
@@ -1,6 +1,8 @@
 package templ
 
-import "net/http"
+import (
+	"net/http"
+)
 
 // ComponentHandler is a http.Handler that renders components.
 type ComponentHandler struct {
@@ -9,25 +11,55 @@ type ComponentHandler struct {
 	ContentType    string
 	ErrorHandler   func(r *http.Request, err error) http.Handler
 	StreamResponse bool
+	FragmentIDs    []any
 }
 
 const componentHandlerErrorMessage = "templ: failed to render template"
 
-func (ch *ComponentHandler) ServeHTTPBuffered(w http.ResponseWriter, r *http.Request) {
+func (ch *ComponentHandler) handleRenderErr(w http.ResponseWriter, r *http.Request, err error) {
+	if ch.ErrorHandler != nil {
+		w.Header().Set("Content-Type", ch.ContentType)
+		ch.ErrorHandler(r, err).ServeHTTP(w, r)
+		return
+	}
+	http.Error(w, componentHandlerErrorMessage, http.StatusInternalServerError)
+}
+
+func (ch *ComponentHandler) ServeHTTPBufferedFragment(w http.ResponseWriter, r *http.Request) {
 	// Since the component may error, write to a buffer first.
 	// This prevents partial responses from being written to the client.
 	buf := GetBuffer()
 	defer ReleaseBuffer(buf)
-	err := ch.Component.Render(r.Context(), buf)
-	if err != nil {
-		if ch.ErrorHandler != nil {
-			w.Header().Set("Content-Type", ch.ContentType)
-			ch.ErrorHandler(r, err).ServeHTTP(w, r)
-			return
-		}
-		http.Error(w, componentHandlerErrorMessage, http.StatusInternalServerError)
+
+	// Render the component into io.Discard, but use the buffer for fragments.
+	if err := RenderFragments(r.Context(), buf, ch.Component, ch.FragmentIDs...); err != nil {
+		ch.handleRenderErr(w, r, err)
+		return
+	}
+
+	// The component rendered successfully, we can write the Content-Type and Status.
+	w.Header().Set("Content-Type", ch.ContentType)
+	if ch.Status != 0 {
+		w.WriteHeader(ch.Status)
+	}
+	// Ignore write error like http.Error() does, because there is
+	// no way to recover at this point.
+	_, _ = w.Write(buf.Bytes())
+}
+
+func (ch *ComponentHandler) ServeHTTPBufferedComplete(w http.ResponseWriter, r *http.Request) {
+	// Since the component may error, write to a buffer first.
+	// This prevents partial responses from being written to the client.
+	buf := GetBuffer()
+	defer ReleaseBuffer(buf)
+
+	// Render the component into the buffer.
+	if err := ch.Component.Render(r.Context(), buf); err != nil {
+		ch.handleRenderErr(w, r, err)
 		return
 	}
+
+	// The component rendered successfully, we can write the Content-Type and Status.
 	w.Header().Set("Content-Type", ch.ContentType)
 	if ch.Status != 0 {
 		w.WriteHeader(ch.Status)
@@ -37,18 +69,39 @@ func (ch *ComponentHandler) ServeHTTPBuffered(w http.ResponseWriter, r *http.Req
 	_, _ = w.Write(buf.Bytes())
 }
 
+func (ch *ComponentHandler) ServeHTTPBuffered(w http.ResponseWriter, r *http.Request) {
+	// If fragments are specified, render only those.
+	if len(ch.FragmentIDs) > 0 {
+		ch.ServeHTTPBufferedFragment(w, r)
+		return
+	}
+
+	// Otherwise, render the complete component.
+	ch.ServeHTTPBufferedComplete(w, r)
+}
+
 func (ch *ComponentHandler) ServeHTTPStreamed(w http.ResponseWriter, r *http.Request) {
+	// If streaming, we do not buffer the response, so set the headers immediately.
 	w.Header().Set("Content-Type", ch.ContentType)
 	if ch.Status != 0 {
 		w.WriteHeader(ch.Status)
 	}
-	if err := ch.Component.Render(r.Context(), w); err != nil {
-		if ch.ErrorHandler != nil {
-			w.Header().Set("Content-Type", ch.ContentType)
-			ch.ErrorHandler(r, err).ServeHTTP(w, r)
+
+	// Pass fragment names to the context if specified.
+	if len(ch.FragmentIDs) > 0 {
+
+		// Render the component into io.Discard, but use the buffer for fragments.
+		if err := RenderFragments(r.Context(), w, ch.Component, ch.FragmentIDs...); err != nil {
+			ch.handleRenderErr(w, r, err)
 			return
 		}
-		http.Error(w, componentHandlerErrorMessage, http.StatusInternalServerError)
+		return
+	}
+
+	// Render the component into the buffer.
+	if err := ch.Component.Render(r.Context(), w); err != nil {
+		ch.handleRenderErr(w, r, err)
+		return
 	}
 }
 
@@ -100,3 +153,11 @@ func WithStreaming() func(*ComponentHandler) {
 		ch.StreamResponse = true
 	}
 }
+
+// WithFragments sets the ids of the fragments to render.
+// If not set, all content is rendered.
+func WithFragments(ids ...any) func(*ComponentHandler) {
+	return func(ch *ComponentHandler) {
+		ch.FragmentIDs = ids
+	}
+}



Test patch:
diff --git a/fragment_test.go b/fragment_test.go
new file mode 100644
index 0000000..7ccef72
--- /dev/null
+++ b/fragment_test.go
@@ -0,0 +1,34 @@
+package templ_test
+
+import (
+	"bytes"
+	"context"
+	"io"
+	"testing"
+
+	"github.com/a-h/templ"
+)
+
+func TestFragment(t *testing.T) {
+	fragmentPage := templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
+		if _, err := io.WriteString(w, "page_contents"); err != nil {
+			return err
+		}
+		fragmentContents := templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
+			_, err := io.WriteString(w, "fragment_contents")
+			return err
+		})
+		return templ.Fragment("fragment").Render(templ.WithChildren(ctx, fragmentContents), w)
+	})
+	t.Run("can render without a HTTP handler", func(t *testing.T) {
+		w := new(bytes.Buffer)
+		if err := templ.RenderFragments(context.Background(), w, fragmentPage, "fragment"); err != nil {
+			t.Fatalf("failed to render: %v", err)
+		}
+
+		// Note that the fragment should have been written to the output.
+		if w.String() != "fragment_contents" {
+			t.Errorf("expected output 'fragment_contents', got '%s'", w.String())
+		}
+	})
+}
diff --git a/generator/generator_test.go b/generator/generator_test.go
index 08ba9e3..ac72df6 100644
--- a/generator/generator_test.go
+++ b/generator/generator_test.go
@@ -70,3 +70,38 @@ templ Hello(name string) {
 		t.Errorf("expected an expression for the package name, template signature (Hello) and for the if (nam), got %#v", op.SourceMap.Expressions)
 	}
 }
+
+func TestIsExpressionAttributeValueURL(t *testing.T) {
+	testCases := []struct {
+		elementName    string
+		attrName       string
+		expectedOutput bool
+	}{
+		{
+			elementName:    "a",
+			attrName:       "href",
+			expectedOutput: true,
+		},
+		{
+			elementName:    "a",
+			attrName:       "class",
+			expectedOutput: false,
+		},
+		{
+			elementName:    "div",
+			attrName:       "class",
+			expectedOutput: false,
+		},
+		{
+			elementName:    "p",
+			attrName:       "href",
+			expectedOutput: false,
+		},
+	}
+
+	for _, testCase := range testCases {
+		if output := isExpressionAttributeValueURL(testCase.elementName, testCase.attrName); output != testCase.expectedOutput {
+			t.Errorf("expected %t got %t", testCase.expectedOutput, output)
+		}
+	}
+}
diff --git a/generator/test-fragment/render_test.go b/generator/test-fragment/render_test.go
new file mode 100644
index 0000000..706d7ba
--- /dev/null
+++ b/generator/test-fragment/render_test.go
@@ -0,0 +1,80 @@
+package testfragment
+
+import (
+	_ "embed"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/a-h/templ"
+	"github.com/a-h/templ/generator/htmldiff"
+)
+
+//go:embed complete.html
+var complete string
+
+func Test(t *testing.T) {
+	tests := []struct {
+		name     string
+		handler  http.Handler
+		expected string
+	}{
+		{
+			name:     "complete pages can be rendered",
+			handler:  templ.Handler(Page()),
+			expected: complete,
+		},
+		{
+			name:     "single fragments can be rendered",
+			handler:  templ.Handler(Page(), templ.WithFragments("content-a")),
+			expected: `<div>Fragment Content A</div>`,
+		},
+		{
+			name:     "multiple fragments can be rendered",
+			handler:  templ.Handler(Page(), templ.WithFragments("content-a", "content-b")),
+			expected: `<div>Fragment Content A</div><div>Fragment Content B</div>`,
+		},
+		{
+			name:     "outer fragments render their contents, even if inner fragments are not requested",
+			handler:  templ.Handler(Page(), templ.WithFragments("outer")),
+			expected: `<div>Outer Fragment Start</div><div>Inner Fragment Content</div><div>Outer Fragment End</div>`,
+		},
+		{
+			name:     "inner fragments can be rendered without the outer fragment",
+			handler:  templ.Handler(Page(), templ.WithFragments("inner")),
+			expected: `<div>Inner Fragment Content</div>`,
+		},
+		{
+			name:     "fragments that don't exist return an empty string",
+			handler:  templ.Handler(Page(), templ.WithFragments("non-existent")),
+			expected: "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+
+			w := httptest.NewRecorder()
+			r := httptest.NewRequest("GET", "/", nil)
+			tt.handler.ServeHTTP(w, r)
+
+			if actualStatusCode := w.Result().StatusCode; http.StatusOK != actualStatusCode {
+				t.Errorf("expected status %d, got %d", http.StatusOK, actualStatusCode)
+			}
+
+			body, err := io.ReadAll(w.Result().Body)
+			if err != nil {
+				t.Errorf("failed to read body: %v", err)
+			}
+
+			diff, err := htmldiff.DiffStrings(tt.expected, string(body))
+			if err != nil {
+				t.Fatalf("failed to diff: %v", err)
+			}
+			if diff != "" {
+				t.Error(diff)
+			}
+		})
+	}
+}
diff --git a/generator/test-fragment/template_templ.go b/generator/test-fragment/template_templ.go
new file mode 100644
index 0000000..7c4618d
--- /dev/null
+++ b/generator/test-fragment/template_templ.go
@@ -0,0 +1,135 @@
+// Code generated by templ - DO NOT EDIT.
+
+package testfragment
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+func Page() templ.Component {
+	return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+		templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+		if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
+			return templ_7745c5c3_CtxErr
+		}
+		templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+		if !templ_7745c5c3_IsBuffer {
+			defer func() {
+				templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+				if templ_7745c5c3_Err == nil {
+					templ_7745c5c3_Err = templ_7745c5c3_BufErr
+				}
+			}()
+		}
+		ctx = templ.InitializeContext(ctx)
+		templ_7745c5c3_Var1 := templ.GetChildren(ctx)
+		if templ_7745c5c3_Var1 == nil {
+			templ_7745c5c3_Var1 = templ.NopComponent
+		}
+		ctx = templ.ClearChildren(ctx)
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div>Page Header</div>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+			templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+			templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+			if !templ_7745c5c3_IsBuffer {
+				defer func() {
+					templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+					if templ_7745c5c3_Err == nil {
+						templ_7745c5c3_Err = templ_7745c5c3_BufErr
+					}
+				}()
+			}
+			ctx = templ.InitializeContext(ctx)
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div>Fragment Content A</div>")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			return nil
+		})
+		templ_7745c5c3_Err = templ.Fragment("content-a").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		templ_7745c5c3_Var3 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+			templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+			templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+			if !templ_7745c5c3_IsBuffer {
+				defer func() {
+					templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+					if templ_7745c5c3_Err == nil {
+						templ_7745c5c3_Err = templ_7745c5c3_BufErr
+					}
+				}()
+			}
+			ctx = templ.InitializeContext(ctx)
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div>Fragment Content B</div>")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			return nil
+		})
+		templ_7745c5c3_Err = templ.Fragment("content-b").Render(templ.WithChildren(ctx, templ_7745c5c3_Var3), templ_7745c5c3_Buffer)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		templ_7745c5c3_Var4 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+			templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+			templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+			if !templ_7745c5c3_IsBuffer {
+				defer func() {
+					templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+					if templ_7745c5c3_Err == nil {
+						templ_7745c5c3_Err = templ_7745c5c3_BufErr
+					}
+				}()
+			}
+			ctx = templ.InitializeContext(ctx)
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div>Outer Fragment Start</div>")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			templ_7745c5c3_Var5 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
+				templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
+				templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
+				if !templ_7745c5c3_IsBuffer {
+					defer func() {
+						templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
+						if templ_7745c5c3_Err == nil {
+							templ_7745c5c3_Err = templ_7745c5c3_BufErr
+						}
+					}()
+				}
+				ctx = templ.InitializeContext(ctx)
+				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div>Inner Fragment Content</div>")
+				if templ_7745c5c3_Err != nil {
+					return templ_7745c5c3_Err
+				}
+				return nil
+			})
+			templ_7745c5c3_Err = templ.Fragment("inner").Render(templ.WithChildren(ctx, templ_7745c5c3_Var5), templ_7745c5c3_Buffer)
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " <div>Outer Fragment End</div>")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			return nil
+		})
+		templ_7745c5c3_Err = templ.Fragment("outer").Render(templ.WithChildren(ctx, templ_7745c5c3_Var4), templ_7745c5c3_Buffer)
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div>Page Footer</div>")
+		if templ_7745c5c3_Err != nil {
+			return templ_7745c5c3_Err
+		}
+		return nil
+	})
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/handler_test.go b/handler_test.go
index c39d6bc..d7aecdd 100644
--- a/handler_test.go
+++ b/handler_test.go
@@ -25,6 +25,29 @@ func TestHandler(t *testing.T) {
 		}
 		return errors.New("handler error")
 	})
+	fragmentPage := templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
+		if _, err := io.WriteString(w, "page_contents\n"); err != nil {
+			return err
+		}
+		fragmentContents := templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
+			_, err := io.WriteString(w, "fragment_contents")
+			return err
+		})
+		return templ.Fragment("fragment").Render(templ.WithChildren(ctx, fragmentContents), w)
+	})
+
+	type nameFragmentKey struct{}
+	var fragmentContentsName = nameFragmentKey{}
+	fragmentPageWithCustomKey := templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
+		if _, err := io.WriteString(w, "page_contents\n"); err != nil {
+			return err
+		}
+		fragmentContents := templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
+			_, err := io.WriteString(w, "fragment_contents")
+			return err
+		})
+		return templ.Fragment(fragmentContentsName).Render(templ.WithChildren(ctx, fragmentContents), w)
+	})
 
 	tests := []struct {
 		name             string
@@ -84,6 +107,34 @@ func TestHandler(t *testing.T) {
 			expectedMIMEType: "text/html; charset=utf-8",
 			expectedBody:     "custom body",
 		},
+		{
+			name:             "fragments can be rendered individually",
+			input:            templ.Handler(fragmentPage, templ.WithFragments("fragment")),
+			expectedStatus:   http.StatusOK,
+			expectedMIMEType: "text/html; charset=utf-8",
+			expectedBody:     "fragment_contents",
+		},
+		{
+			name:             "fragments can be rendered with a custom key type",
+			input:            templ.Handler(fragmentPageWithCustomKey, templ.WithFragments(fragmentContentsName)),
+			expectedStatus:   http.StatusOK,
+			expectedMIMEType: "text/html; charset=utf-8",
+			expectedBody:     "fragment_contents",
+		},
+		{
+			name:             "fragments can be rendered within a page",
+			input:            templ.Handler(fragmentPage),
+			expectedStatus:   http.StatusOK,
+			expectedMIMEType: "text/html; charset=utf-8",
+			expectedBody:     "page_contents\nfragment_contents",
+		},
+		{
+			name:             "fragments can be streamed",
+			input:            templ.Handler(fragmentPage, templ.WithFragments("fragment"), templ.WithStreaming()),
+			expectedStatus:   http.StatusOK,
+			expectedMIMEType: "text/html; charset=utf-8",
+			expectedBody:     "fragment_contents",
+		},
 	}
 	for _, tt := range tests {
 		tt := tt