Repository: gohugoio/hugo (2026-03-28T18:14:43Z)

Issue:
Intermittent problem with AddResource in content adapter
See <https://discourse.gohugo.io/t/56922/11>.

This isn't anything new.

In short, creating a page resource (an image) by setting `content.value` to the string returned by a global resource's `.Content` value results in intermittent errors:

> error calling Resize: failed to resize image "/home/kijana/Code/hugo-forum-topic-54852/content/products/_content.gotmpl": resize /products/window-frame/01.jpg: invalid JPEG format: short Huffman data

But the image is not corrupt in any way (views fine in any app, exiftool likes it, ImageMagick has no complaints, etc.).

In some cases I had to run this command 20 times to trigger the error:

```text
rm -rf resources && hugo
```

The page resource is created without error if I set `content.value` to the global resource itself, which per the documentation is noted to be "more efficient".






Discussion:
I had Claude Sonnet 4.6 take a look at this...

Root cause: `Source.ValueAsOpenReadSeekCloser()` in `page_frontmatter.go:557` used `hugio.NewOpenReadSeekCloser` which wraps a single shared `strings.Reader` and seeks it to position `0` before returning it on each call. When multiple goroutines call `ReadSeekCloser()` concurrently on the same resource (e.g., hashing, publishing, and image decode/resize), their reads interleave on the shared reader, corrupting binary data like JPEGs.

Fix: Changed `ValueAsOpenReadSeekCloser()` to return a closure that creates a fresh `strings.Reader` on each call, matching the pattern used elsewhere in the codebase (e.g., `cloneWithUpdates`):

```go
func (s Source) ValueAsOpenReadSeekCloser() hugio.OpenReadSeekCloser {
    content := s.ValueAsString()
    return func() (hugio.ReadSeekCloser, error) {
        return hugio.NewReadSeekerNoOpCloserFromString(content), nil
    }
}
```

The bug only triggered when `content.value` was a string (e.g., from `.Content`); when it was a `resource.Resource`, a different code path was used that reads directly from the resource's own (safe) opener.

I'll submit a PR for this in a bit.
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.



Patch:
diff --git a/resources/page/pagemeta/page_frontmatter.go b/resources/page/pagemeta/page_frontmatter.go
index b5f9149b763..7c32e89e61a 100644
--- a/resources/page/pagemeta/page_frontmatter.go
+++ b/resources/page/pagemeta/page_frontmatter.go
@@ -555,7 +555,10 @@ func (s Source) ValueAsString() string {
 }
 
 func (s Source) ValueAsOpenReadSeekCloser() hugio.OpenReadSeekCloser {
-	return hugio.NewOpenReadSeekCloser(hugio.NewReadSeekerNoOpCloserFromString(s.ValueAsString()))
+	content := s.ValueAsString()
+	return func() (hugio.ReadSeekCloser, error) {
+		return hugio.NewReadSeekerNoOpCloserFromString(content), nil
+	}
 }
 
 // FrontMatterOnlyValues holds values that can only be set via front matter.


Test patch:
diff --git a/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go b/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go
index 30b6cab3372..2b3aa6e29db 100644
--- a/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go
+++ b/hugolib/pagesfromdata/pagesfromgotmpl_integration_test.go
@@ -893,3 +893,29 @@ baseURL = "https://example.com"
 	// _content.gotmpl which was siblings of index.md (leaf bundles) was mistakingly classified as a content resource.
 	hugolib.Test(t, files)
 }
+
+// Issue 14684
+func TestPagesFromGoTmplAddResourceFromStringContent(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term", "rss", "sitemap"]
+baseURL = "https://example.com"
+-- assets/a/pixel.png --
+iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
+-- layouts/single.html --
+{{ with .Resources.Get "pixel.png" }}
+{{ with .Resize "1x1" }}Resized: {{ .Width }}x{{ .Height }}|{{ end }}
+{{ end }}
+-- content/_content.gotmpl --
+{{ $pixel := resources.Get "a/pixel.png" }}
+{{ $content := dict "mediaType" $pixel.MediaType.Type "value" $pixel.Content }}
+{{ $.AddPage (dict "path" "p1" "title" "p1") }}
+{{ $.AddResource (dict "path" "p1/pixel.png" "content" $content) }}
+`
+
+	b := hugolib.Test(t, files)
+
+	b.AssertFileContent("public/p1/index.html", "Resized: 1x1|")
+}
diff --git a/resources/page/pagemeta/page_frontmatter_test.go b/resources/page/pagemeta/page_frontmatter_test.go
index 39074ee15f2..d99d75248b0 100644
--- a/resources/page/pagemeta/page_frontmatter_test.go
+++ b/resources/page/pagemeta/page_frontmatter_test.go
@@ -14,6 +14,7 @@
 package pagemeta_test
 
 import (
+	"io"
 	"strings"
 	"testing"
 	"time"
@@ -154,6 +155,39 @@ func TestFrontMatterDatesDefaultKeyword(t *testing.T) {
 	c.Assert(d.PageConfigLate.Dates.ExpiryDate.IsZero(), qt.Equals, true)
 }
 
+// Issue 14684
+// Each call to the opener must return an independent reader. With the old
+// implementation, opener() returned the same shared reader seeked to 0, so a
+// second open would reset the position of a reader already in use.
+func TestSourceValueAsOpenReadSeekCloserIsIndependent(t *testing.T) {
+	c := qt.New(t)
+	s := pagemeta.Source{Value: "abcdefgh"}
+	opener := s.ValueAsOpenReadSeekCloser()
+
+	r1, err := opener()
+	c.Assert(err, qt.IsNil)
+	defer r1.Close()
+
+	// Partially consume r1.
+	buf := make([]byte, 4)
+	_, err = io.ReadFull(r1, buf)
+	c.Assert(err, qt.IsNil)
+	c.Assert(string(buf), qt.Equals, "abcd")
+
+	// Open a second reader and fully consume it.
+	r2, err := opener()
+	c.Assert(err, qt.IsNil)
+	defer r2.Close()
+	all, err := io.ReadAll(r2)
+	c.Assert(err, qt.IsNil)
+	c.Assert(string(all), qt.Equals, "abcdefgh")
+
+	// r1's position must be unaffected; it should yield the remaining half.
+	rest, err := io.ReadAll(r1)
+	c.Assert(err, qt.IsNil)
+	c.Assert(string(rest), qt.Equals, "efgh")
+}
+
 func TestContentMediaTypeFromMarkup(t *testing.T) {
 	c := qt.New(t)
 	logger := loggers.NewDefault()