Help us shape the future of Go! Take the 2026 Go Developer Survey

collage

module
v0.69.0 Latest Latest
Warning

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

Go to latest
Published: Oct 20, 2026 License: MIT

README

collage logo

collage

Latest version CI status Go reference Go version Documentation

A Go framework for server-side, component-based rendering.

Documentation: collage.furkanbaytekin.dev — the guide, from a first page to deployment. The site is itself a collage application, exported to static files; its source is Elagoht/collage-docs.

A page is a layout fragment wrapped around a content fragment, and a fragment is a template plus an optional data handler plus the slots it exposes to its children. Fragments compose; pages are configuration. Output is cached by dependency tag and invalidated by tag, not by guessing TTLs.

Not everything a site serves is HTML, so two things beside pages: documents — routed, cacheable non-HTML responses such as a sitemap, a feed or a robots.txt, whose handler returns bytes — and assets, a mounted fs.FS served with Range support and its own Cache-Control.

No dependencies: the standard library, and nothing else. Go 1.26.

Status: pre-1.0. The API can still change between minor versions. Every change that breaks an application is listed first, under Breaking, in CHANGELOG.md, with what to do instead; patch versions never break anything. The aim for 1.0 is an API that then stays put.

Getting started

Three commands and a site is running.

go install github.com/Elagoht/collage/cmd/collage@latest

collage new mysite --template demo
cd mysite && go mod tidy
cp .env.example .env.development
collage dev

http://localhost:6060 — a home page and a page of live demos: an API action, a form, a fragment with its own URL and a JSON document, with the tests that drive them. collage dev reads .env.development; PORT=8080 collage dev moves it. Go 1.26 is the only requirement; go install puts collage in $(go env GOPATH)/bin, which is on your PATH if you have installed any Go tool before.

Then, in that directory:

go test ./...    # the tests it came with, written with pkg/collagetest
collage build    # -> bin/mysite, the binary you deploy
collage export   # -> dist/, static files, for a site that needs no server
collage serve    # serves dist/ the way a static host would

Under collage dev, saving a template reloads the page in the browser, and saving Go code rebuilds and restarts the program and then reloads the page — no external watcher, nothing to install. A change that does not compile leaves the last good build running. Plain collage new mysite, without --template demo, starts from one page saying hello instead — the default.

Where to look next: pages/ has one file per page, by area, and fragments/pages/ each page's content beside it; fragments/layouts/ the layout, actions/ the endpoints with their handlers in actions/funcs/; routes.go registers them, and main.go has the configuration. collage add page blog/post writes a new page in that layout and registers it. The rest of this file is what those are made of, and docs/ is the detail.

Adding collage to a project you already have is go get github.com/Elagoht/collage and the application below.

A minimal application

package main

import (
	"context"
	"log"
	"time"

	"github.com/Elagoht/collage/pkg/collage"
)

// homeData is what the home page's template renders with.
type homeData struct {
	Title string
}

func main() {
	app, err := collage.New(&collage.Config{
		Server: collage.ServerConfig{
			Host: "localhost",
			Port: 6060,
		},
		Template: collage.TemplateConfig{
			Root:      "templates",
			Extension: ".html",
			DevMode:   true,
		},
		Cache: collage.CacheConfig{
			Enabled:    true,
			Type:       "memory",
			DefaultTTL: 5 * time.Minute,
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	layout := collage.NewFragment("layout", "layouts/master.html").Build()

	homeContent := collage.NewFragment("home-content", "pages/home.html").
		WithData(collage.LoadTagged(func(_ context.Context, _ *collage.RenderContext) (homeData, []string, error) {
			return homeData{Title: "Welcome home"}, []string{"homepage"}, nil
		})).
		Build()

	homePage := collage.NewPage("home").
		WithLayouts(layout).
		WithContent(homeContent).
		WithPath("en", "/").
		Incremental(5 * time.Minute).
		WithDependencyTags("homepage").
		Build()

	if err := app.RegisterPage(homePage); err != nil {
		log.Fatal(err)
	}

	log.Println("serving on http://localhost:6060")
	if err := app.ListenAndServe(); err != nil {
		log.Fatal(err)
	}
}
<!-- templates/layouts/master.html -->
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>My site</title></head>
<body>{{slot "content"}}</body>
</html>
<!-- templates/pages/home.html -->
<h1>{{.Title}}</h1>

Later, when the homepage's content changes:

err := app.InvalidateTags(ctx, "homepage")
Plugins

A plugin is an ordinary Go module that implements collage.Plugin. Nothing about one shipped by the framework's author would be privileged, which is why none ship here: they are separate projects, fetched with go get and registered in Config.Plugins like any other dependency.

import optimage "github.com/Elagoht/collage-opti-image"

app, err := collage.New(&collage.Config{
	Plugins:      []collage.Plugin{optimage.New(optimage.Options{})},
	PluginConfig: pluginConfig,
})

A plugin can observe a request, rewrite a page's HTML or a document's bytes, and contribute pages, documents, mounts and template functions of its own. See docs/plugins.md.

What it guarantees

  • No silent failures. A fragment naming a template that does not exist, a fragment bound into a slot its template never calls, a template reading a field its data's type does not have, a page that does not validate, a page referencing an error page that was never registered — all of these fail at startup, by name, not on the first request.
  • Deterministic output. Templates execute in order, depth-first; sibling fragments fetch their data concurrently, but what they hoist is placed by declaration order, not by which finished first. So the same page rendered twice from the same inputs is byte-identical. That is what makes caching and ETags sound.
  • A failure has a defined blast radius. A Required() fragment's failure fails the page; an optional one falls back or renders nothing. A panic in a data handler or a template fails that fragment, not the process.
  • A broken render is never cached. If any fragment failed — even one a fallback covered for — the page is served but not stored. Error responses are no-store and are never cached either.
  • "Missing" and "broken" are different. A data handler that wraps collage.ErrNotFound produces a 404 and the page's own not-found page; anything else produces a 500 and its error page.
  • Production error pages leak nothing. Fragment names, error chains, and stacks appear only in development mode.
  • A URL means one thing. The locale comes from the path and nothing else, so a link, a crawler and a CDN all see what the reader sees. Content that depends on a header is declared with collage.Vary, which keys the cache on what the application resolved and sends the Vary header downstream caches need.
  • Invalidation reports what it did. InvalidateTagsN returns the number of keys it reached, and a partial failure is an error rather than a silent success.
  • A non-HTML route fails as a non-HTML route. A document's failure, and a missing asset, answer text/plain — a crawler asking for sitemap.xml and a browser asking for a stylesheet never get a web page back.
  • Assets are files, not cache entries. A mount is served with http.ServeContent, so Range and 206 work and audio seeks; it never enters the page cache, so a 50 MB download cannot evict thousands of pages.
  • A mount cannot silently shadow a route. A mount or handler prefix that would swallow a registered route is refused at startup, naming both, in either registration order.

Extending the framework

Four seams, all reachable from pkg/collage alone.

Your own HTTP. Collage renders pages; an API, auth or language negotiation is written the way you already would. app.Use takes standard net/http middleware, which runs before routing and inside the framework's panic guard and metrics, and whatever it puts in the request context is what data handlers read. app.Handle mounts any http.Handler under a prefix — collage applies no forgery check, body limit or cache to it, and no middleware: it brings its own, unless mounted collage.WithMiddleware().

app.Use(requireSession)                         // pages, documents, actions
app.Handle("/api/", requireToken(apiRouter))    // chi, echo, http.ServeMux, a gRPC gateway
app.Handle("/admin/api/", adminAPI, collage.WithMiddleware()) // inside the site's middleware

docs/http.md has both, and collage.Vary for a cached page whose content depends on a header.

A cache of your own. Implement collage.Cache and set it on CacheConfig.Store; Cache.Type is then ignored. Implement collage.TaggedCache too and the framework calls SetTagged, so your store indexes the dependency tags itself.

app, err := collage.New(&collage.Config{
	Template: collage.TemplateConfig{Root: "templates"},
	Cache: collage.CacheConfig{
		Enabled: true,
		Store:   newCache(), // implements collage.Cache
	},
})

Five methods, no framework internals, and collage.ETag(content) if you have no reason to derive your own ETags. docs/caching.md has a complete implementation.

A cache a fleet shares. Instances behind a load balancer each have their own memory, so an invalidation reaches one of them. A store that implements collage.SharedCache is shared by all: pages and collage.Cached data live in it, and an invalidation on any instance reaches every one. collage-redis is one:

store, err := redis.New(redis.Config{URL: os.Getenv("REDIS_URL")})
if err != nil {
	log.Fatal(err)
}
app, err := collage.New(&collage.Config{
	Cache: collage.CacheConfig{Enabled: true, Store: store},
})

docs/caching.md says what core does with it and has collagetest.SharedCacheSuite for checking your own.

Template functions of your own. TemplateConfig.Funcs is merged over the built-ins at construction, so an entry under a built-in name replaces it. It has to be set before New, because that is where templates are parsed.

app, err := collage.New(&collage.Config{
	Template: collage.TemplateConfig{
		Root: "templates",
		Funcs: template.FuncMap{
			"money": func(cents int64) string { return fmt.Sprintf("$%d.%02d", cents/100, cents%100) },
		},
	},
})

Documents and assets. Neither is an extension point so much as a second and third kind of route, reachable from pkg/collage alone.

sitemap := collage.NewDocument("sitemap", "application/xml").
	WithPath("en", "/sitemap.xml").
	WithHandler(func(ctx context.Context, rc *collage.RenderContext) ([]byte, []string, error) {
		body, err := buildSitemap(store.List())
		return body, []string{"blog:posts"}, err
	}).
	Incremental(time.Hour).
	WithDependencyTags("blog:posts").
	Build()

err := app.RegisterDocument(sitemap)

// os.OpenRoot, not os.DirFS: os.DirFS does not prevent symlink traversal.
root, err := os.OpenRoot("./static")
if err != nil {
	log.Fatal(err)
}
err = app.Mount("/static/", root.FS(), collage.WithCacheControl("public, max-age=3600"))

docs/documents.md and docs/assets.md cover both, including what they deliberately do not do.

A plugin. Implement collage.Plugin and whichever hooks you need; the registry finds them by type assertion, so assert the interfaces you meant to implement.

// stamp post-processes every rendered page.
type stamp struct{}

func (s *stamp) Name() string                                 { return "stamp" }
func (s *stamp) Version() string                              { return "1.0.0" }
func (s *stamp) Init(_ context.Context, host collage.Host) error { return nil }
func (s *stamp) Shutdown(_ context.Context) error             { return nil }

func (s *stamp) OnAfterRender(_ context.Context, ev *collage.AfterRenderEvent) error {
	ev.HTML = append(ev.HTML, "<!-- stamped -->"...)
	return nil
}

var (
	_ collage.Plugin          = (*stamp)(nil)
	_ collage.AfterRenderHook = (*stamp)(nil)
)

err := app.RegisterPlugin(&stamp{})

Details in docs/caching.md, docs/fragments.md and docs/plugins.md.

Documentation

Document Covers
docs/architecture.md Package layout, the request lifecycle, why data is fetched concurrently but markup written in order, why plugins get a Host
docs/fragments.md Fragments, slots, layouts, data handlers, timeouts, the failure policy, template functions
docs/documents.md Document: non-HTML responses, the handler contract, plain-text errors, the three hooks, static builds
docs/assets.md App.Mount: fs.FS assets, Range and ETags, why os.OpenRoot rather than os.DirFS, and the limitations
docs/caching.md Render strategies, the cache key, ETags, Vary, dependency tags, invalidation, custom caches
docs/plugins.md The Plugin contract, the Host, every hook, dispatch and error semantics, lifecycle
docs/routing.md Path patterns, locales, redirects, error-page resolution, registration errors
docs/http.md app.Use middleware, app.Handle for your own handlers, collage.Vary
docs/actions.md Action: methods, forms, ActionResult, request-forgery tokens, fragments at their own URLs
docs/testing.md collagetest: driving app.Handler() from a test like a browser — cookies, forms with their hidden fields, redirects
docs/deployment.md Building a binary, containers, signals, TLS, the cache in production, health checks
docs/cli.md collage new/add/dev/build, the scaffold's layout, plugin subcommands, and the static site builder

The CLI

collage new myblog          # scaffold a runnable project
collage add page blog/post  # write a page, an action, a document into it
collage dev                 # run it, rebuilding and restarting on every Go change
collage build               # compile the binary you deploy
collage export              # render the statically-buildable pages to dist/
collage check               # find links to pages, actions and locales that do not exist
collage plugin add owner/name  # fetch a plugin, add it to plugins.go and its settings
collage plugin del owner/name  # take a plugin out again, and its settings with it
collage plugin check        # check a plugin repository against what add enforces
collage serve               # serve dist/ the way a static host would

Contributing and security

CONTRIBUTING.md has what a change needs to be merged. A vulnerability is reported privately — see SECURITY.md, not an issue.

License

MIT — see LICENSE.

Directories

Path Synopsis
cmd
collage command
Command collage is the collage CLI: it scaffolds new projects (collage new), drives an existing project's development loop (collage dev), compiles it into the binary you deploy (collage build), renders it to static files (collage export), and serves such an export the way a static host would (collage serve).
Command collage is the collage CLI: it scaffolds new projects (collage new), drives an existing project's development loop (collage dev), compiles it into the binary you deploy (collage build), renders it to static files (collage export), and serves such an export the way a static host would (collage serve).
internal
ascii
Package ascii is the case folding collage uses on anything a request carries: a path, a header, a host, the markup a page rendered.
Package ascii is the case folding collage uses on anything a request carries: a path, a header, a host, the markup a page rendered.
build
Package build renders a collage application's cacheable pages to static files on disk.
Package build renders a collage application's cacheable pages to static files on disk.
cache
Package cache defines the render-output cache used by the HTTP handler: an interface, a collision-resistant key scheme, and an in-memory implementation.
Package cache defines the render-output cache used by the HTTP handler: an interface, a collision-resistant key scheme, and an in-memory implementation.
cli
Package cli implements the collage command-line tool: scaffolding a new project, and driving an existing one's dev and static-build workflows.
Package cli implements the collage command-line tool: scaffolding a new project, and driving an existing one's dev and static-build workflows.
cli/plugincontract
Package plugincontract is the rule a module meets to be a collage plugin "collage plugin add" accepts: a manifest at its root, a well-formed name, a New it can call without configuration, a release tag.
Package plugincontract is the rule a module meets to be a collage plugin "collage plugin add" accepts: a manifest at its root, a well-formed name, a New it can call without configuration, a release tag.
core
Package core is the application orchestrator: it owns one instance of every subsystem — the template engine, the render engine, the cache, the dependency tracker, the router, the plugin registry, and observability — and exposes them through a single App with a server lifecycle.
Package core is the application orchestrator: it owns one instance of every subsystem — the template engine, the render engine, the cache, the dependency tracker, the router, the plugin registry, and observability — and exposes them through a single App with a server lifecycle.
core/inspecttest/a/models
Package models is a test fixture: it shares its name, and a type name, with its sibling under b, to collide in the inspect type table.
Package models is a test fixture: it shares its name, and a type name, with its sibling under b, to collide in the inspect type table.
core/inspecttest/b/models
Package models is a test fixture: see ../../a/models.
Package models is a test fixture: see ../../a/models.
csrf
Package csrf issues and verifies cross-site request forgery tokens.
Package csrf issues and verifies cross-site request forgery tokens.
datacache
Package datacache keeps the data fragments fetch, across renders, so that twenty pages showing one author ask the upstream for that author once.
Package datacache keeps the data fragments fetch, across renders, so that twenty pages showing one author ask the upstream for that author once.
dependency
Package dependency tracks which cache keys were produced from which dependency tags, and resolves tags back to the keys they invalidate.
Package dependency tracks which cache keys were produced from which dependency tags, and resolves tags back to the keys they invalidate.
devhost
Package devhost decides whether a request's Host header names this machine, which is what keeps a development server from answering a page on another site.
Package devhost decides whether a request's Host header names this machine, which is what keeps a development server from answering a page on another site.
envfile
Package envfile reads the environment files "collage dev" and collage.Main load: KEY=value lines a variable already set in the environment wins over.
Package envfile reads the environment files "collage dev" and collage.Main load: KEY=value lines a variable already set in the environment wins over.
httpx
Package httpx serves rendered pages over HTTP: it wires the router, the render engine, the cache, the dependency tracker, the plugin registry, and observability into one request lifecycle.
Package httpx serves rendered pages over HTTP: it wires the router, the render engine, the cache, the dependency tracker, the plugin registry, and observability into one request lifecycle.
netaddr
Package netaddr holds the address matching that more than one package needs: reading a request's direct peer and asking whether it is one of a set of trusted ranges, and whether a request arrived over TLS.
Package netaddr holds the address matching that more than one package needs: reading a request's direct peer and asking whether it is one of a set of trusted ranges, and whether a request arrived over TLS.
observability
Package observability defines the framework's metrics and tracing interfaces.
Package observability defines the framework's metrics and tracing interfaces.
plugin
Package plugin defines the plugin system: the Plugin contract, the narrow Host capability surface plugins receive at startup, and a Registry that owns registration, lifecycle, and hook dispatch.
Package plugin defines the plugin system: the Plugin contract, the narrow Host capability surface plugins receive at startup, and a Registry that owns registration, lifecycle, and hook dispatch.
render
Package render composes a page's fragment tree into HTML.
Package render composes a page's fragment tree into HTML.
router
Package router resolves incoming HTTP requests to pages, documents, redirects, or a not-found result: a radix tree per locale shared by page and document paths, a shared radix tree for redirects, and locale resolution from the request path — the only source of a locale there is.
Package router resolves incoming HTTP requests to pages, documents, redirects, or a not-found result: a radix tree per locale shared by page and document paths, a shared radix tree for redirects, and locale resolution from the request path — the only source of a locale there is.
template
Package template wraps a Go template engine behind a narrow interface so the underlying implementation (currently html/template) can be swapped without touching callers.
Package template wraps a Go template engine behind a narrow interface so the underlying implementation (currently html/template) can be swapped without touching callers.
template/typecheck
Package typecheck walks a parsed template against the Go type of the data it will run with, and reports what is certain to fail when it renders: a field the type does not have, a method called with the wrong number of arguments, with an argument its parameter cannot take, or through a value that cannot reach it, a range over something that cannot be ranged over.
Package typecheck walks a parsed template against the Go type of the data it will run with, and reports what is certain to fail when it renders: a field the type does not have, a method called with the wrong number of arguments, with an argument its parameter cannot take, or through a value that cannot reach it, a range over something that cannot be ranged over.
term
Package term decides whether output is going somewhere that can take colour, and writes it either way.
Package term decides whether output is going somewhere that can take colour, and writes it either way.
pkg
collage
Package collage is the public API of the collage framework: a Go framework for server-side component-based rendering built from composable fragments and pages.
Package collage is the public API of the collage framework: a Go framework for server-side component-based rendering built from composable fragments and pages.
collagetest
Package collagetest drives a collage application from a test the way a browser does: through its http.Handler, with cookies kept between requests and forms submitted with what the page put in them.
Package collagetest drives a collage application from a test the way a browser does: through its http.Handler, with cookies kept between requests and forms submitted with what the page put in them.

Jump to

Keyboard shortcuts

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