Caddy: Allow template inheritance

Created on 29 Mar 2018  Â·  7Comments  Â·  Source: caddyserver/caddy

Go's text/template package is built for composability, but sharing context between templates is currently impossible. The functions (*Template).New “allows one template to invoke another with a {{template}} action” — can we modify httpserver.Include or .ContextInclude in caddyhttp/httpserver/tplcontext.go to add user-requested templates to a template’s “namespace” rather than rendering and executing a template immediately? I’m not 100% sure of the syntax here, but I’d imagine an end-user would do something like:

shell.tmpl:

{{define "shell"}}
    <!doctype html>
    <title>{{block "title"}}default title{{end}}</title>
    <link rel="stylesheet" type="text/css" href="/resources/main.css"></style>
    <div id="content">
    {{block "content"}}{{end}}
    </div>
{{end}}

and index.tmpl:

{{.Include "shell.tmpl"}}
{{define "title"}}index page{{end}}
{{define "content"}}welcome to my website!{{end}}
{{template "shell"}}

Ideally, I’d like to be able to combine the {{define "shell"}}, {{.Include "x.tmpl"}}, and {{template x}} actions into one step; I personally like Twig’s syntax for template inheritance where a subtemplate just {% extends "base.html" %} and then redefines blocks from the parent template. I’m not sure if that would need to be implemented within the template engine though. An alternate syntax could look more like Twig’s:

index.tmpl:

{{.Extend "shell.tmpl"}}
{{define "title"}}index page{{end}}
...

I’d be happy to contribute a fix here or in some other package; what direction should we go with this?

discussion feature request

Most helpful comment

No longer interested in this feature -- I've decided static site generators better suit my needs. Thanks for checking up on this stale issue, Matt. :smile:

All 7 comments

Thinking about this a bit more, it seems that two things are at odds here.

  1. Web developers would like to write templates that arbitrarily include and extend other templates on the server.
  2. To include an “external” (i.e. not defined in the same file) template A in a template B, B’s object within Go must have loaded/parsed/included A before B is rendered. Knowing which templates B might want to include requires looking at B before the template renderer does.

Ultimately, I’d like to avoid having to write down a static list of all the templates I’d like to render (I don’t think that’s very versatile or elegant) but we also need to reconcile that with the need for pre-loaded templates. This solution on Reddit requires writing a list of template inheritances at compile time, which is unacceptable in my opinion. This blog post, also, hard-codes the one allowed base template at compile-time.

The linked Reddit post also contained an interesting tidbit, that “html/template caches templates in memory once parsed”, which might make some kind of stateful system — one that allows that caching system to work — make sense. Maybe it would be acceptable to pre-parse templates for some “extends” action for files to load if the caching would speed things up? Adam Crossland wrote an extension to html/template to allow nested templates — I might investigate putting that into a Caddy plugin. It seems to be more-or-less complete, although it hasn’t been updated in about 6 years. (!) There’s also plenty of other template engines; Jet jumps out to me due to it… not changing too much from the default text/template while promising speed and adding the one feature I want (haha).

Anyways, there’s probably a balance to be struck here between a super-fully-featured engine like Hero which can run any arbitrary Go code and the intentionally super-simple and limited built-in Go templates. I’m not sure where you want to settle within there; I like #1958’s idea of using Swig, which seems to be “just a bit more” than the default. Perhaps the place for a richer templating engine is in a plugin? If so, I think that would be a valuable addition to the ecosystem.

Thanks for thinking this through so thoroughly, @9999years --- that's awesome!

I have a few thoughts. This sounds like a really great feature and I think you're not the first to ask for it (the link escapes my memory), so I think it's worth pursuing.

We strive to minimize dependencies as much as possible. In fact, it's probably my main concern with this: how far can this feature come along with just the standard library? Perhaps diving into coding something with the standard lib is the next step, mock up a proof-of-concept and see how far that goes without becoming unwieldy or impractical.

I'd be happy to have some advanced templating baked into our standard templates directive -- as long as the cost isn't too high (dependencies, added complexity including cognitive overhead, etc).

Looking forward to seeing what you can come up with!

OK, I’ve got a simple proof-of-concept at 9999years/caddy which allows writing:

base.tmpl:

<!doctype html>
<title>{{block "title" .}}{{end}}</title>
{{block "includes" .}}{{end}}
<div id="content">
{{block "content" .}}{{end}}
</div>

index.tmpl:

{{.Include "base.tmpl"}}
{{define "title"}}index{{end}}
{{define "content"}}welcome to the index{{end}}

Which is rendered as:

<!doctype html>
<title>index</title>

<div id="content">
welcome to the index
</div>

Which is a great start! The main limitation is that blocks can’t be redefined unless their first definition was empty (i.e. to have default content):

base.tmpl:

{{define "base"}}
{{block "content" .}}default{{end}}
{{end}}

index.tmpl:

{{.Include "base.tmpl"}}
{{define "content"}}non-default{{end}}
{{template "base" .}}

Renders default. I’m not sure how re-definition might be implemented.

Tests still need to be written. The implementation works by adding a Template field to httpserver.Context which is passed to httpserver.ContextInclude, which then calls Parse on the given template if it’s non-nil rather than a new one.

FYI, you can just pass related context with parameter. For example:

shell.tmpl:

{{$map := index .Args 0}}
{{$title := $map.title}}
{{$content := $map.content}}

<!doctype html>
<title>{{$title}}</title>
<link rel="stylesheet" type="text/css" href="/resources/main.css"></style>
<div id="content">
{{$content}}
</div>

and index.tmpl:

{{.Include "shell.tmpl" (.Map "title" "index page" "content" "welcome to my website!")}}

BTW, I use this way to construct my blog (https://github.com/tw4452852/totorow/tree/master/web).

Sounds great so far! Looking forward to seeing if it turns out. :)

Is this feature still needed/desired in v2? Does {{include "file"}} not suffice? If it is not needed in v2, I will probably close this issue. But if there's still work to do, let's figure it out.

No longer interested in this feature -- I've decided static site generators better suit my needs. Thanks for checking up on this stale issue, Matt. :smile:

Was this page helpful?
0 / 5 - 0 ratings