temple

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 28, 2025 License: MIT Imports: 16 Imported by: 0

README

temple

temple is an HTML templating library for Go. It doesn't use its own syntax, preferring to use the standard library's html/templates. temple's purpose is to make working with the standard library's templates a little bit more manageable.

It is for people who want to render HTML from their servers like it's 2003 again.

Approach

Component-driven

Go templates come in two parts: the template literal, and the data to render that template literal with. These two parts are tied together in a fragile protocol that the compiler does not help you at all with. You need to make sure that the data to render is passed in the format the template literal expects, and that when one changes the other changes, as well.

temple addresses this by wrapping both parts inside an abstraction called a "Component". A Component is anything that can surface a template literal.

A Component that can be rendered as a standalone page, instead of just being part of another page, is called a Page. When a Page is rendered the data passed to the template literal will contain the Page itself as $.Page. In this way, temple codifies the data being passed in, and identifies the template literal it is passed to at the same time. A Page can serve as the interface that a template is accessed through, exposing the data it expects and the format it expects it in. A Component allows a Page to reference another template through an interface that exposes the data it expects and the format it expects it in.

Access to global state

It's a pain to try and build everything out of Components that you explicitly pass state through. Sometimes you want something like your site's name to be configurable, and you don't want to pipe that through every Component. To that end, temple defines a "Site" type. The Site will be included in the data passed to every template, as $.Site.

JavaScript and CSS stay with their Components

A Component can optionally indicate that it embeds JavaScript directly into the HTML, links to a JavaScript file loaded at runtime, embeds CSS directly into the HTML, links to a CSS file loaded at runtime, or any combination of these approaches. JavaScript and CSS can get loaded only on the pages they are needed for by tagging along with Components when the Component is rendered.

Any of these resources can also declare a relationship to another resource, controlling the order in which they get rendered to the page, allowing for fine-grained control over how resources end up being loaded in the HTML. By default, if any Component's Embedder or Linker methods return more than one resource in a single slice, those resources will be rendered in the order they appear in the slice. Explicitly declaring a relationship to any other resources disables this implicit relationship, as does setting the DisableImplicitOrdering property on the resource to true.

HTML agnostic

temple is a layer on top of html/template, but it isn't attempting to proscribe how you write your HTML. It strives to offer the least-restrictive possible interface on top of html/template; just enough to create its Components system and resource-loading system.

Resilient to errors

Every Site can fill a ServerErrorPager interface, which describes an error page to render if there's a problem rendering the page. By default, Pages are rendered into memory before being copied over the wire, to prevent half a template from being sent. To disable this default behavior, have either a Site, Page, or Component (depending on whether you want the behavior disabled globally, per-Page, or only for Pages that include the Component) implement the RenderConfigurer interface, and have it return RenderOptionDisablePageBuffering as one of the RenderOptions:

func (Site) ConfigureRender() []temple.RenderOption {
    return []temple.RenderOption{
        temple.RenderOptionDisablePageBuffering(true),
    }
}

This will disable the buffering, allowing the HTML to be written as a stream.

Examples

See the runnable, tested examples on pkg.go.dev.

Documentation

Overview

Package temple provides an HTML rendering framework built on top of the html/template package.

temple is organized around Components and Pages. A Component is some piece of the HTML document that you want included in the page's output. A Page is a Component that gets rendered itself rather than being included in another Component. The homepage of a site is probably a Page; the site's navbar is probably a Component, as is the base layout that all pages have in common.

temple also has the concept of a Site. Each server should have a Site, which acts as a singleton for the server and provides the fs.FS containing the templates that Components are using. A Site will also be available at render time, as .Site, so it can hold configuration data used across all pages.

To render a page, pass it to the Render function. The page itself will be made available as .Page within the template, and the Site will be available as .Site.

Components tend to be structs, with properties for whatever data they want to pass to their templates. When a Component relies on another Component, our homepage including a navbar for example, a good practice is to make an instance of the navbar Component a property on the homepage Component struct. That allows the homepage to select, e.g., which link in the navbar is highlighted as active. It's also a good idea to include the navbar Component in the output of a UseComponents method on the homepage Component, so all its methods (the templates it uses, any CSS or JS that it embeds or links to, any Components _it_ relies on...) will all get included whenever the homepage Component is rendered.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrNoTemplatePath is returned when a template path is needed, but
	// none are supplied.
	ErrNoTemplatePath = errors.New("need at least one template path")

	// ErrTemplatePatternMatchesNoFiles is returned when a template path is
	// a pattern, but that pattern doesn't match any files.
	ErrTemplatePatternMatchesNoFiles = errors.New("pattern matches no files")
)
View Source
var (
	// ErrCSSInlineTemplatePathNotSet is returned when the TemplatePath
	// property on a CSSInline is not set, which isn't valid. The
	// TemplatePath controls what CSS to embed, so it should never be
	// omitted.
	ErrCSSInlineTemplatePathNotSet = errors.New("CSSInline TemplatePath must be set")
)
View Source
var (
	// ErrJSInlineTemplatePathNotSet is returned when the TemplatePath
	// property on a JSInline is not set, which isn't valid. The
	// TemplatePath controls what JavaScript to embed, so it should never
	// be omitted.
	ErrJSInlineTemplatePathNotSet = errors.New("JSInline TemplatePath must be set")
)
View Source
var (
	// ErrResourceCycle is returned when a dependency cycle between
	// resources is found. This should never happen; it means that the
	// resource another resource depends on itself depends on that other
	// resource. It always indicates a misconfiguration of the resource
	// dependency graph, and means that the ResourceRelationship returned
	// from the RelationCalculator property on a struct is problematic.
	ErrResourceCycle = errors.New("resource cycle detected")
)

Functions

func LoggingContext

func LoggingContext(ctx context.Context, logger *slog.Logger) context.Context

LoggingContext returns a context.Context with the slog.Logger embedded in it in such a way that temple will be able to find it. Passing the returned context.Context to temple functions will let temple write its logging output to that logger.

func Render

func Render[SiteType Site, PageType Page](ctx context.Context, out io.Writer, site SiteType, page PageType)

Render renders the passed Page to the Writer. If it can't, a server error page is written instead. If the Site implements ServerErrorPager, that will be rendered; if not, a simple text page indicating a server error will be written.

Example (Basic)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type BasicSite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

type BasicHomePage struct {
	Name   string
	Layout BasicLayout
}

func (BasicHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (page BasicHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		page.Layout,
	}
}

func (BasicHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (page BasicHomePage) ExecutedTemplate(_ context.Context) string {
	return page.Layout.BaseTemplate()
}

type BasicLayout struct {
}

func (layout BasicLayout) Templates(_ context.Context) []string {
	return []string{layout.BaseTemplate()}
}

func (BasicLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.Name }}. This is my home page.{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := BasicSite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := BasicHomePage{
		Name:   "Visitor",
		Layout: BasicLayout{},
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title></head>
	<body>
		Hello, Visitor. This is my home page.</body>
</html>
Example (DuplicateComponents)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type DuplicateComponentsSite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

type DuplicateComponentsHomePage struct {
	Name   string
	Layout DuplicateComponentsLayout
	Avatar DuplicateComponentsAvatar
}

func (DuplicateComponentsHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (page DuplicateComponentsHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		page.Layout,
		page.Avatar,
	}
}

func (DuplicateComponentsHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (page DuplicateComponentsHomePage) ExecutedTemplate(_ context.Context) string {
	return page.Layout.BaseTemplate()
}

type DuplicateComponentsLayout struct {
	Avatar DuplicateComponentsAvatar
}

func (layout DuplicateComponentsLayout) Templates(_ context.Context) []string {
	return []string{layout.BaseTemplate()}
}

func (DuplicateComponentsLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func (layout DuplicateComponentsLayout) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		layout.Avatar,
	}
}

type DuplicateComponentsAvatar struct {
	URL string
}

func (DuplicateComponentsAvatar) Templates(_ context.Context) []string {
	return []string{"avatar.html.tmpl"}
}

func (DuplicateComponentsAvatar) Key(_ context.Context) string {
	return "avatar.html.tmpl"
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl":   `{{ define "body" }}Hello, {{ .Page.Name }}. This is my home page. Here is your avatar: {{ block "avatar" .Page.Avatar }}{{ end }}{{ end }}`,
		"avatar.html.tmpl": `{{ define "avatar" }}<img src="{{ .URL }}">{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		<nav>
			<h1>{{ .Site.Title }}</h1>
			<span class="align-right">{{ block "avatar" .Page.Layout.Avatar }}{{ end }}</span>
		</nav>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := DuplicateComponentsSite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := DuplicateComponentsHomePage{
		Name: "Visitor",
		Layout: DuplicateComponentsLayout{
			Avatar: DuplicateComponentsAvatar{
				URL: "https://www.example.com/me.jpg",
			},
		},
		Avatar: DuplicateComponentsAvatar{
			URL: "https://www.example.com/me.jpg",
		},
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title></head>
	<body>
		<nav>
			<h1>My Example Site</h1>
			<span class="align-right"><img src="https://www.example.com/me.jpg"></span>
		</nav>
		Hello, Visitor. This is my home page. Here is your avatar: <img src="https://www.example.com/me.jpg"></body>
</html>
Example (DuplicateResources)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type DuplicateResourcesSite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

type DuplicateResourcesHomePage struct {
	Name   string
	Layout DuplicateResourcesLayout
}

func (DuplicateResourcesHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (page DuplicateResourcesHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		page.Layout,
	}
}

func (DuplicateResourcesHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (page DuplicateResourcesHomePage) ExecutedTemplate(_ context.Context) string {
	return page.Layout.BaseTemplate()
}

func (DuplicateResourcesHomePage) LinkCSS(_ context.Context) []temple.CSSLink {
	return []temple.CSSLink{
		{Href: "https://example.com/b.css", Type: "text/css", Rel: "stylesheet"},
		{Href: "https://example.com/c.css", Type: "text/css", Rel: "stylesheet"},
	}
}

func (DuplicateResourcesHomePage) EmbedCSS(_ context.Context) []temple.CSSInline {
	return []temple.CSSInline{
		{TemplatePath: "b.inline.css"},
		{TemplatePath: "c.inline.css"},
	}
}

func (DuplicateResourcesHomePage) LinkJS(_ context.Context) []temple.JSLink {
	return []temple.JSLink{
		{Src: "https://example.com/b.js", Type: "text/javascript"},
		{Src: "https://example.com/c.js", Type: "text/javascript"},
	}
}

func (DuplicateResourcesHomePage) EmbedJS(_ context.Context) []temple.JSInline {
	return []temple.JSInline{
		{TemplatePath: "b.inline.js"},
		{TemplatePath: "c.inline.js"},
	}
}

type DuplicateResourcesLayout struct {
}

func (layout DuplicateResourcesLayout) Templates(_ context.Context) []string {
	return []string{layout.BaseTemplate()}
}

func (DuplicateResourcesLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func (DuplicateResourcesLayout) LinkCSS(_ context.Context) []temple.CSSLink {
	return []temple.CSSLink{
		{Href: "https://example.com/a.css", Type: "text/css", Rel: "stylesheet"},
		{Href: "https://example.com/b.css", Type: "text/css", Rel: "stylesheet"},
	}
}

func (DuplicateResourcesLayout) EmbedCSS(_ context.Context) []temple.CSSInline {
	return []temple.CSSInline{
		{TemplatePath: "a.inline.css"},
		{TemplatePath: "b.inline.css"},
	}
}

func (DuplicateResourcesLayout) LinkJS(_ context.Context) []temple.JSLink {
	return []temple.JSLink{
		{Src: "https://example.com/a.js", Type: "text/javascript"},
		{Src: "https://example.com/b.js", Type: "text/javascript"},
	}
}

func (DuplicateResourcesLayout) EmbedJS(_ context.Context) []temple.JSInline {
	return []temple.JSInline{
		{TemplatePath: "a.inline.js"},
		{TemplatePath: "b.inline.js"},
	}
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.Name }}. This is my home page.{{ end }}`,
		"a.inline.css":   `body { color: red; }`,
		"b.inline.css":   `html { background-color: black; }`,
		"c.inline.css":   `a { color: yellow; }`,
		"a.inline.js":    `alert('a!');`,
		"b.inline.js":    `alert('b!');`,
		"c.inline.js":    `alert('c!');`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := DuplicateResourcesSite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := DuplicateResourcesHomePage{
		Name:   "Visitor",
		Layout: DuplicateResourcesLayout{},
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title><link href="https://example.com/a.css" rel="stylesheet" type="text/css">
<link href="https://example.com/b.css" rel="stylesheet" type="text/css">
<link href="https://example.com/c.css" rel="stylesheet" type="text/css">
<style>
body { color: red; }
</style>
<style>
html { background-color: black; }
</style>
<style>
a { color: yellow; }
</style>
<script type="text/javascript" src="https://example.com/a.js"></script>
<script type="text/javascript" src="https://example.com/b.js"></script>
<script type="text/javascript" src="https://example.com/c.js"></script>
<script>
alert('a!');
</script>
<script>
alert('b!');
</script>
<script>
alert('c!');
</script>
</head>
	<body>
		Hello, Visitor. This is my home page.</body>
</html>
Example (FuncMaps)
package main

import (
	"context"
	"html/template"
	"log/slog"
	"os"
	"strings"

	"impractical.co/temple"
)

type FuncMapSite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

func (FuncMapSite) FuncMap(_ context.Context) template.FuncMap {
	// these functions will be available to all components and pages
	return template.FuncMap{
		"applesAndOranges": func(input []string) []string {
			for pos, fruit := range input {
				if strings.ToLower(fruit) == "apples" {
					input[pos] = "oranges"
				}
			}
			return input
		},
	}
}

type FuncMapHomePage struct {
	Name   string
	Fruits []string
	Layout FuncMapLayout
}

func (FuncMapHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (page FuncMapHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		page.Layout,
	}
}

func (FuncMapHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (page FuncMapHomePage) ExecutedTemplate(_ context.Context) string {
	return page.Layout.BaseTemplate()
}

func (FuncMapHomePage) FuncMap(_ context.Context) template.FuncMap {
	return template.FuncMap{
		// this function is only available to the homepage
		"humanize": func(input []string) string {
			if len(input) < 1 {
				return ""
			}
			if len(input) < 2 {
				return strings.Join(input, " and ")
			}
			input[len(input)-1] = "and " + input[len(input)-1]
			return strings.Join(input, ", ")
		},
	}
}

type FuncMapLayout struct {
}

func (layout FuncMapLayout) Templates(_ context.Context) []string {
	return []string{layout.BaseTemplate()}
}

func (FuncMapLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.Name }}. This is my home page. I like {{ humanize (applesAndOranges .Page.Fruits) }}.{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := FuncMapSite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := FuncMapHomePage{
		Name:   "Visitor",
		Fruits: []string{"apples", "bananas", "oranges"},
		Layout: FuncMapLayout{},
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title></head>
	<body>
		Hello, Visitor. This is my home page. I like oranges, bananas, and oranges.</body>
</html>
Example (InlineCSS)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type InlineCSSSite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

type InlineCSSHomePage struct {
	Layout    InlineCSSLayout
	User      string
	FontColor string
}

func (InlineCSSHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (h InlineCSSHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		h.Layout,
	}
}

func (InlineCSSHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (h InlineCSSHomePage) ExecutedTemplate(_ context.Context) string {
	return h.Layout.BaseTemplate()
}

func (InlineCSSHomePage) EmbedCSS(_ context.Context) []temple.CSSInline {
	return []temple.CSSInline{
		{TemplatePath: "home.css.tmpl"},
	}
}

type InlineCSSLayout struct {
}

func (b InlineCSSLayout) Templates(_ context.Context) []string {
	return []string{b.BaseTemplate()}
}

func (InlineCSSLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.User }}. This is my home page.{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
		"home.css.tmpl": "body { font: {{ .Page.FontColor }}; }",
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := InlineCSSSite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := InlineCSSHomePage{
		Layout:    InlineCSSLayout{},
		User:      "Visitor",
		FontColor: "red",
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title><style>
body { font: red; }
</style>
</head>
	<body>
		Hello, Visitor. This is my home page.</body>
</html>
Example (InlineCSSWithDisableImplicitDependency)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type InlineCSSDisableImplicitDependencySite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

type InlineCSSDisableImplicitDependencyHomePage struct {
	Layout    InlineCSSDisableImplicitDependencyLayout
	User      string
	FontColor string
}

func (InlineCSSDisableImplicitDependencyHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (h InlineCSSDisableImplicitDependencyHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		h.Layout,
	}
}

func (InlineCSSDisableImplicitDependencyHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (h InlineCSSDisableImplicitDependencyHomePage) ExecutedTemplate(_ context.Context) string {
	return h.Layout.BaseTemplate()
}

func (InlineCSSDisableImplicitDependencyHomePage) EmbedCSS(_ context.Context) []temple.CSSInline {
	return []temple.CSSInline{
		// because they're all declared in a single slice like this, an
		// implicit constraint will be created that b must be loaded
		// before a, and a must be loaded before c.
		//
		// However, because DisableImplicitOrdering is set on b, a will
		// have no dependency on b, and the normal lexicographical
		// ordering will decide to put a first.
		{TemplatePath: "b.css.tmpl", DisableImplicitOrdering: true},
		{TemplatePath: "a.css.tmpl"},
		{TemplatePath: "c.css.tmpl"},
	}
}

type InlineCSSDisableImplicitDependencyLayout struct {
}

func (b InlineCSSDisableImplicitDependencyLayout) Templates(_ context.Context) []string {
	return []string{b.BaseTemplate()}
}

func (InlineCSSDisableImplicitDependencyLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.User }}. This is my home page.{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
		"a.css.tmpl": "body { font: {{ .Page.FontColor }}; }",
		"b.css.tmpl": "html { margin: 0; }",
		"c.css.tmpl": "html { padding: 0; }",
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := InlineCSSDisableImplicitDependencySite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := InlineCSSDisableImplicitDependencyHomePage{
		Layout:    InlineCSSDisableImplicitDependencyLayout{},
		User:      "Visitor",
		FontColor: "red",
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title><style>
body { font: red; }
</style>
<style>
html { margin: 0; }
</style>
<style>
html { padding: 0; }
</style>
</head>
	<body>
		Hello, Visitor. This is my home page.</body>
</html>
Example (InlineCSSWithExplicitDependency)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type InlineCSSExplicitDependencySite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

type InlineCSSExplicitDependencyHomePage struct {
	Layout    InlineCSSExplicitDependencyLayout
	User      string
	FontColor string
}

func (InlineCSSExplicitDependencyHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (h InlineCSSExplicitDependencyHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		h.Layout,
	}
}

func (InlineCSSExplicitDependencyHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (h InlineCSSExplicitDependencyHomePage) ExecutedTemplate(_ context.Context) string {
	return h.Layout.BaseTemplate()
}

func (InlineCSSExplicitDependencyHomePage) EmbedCSS(_ context.Context) []temple.CSSInline {
	// we want all our dependencies to render before the
	// dependencies from InlineCSSExplicitDependencyLayout.
	//
	// we also want c.css.tmpl to render before a.css.tmpl.
	beforeGlobalInlines := func(_ context.Context, other temple.CSSInline) temple.ResourceRelationship {
		if other.TemplatePath == "global.css.tmpl" {
			return temple.ResourceRelationshipBefore
		}
		return temple.ResourceRelationshipNeutral
	}
	beforeGlobalLinks := func(_ context.Context, other temple.CSSLink) temple.ResourceRelationship {
		if other.Href == "https://example.com/global/a.css" {
			return temple.ResourceRelationshipBefore
		}
		return temple.ResourceRelationshipNeutral
	}
	return []temple.CSSInline{
		{TemplatePath: "a.css.tmpl", CSSInlineRelationCalculator: beforeGlobalInlines, CSSLinkRelationCalculator: beforeGlobalLinks},
		{TemplatePath: "b.css.tmpl", CSSInlineRelationCalculator: beforeGlobalInlines, CSSLinkRelationCalculator: beforeGlobalLinks},
		{TemplatePath: "c.css.tmpl", CSSLinkRelationCalculator: beforeGlobalLinks, CSSInlineRelationCalculator: func(ctx context.Context, other temple.CSSInline) temple.ResourceRelationship {
			if other.TemplatePath == "a.css.tmpl" {
				return temple.ResourceRelationshipBefore
			}
			return beforeGlobalInlines(ctx, other)
		}},
	}
}

type InlineCSSExplicitDependencyLayout struct {
}

func (b InlineCSSExplicitDependencyLayout) Templates(_ context.Context) []string {
	return []string{b.BaseTemplate()}
}

func (InlineCSSExplicitDependencyLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func (InlineCSSExplicitDependencyLayout) EmbedCSS(_ context.Context) []temple.CSSInline {
	return []temple.CSSInline{
		{TemplatePath: "global.css.tmpl"},
	}
}

func (InlineCSSExplicitDependencyLayout) LinkCSS(_ context.Context) []temple.CSSLink {
	return []temple.CSSLink{
		{Href: "https://example.com/global/a.css"},
	}
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.User }}. This is my home page.{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
		"a.css.tmpl":      "body { font: {{ .Page.FontColor }}; }",
		"b.css.tmpl":      "html { margin: 0; }",
		"c.css.tmpl":      "html { padding: 0; }",
		"global.css.tmpl": "body { margin: 0; }",
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := InlineCSSExplicitDependencySite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := InlineCSSExplicitDependencyHomePage{
		Layout:    InlineCSSExplicitDependencyLayout{},
		User:      "Visitor",
		FontColor: "red",
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title><style>
html { margin: 0; }
</style>
<style>
html { padding: 0; }
</style>
<style>
body { font: red; }
</style>
<link href="https://example.com/global/a.css">
<style>
body { margin: 0; }
</style>
</head>
	<body>
		Hello, Visitor. This is my home page.</body>
</html>
Example (InlineCSSWithImplicitDependency)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type InlineCSSImplicitDependencySite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

type InlineCSSImplicitDependencyHomePage struct {
	Layout    InlineCSSImplicitDependencyLayout
	User      string
	FontColor string
}

func (InlineCSSImplicitDependencyHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (h InlineCSSImplicitDependencyHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		h.Layout,
	}
}

func (InlineCSSImplicitDependencyHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (h InlineCSSImplicitDependencyHomePage) ExecutedTemplate(_ context.Context) string {
	return h.Layout.BaseTemplate()
}

func (InlineCSSImplicitDependencyHomePage) EmbedCSS(_ context.Context) []temple.CSSInline {
	return []temple.CSSInline{
		// because they're all declared in a single slice like this, an
		// implicit constraint will be created that a must be loaded
		// before b, and b must be loaded before c.
		{TemplatePath: "a.css.tmpl"},
		{TemplatePath: "b.css.tmpl"},
		{TemplatePath: "c.css.tmpl"},
	}
}

type InlineCSSImplicitDependencyLayout struct {
}

func (b InlineCSSImplicitDependencyLayout) Templates(_ context.Context) []string {
	return []string{b.BaseTemplate()}
}

func (InlineCSSImplicitDependencyLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.User }}. This is my home page.{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
		"a.css.tmpl": "body { font: {{ .Page.FontColor }}; }",
		"b.css.tmpl": "html { margin: 0; }",
		"c.css.tmpl": "html { padding: 0; }",
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := InlineCSSImplicitDependencySite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := InlineCSSImplicitDependencyHomePage{
		Layout:    InlineCSSImplicitDependencyLayout{},
		User:      "Visitor",
		FontColor: "red",
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title><style>
body { font: red; }
</style>
<style>
html { margin: 0; }
</style>
<style>
html { padding: 0; }
</style>
</head>
	<body>
		Hello, Visitor. This is my home page.</body>
</html>
Example (InlineJS)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type InlineJSSite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

type InlineJSHomePage struct {
	Layout InlineJSLayout
	User   string
}

func (InlineJSHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (h InlineJSHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		h.Layout,
	}
}

func (InlineJSHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (h InlineJSHomePage) ExecutedTemplate(_ context.Context) string {
	return h.Layout.BaseTemplate()
}

func (InlineJSHomePage) EmbedJS(_ context.Context) []temple.JSInline {
	return []temple.JSInline{
		{TemplatePath: "home.js.tmpl"},
		{TemplatePath: "footer.js.tmpl", PlaceInFooter: true},
	}
}

type InlineJSLayout struct {
}

func (b InlineJSLayout) Templates(_ context.Context) []string {
	return []string{b.BaseTemplate()}
}

func (InlineJSLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.User }}. This is my home page.{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
		"home.js.tmpl":   `alert("hello, {{ .Page.User }}");`,
		"footer.js.tmpl": `document.write("this was called from the footer");`,
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := InlineJSSite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := InlineJSHomePage{
		Layout: InlineJSLayout{},
		User:   "Visitor",
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title><script>
alert("hello, Visitor");
</script>
</head>
	<body>
		Hello, Visitor. This is my home page.<script>
document.write("this was called from the footer");
</script>
</body>
</html>
Example (InlineJSWithDisableImplicitDependency)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type InlineJSDisableImplicitDependencySite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

type InlineJSDisableImplicitDependencyHomePage struct {
	Layout InlineJSDisableImplicitDependencyLayout
	User   string
}

func (InlineJSDisableImplicitDependencyHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (h InlineJSDisableImplicitDependencyHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		h.Layout,
	}
}

func (InlineJSDisableImplicitDependencyHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (h InlineJSDisableImplicitDependencyHomePage) ExecutedTemplate(_ context.Context) string {
	return h.Layout.BaseTemplate()
}

func (InlineJSDisableImplicitDependencyHomePage) EmbedJS(_ context.Context) []temple.JSInline {
	return []temple.JSInline{
		// because these are all declared in a single component,
		// they'll have an implicit ordering when rendered. JavaScript
		// resources get divided into two groups; those with
		// PlaceInFooter true and those with PlaceInFooter false, then
		// ordered by their constraints. So header.b.js.tmpl will
		// always be rendered before header.a.js.tmpl, which will
		// always be rendered before header.c.js.tmpl. Likewise,
		// footer.b.js.tmpl will always be rendered before
		// footer.a.js.tmpl, which will always be rendered before
		// footer.c.js.tmpl.
		//
		// However, because DisableImplicitOrdering is set on
		// header.b.js.tmpl and footer.b.js.tmpl, header.a.js.tmpl will
		// have no dependency on header.b.js.tmpl, and footer.a.js.tmpl
		// will have no dependency on footer.b.js.tmpl, and the normal
		// lexicographical ordering will decide to put header.a.js.tmpl
		// and footer.a.js.tmpl first.
		{TemplatePath: "header.b.js.tmpl", DisableImplicitOrdering: true},
		{TemplatePath: "footer.b.js.tmpl", PlaceInFooter: true, DisableImplicitOrdering: true},
		{TemplatePath: "header.a.js.tmpl"},
		{TemplatePath: "footer.a.js.tmpl", PlaceInFooter: true},
		{TemplatePath: "header.c.js.tmpl"},
		{TemplatePath: "footer.c.js.tmpl", PlaceInFooter: true},
	}
}

type InlineJSDisableImplicitDependencyLayout struct {
}

func (b InlineJSDisableImplicitDependencyLayout) Templates(_ context.Context) []string {
	return []string{b.BaseTemplate()}
}

func (InlineJSDisableImplicitDependencyLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.User }}. This is my home page.{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
		"header.a.js.tmpl": `alert("hello, {{ .Page.User }}");`,
		"footer.a.js.tmpl": `document.write("this was called from the footer");`,
		"header.b.js.tmpl": `console.log("header.b.js loaded");`,
		"footer.b.js.tmpl": `document.write("footer.b.js loaded");`,
		"header.c.js.tmpl": `console.log("header.c.js loaded");`,
		"footer.c.js.tmpl": `document.write("footer.c.js loaded");`,
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := InlineJSDisableImplicitDependencySite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := InlineJSDisableImplicitDependencyHomePage{
		Layout: InlineJSDisableImplicitDependencyLayout{},
		User:   "Visitor",
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title><script>
alert("hello, Visitor");
</script>
<script>
console.log("header.b.js loaded");
</script>
<script>
console.log("header.c.js loaded");
</script>
</head>
	<body>
		Hello, Visitor. This is my home page.<script>
document.write("this was called from the footer");
</script>
<script>
document.write("footer.b.js loaded");
</script>
<script>
document.write("footer.c.js loaded");
</script>
</body>
</html>
Example (InlineJSWithExplicitDependency)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type InlineJSExplicitDependencySite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

type InlineJSExplicitDependencyHomePage struct {
	Layout InlineJSExplicitDependencyLayout
	User   string
}

func (InlineJSExplicitDependencyHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (h InlineJSExplicitDependencyHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		h.Layout,
	}
}

func (InlineJSExplicitDependencyHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (h InlineJSExplicitDependencyHomePage) ExecutedTemplate(_ context.Context) string {
	return h.Layout.BaseTemplate()
}

func (InlineJSExplicitDependencyHomePage) EmbedJS(_ context.Context) []temple.JSInline {
	// we want all our dependencies to render before the dependencies from
	// InlineJSExplicitDependencyLayout.
	//
	// we also want header.c.js.tmpl to render before header.a.js.tmpl and
	// footer.c.js.tmpl before footer.a.js.tmpl.
	beforeGlobalInlines := func(_ context.Context, other temple.JSInline) temple.ResourceRelationship {
		if other.TemplatePath == "global-header.js.tmpl" {
			return temple.ResourceRelationshipBefore
		}
		if other.TemplatePath == "global-footer.js.tmpl" {
			return temple.ResourceRelationshipBefore
		}
		return temple.ResourceRelationshipNeutral
	}
	beforeGlobalLinks := func(_ context.Context, other temple.JSLink) temple.ResourceRelationship {
		if other.Src == "https://example.com/global/a.js" {
			return temple.ResourceRelationshipBefore
		}
		if other.Src == "https://example.com/global/b.js" {
			return temple.ResourceRelationshipBefore
		}
		return temple.ResourceRelationshipNeutral
	}
	return []temple.JSInline{
		{
			TemplatePath:               "header.a.js.tmpl",
			JSInlineRelationCalculator: beforeGlobalInlines,
			JSLinkRelationCalculator:   beforeGlobalLinks,
		},
		{
			TemplatePath:               "footer.a.js.tmpl",
			PlaceInFooter:              true,
			JSInlineRelationCalculator: beforeGlobalInlines,
			JSLinkRelationCalculator:   beforeGlobalLinks,
		},
		{
			TemplatePath:               "header.b.js.tmpl",
			JSInlineRelationCalculator: beforeGlobalInlines,
			JSLinkRelationCalculator:   beforeGlobalLinks,
		},
		{
			TemplatePath:               "footer.b.js.tmpl",
			PlaceInFooter:              true,
			JSInlineRelationCalculator: beforeGlobalInlines,
			JSLinkRelationCalculator:   beforeGlobalLinks,
		},
		{
			TemplatePath: "header.c.js.tmpl",
			JSInlineRelationCalculator: func(ctx context.Context, other temple.JSInline) temple.ResourceRelationship {
				if other.TemplatePath == "header.a.js.tmpl" {
					return temple.ResourceRelationshipBefore
				}
				return beforeGlobalInlines(ctx, other)
			},
			JSLinkRelationCalculator: beforeGlobalLinks,
		},
		{
			TemplatePath:  "footer.c.js.tmpl",
			PlaceInFooter: true,
			JSInlineRelationCalculator: func(ctx context.Context, other temple.JSInline) temple.ResourceRelationship {
				if other.TemplatePath == "footer.a.js.tmpl" {
					return temple.ResourceRelationshipBefore
				}
				return beforeGlobalInlines(ctx, other)
			},
			JSLinkRelationCalculator: beforeGlobalLinks,
		},
	}
}

type InlineJSExplicitDependencyLayout struct {
}

func (b InlineJSExplicitDependencyLayout) Templates(_ context.Context) []string {
	return []string{b.BaseTemplate()}
}

func (InlineJSExplicitDependencyLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func (InlineJSExplicitDependencyLayout) EmbedJS(_ context.Context) []temple.JSInline {
	return []temple.JSInline{
		{TemplatePath: "global-header.js.tmpl"},
		{TemplatePath: "global-footer.js.tmpl", PlaceInFooter: true},
	}
}

func (InlineJSExplicitDependencyLayout) LinkJS(_ context.Context) []temple.JSLink {
	return []temple.JSLink{
		{Src: "https://example.com/global/a.js"},
		{Src: "https://example.com/global/b.js"},
	}
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.User }}. This is my home page.{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
		"header.a.js.tmpl":      `alert("hello, {{ .Page.User }}");`,
		"footer.a.js.tmpl":      `document.write("this was called from the footer");`,
		"header.b.js.tmpl":      `console.log("header.b.js loaded");`,
		"footer.b.js.tmpl":      `document.write("footer.b.js loaded");`,
		"header.c.js.tmpl":      `console.log("header.c.js loaded");`,
		"footer.c.js.tmpl":      `document.write("footer.c.js loaded");`,
		"global-header.js.tmpl": `console.log("global header loaded");`,
		"global-footer.js.tmpl": `console.log("global footer loaded");`,
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := InlineJSExplicitDependencySite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := InlineJSExplicitDependencyHomePage{
		Layout: InlineJSExplicitDependencyLayout{},
		User:   "Visitor",
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title><script>
console.log("header.b.js loaded");
</script>
<script>
console.log("header.c.js loaded");
</script>
<script>
alert("hello, Visitor");
</script>
<script src="https://example.com/global/a.js"></script>
<script src="https://example.com/global/b.js"></script>
<script>
console.log("global header loaded");
</script>
</head>
	<body>
		Hello, Visitor. This is my home page.<script>
document.write("footer.b.js loaded");
</script>
<script>
document.write("footer.c.js loaded");
</script>
<script>
document.write("this was called from the footer");
</script>
<script>
console.log("global footer loaded");
</script>
</body>
</html>
Example (InlineJSWithImplicitDependency)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type InlineJSImplicitDependencySite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

type InlineJSImplicitDependencyHomePage struct {
	Layout InlineJSImplicitDependencyLayout
	User   string
}

func (InlineJSImplicitDependencyHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (h InlineJSImplicitDependencyHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		h.Layout,
	}
}

func (InlineJSImplicitDependencyHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (h InlineJSImplicitDependencyHomePage) ExecutedTemplate(_ context.Context) string {
	return h.Layout.BaseTemplate()
}

func (InlineJSImplicitDependencyHomePage) EmbedJS(_ context.Context) []temple.JSInline {
	return []temple.JSInline{
		// because these are all declared in a single component,
		// they'll have an implicit ordering when rendered. JavaScript
		// resources get divided into two groups; those with
		// PlaceInFooter true and those with PlaceInFooter false, then
		// ordered by their constraints. So header.a.js.tmpl will
		// always be rendered before header.b.js.tmpl, which will
		// always be rendered before header.c.js.tmpl. Likewise,
		// footer.a.js.tmpl will always be rendered before
		// footer.b.js.tmpl, which will always be rendered before
		// footer.c.js.tmpl.
		{TemplatePath: "header.a.js.tmpl"},
		{TemplatePath: "footer.a.js.tmpl", PlaceInFooter: true},
		{TemplatePath: "header.b.js.tmpl"},
		{TemplatePath: "footer.b.js.tmpl", PlaceInFooter: true},
		{TemplatePath: "header.c.js.tmpl"},
		{TemplatePath: "footer.c.js.tmpl", PlaceInFooter: true},
	}
}

type InlineJSImplicitDependencyLayout struct {
}

func (b InlineJSImplicitDependencyLayout) Templates(_ context.Context) []string {
	return []string{b.BaseTemplate()}
}

func (InlineJSImplicitDependencyLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.User }}. This is my home page.{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
		"header.a.js.tmpl": `alert("hello, {{ .Page.User }}");`,
		"footer.a.js.tmpl": `document.write("this was called from the footer");`,
		"header.b.js.tmpl": `console.log("header.b.js loaded");`,
		"footer.b.js.tmpl": `document.write("footer.b.js loaded");`,
		"header.c.js.tmpl": `console.log("header.c.js loaded");`,
		"footer.c.js.tmpl": `document.write("footer.c.js loaded");`,
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := InlineJSImplicitDependencySite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := InlineJSImplicitDependencyHomePage{
		Layout: InlineJSImplicitDependencyLayout{},
		User:   "Visitor",
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title><script>
alert("hello, Visitor");
</script>
<script>
console.log("header.b.js loaded");
</script>
<script>
console.log("header.c.js loaded");
</script>
</head>
	<body>
		Hello, Visitor. This is my home page.<script>
document.write("this was called from the footer");
</script>
<script>
document.write("footer.b.js loaded");
</script>
<script>
document.write("footer.c.js loaded");
</script>
</body>
</html>
Example (LinkedCSS)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type LinkedCSSSite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

type LinkedCSSHomePage struct {
	Layout LinkedCSSLayout
	User   string
}

func (LinkedCSSHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (h LinkedCSSHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		h.Layout,
	}
}

func (LinkedCSSHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (h LinkedCSSHomePage) ExecutedTemplate(_ context.Context) string {
	return h.Layout.BaseTemplate()
}

func (LinkedCSSHomePage) LinkCSS(_ context.Context) []temple.CSSLink {
	return []temple.CSSLink{
		{Href: "https://example.com/a.css", Rel: "stylesheet"},
	}
}

type LinkedCSSLayout struct {
}

func (b LinkedCSSLayout) Templates(_ context.Context) []string {
	return []string{b.BaseTemplate()}
}

func (LinkedCSSLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.User }}. This is my home page.{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := LinkedCSSSite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := LinkedCSSHomePage{
		Layout: LinkedCSSLayout{},
		User:   "Visitor",
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title><link href="https://example.com/a.css" rel="stylesheet">
</head>
	<body>
		Hello, Visitor. This is my home page.</body>
</html>
Example (LinkedCSSWithDisableImplicitDependency)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type LinkedCSSDisableImplicitDependencySite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

type LinkedCSSDisableImplicitDependencyHomePage struct {
	Layout    LinkedCSSDisableImplicitDependencyLayout
	User      string
	FontColor string
}

func (LinkedCSSDisableImplicitDependencyHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (h LinkedCSSDisableImplicitDependencyHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		h.Layout,
	}
}

func (LinkedCSSDisableImplicitDependencyHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (h LinkedCSSDisableImplicitDependencyHomePage) ExecutedTemplate(_ context.Context) string {
	return h.Layout.BaseTemplate()
}

func (LinkedCSSDisableImplicitDependencyHomePage) LinkCSS(_ context.Context) []temple.CSSLink {
	return []temple.CSSLink{
		// because they're all declared in a single slice like this, an
		// implicit constraint will be created that b must be loaded
		// before a, and a must be loaded before c.
		//
		// However, because DisableImplicitOrdering is set on b, a will
		// have no dependency on b, and the normal lexicographical
		// ordering will decide to put a first.
		{Href: "https://example.com/b.css", DisableImplicitOrdering: true},
		{Href: "https://example.com/a.css"},
		{Href: "https://example.com/c.css"},
	}
}

type LinkedCSSDisableImplicitDependencyLayout struct {
}

func (b LinkedCSSDisableImplicitDependencyLayout) Templates(_ context.Context) []string {
	return []string{b.BaseTemplate()}
}

func (LinkedCSSDisableImplicitDependencyLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.User }}. This is my home page.{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := LinkedCSSDisableImplicitDependencySite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := LinkedCSSDisableImplicitDependencyHomePage{
		Layout:    LinkedCSSDisableImplicitDependencyLayout{},
		User:      "Visitor",
		FontColor: "red",
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title><link href="https://example.com/a.css">
<link href="https://example.com/b.css">
<link href="https://example.com/c.css">
</head>
	<body>
		Hello, Visitor. This is my home page.</body>
</html>
Example (LinkedCSSWithExplicitDependency)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type LinkedCSSExplicitDependencySite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

type LinkedCSSExplicitDependencyHomePage struct {
	Layout LinkedCSSExplicitDependencyLayout
	User   string
}

func (LinkedCSSExplicitDependencyHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (h LinkedCSSExplicitDependencyHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		h.Layout,
	}
}

func (LinkedCSSExplicitDependencyHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (h LinkedCSSExplicitDependencyHomePage) ExecutedTemplate(_ context.Context) string {
	return h.Layout.BaseTemplate()
}

func (LinkedCSSExplicitDependencyHomePage) LinkCSS(_ context.Context) []temple.CSSLink {
	// we want all our dependencies to render before the
	// dependencies from LinkedCSSExplicitDependencyLayout.
	//
	// we also want https://example.com/c.css to render before
	// https://example.com/a.css.
	beforeGlobalInlines := func(_ context.Context, other temple.CSSInline) temple.ResourceRelationship {
		if other.TemplatePath == "global.css.tmpl" {
			return temple.ResourceRelationshipBefore
		}
		return temple.ResourceRelationshipNeutral
	}
	beforeGlobalLinks := func(_ context.Context, other temple.CSSLink) temple.ResourceRelationship {
		if other.Href == "https://example.com/global/a.css" {
			return temple.ResourceRelationshipBefore
		}
		return temple.ResourceRelationshipNeutral
	}
	return []temple.CSSLink{
		{Href: "https://example.com/a.css", CSSInlineRelationCalculator: beforeGlobalInlines, CSSLinkRelationCalculator: beforeGlobalLinks},
		{Href: "https://example.com/b.css", CSSInlineRelationCalculator: beforeGlobalInlines, CSSLinkRelationCalculator: beforeGlobalLinks},
		{Href: "https://example.com/c.css", CSSInlineRelationCalculator: beforeGlobalInlines, CSSLinkRelationCalculator: func(ctx context.Context, other temple.CSSLink) temple.ResourceRelationship {
			if other.Href == "https://example.com/a.css" {
				return temple.ResourceRelationshipBefore
			}
			return beforeGlobalLinks(ctx, other)
		}},
	}
}

type LinkedCSSExplicitDependencyLayout struct {
}

func (b LinkedCSSExplicitDependencyLayout) Templates(_ context.Context) []string {
	return []string{b.BaseTemplate()}
}

func (LinkedCSSExplicitDependencyLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func (LinkedCSSExplicitDependencyLayout) EmbedCSS(_ context.Context) []temple.CSSInline {
	return []temple.CSSInline{
		{TemplatePath: "global.css.tmpl"},
	}
}

func (LinkedCSSExplicitDependencyLayout) LinkCSS(_ context.Context) []temple.CSSLink {
	return []temple.CSSLink{
		{Href: "https://example.com/global/a.css"},
	}
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.User }}. This is my home page.{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
		"global.css.tmpl": "body { margin: 0; }",
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := LinkedCSSExplicitDependencySite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := LinkedCSSExplicitDependencyHomePage{
		Layout: LinkedCSSExplicitDependencyLayout{},
		User:   "Visitor",
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title><link href="https://example.com/b.css">
<link href="https://example.com/c.css">
<link href="https://example.com/a.css">
<link href="https://example.com/global/a.css">
<style>
body { margin: 0; }
</style>
</head>
	<body>
		Hello, Visitor. This is my home page.</body>
</html>
Example (LinkedCSSWithImplicitDependency)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type LinkedCSSImplicitDependencySite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

type LinkedCSSImplicitDependencyHomePage struct {
	Layout    LinkedCSSImplicitDependencyLayout
	User      string
	FontColor string
}

func (LinkedCSSImplicitDependencyHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (h LinkedCSSImplicitDependencyHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		h.Layout,
	}
}

func (LinkedCSSImplicitDependencyHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (h LinkedCSSImplicitDependencyHomePage) ExecutedTemplate(_ context.Context) string {
	return h.Layout.BaseTemplate()
}

func (LinkedCSSImplicitDependencyHomePage) LinkCSS(_ context.Context) []temple.CSSLink {
	return []temple.CSSLink{
		// because they're all declared in a single slice like this, an
		// implicit constraint will be created that a must be loaded
		// before b, and b must be loaded before c.
		{Href: "https://example.com/a.css"},
		{Href: "https://example.com/b.css"},
		{Href: "https://example.com/c.css"},
	}
}

type LinkedCSSImplicitDependencyLayout struct {
}

func (b LinkedCSSImplicitDependencyLayout) Templates(_ context.Context) []string {
	return []string{b.BaseTemplate()}
}

func (LinkedCSSImplicitDependencyLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.User }}. This is my home page.{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := LinkedCSSImplicitDependencySite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := LinkedCSSImplicitDependencyHomePage{
		Layout:    LinkedCSSImplicitDependencyLayout{},
		User:      "Visitor",
		FontColor: "red",
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title><link href="https://example.com/a.css">
<link href="https://example.com/b.css">
<link href="https://example.com/c.css">
</head>
	<body>
		Hello, Visitor. This is my home page.</body>
</html>
Example (LinkedJS)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type LinkedJSSite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

type LinkedJSHomePage struct {
	Layout LinkedJSLayout
	User   string
}

func (LinkedJSHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (h LinkedJSHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		h.Layout,
	}
}

func (LinkedJSHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (h LinkedJSHomePage) ExecutedTemplate(_ context.Context) string {
	return h.Layout.BaseTemplate()
}

func (LinkedJSHomePage) LinkJS(_ context.Context) []temple.JSLink {
	return []temple.JSLink{
		{Src: "https://example.com/a.js"},
		{Src: "https://example.com/b.js", PlaceInFooter: true},
	}
}

type LinkedJSLayout struct {
}

func (b LinkedJSLayout) Templates(_ context.Context) []string {
	return []string{b.BaseTemplate()}
}

func (LinkedJSLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.User }}. This is my home page.{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := LinkedJSSite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := LinkedJSHomePage{
		Layout: LinkedJSLayout{},
		User:   "Visitor",
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title><script src="https://example.com/a.js"></script>
</head>
	<body>
		Hello, Visitor. This is my home page.<script src="https://example.com/b.js"></script>
</body>
</html>
Example (LinkedJSWithDisableImplicitDependency)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type LinkedJSDisableImplicitDependencySite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

type LinkedJSDisableImplicitDependencyHomePage struct {
	Layout LinkedJSDisableImplicitDependencyLayout
	User   string
}

func (LinkedJSDisableImplicitDependencyHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (h LinkedJSDisableImplicitDependencyHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		h.Layout,
	}
}

func (LinkedJSDisableImplicitDependencyHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (h LinkedJSDisableImplicitDependencyHomePage) ExecutedTemplate(_ context.Context) string {
	return h.Layout.BaseTemplate()
}

func (LinkedJSDisableImplicitDependencyHomePage) LinkJS(_ context.Context) []temple.JSLink {
	return []temple.JSLink{
		// because these are all declared in a single component,
		// they'll have an implicit ordering when rendered. JavaScript
		// resources get divided into two groups; those with
		// PlaceInFooter true and those with PlaceInFooter false, then
		// ordered by their constraints. So header.b.js will
		// always be rendered before header.a.js, which will
		// always be rendered before header.c.js. Likewise,
		// footer.b.js will always be rendered before
		// footer.a.js, which will always be rendered before
		// footer.c.js.
		//
		// However, because DisableImplicitOrdering is set on
		// header.b.js and footer.b.js, header.a.js will have no
		// dependency on header.b.js, and footer.a.js will have no
		// dependency on footer.b.js, and the normal lexicographical
		// ordering will decide to put header.a.js and footer.a.js
		// first.
		{Src: "https://example.com/header.b.js", DisableImplicitOrdering: true},
		{Src: "https://example.com/footer.b.js", PlaceInFooter: true, DisableImplicitOrdering: true},
		{Src: "https://example.com/header.a.js"},
		{Src: "https://example.com/footer.a.js", PlaceInFooter: true},
		{Src: "https://example.com/header.c.js"},
		{Src: "https://example.com/footer.c.js", PlaceInFooter: true},
	}
}

type LinkedJSDisableImplicitDependencyLayout struct {
}

func (b LinkedJSDisableImplicitDependencyLayout) Templates(_ context.Context) []string {
	return []string{b.BaseTemplate()}
}

func (LinkedJSDisableImplicitDependencyLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.User }}. This is my home page.{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := LinkedJSDisableImplicitDependencySite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := LinkedJSDisableImplicitDependencyHomePage{
		Layout: LinkedJSDisableImplicitDependencyLayout{},
		User:   "Visitor",
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title><script src="https://example.com/header.a.js"></script>
<script src="https://example.com/header.b.js"></script>
<script src="https://example.com/header.c.js"></script>
</head>
	<body>
		Hello, Visitor. This is my home page.<script src="https://example.com/footer.a.js"></script>
<script src="https://example.com/footer.b.js"></script>
<script src="https://example.com/footer.c.js"></script>
</body>
</html>
Example (LinkedJSWithExplicitDependency)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type LinkedJSExplicitDependencySite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

type LinkedJSExplicitDependencyHomePage struct {
	Layout LinkedJSExplicitDependencyLayout
	User   string
}

func (LinkedJSExplicitDependencyHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (h LinkedJSExplicitDependencyHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		h.Layout,
	}
}

func (LinkedJSExplicitDependencyHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (h LinkedJSExplicitDependencyHomePage) ExecutedTemplate(_ context.Context) string {
	return h.Layout.BaseTemplate()
}

func (LinkedJSExplicitDependencyHomePage) LinkJS(_ context.Context) []temple.JSLink {
	// we want all our dependencies to render before the dependencies from
	// LinkedJSExplicitDependencyLayout.
	//
	// we also want header.c.js.tmpl to render before header.a.js.tmpl and
	// footer.c.js.tmpl before footer.a.js.tmpl.
	beforeGlobalInlines := func(_ context.Context, other temple.JSInline) temple.ResourceRelationship {
		if other.TemplatePath == "global-header.js.tmpl" {
			return temple.ResourceRelationshipBefore
		}
		if other.TemplatePath == "global-footer.js.tmpl" {
			return temple.ResourceRelationshipBefore
		}
		return temple.ResourceRelationshipNeutral
	}
	beforeGlobalLinks := func(_ context.Context, other temple.JSLink) temple.ResourceRelationship {
		if other.Src == "https://example.com/global/a.js" {
			return temple.ResourceRelationshipBefore
		}
		if other.Src == "https://example.com/global/b.js" {
			return temple.ResourceRelationshipBefore
		}
		return temple.ResourceRelationshipNeutral
	}
	return []temple.JSLink{
		{
			Src:                        "https://example.com/header.a.js",
			JSInlineRelationCalculator: beforeGlobalInlines,
			JSLinkRelationCalculator:   beforeGlobalLinks,
		},
		{
			Src:                        "https://example.com/footer.a.js",
			PlaceInFooter:              true,
			JSInlineRelationCalculator: beforeGlobalInlines,
			JSLinkRelationCalculator:   beforeGlobalLinks,
		},
		{
			Src:                        "https://example.com/header.b.js",
			JSInlineRelationCalculator: beforeGlobalInlines,
			JSLinkRelationCalculator:   beforeGlobalLinks,
		},
		{
			Src:                        "https://example.com/footer.b.js",
			PlaceInFooter:              true,
			JSInlineRelationCalculator: beforeGlobalInlines,
			JSLinkRelationCalculator:   beforeGlobalLinks,
		},
		{
			Src:                        "https://example.com/header.c.js",
			JSInlineRelationCalculator: beforeGlobalInlines,
			JSLinkRelationCalculator: func(ctx context.Context, other temple.JSLink) temple.ResourceRelationship {
				if other.Src == "https://example.com/header.a.js" {
					return temple.ResourceRelationshipBefore
				}
				return beforeGlobalLinks(ctx, other)
			},
		},
		{
			Src:                        "https://example.com/footer.c.js",
			PlaceInFooter:              true,
			JSInlineRelationCalculator: beforeGlobalInlines,
			JSLinkRelationCalculator: func(ctx context.Context, other temple.JSLink) temple.ResourceRelationship {
				if other.Src == "https://example.com/footer.a.js" {
					return temple.ResourceRelationshipBefore
				}
				return beforeGlobalLinks(ctx, other)
			},
		},
	}
}

type LinkedJSExplicitDependencyLayout struct {
}

func (b LinkedJSExplicitDependencyLayout) Templates(_ context.Context) []string {
	return []string{b.BaseTemplate()}
}

func (LinkedJSExplicitDependencyLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func (LinkedJSExplicitDependencyLayout) EmbedJS(_ context.Context) []temple.JSInline {
	return []temple.JSInline{
		{TemplatePath: "global-header.js.tmpl"},
		{TemplatePath: "global-footer.js.tmpl", PlaceInFooter: true},
	}
}

func (LinkedJSExplicitDependencyLayout) LinkJS(_ context.Context) []temple.JSLink {
	return []temple.JSLink{
		{Src: "https://example.com/global/a.js"},
		{Src: "https://example.com/global/b.js"},
	}
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.User }}. This is my home page.{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
		"global-header.js.tmpl": `console.log("global header loaded");`,
		"global-footer.js.tmpl": `console.log("global footer loaded");`,
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := LinkedJSExplicitDependencySite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := LinkedJSExplicitDependencyHomePage{
		Layout: LinkedJSExplicitDependencyLayout{},
		User:   "Visitor",
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title><script src="https://example.com/header.b.js"></script>
<script src="https://example.com/header.c.js"></script>
<script src="https://example.com/header.a.js"></script>
<script src="https://example.com/global/a.js"></script>
<script src="https://example.com/global/b.js"></script>
<script>
console.log("global header loaded");
</script>
</head>
	<body>
		Hello, Visitor. This is my home page.<script src="https://example.com/footer.b.js"></script>
<script src="https://example.com/footer.c.js"></script>
<script src="https://example.com/footer.a.js"></script>
<script>
console.log("global footer loaded");
</script>
</body>
</html>
Example (LinkedJSWithImplicitDependency)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type LinkedJSImplicitDependencySite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

type LinkedJSImplicitDependencyHomePage struct {
	Layout LinkedJSImplicitDependencyLayout
	User   string
}

func (LinkedJSImplicitDependencyHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (h LinkedJSImplicitDependencyHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		h.Layout,
	}
}

func (LinkedJSImplicitDependencyHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (h LinkedJSImplicitDependencyHomePage) ExecutedTemplate(_ context.Context) string {
	return h.Layout.BaseTemplate()
}

func (LinkedJSImplicitDependencyHomePage) LinkJS(_ context.Context) []temple.JSLink {
	return []temple.JSLink{
		// because these are all declared in a single component,
		// they'll have an implicit ordering when rendered. JavaScript
		// resources get divided into two groups; those with
		// PlaceInFooter true and those with PlaceInFooter false, then
		// ordered by their constraints. So header.a.js will
		// always be rendered before header.b.js, which will
		// always be rendered before header.c.js. Likewise,
		// footer.a.js will always be rendered before
		// footer.b.js, which will always be rendered before
		// footer.c.js.
		{Src: "https://example.com/header.a.js"},
		{Src: "https://example.com/footer.a.js", PlaceInFooter: true},
		{Src: "https://example.com/header.b.js"},
		{Src: "https://example.com/footer.b.js", PlaceInFooter: true},
		{Src: "https://example.com/header.c.js"},
		{Src: "https://example.com/footer.c.js", PlaceInFooter: true},
	}
}

type LinkedJSImplicitDependencyLayout struct {
}

func (b LinkedJSImplicitDependencyLayout) Templates(_ context.Context) []string {
	return []string{b.BaseTemplate()}
}

func (LinkedJSImplicitDependencyLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

func main() {
	// normally you'd use something like embed.FS or os.DirFS for this
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.User }}. This is my home page.{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
	</body>
</html>`,
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := LinkedJSImplicitDependencySite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := LinkedJSImplicitDependencyHomePage{
		Layout: LinkedJSImplicitDependencyLayout{},
		User:   "Visitor",
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>My Example Site</title><script src="https://example.com/header.a.js"></script>
<script src="https://example.com/header.b.js"></script>
<script src="https://example.com/header.c.js"></script>
</head>
	<body>
		Hello, Visitor. This is my home page.<script src="https://example.com/footer.a.js"></script>
<script src="https://example.com/footer.b.js"></script>
<script src="https://example.com/footer.c.js"></script>
</body>
</html>
Example (Test)
package main

import (
	"context"
	"log/slog"
	"os"

	"impractical.co/temple"
)

type ErrorSite struct {
	// anonymously embedding a *CachedSite makes MySite a Site implementation
	*temple.CachedSite

	// a configurable title for our site
	Title string
}

func (ErrorSite) ServerErrorPage(_ context.Context) temple.Page {
	return ErrorPage{}
}

type ErrorHomePage struct {
	Name   string
	Layout ErrorLayout
}

func (ErrorHomePage) Templates(_ context.Context) []string {
	return []string{"home.html.tmpl"}
}

func (h ErrorHomePage) UseComponents(_ context.Context) []temple.Component {
	return []temple.Component{
		h.Layout,
	}
}

func (ErrorHomePage) Key(_ context.Context) string {
	return "home.html.tmpl"
}

func (h ErrorHomePage) ExecutedTemplate(_ context.Context) string {
	return h.Layout.BaseTemplate()
}

type ErrorLayout struct {
}

func (b ErrorLayout) Templates(_ context.Context) []string {
	return []string{b.BaseTemplate()}
}

func (ErrorLayout) BaseTemplate() string {
	return "base.html.tmpl"
}

type ErrorPage struct{}

func (ErrorPage) Templates(_ context.Context) []string {
	return []string{"server_error.html.tmpl"}
}

func (ErrorPage) Key(_ context.Context) string {
	return "server_error.html.tmpl"
}

func (ErrorPage) ExecutedTemplate(_ context.Context) string {
	return "server_error.html.tmpl"
}

func main() {
	// for example purposes, we're just hardcoding values
	var templates = staticFS{
		"home.html.tmpl": `{{ define "body" }}Hello, {{ .Page.Name }}. This is my home page.{{ end }}`,
		"base.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>{{ .Site.Title }}</title>
		{{- .CSS -}}
		{{- .HeaderJS -}}
	</head>
	<body>
		{{ block "body" . }}{{ end }}
		{{- .FooterJS -}}
		<!-- purposefully throw an error here -->
		{{ .ImaginaryData }}
	</body>
</html>`,
		"server_error.html.tmpl": `
<!doctype html>
<html lang="en">
	<head>
		<title>Server Error</title>
	</head>
	<body>
		<h1>Server error</h1>
		<p>Something went wrong, sorry about that. We're working on fixing it now.</p>
	</body>
</html>`,
	}

	// usually the context comes from the request, but here we're building it from scratch and adding a logger
	ctx := temple.LoggingContext(context.Background(), slog.Default())

	site := ErrorSite{
		CachedSite: temple.NewCachedSite(templates),
		Title:      "My Example Site",
	}
	page := ErrorHomePage{
		Name:   "Visitor",
		Layout: ErrorLayout{},
	}
	temple.Render(ctx, os.Stdout, site, page)

}
Output:

<!doctype html>
<html lang="en">
	<head>
		<title>Server Error</title>
	</head>
	<body>
		<h1>Server error</h1>
		<p>Something went wrong, sorry about that. We're working on fixing it now.</p>
	</body>
</html>

func SetInLogger added in v0.3.0

func SetInLogger(ctx context.Context, args ...any) context.Context

SetInLogger updates the slog.Logger embedded in the passed context.Context to associate the passed keys and values with it. Any log lines temple prints using the returned context.Context will include the passed keys and values.

Types

type CSSEmbedder

type CSSEmbedder interface {
	// EmbedCSS returns the CSSInline values that describe the CSS to embed
	// directly in the output HTML.
	EmbedCSS(context.Context) []CSSInline
}

CSSEmbedder is an interface that Components can fulfill to include some CSS that should be embedded directly into the rendered HTML. The contents will be made available to the template as .CSS.

type CSSInline added in v0.3.0

type CSSInline struct {
	// TemplatePath is the path, relative to the Site's TemplateDir, to the
	// template that should be rendered to get the contents of the CSS
	// <style> block. The template should not include the <style> tags. A
	// CSSRenderData value will be passed as the template data, with the
	// CSSInline property set to this value.
	TemplatePath string

	// Blocking is the value of the blocking attribute for the <style> tag
	// that will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/style#blocking
	// for more information.
	Blocking string

	// Media is the value of the media attribute for the <style> tag that
	// will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/style#media
	// for more information.
	Media string

	// Nonce is the value of the nonce attribute for the <style> tag that
	// will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/style#nonce
	// for more information.
	Nonce string

	// Title is the value of the title attribute for the <style> tag that
	// will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/style#title
	// for more information.
	Title string

	// Attrs holds any additional non-standard or unsupported attributes
	// that should be set on the <style> tag that will be generated.
	Attrs map[string]string

	// DisableElementMerge, when set to true, prevents a <style> block from
	// being merged with any other <style> block.
	DisableElementMerge bool

	// DisableImplicitOrdering, when set to true, disables the implicit
	// ordering of resources within a Component for this block. It will not
	// be required to come after the block before it in the []CSSInline,
	// and the block after it will not be required to be rendered after it.
	DisableImplicitOrdering bool

	// CSSLinkRelationCalculator can be used to control how this <link> tag
	// gets rendered in relation to any other CSS <link> tag. If the
	// function returns ResourceRelationshipAfter, this <link> tag will
	// always come after the other <link> tag in the HTML document. If the
	// function returns ResourceRelationshipBefore, this <link> tag will
	// always come before the other <link> tag in the HTML document. If the
	// function returns ResourceRelationshipNeutral, no guarantees are made
	// about where the CSS resources will appear relative to each other in
	// the HTML document.
	//
	// If this <link> tag has no requirements about its positioning
	// relative to other CSS resources, just let this property be nil.
	CSSLinkRelationCalculator func(context.Context, CSSLink) ResourceRelationship

	// CSSInlineRelationCalculator can be used to control how this <style>
	// block gets rendered in relation to any other <style> block. If the
	// function returns ResourceRelationshipAfter, this <style> block will
	// always come after the other <style> block in the HTML document. If
	// the function returns ResourceRelationshipBefore, this <style> block
	// will always come before the other <style> block in the HTML
	// document. If the function returns ResourceRelationshipNeutral, no
	// guarantees are made about where the CSS resources will appear
	// relative to each other in the HTML document.
	//
	// If this <style> block has no requirements about its positioning
	// relative to other CSS resources, just let this property be nil.
	CSSInlineRelationCalculator func(context.Context, CSSInline) ResourceRelationship
}

CSSInline holds the necessary information to embed CSS directly into a page's HTML output, inside a <style> tag.

The TemplatePath should point to a template containing the CSS to render. The template can use any of the data in the CSSRenderData. The <style> tag should not be included; it will be generated from the rest of the properties.

temple may, but is not guaranteed to, merge <style> blocks it identifies as mergeable. At the moment, those merge semantics are that all properties except the relation calculators are identical, but that logic is subject to change. If it is important that a <style> block not be merged into any other <style> block, set DisableElementMerge to true.

type CSSLink struct {
	// Href is the URL to load the CSS from. It will be used verbatim as
	// the <link> element's href attribute. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/link#href
	// for more information.
	Href string

	// Rel is the value of the rel attribute for the <link> tag that will be
	// generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel
	// for more information.
	Rel string

	// As is the value of the as attribute for the <link> tag that will be
	// generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/link#as
	// for more information.
	As string

	// Blocking is the value of the blocking attribute for the <link> tag
	// that will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/link#blocking
	// for more information.
	Blocking string

	// CrossOrigin is the value of the crossorigin attribute for the <link>
	// tag that will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/crossorigin
	// for more information.
	CrossOrigin string

	// Disabled indicates whether to include the disabled attribute in the
	// <link> tag that will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/link#disabled
	// for more information.
	Disabled bool

	// FetchPriority is the value of the fetchpriority attribute for the
	// <link> tag that will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/link#fetchpriority
	// for more information.
	FetchPriority string

	// Integrity is the value of the integrity attribute for the <link> tag
	// that will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/link#integrity
	// for more information.
	Integrity string

	// Media is the value of the media attribute for the <link> tag that
	// will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/link#media
	// for more information.
	Media string

	// ReferrerPolicy is the value of the referrerpolicy attribute for the
	// <link> tag that will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/link#referrerpolicy
	// for more information.
	ReferrerPolicy string

	// Title is the value of the title attribute for the <link> tag that
	// will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/link#title
	// for more information.
	Title string

	// Type is the value of the type attribute for the <link> tag that will
	// be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/link#type
	// for more information.
	Type string

	// Attrs holds any additional non-standard or unsupported attributes
	// that should be set on the <link> tag that will be generated.
	Attrs map[string]string

	// DisableImplicitOrdering, when set to true, disables the implicit
	// ordering of resources within a Component for this link. It will not
	// be required to come after the link before it in the []CSSLink, and
	// the link after it will not be required to be rendered after it.
	DisableImplicitOrdering bool

	// CSSLinkRelationCalculator can be used to control how this <link> tag
	// gets rendered in relation to any other CSS link tag. If the function
	// returns ResourceRelationshipAfter, this <link> tag will always come
	// after the other link in the HTML document. If the function returns
	// ResourceRelationshipBefore, this <link> tag will always come before
	// the other link in the HTML document. If the function returns
	// ResourceRelationshipNeutral, no guarantees are made about where the
	// CSS resources will appear relative to each other in the HTML
	// document.
	//
	// If this <link> tag has no requirements about its positioning
	// relative to other CSS resources, just let this property be nil.
	CSSLinkRelationCalculator func(context.Context, CSSLink) ResourceRelationship

	// CSSInlineRelationCalculator can be used to control how this <link>
	// tag gets rendered in relation to any <style> block. If the function
	// returns ResourceRelationshipAfter, this <link> tag will always come
	// after the <style> block in the HTML document. If the function
	// returns ResourceRelationshipBefore, this <link> tag will always come
	// before the <style> block in the HTML document. If the function
	// returns ResourceRelationshipNeutral, no guarantees are made about
	// where the CSS resources will appear relative to each other in the
	// HTML document.
	//
	// If this <link> tag has no requirements about its positioning
	// relative to other CSS resources, just let this property be nil.
	CSSInlineRelationCalculator func(context.Context, CSSInline) ResourceRelationship

	// TemplatePath is the path, relative to the Site's TemplateDir, to the
	// template that should be rendered to construct the <link> tag from
	// this struct. If left empty, the default template will be used, but
	// it can be specified to override the template if desired. A
	// CSSRenderData will be passed to the template with the CSSLink
	// property set.
	TemplatePath string
}

CSSLink holds the necessary information to include CSS in a page's HTML output as a <link>, which downloads the CSS file from another URL.

The Href should point to the URL to load the CSS file from.

type CSSLinker

type CSSLinker interface {
	// LinkCSS returns a list of CSSLink values that describe the CSS links
	// to include in the output HTML.
	LinkCSS(context.Context) []CSSLink
}

CSSLinker is an interface that Components can fulfill to include some CSS that should be loaded through a <link> element in the template. The contents will be made available to the template as .CSS.

type CSSRenderData added in v0.3.0

type CSSRenderData[SiteType Site, PageType Page] struct {
	// CSS is the CSSInline struct being rendered. It may be empty if this
	// data is for a CSSLink instead.
	CSS CSSInline

	// CSSLink is the CSSLink struct being rendered. It may be empty if
	// this data is for a CSSInline instead.
	CSSLink CSSLink

	// Site is the caller-defined site type, used for including globals.
	Site SiteType

	// Page is the specific page the CSS is being rendered into.
	Page PageType
}

CSSRenderData holds the information passed to the template when rendering the template for a CSSInline or CSSLink.

type CachedSite

type CachedSite struct {
	// contains filtered or unexported fields
}

CachedSite is an implementation of the Site interface that can be embedded in other Site implementations. It fulfills the Site interface and the TemplateCacher interface, caching templates in memory and exposing the template fs.FS passed to it in NewCachedSite. A CachedSite must be instantiated through NewCachedSite, its empty value is not usable.

func NewCachedSite

func NewCachedSite(templates fs.FS) *CachedSite

NewCachedSite returns a CachedSite instance that is ready to be used.

func (*CachedSite) GetCachedResource added in v0.3.0

func (s *CachedSite) GetCachedResource(_ context.Context, key string) *string

GetCachedResource returns the cached resource associated with the passed key, if one exists. If no resource is cached for that key, it returns nil.

It can safely be used by multiple goroutines.

func (*CachedSite) GetCachedTemplate

func (s *CachedSite) GetCachedTemplate(_ context.Context, key string) *template.Template

GetCachedTemplate returns the cached template associated with the passed key, if one exists. If no template is cached for that key, it returns nil.

It can safely be used by multiple goroutines.

func (*CachedSite) SetCachedResource added in v0.3.0

func (s *CachedSite) SetCachedResource(_ context.Context, key, resource string)

SetCachedResource caches a resource for the given key.

It can safely be used by multiple goroutines.

func (*CachedSite) SetCachedTemplate

func (s *CachedSite) SetCachedTemplate(_ context.Context, key string, tmpl *template.Template)

SetCachedTemplate caches a template for the given key.

It can safely be used by multiple goroutines.

func (*CachedSite) TemplateDir

func (s *CachedSite) TemplateDir(_ context.Context) fs.FS

TemplateDir returns an fs.FS containing all the templates needed to render a Site's Components. In this case, we just pass back what the consumer passed in.

type Component

type Component interface {
	// Templates returns a list of filepaths to html/template contents that
	// need to be parsed before the component can be rendered.
	Templates(context.Context) []string
}

Component is an interface for a UI component that can be rendered to HTML.

type ComponentUser

type ComponentUser interface {
	// UseComponents returns the Components that this Component relies on.
	UseComponents(context.Context) []Component
}

ComponentUser is an interface that a Component can optionally implement to list the Components that it relies upon. These Components will automatically have the appropriate methods called if they implement any of the optional interfaces. A Component that uses other Components but doesn't return them with this interface is responsible for including the output of any optional interfaces that they implement in its own implementation of those interfaces and including their Templates output in its own Templates output.

type FuncMapExtender

type FuncMapExtender interface {
	// FuncMap returns an html/template.FuncMap containing all the
	// functions that the Component is adding to the FuncMap.
	FuncMap(context.Context) template.FuncMap
}

FuncMapExtender is an interface that Components can fulfill to add to the map of functions available to them when rendering.

type JSEmbedder

type JSEmbedder interface {
	// EmbedJS returns the JavaScript, without <script> tags, that should
	// be embedded directly in the output HTML.
	EmbedJS(context.Context) []JSInline
}

JSEmbedder is an interface that Components can fulfill to include some JavaScript that should be embedded directly into the rendered HTML. The contents will be made available to the template as .HeaderJS or .FooterJS, depending on whether their PlaceInFooter property is set to true or not.

type JSInline added in v0.3.0

type JSInline struct {
	// TemplatePath is the path, relative to the Site's TemplateDir, to the
	// template that should be rendered to get the contents of the
	// JavaScript <script> tag. The template should not include the
	// <script> tags. A JSRenderData value will be passed as the template
	// data, with the JSInline property set to this value.
	TemplatePath string

	// CrossOrigin is the value of the crossorigin attribute for the
	// <script> tag that will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#crossorigin
	// for more information.
	CrossOrigin string

	// NoModule indicates whether to include the nomodule attribute for the
	// <script> tag that will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#nomodule
	// for more information.
	NoModule bool

	// Nonce is the value of the nonce attribute for the <script> tag that
	// will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#nonce
	// for more information.
	Nonce string

	// ReferrerPolicy is the value of the referrerpolicy attribute for the
	// <script> tag that will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#referrerpolicy
	// for more information.
	ReferrerPolicy string

	// Type is the value of the type attribute for the <script> tag that
	// will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#type
	// for more information.
	Type string

	// Attrs holds any additional non-standard or unsupported attributes
	// that should be set on the <script> tag that will be generated.
	Attrs map[string]string

	// DisableElementMerge, when set to true, prevents a <script> tag from
	// being merged with any other <script> tag.
	DisableElementMerge bool

	// PlaceInFooter, when set to true, makes this JavaScript part of the
	// FooterJS property of RenderData. Otherwise, it is part of the
	// HeaderJS property of RenderData. This separation exists so some
	// JavaScript can be loaded in the <head> of the document, while other
	// JavaScript can be loaded after the rest of the document has been
	// loaded. Where these properties end up actually being placed is up to
	// the template, but that is the intention.
	PlaceInFooter bool

	// DisableImplicitOrdering, when set to true, disables the implicit
	// ordering of resources within a Component for this block. It will not
	// be required to come after the block before it in the []JSInline, and
	// the block after it will not be required to be rendered after it.
	DisableImplicitOrdering bool

	// JSInlineRelationCalculator can be used to control how this <script>
	// tag gets rendered in relation to any other <script> tag. If the
	// function returns ResourceRelationshipAfter, this <script> tag will
	// always come after the other <script> tag in the HTML document. If
	// the function returns ResourceRelationshipBefore, this <script> tag
	// will always come before the other <script> tag in the HTML document.
	// If the function returns ResourceRelationshipNeutral, no guarantees
	// are made about where the JavaScript resources will appear relative
	// to each other in the HTML document.
	//
	// If this <script> tag has no requirements about its positioning
	// relative to other JavaScript resources, just let this property be
	// nil.
	JSInlineRelationCalculator func(context.Context, JSInline) ResourceRelationship

	// JSLinkRelationCalculator can be used to control how this <script> tag
	// gets rendered in relation to any other JavaScript <script> tag. If the
	// function returns ResourceRelationshipAfter, this <script> tag will
	// always come after the other <script> tag in the HTML document. If the
	// function returns ResourceRelationshipBefore, this <script> tag will
	// always come before the other <script> tag in the HTML document. If the
	// function returns ResourceRelationshipNeutral, no guarantees are made
	// about where the JavaScript resources will appear relative to each other in
	// the HTML document.
	//
	// These orderings are only guaranteed when comparing resources with
	// the same PlaceInFooter values.
	//
	// If this <script> tag has no requirements about its positioning
	// relative to other JavaScript resources, just let this property be nil.
	JSLinkRelationCalculator func(context.Context, JSLink) ResourceRelationship
}

JSInline holds the necessary information to embed JavaScript directly into a page's HTML output, inside a <script> tag.

The TemplatePath should point to a template containing the JavaScript to render. The template can use any of the data in the JSRenderData. The <script> tag should not be included; it will be generated from the rest of the properties.

temple may, but is not guaranteed to, merge <script> blocks it identifies as mergeable. At the moment, those merge semantics are that all properties except the relation calculators are identical, but that logic is subject to change. If it is important that a <script> block not be merged into any other <script> block, set DisableElementMerge to true.

type JSLink struct {
	// Src is the URL to load the JavaScript from. It will be used verbatim
	// as the <script> element's src attribute. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#src
	// for more information.
	Src string

	// Async indicates whether to include the async attribute for the
	// <script> tag that will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#async
	// for more information.
	Async bool

	// AttributionSrc, if set to true, will include the attributionsrc
	// attribute in the <script> tag that will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#attributionsrc
	// for more information.
	AttributionSrc bool

	// AttributionSrcURLs, if non-empty, will set the value of the
	// attributionsrc attribute in the <script> tag that will be generated.
	// See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#attributionsrc
	// for more information.
	//
	// If AttributionSrcURLs is set, AttributionSrc must be set to true, or
	// AttributionSrcURLs will have no effect.
	AttributionSrcURLs string

	// Blocking is the value of the blocking attribute for the <script> tag
	// that will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#blocking
	// for more information.
	Blocking string

	// CrossOrigin is the value of the crossorigin attribute for the
	// <script> tag that will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#crossorigin
	// for more information.
	CrossOrigin string

	// Defer indicates whether to include the defer attribute for the
	// <script> tag that will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#defer
	// for more information.
	Defer bool

	// FetchPriority is the value of the fetchpriority attribute for the
	// <script> tag that will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#fetchpriority
	// for more information.
	FetchPriority string

	// Integrity is the value of the integrity attribute for the <script>
	// tag that will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#integrity
	// for more information.
	Integrity string

	// NoModule indicates whether to include the nomodule attribute for the
	// <script> tag that will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#nomodule
	// for more information.
	NoModule bool

	// Nonce is the value of the nonce attribute for the <script> tag that
	// will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#nonce
	// for more information.
	Nonce string

	// ReferrerPolicy is the value of the referrerpolicy attribute for the
	// <script> tag that will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#referrerpolicy
	// for more information.
	ReferrerPolicy string

	// Type is the value of the type attribute for the <script> tag that
	// will be generated. See
	// https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#type
	// for more information.
	Type string

	// Attrs holds any additional non-standard or unsupported attributes
	// that should be set on the <script> tag that will be generated.
	Attrs map[string]string

	// TemplatePath is the path, relative to the Site's TemplateDir, to the
	// template that should be rendered to construct the <script> tag from
	// this struct. If left empty, the default template will be used, but
	// it can be specified to override the template if desired. A
	// JSRenderData will be passed to the template with the JSLink property
	// set.
	TemplatePath string

	// PlaceInFooter, when set to true, makes this JavaScript part of the
	// FooterJS property of RenderData. Otherwise, it is part of the
	// HeaderJS property of RenderData. This separation exists so some
	// JavaScript can be loaded in the <head> of the document, while other
	// JavaScript can be loaded after the rest of the document has been
	// loaded. Where these properties end up actually being placed is up to
	// the template, but that is the intention.
	PlaceInFooter bool

	// DisableImplicitOrdering, when set to true, disables the implicit
	// ordering of resources within a Component for this link. It will not
	// be required to come after the link before it in the []JSLink, and
	// the link after it will not be required to be rendered after it.
	DisableImplicitOrdering bool

	// JSInlineRelationCalculator can be used to control how this <script>
	// tag gets rendered in relation to any other <script> tag. If the
	// function returns ResourceRelationshipAfter, this <script> tag will
	// always come after the other <script> tag in the HTML document. If
	// the function returns ResourceRelationshipBefore, this <script> tag
	// will always come before the other <script> tag in the HTML document.
	// If the function returns ResourceRelationshipNeutral, no guarantees
	// are made about where the JavaScript resources will appear relative
	// to each other in the HTML document.
	//
	// If this <script> tag has no requirements about its positioning
	// relative to other JavaScript resources, just let this property be
	// nil.
	JSInlineRelationCalculator func(context.Context, JSInline) ResourceRelationship

	// JSLinkRelationCalculator can be used to control how this <script> tag
	// gets rendered in relation to any other JavaScript <script> tag. If the
	// function returns ResourceRelationshipAfter, this <script> tag will
	// always come after the other <script> tag in the HTML document. If the
	// function returns ResourceRelationshipBefore, this <script> tag will
	// always come before the other <script> tag in the HTML document. If the
	// function returns ResourceRelationshipNeutral, no guarantees are made
	// about where the JavaScript resources will appear relative to each other in
	// the HTML document.
	//
	// These orderings are only guaranteed when comparing resources with
	// the same PlaceInFooter values.
	//
	// If this <script> tag has no requirements about its positioning
	// relative to other JavaScript resources, just let this property be nil.
	JSLinkRelationCalculator func(context.Context, JSLink) ResourceRelationship
}

JSLink holds the necessary information to include JavaScript in a page's HTML output as a <script> with a src attribute that downloads the JavaScript file from another URL.

The Src should point to the URL to load the JavaScript file from.

type JSLinker

type JSLinker interface {
	// LinkJS returns a list of URLs to JavaScript files that should be
	// linked to from the output HTML.
	//
	// If this Component embeds any other Components, it should include
	// their LinkJS output in its own LinkJS output.
	LinkJS(context.Context) []JSLink
}

JSLinker is an interface that Components can fulfill to include some JavaScript that should be loaded separately from the HTML document, using a <script> tag with a src attribute. The contents will be made available to the template as .HeaderJS or .FooterJS, depending on whether their PlaceInFooter property is set to true or not.

type JSRenderData added in v0.3.0

type JSRenderData[SiteType Site, PageType Page] struct {
	// JS is the JSInline struct being rendered. It may be empty if this
	// data is for a JSLink instead.
	JS JSInline

	// JSLink is the JSLink struct being rendered. It may be empty if this
	// data is for a JSInline instead.
	JSLink JSLink

	// Site is the caller-defined site type, used for including globals.
	Site SiteType

	// Page is the specific page the JS is being rendered into.
	Page PageType
}

JSRenderData holds the information passed to the template when rendering the template for a JSInline or JSLink.

type Page added in v0.4.0

type Page interface {
	Component

	// Key is a unique key to use when caching this page so it doesn't need
	// to be re-parsed. A good key is consistent, but unique per Page.
	Key(context.Context) string

	// ExecutedTemplate is the template that needs to actually be executed
	// when rendering the page.
	//
	// This is usually not the template for the Component defining the
	// page; it's usually the "base" template that Component defining the
	// page fills blocks in.
	ExecutedTemplate(context.Context) string
}

Page is an interface for a page that can be passed to Render. It defines a single logical page of the application, composed of one or more Components. It should contain all the information needed to render the Components to HTML.

type RenderConfigurer added in v0.4.0

type RenderConfigurer interface {
	// ConfigureRender returns a list of options that modify how the Render
	// function behaves.
	ConfigureRender() []RenderOption
}

RenderConfigurer is an interface that a Site, Page, or Component can fill that will modify how Render behaves when that Site, Page, or Component is being rendered. In case more than one Site, Page, or Component implements RenderConfigurer, the Component will beat the Page in any conflict, and the Page will beat the Site. Multiple Components with conflicting [RenderOption]s have undefined behavior and the winner is not guaranteed.

type RenderData

type RenderData[SiteType Site, PageType Page] struct {
	// Site is an instance of the Site type, containing all the
	// configuration and information about a Site. This can be used to
	// avoid passing global configuration options to every single page.
	Site SiteType

	// Page is the information for a specific page, embedded in that page's
	// Page type.
	Page PageType

	// CSS contains any <style> or <link> tags containing CSS that need to
	// be rendered. It should be rendered as part of the page's <head> tag.
	CSS template.HTML

	// HeaderJS contains any <script> tags intended to be rendered at the
	// head of the document, before any displayed elements have been
	// loaded. It is usually rendered in the page's <head> tag.
	HeaderJS template.HTML

	// FooterJS contains any <script> tags intended to be rendered at the
	// end of a document, after any displayed elements have been loaded. It
	// is usually rendered right before the page's closing </body> tag.
	FooterJS template.HTML
}

RenderData is the data that is passed to a page when rendering it.

type RenderOption added in v0.4.0

type RenderOption interface {
	// contains filtered or unexported methods
}

RenderOption is a private interface that modifies how the Render function behaves. It's returned by a RenderConfigurer.

func RenderOptionDisablePageBuffering added in v0.4.0

func RenderOptionDisablePageBuffering(disable bool) RenderOption

RenderOptionDisablePageBuffering provides a RenderOption for controlling whether the rendered HTML will be buffered in memory before being written to the [http.ResponseWriter] or not. The default is to buffer the HTML so any errors in rendering it can be caught before the first byte is written. To disable this default, return this function as a RenderOption from a RenderConfigurer, passing `true`. To override a previous RenderOption and turn the buffering back on, pass `false`.

type ResourceCacher added in v0.3.0

type ResourceCacher interface {
	// GetCachedResource returns the *string specified by the passed key.
	// It should return nil if the template hasn't been cached yet.
	GetCachedResource(ctx context.Context, key string) *string

	// SetCachedResource stores the passed string under the passed key, for
	// later retrieval with GetCachedResource.
	//
	// Any errors encountered should be logged, but as this is a
	// best-effort operation, will not be surfaced outside the function.
	SetCachedResource(ctx context.Context, key, resource string)
}

ResourceCacher is an optional interface for Sites. Those fulfilling it can cache the templates their resources use, to save on the overhead of parsing the template each time. The templates being parsed for a given key should be the same every time, as should the template getting executed, but the data may still be different, so the output HTML cannot be safely presumed to be cacheable.

type ResourceRelationship added in v0.3.0

type ResourceRelationship string

ResourceRelationship controls the relationship between two resources. It's used to control the order in which CSS and JavaScript resources are rendered to the page.

const (
	// ResourceRelationshipAfter indicates that the resource should be
	// rendered after the resource it's being compared to.
	ResourceRelationshipAfter ResourceRelationship = "after"

	// ResourceRelationshipBefore indicates that the resource should be
	// rendered before the resource it's being compared to.
	ResourceRelationshipBefore ResourceRelationship = "before"

	// ResourceRelationshipNeutral indicates that the resource has no
	// restrictions about where it's rendered in relation to the resource
	// it's being compared to. Generally, for performance reasons, it's
	// preferred to omit the relationship calculation function entirely
	// rather than returning a ResourceRelationshipNeutral value, but if
	// the function could return other values when compared to other
	// resources, ResourceRelationshipNeutral is necessary to be able to
	// indicate that no relationship exists between these particular
	// resources.
	ResourceRelationshipNeutral ResourceRelationship = "neutral"
)

type ServerErrorPager

type ServerErrorPager interface {
	ServerErrorPage(ctx context.Context) Page
}

ServerErrorPager defines an interface that Sites can optionally implement. If a Site implements ServerErrorPager and Render encounters an error, the output of ServerErrorPage will be rendered.

type Site

type Site interface {
	// TemplateDir returns an fs.FS containing all the templates needed to
	// render every Page on the Site.
	//
	// The path to templates within the fs.FS should match the output of
	// TemplatePaths for Components.
	TemplateDir(ctx context.Context) fs.FS
}

Site is an interface for the singleton that will be used to render HTML. Consumers should use it to store any clients or cross-request state they need, and use it to render Pages.

A Site needs to be able to surface the templates it relies on as an fs.FS.

type TemplateCacher

type TemplateCacher interface {
	// GetCachedTemplate returns the *template.Template specified by the
	// passed key. It should return nil if the template hasn't been cached
	// yet.
	GetCachedTemplate(ctx context.Context, key string) *template.Template

	// SetCachedTemplate stores the passed *template.Template under the
	// passed key, for later retrieval with GetCachedTemplate.
	//
	// Any errors encountered should be logged, but as this is a
	// best-effort operation, will not be surfaced outside the function.
	SetCachedTemplate(ctx context.Context, key string, tmpl *template.Template)
}

TemplateCacher is an optional interface for Sites. Those fulfilling it can cache their template parsing using the output of Key from each Page to save on the overhead of parsing the template each time. The templates being parsed for a given key should be the same every time, as should the template getting executed, but the data may still be different, so the output HTML cannot be safely presumed to be cacheable.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL