{"id":"b13a8f025ebea695","repo":"gohugoio/hugo","slug":"source-must-be-a-map-got-t","errorCode":null,"errorMessage":"source must be a map, got %T","messagePattern":"source must be a map, got %T","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"tpl/collections/merge.go","lineNumber":61,"sourceCode":"\t}\n\n\treturn result, nil\n}\n\n// merge creates a copy of dst and merges src into it.\nfunc (ns *Namespace) merge(src, dst any) (any, error) {\n\tvdst, vsrc := reflect.ValueOf(dst), reflect.ValueOf(src)\n\n\tif vdst.Kind() != reflect.Map {\n\t\treturn nil, fmt.Errorf(\"destination must be a map, got %T\", dst)\n\t}\n\n\tif !hreflect.IsTruthfulValue(vsrc) {\n\t\treturn dst, nil\n\t}\n\n\tif vsrc.Kind() != reflect.Map {\n\t\treturn nil, fmt.Errorf(\"source must be a map, got %T\", src)\n\t}\n\n\tif vsrc.Type().Key() != vdst.Type().Key() {\n\t\treturn nil, fmt.Errorf(\"incompatible map types, got %T to %T\", src, dst)\n\t}\n\n\treturn mergeMap(vdst, vsrc).Interface(), nil\n}\n\nfunc caseInsensitiveLookup(m, k reflect.Value) (reflect.Value, bool) {\n\tif m.Type().Key().Kind() != reflect.String || k.Kind() != reflect.String {\n\t\t// Fall back to direct lookup.\n\t\tv := m.MapIndex(k)\n\t\treturn v, hreflect.IsTruthfulValue(v)\n\t}\n\n\tk2 := reflect.New(m.Type().Key()).Elem()\n","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/gohugoio/hugo/blob/8a468df065a75c1c7cf9f6850f32148746590ea5/tpl/collections/merge.go#L43-L79","documentation":"After confirming the destination is a map, Hugo's `merge` checks each source parameter. A source that is truthy (non-nil, non-empty) but not a map cannot be merged, so merge fails with this error naming the offending type. Falsy sources (nil, empty) are silently skipped and the destination is returned unchanged.","triggerScenarios":"`{{ merge $src $dst }}` where `$src` is a non-empty slice, string, number, or other non-map value. With more than two parameters, any earlier parameter that is a non-empty non-map triggers it, e.g. `merge .Params.list site.Params.opts`.","commonSituations":"Front matter key expected to be a map is authored as a list; passing `.Pages` or the result of `where` into merge; merging `.Params.foo` on pages where some pages define `foo` as a string; upgrading themes where a params key changed shape.","solutions":["Inspect the source type with `{{ printf \"%T\" $src }}` and fix the data so it's a map.","If the source may legitimately be empty/missing, that's fine (it's skipped) — but ensure non-empty values are maps in all content files/data files.","Use `where`/`append` for slices; merge is only for dicts.","Normalize params across pages via cascade in front matter so the key always has map shape."],"exampleFix":"# before (front matter)\nextra:\n  - one\n  - two\n# after\nextra:\n  first: one\n  second: two","handlingStrategy":"type-guard","validationCode":"{{ if and (reflect.IsMap $dst) (reflect.IsMap $src) }}{{ $merged := merge $dst $src }}{{ end }}","typeGuard":"{{ $bothMaps := and (reflect.IsMap $dst) (reflect.IsMap $src) }}","tryCatchPattern":"{{ with try (merge $dst $src) }}{{ with .Err }}{{ warnf \"merge source invalid: %s\" . }}{{ end }}{{ end }}","preventionTips":["Check every argument to merge is a map, not just the first","Default missing params with `| default dict` so the source is always a map","Remember merge takes maps only — use append/union for slices"],"tags":["hugo","templates","merge","type-mismatch"],"analyzedSha":"8a468df065a75c1c7cf9f6850f32148746590ea5","analyzedAt":"2026-07-31T21:23:07.045Z","schemaVersion":2}