diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index a10b580b9..64faaf6e2 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -61,3 +61,5 @@ jobs:
echo "docs/en/latest/reference/api-reference.md has changed. Please run 'make generate-crd-docs' and commit the updated file."
exit 1
fi
+ - name: Check CRD Docs Links
+ run: make check-crd-doc-links
diff --git a/Makefile b/Makefile
index 260793e42..d78dfff0e 100644
--- a/Makefile
+++ b/Makefile
@@ -468,6 +468,10 @@ generate-crd-docs: manifests ## Generate CRD reference documentation in a single
--max-depth=100
@echo "CRD reference documentation generated at $(CRD_DOCS_OUTPUT)"
+.PHONY: check-crd-doc-links
+check-crd-doc-links: ## Check links to headings in the generated CRD reference
+ @python3 scripts/check-crd-doc-links.py $(CRD_DOCS_OUTPUT)
+
.PHONY: generate-crd-docs-grouped
generate-crd-docs-grouped: manifests ## Generate CRD reference documentation grouped by API group
@mkdir -p docs/crd/groups
diff --git a/docs/assets/crd/config.yaml b/docs/assets/crd/config.yaml
index c224cc429..88e5ec16f 100644
--- a/docs/assets/crd/config.yaml
+++ b/docs/assets/crd/config.yaml
@@ -28,3 +28,22 @@ render:
# Version of Kubernetes to use when generating links to Kubernetes API documentation.
# NOTE: Quotes are required, otherwise the value will be intepreted as a number so versions ending with `0` like 1.30 would be covreted to "1.3" in results.
kubernetesVersion: "1.30"
+ # Link types defined outside the generated API packages to their authoritative references.
+ # Without these mappings, crd-ref-docs treats them as local types and emits fragments for
+ # headings that are not part of this document.
+ knownTypes:
+ - name: Group
+ package: sigs.k8s.io/gateway-api/apis/v1
+ link: https://pkg.go.dev/sigs.k8s.io/gateway-api@v1.6.0/apis/v1#Group
+ - name: Kind
+ package: sigs.k8s.io/gateway-api/apis/v1
+ link: https://pkg.go.dev/sigs.k8s.io/gateway-api@v1.6.0/apis/v1#Kind
+ - name: ObjectName
+ package: sigs.k8s.io/gateway-api/apis/v1
+ link: https://pkg.go.dev/sigs.k8s.io/gateway-api@v1.6.0/apis/v1#ObjectName
+ - name: SectionName
+ package: sigs.k8s.io/gateway-api/apis/v1
+ link: https://pkg.go.dev/sigs.k8s.io/gateway-api@v1.6.0/apis/v1#SectionName
+ - name: Duration
+ package: time
+ link: https://pkg.go.dev/time#Duration
diff --git a/docs/assets/template/type.tpl b/docs/assets/template/type.tpl
index 57f6a4339..0c499c8fb 100644
--- a/docs/assets/template/type.tpl
+++ b/docs/assets/template/type.tpl
@@ -50,9 +50,15 @@
{{ end }}
-{{ if $type.References -}}
+{{ $renderedReferences := list -}}
+{{ range $type.SortedReferences -}}
+{{ if markdownShouldRenderType . -}}
+{{ $renderedReferences = append $renderedReferences . -}}
+{{ end -}}
+{{ end -}}
+{{ if $renderedReferences -}}
_Appears in:_
-{{- range $type.SortedReferences }}
+{{- range $renderedReferences }}
- {{ markdownRenderTypeLink . }}
{{- end }}
{{- end }}
diff --git a/docs/en/latest/reference/api-reference.md b/docs/en/latest/reference/api-reference.md
index 84fc1a98c..830e6ed0b 100644
--- a/docs/en/latest/reference/api-reference.md
+++ b/docs/en/latest/reference/api-reference.md
@@ -234,10 +234,10 @@ _Base type:_ `LocalPolicyTargetReferenceWithSectionName`
| Field | Description |
| --- | --- |
-| `group` _[Group](#group)_ | Group is the group of the target resource. |
-| `kind` _[Kind](#kind)_ | Kind is kind of the target resource. |
-| `name` _[ObjectName](#objectname)_ | Name is the name of the target resource. |
-| `sectionName` _[SectionName](#sectionname)_ | SectionName is the name of a section within the target resource. When unspecified, this targetRef targets the entire resource. In the following resources, SectionName is interpreted as the following:
• Gateway: Listener name
• HTTPRoute: HTTPRouteRule name
• Service: Port name
If a SectionName is specified, but does not exist on the targeted object, the Policy must fail to attach, and the policy implementation should record a `ResolvedRefs` or similar Condition in the Policy's status. |
+| `group` _[Group](https://pkg.go.dev/sigs.k8s.io/gateway-api@v1.6.0/apis/v1#Group)_ | Group is the group of the target resource. |
+| `kind` _[Kind](https://pkg.go.dev/sigs.k8s.io/gateway-api@v1.6.0/apis/v1#Kind)_ | Kind is kind of the target resource. |
+| `name` _[ObjectName](https://pkg.go.dev/sigs.k8s.io/gateway-api@v1.6.0/apis/v1#ObjectName)_ | Name is the name of the target resource. |
+| `sectionName` _[SectionName](https://pkg.go.dev/sigs.k8s.io/gateway-api@v1.6.0/apis/v1#SectionName)_ | SectionName is the name of a section within the target resource. When unspecified, this targetRef targets the entire resource. In the following resources, SectionName is interpreted as the following:
• Gateway: Listener name
• HTTPRoute: HTTPRouteRule name
• Service: Port name
If a SectionName is specified, but does not exist on the targeted object, the Policy must fail to attach, and the policy implementation should record a `ResolvedRefs` or similar Condition in the Policy's status. |
_Appears in:_
@@ -664,8 +664,7 @@ _Appears in:_
| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#condition-v1-meta) array_ | |
-_Appears in:_
-- [ConsumerStatus](#consumerstatus)
+
#### Timeout
@@ -804,7 +803,7 @@ ActiveHealthCheck defines the active upstream health check configuration.
| Field | Description |
| --- | --- |
| `type` _string_ | Type is the health check type. Can be `http`, `https`, or `tcp`. |
-| `timeout` _[Duration](#duration)_ | Timeout sets health check timeout in seconds. |
+| `timeout` _[Duration](https://pkg.go.dev/time#Duration)_ | Timeout sets health check timeout in seconds. |
| `concurrency` _integer_ | Concurrency sets the number of targets to be checked at the same time. |
| `host` _string_ | Host sets the upstream host. |
| `port` _integer_ | Port sets the upstream port. |
diff --git a/scripts/check-crd-doc-links.py b/scripts/check-crd-doc-links.py
new file mode 100644
index 000000000..1e508dd41
--- /dev/null
+++ b/scripts/check-crd-doc-links.py
@@ -0,0 +1,74 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+"""Verify that generated CRD reference self-links have matching headings."""
+
+from __future__ import annotations
+
+import re
+import sys
+from collections import defaultdict
+from pathlib import Path
+from urllib.parse import unquote
+
+
+HEADING_PATTERN = re.compile(r"^#{1,6}\s+(.+?)\s*#*\s*$")
+SELF_LINK_PATTERN = re.compile(r"\]\(#([^)]+)\)")
+
+
+def heading_slug(heading: str) -> str:
+ """Match Docusaurus' generated IDs for the plain headings in this file."""
+ return re.sub(r"[^\w -]", "", heading.lower()).replace(" ", "-")
+
+
+def main() -> int:
+ path = Path(sys.argv[1] if len(sys.argv) > 1 else "docs/en/latest/reference/api-reference.md")
+ lines = path.read_text(encoding="utf-8").splitlines()
+
+ anchors: set[str] = set()
+ occurrences: defaultdict[str, int] = defaultdict(int)
+ for line in lines:
+ match = HEADING_PATTERN.match(line)
+ if not match:
+ continue
+ base = heading_slug(match.group(1))
+ count = occurrences[base]
+ occurrences[base] += 1
+ anchors.add(base if count == 0 else f"{base}-{count}")
+
+ missing: list[tuple[int, str]] = []
+ checked = 0
+ for line_number, line in enumerate(lines, start=1):
+ for match in SELF_LINK_PATTERN.finditer(line):
+ checked += 1
+ fragment = unquote(match.group(1))
+ if fragment not in anchors:
+ missing.append((line_number, fragment))
+
+ if missing:
+ print(f"Broken generated CRD self-links in {path}:", file=sys.stderr)
+ for line_number, fragment in missing:
+ print(f" line {line_number}: #{fragment}", file=sys.stderr)
+ return 1
+
+ print(f"Generated CRD self-links: {checked} checked, all targets exist.")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())