From: Bjørn Erik Pedersen Date: Wed, 1 Apr 2026 15:20:43 +0000 (+0200) Subject: tpl/css: Support @import "hugo:vars" for CSS custom properties in css.Build X-Git-Url: http://git.maquefel.me/?a=commitdiff_plain;h=5d09b5e32a4d0e9b3fe8797c91804f6a7804bb5a;p=brevno-suite%2Fhugo tpl/css: Support @import "hugo:vars" for CSS custom properties in css.Build Closes #14699 Co-Authored-By: Claude Opus 4.6 (1M context) --- diff --git a/internal/js/esbuild/options.go b/internal/js/esbuild/options.go index 1e522f0ea..716e4e7cd 100644 --- a/internal/js/esbuild/options.go +++ b/internal/js/esbuild/options.go @@ -207,6 +207,9 @@ type ExternalOptions struct { // See https://esbuild.github.io/api/#jsx-import-source JSXImportSource string + // User defined CSS variables. Will be available as CSS global scope CSS variables via @import "hugo:vars". + Vars map[string]any + // There is/was a bug in WebKit with severe performance issue with the tracking // of TDZ checks in JavaScriptCore. // diff --git a/internal/js/esbuild/resolve.go b/internal/js/esbuild/resolve.go index 4349bdcc2..45fd4e405 100644 --- a/internal/js/esbuild/resolve.go +++ b/internal/js/esbuild/resolve.go @@ -19,10 +19,12 @@ import ( "os" "path/filepath" "slices" + "sort" "strings" "github.com/evanw/esbuild/pkg/api" "github.com/gohugoio/hugo/common/hmaps" + "github.com/gohugoio/hugo/common/types/css" "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/resources" @@ -34,12 +36,13 @@ const ( NsHugoImport = "ns-hugo-imp" NsHugoImportResolveFunc = "ns-hugo-imp-func" nsHugoParams = "ns-hugo-params" + nsHugoVars = "ns-hugo-vars" pathHugoConfigParams = "@params/config" stdinImporter = "" ) -var hugoNamespaces = []string{NsHugoImport, NsHugoImportResolveFunc, nsHugoParams} +var hugoNamespaces = []string{NsHugoImport, NsHugoImportResolveFunc, nsHugoParams, nsHugoVars} const ( PrefixHugoVirtual = "__hu_v" @@ -371,5 +374,54 @@ func createBuildPlugins(rs *resources.Spec, assetsResolver *fsResolver, depsMana }, } - return []api.Plugin{importResolver, paramsPlugin}, nil + varsPlugin := api.Plugin{ + Name: "hugo-vars-plugin", + Setup: func(build api.PluginBuild) { + build.OnResolve(api.OnResolveOptions{Filter: `^hugo:vars$`}, + func(args api.OnResolveArgs) (api.OnResolveResult, error) { + return api.OnResolveResult{ + Path: args.Path, + Namespace: nsHugoVars, + }, nil + }) + build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: nsHugoVars}, + func(args api.OnLoadArgs) (api.OnLoadResult, error) { + return api.OnLoadResult{ + Contents: createCSSVarsStyleSheet(opts.Vars), + Loader: api.LoaderCSS, + }, nil + }) + }, + } + + return []api.Plugin{importResolver, paramsPlugin, varsPlugin}, nil +} + +// createCSSVarsStyleSheet creates a CSS custom properties stylesheet from the given vars. +// The result is a :root block with CSS custom properties. +func createCSSVarsStyleSheet(vars map[string]any) *string { + if len(vars) == 0 { + // We need to return a non-nil pointer to an empty string to avoid ESBuild treating this as a missing file. + s := "" + return &s + } + + var varsSlice []string + for k, v := range vars { + if !strings.HasPrefix(k, "--") { + k = "--" + k + } + + switch v.(type) { + case css.QuotedString: + // E.g. Arial, sans-serif. + varsSlice = append(varsSlice, fmt.Sprintf(" %s: %q;", k, v)) + default: + varsSlice = append(varsSlice, fmt.Sprintf(" %s: %v;", k, v)) + } + } + sort.Strings(varsSlice) + s := ":root {\n" + strings.Join(varsSlice, "\n") + "\n}\n" + + return &s } diff --git a/tpl/css/build_integration_test.go b/tpl/css/build_integration_test.go index 78682ff83..b659b9965 100644 --- a/tpl/css/build_integration_test.go +++ b/tpl/css/build_integration_test.go @@ -430,3 +430,128 @@ disableKinds = ["taxonomy", "term"] b := hugolib.Test(t, files, hugolib.TestOptOsFs(), hugolib.TestOptWithNpmInstall()) b.AssertFileContent("public/css/main.css", `--bs-indigo: #6610f2;`) } + +func TestCSSBuildVars(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- assets/css/main.css -- +@import "hugo:vars"; + +body { + background-color: var(--primary-color); + font-size: var(--font-size); + color: var(--text-color); +} +-- layouts/home.html -- +{{ with resources.Get "css/main.css" }} +{{ $opts := dict + "vars" (dict "primary-color" "blue" "font-size" "24px" "text-color" "#333" "--already-prefixed" "red") +}} +{{ with . | css.Build $opts }} + +{{ end }} +{{ end }} +` + + b := hugolib.Test(t, files, hugolib.TestOptOsFs()) + b.AssertFileContent("public/css/main.css", + "--primary-color: blue;", + "--font-size: 24px;", + "--text-color: #333;", + "--already-prefixed: red;", + "background-color: var(--primary-color)", + ) +} + +func TestCSSBuildVarsEmpty(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- assets/css/main.css -- +@import "hugo:vars"; + +body { + background-color: red; +} +-- layouts/home.html -- +{{ with resources.Get "css/main.css" }} +{{ with . | css.Build }} + +{{ end }} +{{ end }} +` + + b := hugolib.Test(t, files, hugolib.TestOptOsFs()) + b.AssertFileContent("public/css/main.css", "background-color: red;") +} + +func TestCSSBuildVarsQuoted(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- assets/css/main.css -- +@import "hugo:vars"; + +body { + font-family: var(--font-family); + color: var(--brand-color); +} +-- layouts/home.html -- +{{ with resources.Get "css/main.css" }} +{{ $opts := dict + "vars" (dict "font-family" (css.Quoted "Arial, sans-serif") "brand-color" "hsl(0, 0%, 20%)") +}} +{{ with . | css.Build $opts }} + +{{ end }} +{{ end }} +` + + b := hugolib.Test(t, files, hugolib.TestOptOsFs()) + b.AssertFileContent("public/css/main.css", + `--font-family: "Arial, sans-serif";`, + "--brand-color: hsl(0, 0%, 20%);", + ) +} + +func TestCSSBuildVarsFromParams(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +[params.styles] +primary-color = "blue" +font-size = "24px" +text-color = "#333" +-- assets/css/main.css -- +@import "hugo:vars"; +-- assets/css/main.css -- +@import "hugo:vars"; + +body { + background-color: var(--primary-color); + font-size: var(--font-size); + color: var(--text-color); +} +-- layouts/home.html -- +{{ with resources.Get "css/main.css" }} +{{ $opts := dict + "vars" site.Params.styles +}} +{{ with . | css.Build $opts }} + +{{ end }} +{{ end }} +` + + b := hugolib.Test(t, files, hugolib.TestOptOsFs()) + b.AssertFileContent("public/css/main.css", + "--primary-color: blue;", + "--font-size: 24px;", + "--text-color: #333;", + ) +}