skip to content

Pages parsed with Go's template.ParseFS render blank or show another page's content — which naming rules explain it, and how do you confirm?

level: seniorimportance: should knowfreq 31%

answer

  1. names come from files, not paths
  2. two directories, one surviving name
  3. a file of only defines has no body
  4. print the set right after parsing
  5. assert the expected names in a test

basics

~20 s

ParseFS names each template by the file's base name, so same-named files in different directories collide and the last parsed wins. A file of only {{define}} blocks has an empty body, so executing its name writes nothing.

solid answer

~40 s

Two rules, both about names. First, `ParseFS`, `ParseFiles` and `ParseGlob` name each template by the file's *base name*, not its path: `pages/index.html` and `admin/index.html` both become `index.html`, and whichever is parsed later silently replaces the other — that is the page showing someone else's content. Second, a page file containing nothing but `{{define "content"}}...{{end}}` has an empty body under its own name, so executing that file's name writes nothing; you must execute the layout that calls the block. I confirm it by printing `t.DefinedTemplates()` immediately after parsing and, better, by asserting the expected names in a test that walks `t.Templates()` or calls `Lookup` for each one. The durable fix is to stop relying on base names: wrap each page in an explicit `{{define "pages/about"}}`, or build one set per page.

code

go · 11 lines
go
//go:embed layouts/*.html pages/*.html
var files embed.FS

func TestTemplateSet(t *testing.T) {
	tmpl := template.Must(template.ParseFS(files, "layouts/*.html", "pages/*.html"))
	for _, name := range []string{"base.html", "index.html", "about.html"} {
		if tmpl.Lookup(name) == nil {
			t.Errorf("missing %q; have %s", name, tmpl.DefinedTemplates())
		}
	}
}

go deeper

for a junior

Know that templates parsed from files are addressed by the file's base name, and that ExecuteTemplate needs exactly that name to render anything.

for a middle

Explain why two same-named files in different directories collapse into one entry, and why a file made only of {{define}} blocks renders nothing when executed under its own name.

for a senior

Demonstrate the diagnosis: print or assert the parsed name set at startup or in a test, then pick a real fix — explicit define names or one set per page — instead of reordering patterns and hoping.

for a principal

Own the naming convention across the whole template tree and require a test over the parsed set, because this bug ships silently and is visible only to whoever opens the affected page.

## The rule that causes it Every file-based parse helper — `template.ParseFiles`, `template.ParseGlob` and `template.ParseFS` — derives the template's name from the file's **base name**, the part after the last slash. Directories are not part of the name. The standard library says so plainly and adds the consequence: when several files with the same base name are parsed, the last one mentioned is the one that results. In a site generator laid out as `layouts/base.html`, `pages/index.html` and `admin/index.html`, the parsed set therefore holds `base.html` and a single `index.html`, and which content it carries depends on the order the patterns were listed. No error, no warning. The page simply renders the other directory's markup, and nothing in the code reads as suspicious. ## The rule that causes the blank page The second failure looks unrelated but is the same idea seen from the other side. Only the text *outside* every `{{define}}` becomes the body of the file's own template. A page file written for a layout system usually contains nothing else: ``` {{define "content"}}<h1>About</h1>{{end}} ``` So the set now contains `about.html` with an empty body, and `content` with the markup. `ExecuteTemplate(w, "about.html", data)` writes only whatever whitespace surrounded the define — an empty page, status 200, no error. The correct call executes the layout: `ExecuteTemplate(w, "base.html", data)`, and the layout's `{{block "content" .}}` or `{{template "content" .}}` pulls the page in. A related shape fails more loudly: if you start from `template.New("root")` and no parsed file is named `root`, the receiver has no body at all and `Execute` returns an error about an incomplete or empty template. That one at least tells you something. ## Confirming it The set is inspectable, so guessing is unnecessary. - `t.DefinedTemplates()` returns a printable string listing every name in the set — the same string the package embeds in its own error messages. Printing it once after parsing shows a collision immediately: the expected count is short by one. - `t.Templates()` returns the associated templates, and `t.Lookup(name)` returns nil for a name that is not there. - Because parsing happens at startup, all of that belongs in a **test**, not in a log line someone might read. A table of expected names checked with `Lookup` turns a silent rendering bug into a red build. Two more things worth checking while you are there. `ParseFS` takes glob patterns and a glob does not cross directory separators, so `*.html` matches only the top level; you list each directory explicitly. And a pattern that matches no files is an error, which is a genuinely useful early warning when a directory gets renamed. ## Why it fails silently Associating a name with a new non-empty body is a legal, intended operation — it is precisely how a page overrides a layout's block. The package has no way to tell a deliberate override from an accidental collision, so it reports nothing. That is exactly why the assertion has to be yours. ## Fixing it properly Reordering the patterns so the "right" file wins is not a fix; it leaves the trap armed for the next person who adds a file. The two durable options: 1. **Name templates explicitly.** Wrap each page's whole content in `{{define "pages/about"}} ... {{end}}` and address it by that name. The name is now a deliberate choice visible in the diff, independent of where the file sits. 2. **One set per page.** Parse the layout once and, for each page, parse that page into a copy of the layout set. Each page then owns its own `content`, and there is nothing left to collide. Either way the invariant to hold onto is simple: *the name of a template is a decision, not a side effect of a file path.* Once a codebase treats it that way, this whole class of bug stops happening.

  • Does template.ParseFS recurse into subdirectories?
    No. Each argument is a glob pattern and a glob does not cross directory separators, so `*.html` matches only the top level. List the directories explicitly — `ParseFS(files, "layouts/*.html", "pages/*.html")`. A pattern that matches no files is reported as an error, which is a useful early warning when someone renames a directory.
  • How do you avoid base-name collisions entirely?
    Stop letting the file path decide the name. Either wrap each file's content in an explicit `{{define "pages/about"}}` and address that name, or build one small set per page from a pre-parsed layout. Both make the template name a reviewed decision rather than a side effect of where the file happens to live.
  • Why does the collision fail silently instead of erroring?
    Because replacing a name's body with a new non-empty one is the supported mechanism for overriding a layout's block. The package cannot distinguish an intended override from an accidental duplicate base name, so it reports nothing. Only an assertion of your own over the parsed set catches it.

saying these in an interview costs you the question

  • Assumes the directory path is part of the template name
  • Expects a duplicate base name to be a parse error
  • Executes the page file and blames the layout for the blank output
  • Uses a *.html pattern and expects subdirectories to match
  • Never inspects the parsed name set before shipping