Repository: gohugoio/hugo (language: go)

Issue:
feat: add parents property on page
Add parents property on page, simplify breadcrumb navigation implementation complexity

community tricks see: https://discourse.gohugo.io/search?q=breadcrumb

---

in HTML:

```html
{{- $page := . -}
<nav class="breadcrumb">
<ul>
{{- range .Parents.Reverse }}
<li><a href="{{ .Permalink }}">{{ .LinkTitle }}</a></li>
{{- end }}
<li class="active">{{ .LinkTitle }}</li>
</ul>
</nav>
```

---

in JSON-LD:

see https://developers.google.com/search/docs/appearance/structured-data/breadcrumb

```html
{{- $breadcrumbItems := slice -}}
{{- range $index, $page := append .Parents.Reverse . -}}
  {{-
  $item := dict
    "@type" "ListItem"
    "position" (add $index 1)
    "name" $page.Title
    "item" $page.Permalink
  -}}
  {{- $breadcrumbItems = append $breadcrumbItems $item -}}
{{- end -}}
<script type="application/ld+json">
{{-
dict
  "@context" "https://schema.org"
  "@type" "BreadcrumbList"
  "itemListElement" $breadcrumbItems
-}}
</script>
```



Discussion / hints:

Fix patch:
diff --git a/docs/content/en/content-management/sections.md b/docs/content/en/content-management/sections.md
index 5ff8846108d..b68a5bdb90a 100644
--- a/docs/content/en/content-management/sections.md
+++ b/docs/content/en/content-management/sections.md
@@ -63,19 +63,16 @@ If you need a specific template for a sub-section, you need to adjust either the
 With the available [section variables and methods](#section-page-variables-and-methods) you can build powerful navigation. One common example would be a partial to show Breadcrumb navigation:
 
 {{< code file="layouts/partials/breadcrumb.html" download="breadcrumb.html" >}}
-<ol  class="nav navbar-nav">
-  {{ template "breadcrumbnav" (dict "p1" . "p2" .) }}
-</ol>
-{{ define "breadcrumbnav" }}
-{{ if .p1.Parent }}
-{{ template "breadcrumbnav" (dict "p1" .p1.Parent "p2" .p2 )  }}
-{{ else if not .p1.IsHome }}
-{{ template "breadcrumbnav" (dict "p1" .p1.Site.Home "p2" .p2 )  }}
-{{ end }}
-<li{{ if eq .p1 .p2 }} class="active" aria-current="page" {{ end }}>
-  <a href="{{ .p1.Permalink }}">{{ .p1.Title }}</a>
+<ol class="nav navbar-nav">
+<ul>
+{{- range .Ancestors.Reverse }}
+<li><a href="{{ .Permalink }}">{{ .Title }}</a></li>
+{{- end }}
+<li class="active" aria-current="page">
+<a href="{{ .Permalink }}">{{ .Title }}</a>
 </li>
-{{ end }}
+</ul>
+</ol>
 {{< /code >}}
 
 ## Section Page Variables and Methods
diff --git a/docs/content/en/variables/page.md b/docs/content/en/variables/page.md
index 07acc483108..2de54ff8beb 100644
--- a/docs/content/en/variables/page.md
+++ b/docs/content/en/variables/page.md
@@ -32,6 +32,9 @@ See [`.Scratch`](/functions/scratch/) for page-scoped, writable variables.
 .Aliases
 : aliases of this page
 
+.Ancestors
+: get the ancestors of each page, simplify [breadcrumb navigation]({{< relref "content-management/sections#example-breadcrumb-navigation" >}}) implementation complexity  
+
 .BundleType
 : the [bundle] type: `leaf`, `branch`, or an empty string if the page is not a bundle.
 
diff --git a/hugolib/page__tree.go b/hugolib/page__tree.go
index 828500e6267..2f9fd2e4681 100644
--- a/hugolib/page__tree.go
+++ b/hugolib/page__tree.go
@@ -178,6 +178,16 @@ func (pt pageTree) Parent() page.Page {
 	return b.p
 }
 
+func (pt pageTree) Ancestors() page.Pages {
+	var ancestors page.Pages
+	parent := pt.Parent()
+	for parent != nil {
+		ancestors = append(ancestors, parent)
+		parent = parent.Parent()
+	}
+	return ancestors
+}
+
 func (pt pageTree) Sections() page.Pages {
 	if pt.p.bucket == nil {
 		return nil
diff --git a/resources/page/page.go b/resources/page/page.go
index f9b8fdcec70..929f04d9371 100644
--- a/resources/page/page.go
+++ b/resources/page/page.go
@@ -407,6 +407,9 @@ type TreeProvider interface {
 	// To get a section's subsections, see Page's Sections method.
 	Parent() Page
 
+	// Ancestors returns the ancestors of each page
+	Ancestors() Pages
+
 	// Sections returns this section's subsections, if any.
 	// Note that for non-sections, this method will always return an empty list.
 	Sections() Pages
diff --git a/resources/page/page_nop.go b/resources/page/page_nop.go
index df3227f0a4d..15f8c395086 100644
--- a/resources/page/page_nop.go
+++ b/resources/page/page_nop.go
@@ -348,6 +348,10 @@ func (p *nopPage) Parent() Page {
 	return nil
 }
 
+func (p *nopPage) Ancestors() Pages {
+	return nil
+}
+
 func (p *nopPage) Path() string {
 	return ""
 }


Test patch:
diff --git a/hugolib/site_sections_test.go b/hugolib/site_sections_test.go
index 2a4c39533a2..ccc8c51cba6 100644
--- a/hugolib/site_sections_test.go
+++ b/hugolib/site_sections_test.go
@@ -181,12 +181,14 @@ PAG|{{ .Title }}|{{ $sect.InSection . }}
 			c.Assert(err, qt.IsNil)
 			c.Assert(active, qt.Equals, true)
 			c.Assert(p.FirstSection(), qt.Equals, p)
+			c.Assert(len(p.Ancestors()), qt.Equals, 1)
 		}},
 		{"l1", func(c *qt.C, p page.Page) {
 			c.Assert(p.Title(), qt.Equals, "L1s")
 			c.Assert(len(p.Pages()), qt.Equals, 4) // 2 pages + 2 sections
 			c.Assert(p.Parent().IsHome(), qt.Equals, true)
 			c.Assert(len(p.Sections()), qt.Equals, 2)
+			c.Assert(len(p.Ancestors()), qt.Equals, 1)
 		}},
 		{"l1,l2", func(c *qt.C, p page.Page) {
 			c.Assert(p.Title(), qt.Equals, "T2_-1")
@@ -195,6 +197,7 @@ PAG|{{ .Title }}|{{ $sect.InSection . }}
 			c.Assert(p.Parent().Title(), qt.Equals, "L1s")
 			c.Assert(p.RelPermalink(), qt.Equals, "/l1/l2/")
 			c.Assert(len(p.Sections()), qt.Equals, 1)
+			c.Assert(len(p.Ancestors()), qt.Equals, 2)
 
 			for _, child := range p.Pages() {
 				if child.IsSection() {
@@ -237,6 +240,7 @@ PAG|{{ .Title }}|{{ $sect.InSection . }}
 			c.Assert(p.Pages()[0].File().Path(), qt.Equals, filepath.FromSlash("l1/l2_2/page_2_2_1.md"))
 			c.Assert(p.Parent().Title(), qt.Equals, "L1s")
 			c.Assert(len(p.Sections()), qt.Equals, 0)
+			c.Assert(len(p.Ancestors()), qt.Equals, 2)
 		}},
 		{"l1,l2,l3", func(c *qt.C, p page.Page) {
 			nilp, _ := p.GetPage("this/does/not/exist")
@@ -245,6 +249,7 @@ PAG|{{ .Title }}|{{ $sect.InSection . }}
 			c.Assert(len(p.Pages()), qt.Equals, 2)
 			c.Assert(p.Parent().Title(), qt.Equals, "T2_-1")
 			c.Assert(len(p.Sections()), qt.Equals, 0)
+			c.Assert(len(p.Ancestors()), qt.Equals, 3)
 
 			l1 := getPage(p, "/l1")
 			isDescendant, err := l1.IsDescendant(p)
@@ -307,6 +312,7 @@ PAG|{{ .Title }}|{{ $sect.InSection . }}
 	}
 
 	c.Assert(home, qt.Not(qt.IsNil))
+	c.Assert(len(home.Ancestors()), qt.Equals, 0)
 
 	c.Assert(len(home.Sections()), qt.Equals, 9)
 	c.Assert(s.Info.Sections(), deepEqualsPages, home.Sections())
diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go
index 30b8e4dff1d..22346c3895b 100644
--- a/resources/page/testhelpers_test.go
+++ b/resources/page/testhelpers_test.go
@@ -416,6 +416,10 @@ func (p *testPage) Parent() Page {
 	panic("not implemented")
 }
 
+func (p *testPage) Ancestors() Pages {
+	panic("not implemented")
+}
+
 func (p *testPage) Path() string {
 	return p.path
 }