From ad1c27b1dacc388e334100f0e8d4df6611fafb3f Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Fri, 06 Nov 2026 11:12:52 +0000
Subject: [PATCH] [#1275] Fix Release Notes and Getting Started page titles (#1276)

---
 opendj-doc-generated-ref/src/main/asciidoc/getting-started/first-steps.adoc                        |    7 +
 opendj-doc-generated-ref/src/main/asciidoc/release-notes/requirements-and-upgrade.adoc             |    8 ++
 opendj-doc-generated-ref/pom.xml                                                                   |   23 +++++
 opendj-doc-generated-ref/src/main/asciidoc/getting-started/overview.adoc                           |    8 ++
 opendj-doc-generated-ref/src/main/asciidoc/release-notes/release-5.2.0.adoc                        |    8 ++
 opendj-doc-generated-ref/src/test/java/org/openidentityplatform/opendj/doc/AntoraPageTitlesIT.java |  115 ++++++++++++++++++++++++++++
 opendj-doc-generated-ref/CHECKS.md                                                                 |   34 ++++++++
 7 files changed, 203 insertions(+), 0 deletions(-)

diff --git a/opendj-doc-generated-ref/CHECKS.md b/opendj-doc-generated-ref/CHECKS.md
index 79e033e..4ab34f5 100644
--- a/opendj-doc-generated-ref/CHECKS.md
+++ b/opendj-doc-generated-ref/CHECKS.md
@@ -98,3 +98,37 @@
 ```sh
 mvn compiler:testCompile surefire:test
 ```
+
+The `man-pages` profile also runs `AntoraPageTitlesIT` during `verify`, after
+Antora conversion. It parses the headers of the four Release Notes and Getting
+Started chapter pages and checks their expected document titles, then renders
+their bodies with Antora's `env-site` attribute to check all 16 published chapter
+and subsection fragment IDs. A `:title:` attribute must not substitute for a document
+header: Asciidoctor can return that attribute as the title even without a header,
+but Antora does not use it.
+A section heading alone must fail: Antora needs a document header
+to populate default xref labels, navigation labels and browser titles. The tests
+also parse both preprocessed books to preserve their chapter titles, IDs and levels
+for PDF generation. Missing generated inputs fail rather than skipping the check.
+
+After preprocessing and Antora conversion, rerun this check from this module:
+
+```sh
+mvn -Pman-pages compiler:testCompile surefire:test@test-antora-page-titles
+```
+
+Chapter sources keep `==` headings for the combined books. With doc-maven-plugin
+3.2.2, keep `:table-caption!:` before each chapter heading: the Antora converter
+uses this existing attribute as the insertion point for `:leveloffset: -1`,
+which promotes the chapter heading to a standalone page title only in its output.
+Do not put the level offset directly in the book sources.
+
+The chapter ID stays on the heading outside `env-site`. On the site, it is emitted
+as a separate inline anchor after the page title: Antora's default UI does not
+render the document ID as an HTML anchor. Keep both forms so incoming fragments
+work in HTML and the combined books retain their chapter IDs. Keep a blank line
+after the anchor's `endif::[]`: Asciidoctor does not end a paragraph at a section
+title, so a heading directly below the anchor joins its paragraph and disappears
+together with its fragment ID. Do not turn the anchor into a block anchor (`[[id]]`
+or `[#id]`) either: a block anchor attaches to the next block and replaces that
+section's automatic ID.
diff --git a/opendj-doc-generated-ref/pom.xml b/opendj-doc-generated-ref/pom.xml
index 0625e54..09b186d 100644
--- a/opendj-doc-generated-ref/pom.xml
+++ b/opendj-doc-generated-ref/pom.xml
@@ -415,6 +415,29 @@
             	</activation>
             <build><finalName>${project.groupId}.${project.artifactId}</finalName>
                 <plugins>
+                    <!-- These tests consume the Antora pages generated during package. -->
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-surefire-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>test-antora-page-titles</id>
+                                <phase>verify</phase>
+                                <goals>
+                                    <goal>test</goal>
+                                </goals>
+                                <configuration>
+                                    <includes>
+                                        <include>**/AntoraPageTitlesIT.java</include>
+                                    </includes>
+                                    <failIfNoTests>true</failIfNoTests>
+                                    <systemPropertyVariables>
+                                        <docBuildDirectory>${project.build.directory}/asciidoc</docBuildDirectory>
+                                    </systemPropertyVariables>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
                     <plugin>
                         <groupId>org.openidentityplatform.opendj</groupId>
                         <artifactId>opendj-doc-maven-plugin</artifactId>
diff --git a/opendj-doc-generated-ref/src/main/asciidoc/getting-started/first-steps.adoc b/opendj-doc-generated-ref/src/main/asciidoc/getting-started/first-steps.adoc
index bd66a5b..8b1ecae 100644
--- a/opendj-doc-generated-ref/src/main/asciidoc/getting-started/first-steps.adoc
+++ b/opendj-doc-generated-ref/src/main/asciidoc/getting-started/first-steps.adoc
@@ -14,11 +14,18 @@
 Copyright 2026 3A Systems, LLC.
 ////
 
+:table-caption!:
 :opendj-example-version: 5.2.0
 
+ifndef::env-site[]
 [#getting-started-first-directory]
+endif::[]
 == Your First Directory
 
+ifdef::env-site[]
+anchor:getting-started-first-directory[]
+endif::[]
+
 This exercise uses the published OpenDJ {opendj-example-version} ZIP distribution on Linux or macOS with Bash. It creates a new evaluation instance, uses a self-signed certificate for LDAPS, and leaves existing installations alone. For Windows, native packages or Docker, use the xref:../install-guide/chap-install.adoc[Installation Guide].
 
 === Prepare the Environment
diff --git a/opendj-doc-generated-ref/src/main/asciidoc/getting-started/overview.adoc b/opendj-doc-generated-ref/src/main/asciidoc/getting-started/overview.adoc
index 69e894b..b9f0735 100644
--- a/opendj-doc-generated-ref/src/main/asciidoc/getting-started/overview.adoc
+++ b/opendj-doc-generated-ref/src/main/asciidoc/getting-started/overview.adoc
@@ -14,9 +14,17 @@
 Copyright 2026 3A Systems, LLC.
 ////
 
+:table-caption!:
+
+ifndef::env-site[]
 [#getting-started-overview]
+endif::[]
 == Directory Concepts
 
+ifdef::env-site[]
+anchor:getting-started-overview[]
+endif::[]
+
 OpenDJ stores directory entries and makes them available to applications through LDAP. A directory is commonly used to hold users, groups and application configuration. Applications bind to the server to authenticate, then search or update entries according to their access rights.
 
 Entry::
diff --git a/opendj-doc-generated-ref/src/main/asciidoc/release-notes/release-5.2.0.adoc b/opendj-doc-generated-ref/src/main/asciidoc/release-notes/release-5.2.0.adoc
index d39563c..53b1961 100644
--- a/opendj-doc-generated-ref/src/main/asciidoc/release-notes/release-5.2.0.adoc
+++ b/opendj-doc-generated-ref/src/main/asciidoc/release-notes/release-5.2.0.adoc
@@ -14,9 +14,17 @@
 Copyright 2026 3A Systems, LLC.
 ////
 
+:table-caption!:
+
+ifndef::env-site[]
 [#release-5-2-0]
+endif::[]
 == OpenDJ 5.2.0
 
+ifdef::env-site[]
+anchor:release-5-2-0[]
+endif::[]
+
 OpenDJ 5.2.0 was published on 21 October 2026. These notes describe that released version. Documentation built from a development branch can also describe changes intended for a later release; a `SNAPSHOT` build is not a published release.
 
 The link:https://github.com/OpenIdentityPlatform/OpenDJ/releases/tag/5.2.0[5.2.0 release record] is the authoritative list of release assets and changes. The link:https://github.com/OpenIdentityPlatform/OpenDJ/compare/5.1.2...5.2.0[complete comparison with 5.1.2] provides the corresponding source history. For newer releases, consult the link:https://github.com/OpenIdentityPlatform/OpenDJ/releases[release list].
diff --git a/opendj-doc-generated-ref/src/main/asciidoc/release-notes/requirements-and-upgrade.adoc b/opendj-doc-generated-ref/src/main/asciidoc/release-notes/requirements-and-upgrade.adoc
index 4a4cae8..a6aa151 100644
--- a/opendj-doc-generated-ref/src/main/asciidoc/release-notes/requirements-and-upgrade.adoc
+++ b/opendj-doc-generated-ref/src/main/asciidoc/release-notes/requirements-and-upgrade.adoc
@@ -14,9 +14,17 @@
 Copyright 2026 3A Systems, LLC.
 ////
 
+:table-caption!:
+
+ifndef::env-site[]
 [#release-requirements]
+endif::[]
 == Requirements and Upgrade Considerations
 
+ifdef::env-site[]
+anchor:release-requirements[]
+endif::[]
+
 === Java and Installation Environment
 
 OpenDJ 5.2.0 requires Java 11 or later. This is the minimum in the link:https://github.com/OpenIdentityPlatform/OpenDJ/blob/5.2.0/README.md[release README]; the server ZIP does not supply a Java runtime. The Docker image supplies its own Java environment.
diff --git a/opendj-doc-generated-ref/src/test/java/org/openidentityplatform/opendj/doc/AntoraPageTitlesIT.java b/opendj-doc-generated-ref/src/test/java/org/openidentityplatform/opendj/doc/AntoraPageTitlesIT.java
new file mode 100644
index 0000000..b6f559f
--- /dev/null
+++ b/opendj-doc-generated-ref/src/test/java/org/openidentityplatform/opendj/doc/AntoraPageTitlesIT.java
@@ -0,0 +1,115 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.openidentityplatform.opendj.doc;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.asciidoctor.Asciidoctor;
+import org.asciidoctor.Options;
+import org.asciidoctor.SafeMode;
+import org.asciidoctor.ast.Document;
+import org.asciidoctor.ast.Section;
+import org.forgerock.testng.ForgeRockTestCase;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Checks the real Antora output after package and the chapter structure used by PDF generation. */
+@Test
+public class AntoraPageTitlesIT extends ForgeRockTestCase {
+    private Asciidoctor asciidoctor;
+    private File buildDirectory;
+
+    @BeforeClass
+    public void start() {
+        buildDirectory = new File(System.getProperty("docBuildDirectory",
+                new File(System.getProperty("basedir", "."), "target/asciidoc").getPath()));
+        asciidoctor = Asciidoctor.Factory.create();
+    }
+
+    @AfterClass(alwaysRun = true)
+    public void shutdown() {
+        if (asciidoctor != null) {
+            asciidoctor.shutdown();
+        }
+    }
+
+    @DataProvider
+    public Object[][] pages() {
+        return new Object[][] {
+            { "release-notes", "release-5.2.0", "OpenDJ 5.2.0",
+              new String[] { "release-5-2-0", "_selected_changes", "_distributions" } },
+            { "release-notes", "requirements-and-upgrade", "Requirements and Upgrade Considerations",
+              new String[] { "release-requirements", "_java_and_installation_environment", "_plan_an_upgrade",
+                             "_compatibility_checks", "_known_issues_and_help" } },
+            { "getting-started", "overview", "Directory Concepts", new String[] { "getting-started-overview" } },
+            { "getting-started", "first-steps", "Your First Directory",
+              new String[] { "getting-started-first-directory", "_prepare_the_environment",
+                             "_set_up_and_start_the_server", "_add_a_person", "_search_and_change_the_entry",
+                             "_restart_and_remove_the_sample_data", "_next_steps" } }
+        };
+    }
+
+    @Test(dataProvider = "pages")
+    public void generatedPageHasDocumentTitle(String guide, String page, String title, String[] fragments) {
+        final File input = new File(buildDirectory, "antora/modules/" + guide + "/pages/" + page + ".adoc");
+        assertThat(input).isFile();
+        // Antora extracts metadata from the header. A full parse can fall back to a section
+        // title and conceal a missing document header, although Antora publishes "Untitled".
+        final Document document = asciidoctor.loadFile(input,
+                Options.builder().safe(SafeMode.SAFE).attributes(Map.of("env-site", ""))
+                        .parseHeaderOnly(true).build());
+        assertThat(document.getDoctitle()).as("document title of %s", input).isEqualTo(title);
+        // Without a header, Asciidoctor can still return the title attribute, which Antora ignores.
+        assertThat(document.hasAttribute("title"))
+                .as("%s must have a document header rather than a :title: attribute", input).isFalse();
+        final String html = asciidoctor.convertFile(input,
+                Options.builder().safe(SafeMode.SAFE).attributes(Map.of("env-site", ""))
+                        .standalone(false).toFile(false).build());
+        for (final String fragment : fragments) {
+            assertThat(html).as("published fragment %s in the rendered body of %s", fragment, input)
+                    .contains("id=\"" + fragment + "\"");
+        }
+    }
+
+    @DataProvider
+    public Object[][] books() {
+        return new Object[][] {
+            { "release-notes", new String[] { "OpenDJ 5.2.0", "Requirements and Upgrade Considerations" },
+              new String[] { "release-5-2-0", "release-requirements" } },
+            { "getting-started", new String[] { "Directory Concepts", "Your First Directory" },
+              new String[] { "getting-started-overview", "getting-started-first-directory" } }
+        };
+    }
+
+    @Test(dataProvider = "books")
+    public void combinedBookKeepsChapters(String guide, String[] titles, String[] ids) {
+        final File input = new File(buildDirectory, "source/" + guide + "/index.adoc");
+        assertThat(input).isFile();
+        final Document document = asciidoctor.loadFile(input,
+                Options.builder().safe(SafeMode.UNSAFE).build());
+        final List<Section> chapters = document.getBlocks().stream()
+                .filter(Section.class::isInstance).map(Section.class::cast).collect(Collectors.toList());
+        assertThat(chapters).extracting(Section::getTitle).containsExactly(titles);
+        assertThat(chapters).extracting(Section::getId).containsExactly(ids);
+        assertThat(chapters).extracting(Section::getLevel).containsOnly(1);
+    }
+}

--
Gitblit v1.10.0