This is an automated email from the ASF dual-hosted git repository. spmallette pushed a commit to branch afd in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit c3b1ce98912764b50bb93b2ffe1b83a03951ff9b Author: Stephen Mallette <[email protected]> AuthorDate: Sat Jul 25 19:40:57 2026 +0000 Fix llms.txt index entries for book landing pages and tutorials The summary-driven split produced wrong llms.txt entries for every book's landing page: the document title broke off to its own page, leaving index.md owning the first chapter, so the index line showed that chapter's title and a scraped description (an image macro, the version line, or a mid-sentence fragment). - Splitter: fold the leading document-title section into the index page so its own llms-summary describes index.md and its content renders there, instead of breaking off to a separate _<doctitle>.md page. - Converter: for article-doctype pages (the tutorials), the title-bearing section is promoted to the document title, so read its llms-summary / allow-oversize from the promoted leading section (not just the Document) and emit the hidden markers on the landing page. - Curation: give the headingless gremlin-language-variants tutorial a titled summary, and replace four recipe summaries that had fallen back to generic or mid-sentence text (collections, shortest-path, edge-move, if-then-based-grouping) with proper descriptions. Result: all 240 pages, including every book/tutorial landing page, have a clean curated llms.txt description; build passes; 412 links resolve; 174 tests pass. Assisted-by: Claude Code:claude-opus-4-8 --- docs/src/recipes/collections.asciidoc | 2 +- docs/src/recipes/edge-move.asciidoc | 2 +- docs/src/recipes/if-then-based-grouping.asciidoc | 2 +- docs/src/recipes/shortest-path.asciidoc | 2 +- .../gremlin-language-variants/index.asciidoc | 3 +++ .../tinkerpop/tinkeradoc/MarkdownConverter.java | 28 +++++++++++++++++++- .../tinkerpop/tinkeradoc/MarkdownSplitter.java | 30 ++++++++++++++++++++++ 7 files changed, 64 insertions(+), 5 deletions(-) diff --git a/docs/src/recipes/collections.asciidoc b/docs/src/recipes/collections.asciidoc index 1dab723f74..5c1d493a53 100644 --- a/docs/src/recipes/collections.asciidoc +++ b/docs/src/recipes/collections.asciidoc @@ -16,7 +16,7 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. //// -[llms-summary="Recipe: A recipe for collections in Gremlin."] +[llms-summary="Recipe: reshaping List and Map collections within a traversal — unfolding, folding, grouping, and reformatting side-effect data."] [[collections]] == Collections diff --git a/docs/src/recipes/edge-move.asciidoc b/docs/src/recipes/edge-move.asciidoc index 25a416d030..5f8efcec55 100644 --- a/docs/src/recipes/edge-move.asciidoc +++ b/docs/src/recipes/edge-move.asciidoc @@ -16,7 +16,7 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. //// -[llms-summary="Recipe: A recipe for moving an edge in Gremlin."] +[llms-summary="Recipe: moving an existing edge from one incident vertex to another by recreating it with new endpoints and dropping the original."] [[edge-move]] == Moving an Edge diff --git a/docs/src/recipes/if-then-based-grouping.asciidoc b/docs/src/recipes/if-then-based-grouping.asciidoc index 5d88b9a6ab..753cbb70e9 100644 --- a/docs/src/recipes/if-then-based-grouping.asciidoc +++ b/docs/src/recipes/if-then-based-grouping.asciidoc @@ -16,7 +16,7 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. //// -[llms-summary="Recipe: Consider the following traversal over the \"modern\" toy graph:"] +[llms-summary="Recipe: bucketing elements into groups by if/then-style conditions using choose() within group()."] [[if-then-based-grouping]] == If-Then Based Grouping diff --git a/docs/src/recipes/shortest-path.asciidoc b/docs/src/recipes/shortest-path.asciidoc index 89a0577e9d..66d71f6b12 100644 --- a/docs/src/recipes/shortest-path.asciidoc +++ b/docs/src/recipes/shortest-path.asciidoc @@ -16,7 +16,7 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. //// -[llms-summary="Recipe: A recipe for shortest path in Gremlin."] +[llms-summary="Recipe: finding the shortest path between vertices with Gremlin, using repeat()/until() and path-length comparison."] [[shortest-path]] == Shortest Path diff --git a/docs/src/tutorials/gremlin-language-variants/index.asciidoc b/docs/src/tutorials/gremlin-language-variants/index.asciidoc index 1a3f285e9f..64779d3915 100644 --- a/docs/src/tutorials/gremlin-language-variants/index.asciidoc +++ b/docs/src/tutorials/gremlin-language-variants/index.asciidoc @@ -24,6 +24,9 @@ image::apache-tinkerpop-logo.png[width=500,link="https://tinkerpop.apache.org"] *x.y.z* +[llms-summary="A stub tutorial that redirects to the reference documentation on Gremlin drivers and language variants."] += Gremlin Language Variants + image::gremlin-house-of-mirrors.png[width=1024] Please see the link:https://tinkerpop.apache.org/docs/x.y.z/reference/#gremlin-drivers-variants[Reference Documentation] diff --git a/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/MarkdownConverter.java b/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/MarkdownConverter.java index e7b617b503..26269ec1e0 100644 --- a/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/MarkdownConverter.java +++ b/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/MarkdownConverter.java @@ -167,9 +167,17 @@ public class MarkdownConverter extends StringConverter { // already appears as an H1 there (matching the HTML backend's single <h1 class="sect0">). // In an article, the title is not part of the content, so emit it as the leading H1. if (title != null && !title.isEmpty() && !isBook(doc)) { + // Article doctype: the title-bearing section is promoted to the document title, so its + // curated llms-summary / allow-oversize attributes are not visible via convertSection. + // They resolve on the Document (when set as document attributes) or on the leading + // section AsciidoctorJ promoted to the title; check both so the landing page carries the + // hidden markers that drive the llms.txt description and the size lint. final StringBuilder head = new StringBuilder(); appendAnchor(head, doc.getId()); - head.append("# ").append(title).append("\n\n").append(content); + head.append("# ").append(title).append("\n\n"); + appendLlmsSummary(head, docLevelAttribute(doc, LLMS_SUMMARY_ATTR)); + appendAllowOversizeMarker(head, docLevelAttribute(doc, LLMS_ALLOW_OVERSIZE_ATTR)); + head.append(content); assembled = head.toString(); } else { assembled = content; @@ -181,6 +189,24 @@ public class MarkdownConverter extends StringConverter { return substituteVersion(doc, assembled); } + /** + * Resolves a doc-level attribute for the article title path: prefers the Document's own + * attribute, falling back to the leading section that AsciidoctorJ promoted to the title (which + * is where a {@code [llms-summary]} on the article's {@code == Title} actually lands). + */ + private static Object docLevelAttribute(final Document doc, final String name) { + final Object onDoc = doc.getAttribute(name); + if (onDoc != null) return onDoc; + for (final StructuralNode block : doc.getBlocks()) { + if (block instanceof Section) { + final Object onSection = block.getAttribute(name); + if (onSection != null) return onSection; + break; // only the first/leading section is the promoted title + } + } + return null; + } + /** Replaces {@code x.y.z} with the resolved TinkerPop version, if available. */ private static String substituteVersion(final Document doc, final String text) { final Object version = firstNonNull(doc.getAttribute("tinkerpop-version"), doc.getAttribute("revnumber")); diff --git a/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/MarkdownSplitter.java b/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/MarkdownSplitter.java index fcc65ba381..8b37863088 100644 --- a/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/MarkdownSplitter.java +++ b/docs/tinkeradoc-extension/src/main/java/org/apache/tinkerpop/tinkeradoc/MarkdownSplitter.java @@ -221,6 +221,18 @@ class MarkdownSplitter { // up to — but not including — the next summarized descendant, which breaks off to its own // page. There is no size-based splitting; page size is an author concern surfaced by the // build's size lint, with intentional exceptions flagged via allow-oversize. + // The document title (the single leading top-level section — the book's "= Title") is the + // landing page: it owns index.md so its own llms-summary describes the index and its content + // renders there, rather than breaking off to a page of its own. Fold it into the root so the + // root preamble + doctitle render together as index.md. Everything below still breaks on + // summaries as usual. + final Node docTitle = leadingDocTitle(root); + if (docTitle != null) { + root.lines.addAll(docTitle.lines); + root.children.remove(docTitle); + root.children.addAll(0, docTitle.children); + } + final Map<String, String> anchorToFile = new LinkedHashMap<>(); final List<PagePlan> plans = new ArrayList<>(); final PagePlan index = new PagePlan(indexFileName, root); @@ -238,6 +250,24 @@ class MarkdownSplitter { return pages; } + /** + * Returns the document-title node to fold into the index page, or {@code null} if there is none. + * The doctitle is the book's leading section: the root's first child, when that child is the sole + * shallowest-level top section (i.e. the other top-level siblings are its heading level or + * deeper). In practice this is the {@code = Title} that AsciidoctorJ renders as the first h1. + */ + private static Node leadingDocTitle(final Node root) { + if (root.children.isEmpty()) return null; + final Node first = root.children.get(0); + // Only treat it as the doctitle when it carries content meant to lead the landing page: it + // has a summary (so it would otherwise wrongly break off) or the root has no preamble of its + // own. Guard against odd trees by requiring it be the shallowest top-level section. + for (final Node sib : root.children) { + if (sib != first && sib.level < first.level) return null; + } + return first; + } + /** * Walks the tree creating a page for every summarized descendant of {@code node} (the node's own * page having already been created). Recurses through the whole tree so nested summarized
