From f772998fa34927f469c70b83acdc72b62895d13e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Mon, 10 Aug 2026 18:10:06 +0200 Subject: [PATCH] Fix resource transformation chaining after content access When a transformed resource had been initialized (e.g. via .Data, .Content or .RelPermalink) before chaining another transformation, the new chain would run on the transformed output instead of the original source, re-running all transformations on their own output. Fixes #15189 Co-authored-by: Claude Fable 5 --- resources/transform.go | 10 ++++++- tpl/css/build_integration_test.go | 43 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/resources/transform.go b/resources/transform.go index 18c768b36..cee11f8ba 100644 --- a/resources/transform.go +++ b/resources/transform.go @@ -82,6 +82,7 @@ func newResourceAdapter(spec *Spec, lazyPublish bool, target transformableResour return &resourceAdapter{ resourceTransformations: &resourceTransformations{}, metaProvider: target, + sourceTarget: target, resourceAdapterInner: &resourceAdapterInner{ ctx: context.Background(), spec: spec, @@ -186,6 +187,12 @@ type resourceAdapter struct { commonResource *resourceTransformations *resourceAdapterInner + + // The original untransformed target. The inner target is replaced with the + // transformed resource once the transformation chain has run, so any new + // transformations appended to the chain must start from this. + sourceTarget transformableResource + metaProvider resource.ResourceMetaProvider } @@ -230,6 +237,7 @@ func (r *resourceAdapter) GetDependencyManager() identity.Manager { func (r resourceAdapter) cloneTo(targetPath string) resource.Resource { newtTarget := r.target.cloneTo(targetPath) + r.sourceTarget = newtTarget.(transformableResource) newInner := &resourceAdapterInner{ ctx: r.ctx, spec: r.spec, @@ -379,7 +387,7 @@ func (r resourceAdapter) TransformWithContext(ctx context.Context, t ...Resource spec: r.spec, Staler: r.Staler, publishOnce: &publishOnce{}, - target: r.target, + target: r.sourceTarget, } return &r, nil diff --git a/tpl/css/build_integration_test.go b/tpl/css/build_integration_test.go index 6ddb23331..e72a1bb03 100644 --- a/tpl/css/build_integration_test.go +++ b/tpl/css/build_integration_test.go @@ -537,6 +537,49 @@ ARTIFACT: {{ .RelPermalink }}|{{ .Permalink }}|{{ .MediaType.Type }} b.AssertFileExists("public/css/main.css.map", true) } +func TestCSSBuildDataArtifactsAndThenFingerprint(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.org/mysite/" +disableKinds = ['page','rss','section','sitemap','taxonomy','term'] +-- assets/css/main.css -- +@import "/fonts/fonts.css"; +body { color: #222; } +-- assets/fonts/fonts.css -- +@font-face { + font-family: 'Comic Neue'; + src: url(ComicNeue-Regular.woff2) format('woff2'), url(ComicNeue-Regular.ttf) format('truetype'); +} +-- assets/fonts/ComicNeue-Regular.ttf -- +fakefontdata +-- assets/fonts/ComicNeue-Regular.woff2 -- +fakefontdata2 +-- layouts/home.html -- +{{ with resources.Get "css/main.css" }} +ORIG CSS SHA256: {{ .Content | crypto.Hash "sha256" }}| +{{ with . | css.Build (dict "minify" true) }} +{{ range .Data.Artifacts }} +ARTIFACT: {{ .RelPermalink }}|{{ .Permalink }}|{{ .MediaType.Type }} +{{ end }} +BUILT CSS SHA256: {{ .Content | crypto.Hash "sha256" }}| +{{ with . | fingerprint }} +FINGERPRINTED: {{ .RelPermalink }}| +{{ end }} +{{ end }} +{{ end }} +` + + b := hugolib.Test(t, files, hugolib.TestOptOsFs()) + b.AssertFileContent("public/index.html", + "ARTIFACT: /mysite/css/ComicNeue-Regular-UA4ODE7N.ttf|", + "ORIG CSS SHA256: a2b878c05df0f346c3854caebd21f00bb0965b46628440b8ccb31ce675af6bfe|", + "BUILT CSS SHA256: 5f8757d5579386b8755e0df759c7eecfd069c385294dfe7bc3695862d7f218b1|", + "FINGERPRINTED: /mysite/css/main.5f8757d5579386b8755e0df759c7eecfd069c385294dfe7bc3695862d7f218b1.css|", + ) +} + // Issue #14623 func TestCSSBuildLoadersPartial(t *testing.T) { t.Parallel()