Core concepts
Fragments and slots
Fragments, the slots they expose, what happens when one fails, and slots filled from content at render time.
A fragment is the unit a page is made of: a template, an optional data handler that fetches what the template renders, and named slots where other fragments go. A layout is a fragment. So is the content of a page, a sidebar, an author card, a comment list. A page is a tree of them, with the layout at the root.
author := collage.NewFragment("author", "fragments/author.html").
WithDataHandler(collage.DataHandler(loadAuthor)).
WithTimeout(time.Second).
WithFallback(anonymousAuthor).
Build()
Building a fragment
collage.NewFragment(name, templatePath) starts a builder, and Build() returns
the *collage.Fragment.
- The name is how the fragment is reported: in error messages, in the
development error panel, in render metadata and traces. Make it say which part of
the page it is —
"post-comments", not"list". - The template path is relative to the template root, extension included:
"fragments/author.html"istemplates/fragments/author.html. A path the template engine did not load isErrTemplateNotFoundwhen the page holding the fragment is registered — including, since v0.11.0, a fragment the page opens at its own URL withWithFragmentPath. Only a fragment a slot resolver returns is checked later, when it renders. See Templates.
Everything else is optional:
| Method | What it sets |
|---|---|
WithDataHandler(h) |
The function that fetches the template's data — see Data handlers |
WithSlot(name, required, allowMultiple) |
Declares a slot |
WithSlotFragment(slot, child) |
Binds a child fragment into a slot |
WithSlotResolver(slot, resolve) |
Fills a slot per render instead |
Required() |
This fragment's failure fails the page |
WithFallback(f) |
What renders when this fragment fails |
WithTimeout(d) |
How long its data handler may take |
Like the page builder, the fragment builder records mistakes rather than stopping
the chain, and BuildErr() returns them. What it recorded stays on the fragment it
built, so every one of them — a slot declared twice, a child bound into a slot that
was never declared, a negative timeout — stops any page whose tree contains the
fragment at registration, whether or not anyone called BuildErr(). Call it when
you want the error at the line that caused it rather than at RegisterPage; see
Pages and layouts.
A fragment with no data handler renders its template with no data. That is right for markup that never changes — a footer, a static notice — and for a layout whose only job is to arrange slots.
Slots
A slot is a named position in a fragment's template. It is declared on the
fragment and written into the template with {{slot "name"}}:
post := collage.NewFragment("post", "pages/post.html").
WithDataHandler(collage.DataHandler(loadPost)).
WithSlot("author", true, false).
WithSlot("related", false, true).
WithSlotFragment("author", author).
WithSlotFragment("related", relatedPosts).
WithSlotFragment("related", popularPosts).
Build()
<!-- templates/pages/post.html -->
<article>
<h1>{{.Title}}</h1>
{{slot "author"}}
<div class="body">{{.Body}}</div>
</article>
<aside>{{slot "related"}}</aside>
WithSlot(name, required, allowMultiple) takes two flags.
required— the slot must have something in it. A required slot with nothing bound is refused at registration (ErrRequiredSlotUnfilled), and checked again before the template runs, so it is caught even if the template never asks for it.allowMultiple— the slot may hold more than one fragment. They render in the order they were bound, one after another. Binding a second fragment into a slot that holds one recordsErrSlotOccupied.
WithSlotFragment records ErrUnknownSlot for a slot that was not declared —
declare slots first — and ErrNilFragment for a nil child. Declaring one name
twice records ErrDuplicateSlot.
In the template, {{slot "name"}} renders what the slot holds as HTML, which is
not escaped again: the children escaped their own values when they rendered. A slot
that holds nothing renders nothing. A name the fragment never declared is an
error, not empty output — a typo in a template would otherwise be a section
that is quietly missing.
Each fragment has its own data
A child does not see its parent's data. Inside fragments/author.html, . is what
loadAuthor returned, not the post. A child that needs something its parent
fetched reads it from the render's shared data, or asks for it itself with
collage.Once or collage.Cached so it is fetched only once — see
Data handlers.
Reusing fragments
A *collage.Fragment can be bound into many slots on many pages; author above
could appear on the post page and on a search result page. A fragment bound twice
renders twice, running its data handler once per binding.
A fragment must not contain itself. Binding one, directly or through a chain of
children, into its own slot is refused at registration with ErrFragmentCycle.
When a fragment fails
A fragment fails when its data handler returns an error, when its template fails
to execute, when a required slot is empty, or when either panics — a panic in a
data handler or a template function is recovered into a *collage.PanicError and
treated as a failure, not a crashed process.
What happens next is the fragment's failure policy, and there are three.
postContent := collage.NewFragment("post", "pages/post.html").
WithDataHandler(collage.DataHandler(loadPost)).
Required().
Build()
comments := collage.NewFragment("comments", "fragments/comments.html").
WithDataHandler(collage.DataHandler(loadComments)).
WithFallback(collage.NewFragment("comments-unavailable", "fragments/comments-unavailable.html").Build()).
Build()
related := collage.NewFragment("related", "fragments/related.html").
WithDataHandler(collage.DataHandler(loadRelated)).
Build()
Required()— the fragment's failure fails the page. Use it for what the page exists to show. The page's error page is served with a 500 — or, when the error wrapscollage.ErrNotFound, its not-found page with a 404. The failure travels through any optional fragments above it: a required fragment inside an optional sidebar still fails the page.WithFallback(f)—frenders in its place. If the fallback fails too, the fragment renders nothing and the page still succeeds: a fallback exists to contain a failure, so its own failure is contained as well, even if something inside it is marked required.- Neither — the fragment renders nothing, and the page is served without it.
collage.ErrNotFound only means "404" on a required fragment. On an optional one it
is a failure like any other, because the page is still renderable without it.
A page served with any failed fragment, fallback or not, is degraded. It is sent to the reader but never cached, so the next request tries again rather than serving the failure for a TTL. A static export does not write a degraded page unless it is told to.
In development a failure is never quiet. A failed fragment that renders nothing leaves an HTML comment with its name and error where its output would have been, and the page carries a panel naming every fragment that failed, with the error — for a template, the file and line — even when a fallback covered for it. When a required fragment fails the whole page, the development error page names that fragment — where the failure started, not the layout it travelled up through. Outside development there is neither.
A slot the template skips
A fragment's children start fetching before its template runs, so that they fetch
at the same time rather than one after another. That means a fragment in a slot
the template decides not to render — {{if .ShowComments}}{{slot "comments"}}{{end}}
— still had its handler started. Its context is cancelled as soon as the template
is done, and its failure is discarded: a fragment that was never rendered cannot
fail the page, even if it is Required().
Timeouts
WithTimeout(d) bounds the fragment's data handler. A fragment that sets none gets
Config.Template.Timeout, which defaults to five seconds; a negative duration
records ErrInvalidTimeout.
recommendations := collage.NewFragment("recommendations", "fragments/recommendations.html").
WithDataHandler(collage.DataHandler(loadRecommendations)).
WithTimeout(300 * time.Millisecond).
WithFallback(nothingToRecommend).
Build()
A timeout pairs naturally with a fallback. A slow recommendation service costs the page 300 milliseconds and a fallback, rather than the reader's patience.
The timeout bounds the context the handler receives, not the handler: a
handler that ignores ctx runs as long as it likes. Pass ctx to every call that
can wait — see Data handlers.
Slots filled per render
Everything above binds children when the program starts. Some pages are arranged by their content instead: a landing page whose sections an editor picks and orders in a CMS, a dashboard of widgets a user chose. Binding those at startup would mean a restart for every change, and code that has to agree with the data about what goes where.
WithSlotResolver fills a slot per render. The resolver receives the render
context and returns the fragments the slot holds this time:
type block struct {
Kind string
Heading string
Text string
}
landing := collage.NewFragment("landing", "pages/landing.html").
WithDataHandler(collage.Effect(func(ctx context.Context, rc *collage.RenderContext) error {
blocks, err := cms.Blocks(ctx, "landing")
if err != nil {
return err
}
rc.Set("blocks", blocks)
return nil
})).
WithSlot("blocks", true, true).
WithSlotResolver("blocks", func(rc *collage.RenderContext) ([]*collage.Fragment, error) {
blocks, _ := collage.Get[[]block](rc, "blocks")
fragments := make([]*collage.Fragment, 0, len(blocks))
for i, b := range blocks {
f, err := blockFragment(i, b)
if err != nil {
return nil, err
}
fragments = append(fragments, f)
}
return fragments, nil
}).
Required().
Build()
A resolver may return fragments that exist for the whole program, or build them on the spot. Building them is how each block gets its own data:
func blockFragment(i int, b block) (*collage.Fragment, error) {
switch b.Kind {
case "hero", "text":
return collage.NewFragment(fmt.Sprintf("block-%d-%s", i, b.Kind), "blocks/"+b.Kind+".html").
WithDataHandler(collage.DataHandler(func(context.Context, *collage.RenderContext) (block, []string, error) {
return b, nil, nil
})).
Build(), nil
}
return nil, fmt.Errorf("landing: unknown block kind %q", b.Kind)
}
The rules:
- The resolver runs after its own fragment's data handler, so it can read what that handler fetched, and before the fragments it returns start theirs — which then run concurrently, like any other children.
- What it returns is held to the slot's rules: at most one fragment unless the
slot allows multiple, at least one if it is required (
ErrRequiredSlotEmpty). An error from the resolver, or a panic, fails its fragment, and that fragment's failure policy applies. - Declare the slot first, with
WithSlot. A slot is filled by a resolver or byWithSlotFragment, never both — mixing them recordsErrSlotResolved. - Its fragments are only checked when they render. Registration cannot see
them, so a returned fragment whose template does not exist fails that render
rather than startup. A mistake its builder recorded is caught the same way: a
returned fragment built with errors fails the fragment that owns the slot, under
that fragment's failure policy, with the returned fragment's name in the error.
Check
BuildErr()yourself when you would rather handle it in the resolver. Templates are all loaded at startup, so the set of block kinds a resolver can use is still fixed by the program.
A page whose sections come from content should also report that content's tags —
here the landing handler could return "landing" as a tag with
collage.DataHandler instead of collage.Effect — so a cached page is dropped when
an editor reorders it. See Caching.
The nesting limit
A fragment tree may be at most 32 levels deep, counting the root. A page that
goes deeper fails to render with ErrMaxDepthExceeded, whatever its fragments'
failure policies, and the error names the chain of fragments that reached the
limit.
Real pages come nowhere near it. The limit exists for the one case registration
cannot rule out: a resolver that returns a fragment which, somewhere below,
resolves to itself again. A bound cycle is already refused at registration as
ErrFragmentCycle.
Fragments with their own URL
A fragment can also be fetched without the page around it — search results
refreshed by a fetch(), a panel swapped in after a form posts. That is declared
on the page, with WithFragmentPath(locale, pattern, fragment), and nothing is
reachable that way unless it is declared. Such a fragment is checked with the page
at registration — its template, its builder's mistakes, its validation. See
Forms and actions.