A secure, file-system-based Go template engine built on Google's safehtml/template. It provides a familiar structure of layouts, pages, and reusable blocks (partials) while ensuring output is safe from XSS vulnerabilities by default.
Go's standard html/template is good, but Google's safehtml/template is better, providing superior, context-aware automatic escaping that offers stronger security guarantees against XSS. However, safehtml/template can be complex to set up, especially for projects using a traditional layout/page/partial structure.
This library provides a simple, opinionated framework around safehtml/template so you can get the security benefits without the setup overhead.
- Secure by Default: Built on
safehtml/templateto provide contextual, automatic output escaping. - Optional Security Relaxing: Disable
safehtmlfor rapid prototyping or projects using HTMX when the strict type system becomes a hindrance. - Layouts, Pages, and Blocks: Organizes templates into a familiar and powerful structure. Render pages within different layouts, or render blocks individually.
- Live Reloading: Automatically re-parses templates on every request for a seamless development experience.
- Production-Ready: Uses Go's
embed.FSto compile all templates and assets into a single binary for production deployments. - Dynamic Rendering: Includes a
d_blockhelper to render blocks dynamically by name—perfect for headless CMS integrations. - Convenient Helpers: Comes with a
localsfunction to easily pass key-value data to blocks. - Framework Integrations: Provides optional, lightweight integration packages for
net/http,Echo,chi, andgin-gonic/gin.
Add the library to your go.mod file:
go get github.com/dryaf/templatesThen import it in your code:
import "github.com/dryaf/templates"-
Create your template files:
. └── files └── templates ├── layouts │ └── application.gohtml └── pages └── home.gohtmlfiles/templates/layouts/application.gohtml:{{define "layout"}} <!DOCTYPE html> <html><body> <h1>Layout</h1> {{block "page" .}}{{end}} </body></html> {{end}}files/templates/pages/home.gohtml:{{define "page"}} <h2>Home Page</h2> <p>Hello, {{.}}!</p> {{end}} -
Write your Go application:
package main import ( "log" "net/http" "github.com/dryaf/templates" "github.com/dryaf/templates/integrations/stdlib" ) func main() { // For development, New() uses the local file system. // For production, you would pass in an embed.FS. tmpls := templates.New() tmpls.AlwaysReloadAndParseTemplates = true // Recommended for development tmpls.MustParseTemplates() renderer := stdlib.FromTemplates(tmpls) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { err := renderer.Render(w, r, http.StatusOK, "home", "World") if err != nil { log.Println(err) http.Error(w, "Internal Server Error", 500) } }) log.Println("Starting server on :8080") http.ListenAndServe(":8080", nil) }
The New function accepts Functional Options for flexible configuration:
WithFileSystem(fs fs.FS): Sets the filesystem (OS or Embed).WithRoot(path string): Sets the root directory for templates.WithFuncMap(fm template.FuncMap): Adds custom helper functions.WithReload(enabled bool): Enables auto-reloading for development.WithDisableSafeHTML(disabled bool): Disables thesafehtmlengine and falls back to standardtext/template.WithDisableTrustedLog(disabled bool): Disables INFO logs when usingtrusted_*_ctxhelpers.WithLogger(logger *slog.Logger): Sets a custom logger.
If you find safehtml's strict type system too restrictive during early development, or for projects that don't require high security (like internal tools using HTMX), you can disable it. The engine will use text/template, and the trusted_* functions will act as literal passthroughs for your raw data.
tmpls := templates.New(
templates.WithDisableSafeHTML(true),
templates.WithDisableTrustedLog(true), // Suppress info logs for bypasses
)// Defaults to OS filesystem, rooted at "files/templates"
tmpls := templates.New(
templates.WithReload(true), // Enable hot-reloading
)//go:embed files/templates
var templatesFS embed.FS
// ...
tmpls := templates.New(
templates.WithFileSystem(&templatesFS),
templates.WithRoot("files/templates"),
)The library returns structured errors that can be checked using errors.Is():
templates.ErrTemplateNotFound: The requested template or layout was not found.templates.ErrBlockNotFound: The requested block was not found.templates.ErrInvalidBlockName: The block name format is invalid (must start with_).
err := tmpls.RenderBlockAsHTMLString("_non_existent", nil)
if errors.Is(err, templates.ErrBlockNotFound) {
// Handle missing block
}The engine expects a specific directory structure by default, located at ./files/templates:
files/templates/layouts/: Contains layout templates. Each file defines a "layout".files/templates/pages/: Contains page templates.files/templates/blocks/: Contains reusable blocks (partials).
- Pages must define
"page": Every template file in thepagesdirectory must define its main content within{{define "page"}}...{{end}}. - Blocks must define
"_name": Every template file in theblocksdirectory must define a template, and that definition's name must match the filename and be prefixed with an underscore. For example,_form.gohtmlmust contain{{define "_form"}}...{{end}}.
You have fine-grained control over how templates are rendered:
"page_name": Renders the page within the default layout (application.gohtml)."layout_name:page_name": Renders the page within a specific layout.":page_name": Renders the page without any layout."_block_name": Renders a specific block by itself.
To render a block whose name is determined at runtime (e.g., from a database or CMS API), you can use d_block. This is a powerful feature for dynamic page composition.
<!-- Instead of this, which requires the block name to be static: -->
{{block "_header" .}}{{end}}
<!-- You can do this: -->
{{d_block .HeaderBlockName .HeaderBlockData}}Passing maps as context to blocks can be verbose. The locals helper function makes it easy to create a map on the fly. It accepts a sequence of key-value pairs.
<!-- Standard block call with locals -->
{{block "_user_card" (locals "Name" "Alice" "Age" 30)}}{{end}}
<!-- Dynamic block call with locals -->
{{locals "Name" "Bob" "Age" 42 | d_block "_user_card"}}_user_card.gohtml:
{{define "_user_card"}}
<div class="card">
<h3>{{.Name}}</h3>
<p>Age: {{.Age}}</p>
</div>
{{end}}The references helper is similar to locals, but it ensures that every value passed to the map is a pointer. This is useful when your template logic expects pointers (e.g. for nil checks or to avoid copying large structs).
If a value is already a pointer, it is used as is. If it is not a pointer, a new pointer to a copy of the value is created.
<!-- Passing values as references -->
{{block "_user_edit" (references "User" .User "IsActive" true)}}This library uses safehtml/template, which provides protection against XSS by default. It contextually escapes variables.
Sometimes, you receive data from a trusted source (like a headless CMS) that you know is safe and should not be escaped. For these cases, you can use the trusted_* template functions, which wrap the input string in the appropriate safehtml type.
trusted_html: For HTML content.trusted_script: For JavaScript code.trusted_style: For CSS style declarations.trusted_stylesheet: For a full CSS stylesheet.trusted_url: For a general URL.trusted_resource_url: For a URL that loads a resource like a script or stylesheet.trusted_identifier: For an HTML ID or name attribute.
Example:
<!-- This will be escaped: -->
<p>{{.UnsafeHTMLFromUser}}</p>
<!-- This will be rendered verbatim, because you are vouching for its safety: -->
<div>
{{trusted_html .SafeHTMLFromCMS}}
</div>Note: If DisableSafeHTML is enabled, these functions will simply return your objects as-is, allowing them to bypass the standard auto-escaper.
For structured logging and auditing, this library provides context-aware versions of d_block and trusted_* functions. These helpers require a context.Context as the first argument.
Same as d_block, but logs errors (e.g. missing blocks) using the Logger attached to Templates.
// In template
{{ d_block_ctx .Ctx "my_block_name" .Data }}Context-aware versions of all trusted_* functions. They log an INFO message when called, which is useful for security auditing to track when and where bypasses are used.
trusted_html_ctxtrusted_script_ctxtrusted_style_ctxtrusted_stylesheet_ctxtrusted_url_ctxtrusted_resource_url_ctxtrusted_identifier_ctx
Usage:
// In template
{{ trusted_html_ctx .Ctx "<b>Bold Content</b>" }}You can disable these logs using WithDisableTrustedLog(true).
The integrations/stdlib package provides a simple renderer for use with net/http.
import "github.com/dryaf/templates/integrations/stdlib"
// --- inside main ---
// Create the renderer, configuring the underlying templates instance with functional options
renderer := stdlib.New(
templates.WithReload(true), // Example option: enable hot-reloading
)
// Use it in an http.HandlerFunc
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
renderer.Render(w, r, http.StatusOK, "home", "Data")
})
// Or create a handler that always renders the same template
http.Handle("/about", renderer.Handler("about", nil))The integrations/echo package provides a renderer for the Echo framework.
import "github.com/dryaf/templates/integrations/echo"
// ...
e := echo.New()
e.Renderer = templates_echo.Renderer(tmpls)
e.GET("/", func(c echo.Context) error {
return c.Render(http.StatusOK, "home", "World")
})The integrations/chi package provides a renderer compatible with the chi router.
import "github.com/dryaf/templates/integrations/chi"
// ...
renderer := chi.FromTemplates(tmpls)
r := chi.NewRouter()
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
renderer.Render(w, r, http.StatusOK, "home", "Chi")
})The integrations/gin package provides a renderer that implements gin.HTMLRender for the Gin framework.
import "github.com/dryaf/templates/integrations/gin"
// ...
router := gin.Default()
router.HTMLRender = templates_gin.New(tmpls)
router.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "home", "Gin")
})The library is considered feature-complete and stable for a v1.0.0 release. Future development will be driven by community feedback and integration requests for new frameworks. Potential ideas include:
- Additional template helper functions.
- Performance optimizations.
Feel free to open an issue to suggest features or improvements.
This project is licensed under the MIT License.