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 ¶
- Variables
- func LoggingContext(ctx context.Context, logger *slog.Logger) context.Context
- func Render[SiteType Site, PageType Page](ctx context.Context, out io.Writer, site SiteType, page PageType)
- func SetInLogger(ctx context.Context, args ...any) context.Context
- type CSSEmbedder
- type CSSInline
- type CSSLink
- type CSSLinker
- type CSSRenderData
- type CachedSite
- func (s *CachedSite) GetCachedResource(_ context.Context, key string) *string
- func (s *CachedSite) GetCachedTemplate(_ context.Context, key string) *template.Template
- func (s *CachedSite) SetCachedResource(_ context.Context, key, resource string)
- func (s *CachedSite) SetCachedTemplate(_ context.Context, key string, tmpl *template.Template)
- func (s *CachedSite) TemplateDir(_ context.Context) fs.FS
- type Component
- type ComponentUser
- type FuncMapExtender
- type JSEmbedder
- type JSInline
- type JSLink
- type JSLinker
- type JSRenderData
- type Page
- type RenderConfigurer
- type RenderData
- type RenderOption
- type ResourceCacher
- type ResourceRelationship
- type ServerErrorPager
- type Site
- type TemplateCacher
Examples ¶
- Render (Basic)
- Render (DuplicateComponents)
- Render (DuplicateResources)
- Render (FuncMaps)
- Render (InlineCSS)
- Render (InlineCSSWithDisableImplicitDependency)
- Render (InlineCSSWithExplicitDependency)
- Render (InlineCSSWithImplicitDependency)
- Render (InlineJS)
- Render (InlineJSWithDisableImplicitDependency)
- Render (InlineJSWithExplicitDependency)
- Render (InlineJSWithImplicitDependency)
- Render (LinkedCSS)
- Render (LinkedCSSWithDisableImplicitDependency)
- Render (LinkedCSSWithExplicitDependency)
- Render (LinkedCSSWithImplicitDependency)
- Render (LinkedJS)
- Render (LinkedJSWithDisableImplicitDependency)
- Render (LinkedJSWithExplicitDependency)
- Render (LinkedJSWithImplicitDependency)
- Render (Test)
Constants ¶
This section is empty.
Variables ¶
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") )
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") )
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") )
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 ¶
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
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 ¶ added in v0.3.0
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 ¶
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 ¶
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
// 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 ¶ added in v0.3.0
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
// 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 // 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 ¶
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.