Repository: gohugoio/hugo (language: go)

Issue:
[Proposal] Add a variable to describe site's content directory
Hi, I use the `Language.ContentDir` to get the relative path of pages to the site root, which is useful for generating the links to view/edit the pages. But the _internal_ use variable was removed on this [commit](https://github.com/gohugoio/hugo/commit/241b21b0fd34d91fccb2ce69874110dceae6f926#diff-c7cf0d263e118b30aba8670e149a434527d8913c7c1d9ce26fc1934044428651L66), is there a workaround to get the relative path of the pages? Or is it possible to expose a function or variable to achieve this?

Thanks.


Discussion / hints:
I suggest you do something ala:

```
{{ with .File }}
{{ $relativeToContentDir := .Path }}
{{ end }}
```

Which isn't the same as what you do today as you would need to know that it's inside `content`, but the thing is, with a composite file system (multiple content dirs), there's no golden way of doing this without knowing more ...

Note that if you're talking about links to the files on disk (and not GitHub), you can do:

```
{{ with .File }}
{{ $absoluteFilename := .Filename }}
{{ end }}
```
We can certainly add "something" to simplify the above, but I don't see what.
Thanks for your suggestions, I want to generate links about the view/edit URLs of GitHub/GitLab, but I didn't figure out a solution.
The `File.Path` isn't enough to describe the path to the site root when `ContentDir` is set.
Is that possible to add a variable on `File` that return the path that includes the `content` or custom `contentDir` folder?
Or can I access the site root path so that I can manually transform the `File.Filename` and get the path I want.
@razonyang This [forum topic](https://discourse.gohugo.io/t/path-to-content-file-from-project-root/44262) might be helpful, specifically:

```text
{{ strings.TrimPrefix (os.Getenv "PWD") .File.Filename }}
```

For this to work, Hugo must be run within the project directory (i.e., cannot use `hugo -s somedir`).


@jmooring Thanks, I thought about this, as you mentioned, `$PWD` isn't always reliable.
This is what I really really want:

```
{{ strings.TrimPrefix hugo.WorkingDir .File.Filename }}
```
@jmooring Thanks a lot, will try later.
`hugo.WorkingDir ` is not implemented---it is what I want.

Fix patch:
diff --git a/commands/hugobuilder.go b/commands/hugobuilder.go
index 20a3228f9f9..20e800aca5d 100644
--- a/commands/hugobuilder.go
+++ b/commands/hugobuilder.go
@@ -990,7 +990,9 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
 	cfg := config.New()
 	cfg.Set("renderToDisk", (c.s == nil && !c.r.renderToMemory) || (c.s != nil && c.s.renderToDisk))
 	watch := c.r.buildWatch || (c.s != nil && c.s.serverWatch)
-	cfg.Set("environment", c.r.environment)
+	if c.r.environment != "" {
+		cfg.Set("environment", c.r.environment)
+	}
 
 	cfg.Set("internal", maps.Params{
 		"running": running,
diff --git a/common/hugo/hugo.go b/common/hugo/hugo.go
index 6402d7b88de..4769852a096 100644
--- a/common/hugo/hugo.go
+++ b/common/hugo/hugo.go
@@ -60,6 +60,7 @@ type HugoInfo struct {
 	// version of go that the Hugo binary was built with
 	GoVersion string
 
+	conf ConfigProvider
 	deps []*Dependency
 }
 
@@ -81,15 +82,26 @@ func (i HugoInfo) IsExtended() bool {
 	return IsExtended
 }
 
+// WorkingDir returns the project working directory.
+func (i HugoInfo) WorkingDir() string {
+	return i.conf.WorkingDir()
+}
+
 // Deps gets a list of dependencies for this Hugo build.
 func (i HugoInfo) Deps() []*Dependency {
 	return i.deps
 }
 
+// ConfigProvider represents the config options that are relevant for HugoInfo.
+type ConfigProvider interface {
+	Environment() string
+	WorkingDir() string
+}
+
 // NewInfo creates a new Hugo Info object.
-func NewInfo(environment string, deps []*Dependency) HugoInfo {
-	if environment == "" {
-		environment = EnvironmentProduction
+func NewInfo(conf ConfigProvider, deps []*Dependency) HugoInfo {
+	if conf.Environment() == "" {
+		panic("environment not set")
 	}
 	var (
 		commitHash string
@@ -107,7 +119,8 @@ func NewInfo(environment string, deps []*Dependency) HugoInfo {
 	return HugoInfo{
 		CommitHash:  commitHash,
 		BuildDate:   buildDate,
-		Environment: environment,
+		Environment: conf.Environment(),
+		conf:        conf,
 		deps:        deps,
 		GoVersion:   goVersion,
 	}
diff --git a/config/allconfig/configlanguage.go b/config/allconfig/configlanguage.go
index b28d5476977..95c5c7edfbc 100644
--- a/config/allconfig/configlanguage.go
+++ b/config/allconfig/configlanguage.go
@@ -101,6 +101,10 @@ func (c ConfigLanguage) DirsBase() config.CommonDirs {
 	return c.m.Base.CommonDirs
 }
 
+func (c ConfigLanguage) WorkingDir() string {
+	return c.m.Base.WorkingDir
+}
+
 func (c ConfigLanguage) Quiet() bool {
 	return c.m.Base.Internal.Quiet
 }
diff --git a/config/configProvider.go b/config/configProvider.go
index ac00c747696..8ed0728bd8a 100644
--- a/config/configProvider.go
+++ b/config/configProvider.go
@@ -64,6 +64,7 @@ type AllProvider interface {
 	Timeout() time.Duration
 	StaticDirs() []string
 	IgnoredErrors() map[string]bool
+	WorkingDir() string
 }
 
 // Provider provides the configuration settings for Hugo.
diff --git a/hugolib/site_new.go b/hugolib/site_new.go
index b0b34e45782..1262cc3aeea 100644
--- a/hugolib/site_new.go
+++ b/hugolib/site_new.go
@@ -261,7 +261,7 @@ func newHugoSitesNew(cfg deps.DepsCfg, d *deps.Deps, sites []*Site) (*HugoSites,
 		dependencies = append(dependencies, depFromMod(m))
 	}
 
-	h.hugoInfo = hugo.NewInfo(h.Configs.Base.Environment, dependencies)
+	h.hugoInfo = hugo.NewInfo(h.Configs.GetFirstLanguageConfig(), dependencies)
 
 	var prototype *deps.Deps
 	for i, s := range sites {
diff --git a/resources/page/site.go b/resources/page/site.go
index 16e070160a4..da97dfe6e54 100644
--- a/resources/page/site.go
+++ b/resources/page/site.go
@@ -444,9 +444,9 @@ func (s testSite) Param(key any) (any, error) {
 }
 
 // NewDummyHugoSite creates a new minimal test site.
-func NewDummyHugoSite(cfg config.Provider) Site {
+func NewDummyHugoSite(conf config.AllProvider) Site {
 	return testSite{
-		h: hugo.NewInfo(hugo.EnvironmentProduction, nil),
+		h: hugo.NewInfo(conf, nil),
 		l: &langs.Language{
 			Lang: "en",
 		},
diff --git a/tpl/cast/docshelper.go b/tpl/cast/docshelper.go
index 2ed28e3c51c..981c515517a 100644
--- a/tpl/cast/docshelper.go
+++ b/tpl/cast/docshelper.go
@@ -29,7 +29,8 @@ func init() {
 		if err := d.Init(); err != nil {
 			panic(err)
 		}
-		d.Site = page.NewDummyHugoSite(newTestConfig())
+		conf := testconfig.GetTestConfig(nil, newTestConfig())
+		d.Site = page.NewDummyHugoSite(conf)
 
 		var namespaces internal.TemplateFuncsNamespaces
 


Test patch:
diff --git a/common/hugo/hugo_test.go b/common/hugo/hugo_test.go
index f2ad0f5c1ef..b0279f11137 100644
--- a/common/hugo/hugo_test.go
+++ b/common/hugo/hugo_test.go
@@ -23,10 +23,12 @@ import (
 func TestHugoInfo(t *testing.T) {
 	c := qt.New(t)
 
-	hugoInfo := NewInfo("", nil)
+	conf := testConfig{environment: "production", workingDir: "/mywork"}
+	hugoInfo := NewInfo(conf, nil)
 
 	c.Assert(hugoInfo.Version(), qt.Equals, CurrentVersion.Version())
 	c.Assert(fmt.Sprintf("%T", VersionString("")), qt.Equals, fmt.Sprintf("%T", hugoInfo.Version()))
+	c.Assert(hugoInfo.WorkingDir(), qt.Equals, "/mywork")
 
 	bi := getBuildInfo()
 	if bi != nil {
@@ -39,6 +41,19 @@ func TestHugoInfo(t *testing.T) {
 	c.Assert(hugoInfo.IsProduction(), qt.Equals, true)
 	c.Assert(hugoInfo.IsExtended(), qt.Equals, IsExtended)
 
-	devHugoInfo := NewInfo("development", nil)
+	devHugoInfo := NewInfo(testConfig{environment: "development"}, nil)
 	c.Assert(devHugoInfo.IsProduction(), qt.Equals, false)
 }
+
+type testConfig struct {
+	environment string
+	workingDir  string
+}
+
+func (c testConfig) Environment() string {
+	return c.environment
+}
+
+func (c testConfig) WorkingDir() string {
+	return c.workingDir
+}
diff --git a/hugolib/config_test.go b/hugolib/config_test.go
index 24275025ccc..4af9b7998c4 100644
--- a/hugolib/config_test.go
+++ b/hugolib/config_test.go
@@ -898,6 +898,29 @@ mainSections: []
 
 }
 
+func TestConfigHugoWorkingDir(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+-- layouts/index.html --
+WorkingDir: {{ hugo.WorkingDir }}|
+
+`
+	b := NewIntegrationTestBuilder(
+		IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+			WorkingDir:  "myworkingdir",
+		},
+	).Build()
+
+	b.AssertFileContent("public/index.html", `
+WorkingDir: myworkingdir|
+`)
+
+}
+
 func TestConfigMergeLanguageDeepEmptyLefSide(t *testing.T) {
 	t.Parallel()
 
diff --git a/langs/i18n/i18n_test.go b/langs/i18n/i18n_test.go
index 491ae3373a6..1ac6144ddaa 100644
--- a/langs/i18n/i18n_test.go
+++ b/langs/i18n/i18n_test.go
@@ -480,7 +480,7 @@ func prepareDeps(afs afero.Fs, cfg config.Provider) (*deps.Deps, *TranslationPro
 	translationProvider := NewTranslationProvider()
 	d.TemplateProvider = tplimpl.DefaultTemplateProvider
 	d.TranslationProvider = translationProvider
-	d.Site = page.NewDummyHugoSite(cfg)
+	d.Site = page.NewDummyHugoSite(d.Conf)
 	if err := d.Compile(nil); err != nil {
 		panic(err)
 	}
diff --git a/resources/page/page_matcher_test.go b/resources/page/page_matcher_test.go
index 990312ed1eb..da47843db98 100644
--- a/resources/page/page_matcher_test.go
+++ b/resources/page/page_matcher_test.go
@@ -25,8 +25,8 @@ import (
 
 func TestPageMatcher(t *testing.T) {
 	c := qt.New(t)
-	developmentTestSite := testSite{h: hugo.NewInfo("development", nil)}
-	productionTestSite := testSite{h: hugo.NewInfo("production", nil)}
+	developmentTestSite := testSite{h: hugo.NewInfo(testConfig{environment: "development"}, nil)}
+	productionTestSite := testSite{h: hugo.NewInfo(testConfig{environment: "production"}, nil)}
 
 	p1, p2, p3 :=
 		&testPage{path: "/p1", kind: "section", lang: "en", site: developmentTestSite},
@@ -156,3 +156,16 @@ func TestDecodeCascadeConfig(t *testing.T) {
 	c.Assert(got, qt.IsNotNil)
 
 }
+
+type testConfig struct {
+	environment string
+	workingDir  string
+}
+
+func (c testConfig) Environment() string {
+	return c.environment
+}
+
+func (c testConfig) WorkingDir() string {
+	return c.workingDir
+}