Repository: gohugoio/hugo (language: go)

Issue:
.Fragments nilpointer when calling .Fragment on another site's page 
This is a known issue, but I forgot when add this new method.

You would need multiple output formats and sites, so it should be rare, but we need to fix it.


Discussion / hints:

Fix patch:
diff --git a/hugolib/page.go b/hugolib/page.go
index 40972d7c57d..a80b28a3e25 100644
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -62,9 +62,8 @@ var (
 var (
 	pageTypesProvider = resource.NewResourceTypesProvider(media.OctetType, pageResourceType)
 	nopPageOutput     = &pageOutput{
-		pagePerOutputProviders:  nopPagePerOutput,
-		ContentProvider:         page.NopPage,
-		TableOfContentsProvider: page.NopPage,
+		pagePerOutputProviders: nopPagePerOutput,
+		ContentProvider:        page.NopPage,
 	}
 )
 
@@ -151,14 +150,6 @@ func (p *pageState) GetIdentity() identity.Identity {
 	return identity.NewPathIdentity(files.ComponentFolderContent, filepath.FromSlash(p.Pathc()))
 }
 
-func (p *pageState) Fragments(ctx context.Context) *tableofcontents.Fragments {
-	p.s.initInit(ctx, p.cp.initToC, p)
-	if p.pageOutput.cp.tableOfContents == nil {
-		return tableofcontents.Empty
-	}
-	return p.pageOutput.cp.tableOfContents
-}
-
 func (p *pageState) HeadingsFiltered(context.Context) tableofcontents.Headings {
 	return nil
 }
@@ -951,8 +942,8 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
 			})
 			p.pageOutput.contentRenderer = lcp
 			p.pageOutput.ContentProvider = lcp
-			p.pageOutput.TableOfContentsProvider = lcp
 			p.pageOutput.PageRenderProvider = lcp
+			p.pageOutput.TableOfContentsProvider = lcp
 		}
 	}
 
diff --git a/hugolib/page__output.go b/hugolib/page__output.go
index 4ef78b61262..25ce26b7a4f 100644
--- a/hugolib/page__output.go
+++ b/hugolib/page__output.go
@@ -57,8 +57,8 @@ func newPageOutput(
 		f:                       f,
 		pagePerOutputProviders:  providers,
 		ContentProvider:         page.NopPage,
-		TableOfContentsProvider: page.NopPage,
 		PageRenderProvider:      page.NopPage,
+		TableOfContentsProvider: page.NopPage,
 		render:                  render,
 		paginator:               pag,
 	}
@@ -84,8 +84,8 @@ type pageOutput struct {
 	contentRenderer page.ContentRenderer
 	pagePerOutputProviders
 	page.ContentProvider
-	page.TableOfContentsProvider
 	page.PageRenderProvider
+	page.TableOfContentsProvider
 
 	// May be nil.
 	cp *pageContentOutput
@@ -97,8 +97,8 @@ func (p *pageOutput) initContentProvider(cp *pageContentOutput) {
 	}
 	p.contentRenderer = cp
 	p.ContentProvider = cp
-	p.TableOfContentsProvider = cp
 	p.PageRenderProvider = cp
+	p.TableOfContentsProvider = cp
 	p.cp = cp
 
 }
diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go
index be65ad9e723..3e61a45133a 100644
--- a/hugolib/page__per_output.go
+++ b/hugolib/page__per_output.go
@@ -342,6 +342,19 @@ func (p *pageContentOutput) Reset() {
 	p.renderHooks = &renderHooks{}
 }
 
+func (p *pageContentOutput) Fragments(ctx context.Context) *tableofcontents.Fragments {
+	p.p.s.initInit(ctx, p.initToC, p.p)
+	if p.tableOfContents == nil {
+		return tableofcontents.Empty
+	}
+	return p.tableOfContents
+}
+
+func (p *pageContentOutput) TableOfContents(ctx context.Context) template.HTML {
+	p.p.s.initInit(ctx, p.initToC, p.p)
+	return p.tableOfContentsHTML
+}
+
 func (p *pageContentOutput) Content(ctx context.Context) (any, error) {
 	p.p.s.initInit(ctx, p.initMain, p.p)
 	return p.content, nil
@@ -380,11 +393,6 @@ func (p *pageContentOutput) Summary(ctx context.Context) template.HTML {
 	return p.summary
 }
 
-func (p *pageContentOutput) TableOfContents(ctx context.Context) template.HTML {
-	p.p.s.initInit(ctx, p.initMain, p.p)
-	return p.tableOfContentsHTML
-}
-
 func (p *pageContentOutput) Truncated(ctx context.Context) bool {
 	if p.p.truncated {
 		return true
diff --git a/hugolib/shortcode_page.go b/hugolib/shortcode_page.go
index 3bc061bc084..20fa22d2fe6 100644
--- a/hugolib/shortcode_page.go
+++ b/hugolib/shortcode_page.go
@@ -63,6 +63,7 @@ var zeroShortcode = prerenderedShortcode{}
 // the best we can do.
 type pageForShortcode struct {
 	page.PageWithoutContent
+	page.TableOfContentsProvider
 	page.ContentProvider
 
 	// We need to replace it after we have rendered it, so provide a
@@ -74,10 +75,11 @@ type pageForShortcode struct {
 
 func newPageForShortcode(p *pageState) page.Page {
 	return &pageForShortcode{
-		PageWithoutContent: p,
-		ContentProvider:    page.NopPage,
-		toc:                template.HTML(tocShortcodePlaceholder),
-		p:                  p,
+		PageWithoutContent:      p,
+		TableOfContentsProvider: p,
+		ContentProvider:         page.NopPage,
+		toc:                     template.HTML(tocShortcodePlaceholder),
+		p:                       p,
 	}
 }
 
@@ -105,7 +107,7 @@ func newPageForRenderHook(p *pageState) page.Page {
 	return &pageForRenderHooks{
 		PageWithoutContent:      p,
 		ContentProvider:         page.NopPage,
-		TableOfContentsProvider: page.NopPage,
+		TableOfContentsProvider: p,
 	}
 }
 
diff --git a/resources/page/page.go b/resources/page/page.go
index 8f89142b3c5..6f6f1d10003 100644
--- a/resources/page/page.go
+++ b/resources/page/page.go
@@ -334,9 +334,6 @@ type PageWithoutContent interface {
 	// Used in change/dependency tracking.
 	identity.Provider
 
-	// Fragments returns the fragments for this page.
-	Fragments(context.Context) *tableofcontents.Fragments
-
 	// Headings returns the headings for this page when a filter is set.
 	// This is currently only triggered with the Related content feature
 	// and the "fragments" type of index.
@@ -407,6 +404,9 @@ type SitesProvider interface {
 type TableOfContentsProvider interface {
 	// TableOfContents returns the table of contents for the page rendered as HTML.
 	TableOfContents(context.Context) template.HTML
+
+	// Fragments returns the fragments for this page.
+	Fragments(context.Context) *tableofcontents.Fragments
 }
 
 // TranslationsProvider provides access to any translations.
diff --git a/resources/page/page_lazy_contentprovider.go b/resources/page/page_lazy_contentprovider.go
index d944b66d34c..2d647e90c17 100644
--- a/resources/page/page_lazy_contentprovider.go
+++ b/resources/page/page_lazy_contentprovider.go
@@ -19,6 +19,7 @@ import (
 
 	"github.com/gohugoio/hugo/lazy"
 	"github.com/gohugoio/hugo/markup/converter"
+	"github.com/gohugoio/hugo/markup/tableofcontents"
 )
 
 // OutputFormatContentProvider represents the method set that is "outputFormat aware" and that we
@@ -73,6 +74,17 @@ func (lcp *LazyContentProvider) Reset() {
 	lcp.init.Reset()
 }
 
+func (lcp *LazyContentProvider) TableOfContents(ctx context.Context) template.HTML {
+	lcp.init.Do(ctx)
+	return lcp.cp.TableOfContents(ctx)
+
+}
+
+func (lcp *LazyContentProvider) Fragments(ctx context.Context) *tableofcontents.Fragments {
+	lcp.init.Do(ctx)
+	return lcp.cp.Fragments(ctx)
+}
+
 func (lcp *LazyContentProvider) Content(ctx context.Context) (any, error) {
 	lcp.init.Do(ctx)
 	return lcp.cp.Content(ctx)
@@ -128,11 +140,6 @@ func (lcp *LazyContentProvider) RenderString(ctx context.Context, args ...any) (
 	return lcp.cp.RenderString(ctx, args...)
 }
 
-func (lcp *LazyContentProvider) TableOfContents(ctx context.Context) template.HTML {
-	lcp.init.Do(ctx)
-	return lcp.cp.TableOfContents(ctx)
-}
-
 func (lcp *LazyContentProvider) ParseAndRenderContent(ctx context.Context, content []byte, renderTOC bool) (converter.ResultRender, error) {
 	lcp.init.Do(ctx)
 	return lcp.cp.ParseAndRenderContent(ctx, content, renderTOC)


Test patch:
diff --git a/hugolib/page__fragments_test.go b/hugolib/page__fragments_test.go
new file mode 100644
index 00000000000..4b17378d594
--- /dev/null
+++ b/hugolib/page__fragments_test.go
@@ -0,0 +1,69 @@
+// Copyright 2023 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hugolib
+
+import "testing"
+
+// #10794
+func TestFragmentsAndToCCrossSiteAccess(t *testing.T) {
+	files := `
+-- hugo.toml --
+baseURL = "https://example.com"
+disableKinds = ["taxonomy", "term", "home"]
+defaultContentLanguage = "en"
+defaultContentLanguageInSubdir = true
+[languages]
+[languages.en]
+weight = 1
+[languages.fr]
+weight = 2
+-- content/p1.en.md --
+---
+title: "P1"
+outputs: ["HTML", "JSON"]
+---
+
+## Heading 1 EN
+
+-- content/p1.fr.md --
+---
+title: "P1"
+outputs: ["HTML", "JSON"]
+---
+
+## Heading 1 FR
+-- layouts/_default/single.html --
+HTML
+-- layouts/_default/single.json --
+{{ $secondSite := index .Sites 1 }}
+{{ $p1 := $secondSite.GetPage "p1" }}
+ToC: {{ $p1.TableOfContents }}
+Fragments : {{ $p1.Fragments.Identifiers }}
+
+
+
+	
+`
+
+	b := NewIntegrationTestBuilder(
+		IntegrationTestConfig{
+			TxtarString: files,
+			T:           t,
+		},
+	).Build()
+
+	b.AssertFileContent("public/en/p1/index.html", "HTML")
+	b.AssertFileContent("public/en/p1/index.json", "ToC: <nav id=\"TableOfContents\">\n  <ul>\n    <li><a href=\"#heading-1-fr\">Heading 1 FR</a></li>\n  </ul>\n</nav>\nFragments : [heading-1-fr]")
+
+}
diff --git a/tpl/page/integration_test.go b/tpl/page/integration_test.go
index 30a4885e0cd..74788377d77 100644
--- a/tpl/page/integration_test.go
+++ b/tpl/page/integration_test.go
@@ -177,3 +177,35 @@ Shortcode in bundled page OK.
 	}
 
 }
+
+// Issue 10791.
+func TestPageTableOfContentsInShortcode(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- config.toml --
+baseURL = 'http://example.com/'
+disableKinds = ["taxonomy", "term"]
+-- content/p1.md --
+---
+title: "P1"
+---
+{{< toc >}}
+
+# Heading 1
+-- layouts/shortcodes/toc.html --
+{{ page.TableOfContents }} 
+-- layouts/_default/single.html --
+{{ .Content }}
+`
+
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+		},
+	).Build()
+
+	b.AssertFileContent("public/p1/index.html", "<nav id=\"TableOfContents\"></nav> \n<h1 id=\"heading-1\">Heading 1</h1>")
+
+}