Embedding Vascula
This page is about the decisions you make when you build Vascula into an application. The exact types are in the Go API reference.
#Data and Allow
Templates get data in three ways, and each has a purpose:
| Option | Readable as | Use it for |
|---|---|---|
Settings | settings | A template's own parameters: a heading, a number of columns, a chosen colour. |
Globals | the key's name | Things every template may read: site-wide configuration, the current locale. |
Data + Allow | the key's name, if allowed | Application data with access control: the signed-in user, the cart, an order. |
Allow is the template's permission list. Build Data with everything the page might need, and give each template only the roots it should read. A template that is not allowed customer cannot read the customer, even though it is in Data.
html, err := tmpl.Render(vascula.Options{
Data: map[string]interface{}{
"product": product, // public
"customer": customer, // personal
},
Allow: []string{"product"}, // this template may not read customer
Settings: map[string]interface{}{"columns": 3},
Globals: map[string]interface{}{"site": siteConfig},
})An allowance is for a whole root: "cart.items" allows all of cart. Do not put anything in a root that some template allowed to read it must not see.
Let templates declare what they need. A good pattern is to store, with each template, the list of roots it reads, and to pass that list as Allow. Check the list when the template is saved with [ExternalNames](/inspection), so a template cannot be saved if it reads something it did not declare.
Build only what was asked for. Since you know each template's roots before rendering, you can skip loading data no template on the page reads.
#Compute values only when read
Some values are costly and most templates never read them: a count that needs a query, a related record. Put a *vascula.Lazy in their place. The render computes it the first time a template uses it, and never otherwise.
package main
import (
"fmt"
"vascula.dev/vascula"
)
func main() {
queries := 0
count := vascula.NewLazy(func() (interface{}, error) {
queries++ // one query, the first time a template uses the count
return 12, nil
})
brand := map[string]interface{}{"name": "Acme", "product_count": count}
for _, src := range []string{`{{ brand.name }}`, `{{ brand.product_count }} products`, `{{ brand | json }}`} {
tmpl, err := vascula.Compile(src)
if err != nil {
panic(err)
}
out, err := tmpl.Render(vascula.Options{Data: map[string]interface{}{"brand": brand}, Allow: []string{"brand"}})
if err != nil {
panic(err)
}
fmt.Println(out, queries)
}
}Acme 0
12 products 1
{"name":"Acme","product_count":12} 1A template sees the computed value however it reaches it: a property, an index, a loop over the object, an alias, printing or serializing the whole object, a filter, a comparison. Nothing can bypass it, so no analysis of the template is needed. A load that fails fails the render.
A Lazy computes once and keeps its result, so create them per request. To load many values in one query, have each Lazy ask a loader that fetches everything the request registered on its first call.
#Reserve your globals
A template could assign a name that hides a global. Reserve each global's name at compilation so that is a compilation error:
tmpl, err := vascula.CompileWithOptions(src, vascula.CompileOptions{
Reserved: []string{"site"},
})#Cache compiled templates
Compile when a template is saved or first used, and keep the *Template. Rendering a compiled template is cheap; compiling it on every request is not.
A compiled template depends on its source and on the CompileOptions you compiled it with (forms, tags, reserved names, limits). Key your cache on all of them, or on a version you change when you change the options. Never key it on Settings or Data: those are render inputs and change per request.
A compiled template is immutable and safe to share between goroutines.
#Children
Resolve turns a name in {% render "name" %} into a Child: a compiled template with its own Allow and Settings. Typical choices:
- Look the name up in a registry of compiled templates.
- Give each child the allowance stored with it, not the caller's. A child is a peer with its own permissions.
- Return an error for names a template may not use. The render fails with your message.
Before rendering, you can walk the whole tree with [RenderTargets](/inspection#the-composition-tree): every name is a literal, so the tree is known in advance, and you can reject cycles or missing children when a template is saved.
#Limits and isolation
Every render is bounded (see Safety and limits). The limits stop a template from looping forever or producing huge output, and they apply to the page as a whole, children included. They do not limit time spent inside your own filters, tags and resolver: bound those yourself.
If you render templates written by people you do not trust, run rendering with a timeout and, ideally, in a process with its own memory limit.
#Showing errors
Errors are written for the template's author: they name the problem and the line and column. Show them in your editor or preview. Do not show them, or the template source, to the visitors of a live page; render a generic error page instead and log the message.
To give authors better guidance, check for [UndeclaredNameError](/go-api#type-undeclarednameerror) and explain how they request access to a root in your system.
#Rendering into a page
Vascula renders strings. Set the response's content type yourself (text/html; charset=utf-8 for HTML), and write the result as-is: it is already escaped.
Never feed rendered output back into Compile. Output is data, not template source.