You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by ah...@apache.org on 2023/01/26 09:48:17 UTC

[isis] branch master updated: ISIS-3328: create a help system where multiple pages can be added

This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/isis.git


The following commit(s) were added to refs/heads/master by this push:
     new dc09821e3c ISIS-3328: create a help system where multiple pages can be added
dc09821e3c is described below

commit dc09821e3ca79c6324b9d8bd70532d3da8a7f1f0
Author: Andi Huber <ah...@apache.org>
AuthorDate: Thu Jan 26 10:48:10 2023 +0100

    ISIS-3328: create a help system where multiple pages can be added
    
    - reuses the previous DocumentationService as WelcomeHelpPage
---
 .../causeway/applib/graph/tree/TreePath.java       |  51 ++++-
 .../applib/graph/tree/TreePath_Default.java        |  44 +++-
 .../causeway/applib/graph/tree/TreePathTest.java   |  33 ++-
 .../extensions/docgen/CausewayModuleExtDocgen.java |  28 ++-
 .../extensions/docgen/applib/HelpNode.java         | 245 +++++++++++++++++++++
 .../HelpPage.java}                                 |  18 +-
 .../extensions/docgen/help/DefaultHelpVm.java      |  63 ------
 .../docgen/help/DefaultHelpVm.layout.xml           |  37 ----
 .../extensions/docgen/helptree/HelpNodeVm-PAGE.svg |  44 ++++
 .../docgen/helptree/HelpNodeVm-TOPIC.svg           |  44 ++++
 .../extensions/docgen/helptree/HelpNodeVm.java     | 127 +++++++++++
 .../docgen/helptree/HelpNodeVm.layout.xml          |  39 ++++
 .../HelpTreeAdapter.java}                          |  36 +--
 .../extensions/docgen/menu/DocumentationMenu.java  |  16 +-
 .../welcome/WelcomeHelpPage.java}                  |  43 ++--
 15 files changed, 703 insertions(+), 165 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/causeway/applib/graph/tree/TreePath.java b/api/applib/src/main/java/org/apache/causeway/applib/graph/tree/TreePath.java
index 958ea40833..e3b660a859 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/graph/tree/TreePath.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/graph/tree/TreePath.java
@@ -19,9 +19,19 @@
 package org.apache.causeway.applib.graph.tree;
 
 import java.io.Serializable;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
 
 import org.springframework.lang.Nullable;
 
+import org.apache.causeway.commons.functional.IndexedConsumer;
+import org.apache.causeway.commons.internal.assertions._Assert;
+import org.apache.causeway.commons.internal.base._Strings;
+import org.apache.causeway.commons.internal.exceptions._Exceptions;
+import org.apache.causeway.commons.internal.primitives._Ints;
+
 /**
  * Provides an unambiguous way to address nodes by position within a tree-structure. Examples:
  * <ul>
@@ -40,13 +50,19 @@ public interface TreePath extends Serializable {
     public TreePath append(int indexWithinSiblings);
 
     /**
-     *
-     * @return a new TreePath instance that represents the parent path of this
+     * Returns a TreePath instance that represents the parent path of this TreePath,
+     * if this is not the root.
      */
     public @Nullable TreePath getParentIfAny();
 
     public boolean isRoot();
 
+    public IntStream streamPathElements();
+
+    public String stringify(String delimiter);
+
+    public Stream<TreePath> streamUpTheHierarchyStartingAtSelf();
+
     // -- CONSTRUCTION
 
     public static TreePath of(final int ... canonicalPath) {
@@ -57,4 +73,35 @@ public interface TreePath extends Serializable {
         return of(0);
     }
 
+    /**
+     * Parses stringified tree path of format {@code <delimiter>0<delimiter>3<delimiter>1} ...,
+     * as returned by {@link TreePath#stringify(String)}.
+     * <p>
+     * For null or empty input the root is returned.
+     * @throws IllegalArgumentException if parsing fails
+     */
+    public static TreePath parse(final @Nullable String treePathStringified, final String delimiter) {
+        if(_Strings.isNullOrEmpty(treePathStringified)) {
+            return root();
+        }
+        _Assert.assertTrue(_Strings.isNotEmpty(delimiter), ()->"non-empty delimiter required");
+
+        // parse the input String into a list of integers
+        final List<Integer> pathElementsAsList =
+            _Strings.splitThenStream(treePathStringified, delimiter)
+            .filter(_Strings::isNotEmpty)
+            .map(pathElement->
+                _Ints.parseInt(pathElement, 10)
+                .orElseThrow(()->
+                    _Exceptions.illegalArgument("illformed treePath '%s' while parsing element '%s' using delimiter '%s'",
+                            treePathStringified, pathElement, delimiter)))
+            .collect(Collectors.toList());
+
+        // convert the list of integers into an array of int
+        final int[] canonicalPath = new int[pathElementsAsList.size()];
+        pathElementsAsList.forEach(IndexedConsumer.zeroBased((index, value)->canonicalPath[index] = value));
+
+        return of(canonicalPath);
+    }
+
 }
diff --git a/api/applib/src/main/java/org/apache/causeway/applib/graph/tree/TreePath_Default.java b/api/applib/src/main/java/org/apache/causeway/applib/graph/tree/TreePath_Default.java
index b6549f38a5..e5b2993ea1 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/graph/tree/TreePath_Default.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/graph/tree/TreePath_Default.java
@@ -22,6 +22,14 @@ import java.util.Arrays;
 import java.util.Objects;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.apache.causeway.commons.internal.assertions._Assert;
+import org.apache.causeway.commons.internal.base._Refs;
+import org.apache.causeway.commons.internal.base._Strings;
+
+import lombok.NonNull;
+import lombok.val;
 
 /**
  * Package private mixin for TreePath.
@@ -32,7 +40,7 @@ class TreePath_Default implements TreePath {
     private final int[] canonicalPath;
     private final int hashCode;
 
-    TreePath_Default(int[] canonicalPath) {
+    TreePath_Default(final int[] canonicalPath) {
         Objects.requireNonNull(canonicalPath, "canonicalPath is required");
         if(canonicalPath.length<1) {
             throw new IllegalArgumentException("canonicalPath must not be empty");
@@ -42,7 +50,7 @@ class TreePath_Default implements TreePath {
     }
 
     @Override
-    public TreePath append(int indexWithinSiblings) {
+    public TreePath append(final int indexWithinSiblings) {
         final int[] newCanonicalPath = new int[canonicalPath.length+1];
         System.arraycopy(canonicalPath, 0, newCanonicalPath, 0, canonicalPath.length);
         newCanonicalPath[canonicalPath.length] = indexWithinSiblings;
@@ -64,10 +72,36 @@ class TreePath_Default implements TreePath {
         return canonicalPath.length==1;
     }
 
+    @Override
+    public String stringify(@NonNull final String delimiter) {
+        _Assert.assertTrue(_Strings.isNotEmpty(delimiter), ()->"non-empty delimiter required");
+        return delimiter + streamPathElements()
+            .mapToObj(i->""+i)
+            .collect(Collectors.joining(delimiter));
+    }
+
+    @Override
+    public IntStream streamPathElements() {
+        return IntStream.of(canonicalPath);
+    }
+
+    @Override
+    public Stream<TreePath> streamUpTheHierarchyStartingAtSelf() {
+        val hasMore = _Refs.booleanRef(true);
+        return Stream.iterate((TreePath)this, __->hasMore.isTrue(), TreePath::getParentIfAny)
+                .filter(x->{
+                    if(x.isRoot()) {
+                        hasMore.setValue(false); // stop the stream only after we have included the root
+                    }
+                    return true;
+                });
+
+    }
+
     // -- OBJECT CONTRACTS
 
     @Override
-    public boolean equals(Object obj) {
+    public boolean equals(final Object obj) {
         if(obj instanceof TreePath_Default) {
             final TreePath_Default other = (TreePath_Default) obj;
             return Arrays.equals(canonicalPath, other.canonicalPath);
@@ -82,9 +116,7 @@ class TreePath_Default implements TreePath {
 
     @Override
     public String toString() {
-        return "/" + IntStream.of(canonicalPath)
-        .mapToObj(i->""+i)
-        .collect(Collectors.joining("/"));
+        return stringify("/");
     }
 
 }
diff --git a/api/applib/src/test/java/org/apache/causeway/applib/graph/tree/TreePathTest.java b/api/applib/src/test/java/org/apache/causeway/applib/graph/tree/TreePathTest.java
index ee348a9379..8669e5501e 100644
--- a/api/applib/src/test/java/org/apache/causeway/applib/graph/tree/TreePathTest.java
+++ b/api/applib/src/test/java/org/apache/causeway/applib/graph/tree/TreePathTest.java
@@ -18,23 +18,27 @@
  */
 package org.apache.causeway.applib.graph.tree;
 
+import java.util.List;
+
 import org.hamcrest.Matchers;
 import org.junit.jupiter.api.Test;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
+import org.apache.causeway.commons.collections.Can;
+
 class TreePathTest {
 
     @Test
-    public void rootConstructor() {
+    void rootConstructor() {
         final TreePath treePath = TreePath.root();
         assertThat(treePath.isRoot(), Matchers.is(true));
         assertThat(treePath.toString(), Matchers.is("/0"));
     }
 
     @Test
-    public void samePathsShouldBeEqual() {
+    void samePathsShouldBeEqual() {
         final TreePath treePath1 = TreePath.of(0, 1, 2, 3);
         final TreePath treePath2 = TreePath.of(0, 1, 2, 3);
         assertEquals(treePath1, treePath2);
@@ -45,4 +49,29 @@ class TreePathTest {
         assertThat(treePath1.toString(), Matchers.is("/0/1/2/3"));
     }
 
+    @Test
+    void hierarchyStreamingOfRoot() {
+        assertEquals(
+                Can.ofCollection(List.of("/0")),
+                TreePath.root()
+                    .streamUpTheHierarchyStartingAtSelf()
+                    .map(TreePath::toString)
+                    .collect(Can.toCan()));
+    }
+
+    @Test
+    void hierarchyStreamingOfNonRoot() {
+        assertEquals(
+                Can.ofCollection(
+                        List.of(
+                                "/0/1/2/3",
+                                "/0/1/2",
+                                "/0/1",
+                                "/0")),
+                TreePath.of(0, 1, 2, 3)
+                    .streamUpTheHierarchyStartingAtSelf()
+                    .map(TreePath::toString)
+                    .collect(Can.toCan()));
+    }
+
 }
diff --git a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/CausewayModuleExtDocgen.java b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/CausewayModuleExtDocgen.java
index e3fb969bce..9228bb1aed 100644
--- a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/CausewayModuleExtDocgen.java
+++ b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/CausewayModuleExtDocgen.java
@@ -18,11 +18,17 @@
  */
 package org.apache.causeway.extensions.docgen;
 
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
 
-import org.apache.causeway.extensions.docgen.help.DocumentationServiceDefault;
+import org.apache.causeway.extensions.docgen.applib.HelpNode.HelpTopic;
 import org.apache.causeway.extensions.docgen.menu.DocumentationMenu;
+import org.apache.causeway.extensions.docgen.topics.welcome.WelcomeHelpPage;
+
+import lombok.val;
 
 /**
  * Adds the {@link DocumentationMenu} with its auto-configured menu entries.
@@ -30,11 +36,29 @@ import org.apache.causeway.extensions.docgen.menu.DocumentationMenu;
  */
 @Configuration
 @Import({
+    // menu providers
     DocumentationMenu.class,
-    DocumentationServiceDefault.class,
+
+    // help pages, as required by the default rootHelpTopic below (in case when to be managed by Spring)
+    WelcomeHelpPage.class
+
 })
 public class CausewayModuleExtDocgen {
 
     public static final String NAMESPACE = "causeway.ext.docgen";
 
+    @Bean(NAMESPACE + "RootHelpTopic")
+    @ConditionalOnMissingBean(HelpTopic.class)
+    @Qualifier("Default")
+    public HelpTopic rootHelpTopic(final WelcomeHelpPage welcomeHelpPage) {
+        val root = HelpTopic.root("Topics");
+
+        root.addPage(welcomeHelpPage);
+
+//        root.subTopic("Legacy")
+//            .addPage(legacyHelpPage);
+
+        return root;
+    }
+
 }
diff --git a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/applib/HelpNode.java b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/applib/HelpNode.java
new file mode 100644
index 0000000000..684926224a
--- /dev/null
+++ b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/applib/HelpNode.java
@@ -0,0 +1,245 @@
+/*
+ *  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.
+ */
+package org.apache.causeway.extensions.docgen.applib;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Stack;
+import java.util.stream.Stream;
+
+import org.springframework.lang.Nullable;
+
+import org.apache.causeway.applib.graph.tree.TreePath;
+import org.apache.causeway.valuetypes.asciidoc.applib.value.AsciiDoc;
+
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+/**
+ * Represents a node in the tree made of topics and {@link HelpPage}s.
+ *
+ * @since 2.x {@index}
+ */
+public interface HelpNode {
+
+    public enum HelpNodeType {
+        TOPIC,
+        PAGE;
+    }
+
+    TreePath getPath();
+    String getTitle();
+    HelpNodeType getHelpNodeType();
+    AsciiDoc getContent();
+
+    // -- PARENT CHILD RELATIONS
+
+    Optional<HelpTopic> getParent();
+    int childCount();
+    Stream<HelpNode> streamChildNodes();
+
+    Optional<HelpNode> getChildNode(int index);
+
+    // -- IMPLEMENTATIONS
+
+    /**
+     * Topic node of the tree, which may contain sub-{@link HelpTopic}s or {@link HelpPageNode}s.
+     */
+    @RequiredArgsConstructor
+    public static final class HelpTopic
+        implements HelpNode {
+
+        public static HelpTopic root(final String topic) {
+            return parented(null, topic);
+        }
+
+        public static HelpTopic parented(final @Nullable HelpTopic parentTopic, final String topic) {
+            return new HelpTopic(parentTopic, nextChildPathOf(parentTopic), new ArrayList<>(), topic);
+        }
+
+        private final @Nullable HelpTopic parentTopic;
+
+        @Getter(onMethod_={@Override})
+        private final @NonNull TreePath path;
+
+        @Getter
+        private final List<HelpNode> childNodes;
+
+        @Getter(onMethod_={@Override})
+        private final String title;
+
+        public HelpTopic subTopic(final String topic) {
+            val childTopic = HelpTopic.parented(this, topic);
+            childNodes.add(childTopic);
+            return childTopic;
+        }
+
+        @Override
+        public HelpNodeType getHelpNodeType() {
+            return HelpNodeType.TOPIC;
+        }
+
+        @Override
+        public AsciiDoc getContent() {
+            return AsciiDoc.valueOf("todo: summarize children"); // TODO
+        }
+
+        public <T extends HelpPage> HelpTopic addPage(final T helpPage) {
+            childNodes.add(new HelpPageNode(this, nextChildPathOf(this), helpPage));
+            return this;
+        }
+
+        @Override
+        public Optional<HelpTopic> getParent() {
+            return Optional.ofNullable(parentTopic);
+        }
+
+        @Override
+        public int childCount() {
+            return childNodes.size();
+        }
+
+        @Override
+        public Stream<HelpNode> streamChildNodes() {
+            return childNodes.stream();
+        }
+
+        /**
+         * Resolves given {@link TreePath} to its corresponding {@link HelpNode} if possible.
+         */
+        public Optional<HelpNode> lookup(final TreePath treePath) {
+            val root = rootTopic();
+            if(treePath.isRoot()) {
+                return Optional.of(root);
+            }
+
+            val stack = new Stack<HelpNode>();
+            stack.push(root);
+
+            treePath.streamPathElements()
+            .skip(1) // skip first path element which is always '0' and corresponds to the root, which we already handled above
+            .forEach(pathElement->{
+                if(stack.isEmpty()) return; // an empty stack corresponds to a not found state
+
+                val currentNode = stack.peek();
+                val child = currentNode.getChildNode(pathElement).orElse(null);
+
+                if(child!=null) {
+                    stack.push(child);
+                } else {
+                    stack.clear(); // not found
+                }
+            });
+
+            return stack.isEmpty()
+                    ? Optional.empty()
+                    : Optional.of(stack.peek());
+        }
+
+        @Override
+        public String toString() {
+            return String.format("HelpTopic[%s, childCount=%s]", getTitle(), childCount());
+        }
+
+        // -- HELPER
+
+        private HelpTopic rootTopic() {
+            var node = this;
+            while(node.getParent().isPresent()) {
+                node = node.getParent().get();
+            }
+            return node;
+        }
+
+        private static @NonNull TreePath nextChildPathOf(final @Nullable HelpTopic parentTopic) {
+            if(parentTopic==null) {
+                return TreePath.root();
+            }
+            return parentTopic.getPath().append(parentTopic.childCount());
+        }
+
+        @Override
+        public Optional<HelpNode> getChildNode(final int index) {
+            return index<childCount()
+                ? Optional.of(getChildNodes().get(index))
+                : Optional.empty();
+        }
+    }
+
+    /**
+     * Leaf node of the tree, referencing a {@link HelpPage}.
+     */
+    @RequiredArgsConstructor
+    public static final class HelpPageNode
+        implements HelpNode {
+
+        private final @NonNull HelpTopic parentTopic;
+
+        @Getter(onMethod_={@Override})
+        private final @NonNull TreePath path;
+
+        @Getter
+        private final HelpPage helpPage;
+
+        @Override
+        public HelpNodeType getHelpNodeType() {
+            return HelpNodeType.PAGE;
+        }
+
+        @Override
+        public String getTitle() {
+            return helpPage.getTitle();
+        }
+
+        @Override
+        public AsciiDoc getContent() {
+            return helpPage.getContent();
+        }
+
+        @Override
+        public Optional<HelpTopic> getParent() {
+            return Optional.of(parentTopic);
+        }
+
+        @Override
+        public int childCount() {
+            return 0;
+        }
+
+        @Override
+        public Stream<HelpNode> streamChildNodes() {
+            return Stream.empty();
+        }
+
+        @Override
+        public Optional<HelpNode> getChildNode(final int index) {
+            return Optional.empty();
+        }
+
+        @Override
+        public String toString() {
+            return String.format("HelpPageNode[%s]", getTitle());
+        }
+
+    }
+
+}
diff --git a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/help/DocumentationService.java b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/applib/HelpPage.java
similarity index 64%
rename from extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/help/DocumentationService.java
rename to extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/applib/HelpPage.java
index 35e105d291..b74945f9d1 100644
--- a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/help/DocumentationService.java
+++ b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/applib/HelpPage.java
@@ -16,21 +16,13 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.causeway.extensions.docgen.help;
+package org.apache.causeway.extensions.docgen.applib;
 
-import org.apache.causeway.extensions.docgen.menu.DocumentationMenu;
+import org.apache.causeway.valuetypes.asciidoc.applib.value.AsciiDoc;
 
-/**
- * Provides the content for the {@link DocumentationMenu} entries.
- * <p>
- * Currently there is only one, namely (<i>help</i>).
- *
- * @see DocumentationMenu
- * @since 2.x {@index}
- */
-public interface DocumentationService {
+public interface HelpPage {
 
-    /** Returns a view-model or value that represents the application's primary help page. */
-    Object getHelp();
+    String getTitle();
+    AsciiDoc getContent();
 
 }
diff --git a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/help/DefaultHelpVm.java b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/help/DefaultHelpVm.java
deleted file mode 100644
index 7294b520a0..0000000000
--- a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/help/DefaultHelpVm.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- *  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.
- */
-package org.apache.causeway.extensions.docgen.help;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
-import org.apache.causeway.applib.ViewModel;
-import org.apache.causeway.applib.annotation.DomainObject;
-import org.apache.causeway.applib.annotation.DomainObjectLayout;
-import org.apache.causeway.applib.annotation.LabelPosition;
-import org.apache.causeway.applib.annotation.Nature;
-import org.apache.causeway.applib.annotation.ObjectSupport;
-import org.apache.causeway.applib.annotation.Property;
-import org.apache.causeway.applib.annotation.PropertyLayout;
-import org.apache.causeway.applib.value.Markup;
-import org.apache.causeway.extensions.docgen.CausewayModuleExtDocgen;
-
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-
-@Named(CausewayModuleExtDocgen.NAMESPACE + ".DefaultHelpVm")
-@DomainObject(nature = Nature.VIEW_MODEL)
-@DomainObjectLayout(
-        named = "Application Help",
-        cssClassFa = "fa-regular fa-circle-question")
-@RequiredArgsConstructor(onConstructor_ = {@Inject})
-public class DefaultHelpVm implements ViewModel {
-
-    private final DocumentationServiceDefault documentationServiceDefault;
-    private final String title;
-
-    @ObjectSupport
-    public String title() {
-        return title;
-    }
-
-    @Property
-    @PropertyLayout(labelPosition = LabelPosition.NONE)
-    @Getter(lazy = true)
-    private final Markup helpContent = new Markup(documentationServiceDefault.getDocumentationAsHtml());
-
-    @Override
-    public String viewModelMemento() {
-        return title;
-    }
-}
diff --git a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/help/DefaultHelpVm.layout.xml b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/help/DefaultHelpVm.layout.xml
deleted file mode 100644
index 1c25442981..0000000000
--- a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/help/DefaultHelpVm.layout.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<!-- 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. -->
-<bs:grid
-    xmlns:cpt="http://causeway.apache.org/applib/layout/component"
-    xmlns:bs="http://causeway.apache.org/applib/layout/grid/bootstrap3"
-    xmlns:lnk="http://causeway.apache.org/applib/layout/links"
-    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-    xsi:schemaLocation="http://causeway.apache.org/applib/layout/component http://causeway.apache.org/applib/layout/component/component.xsd http://causeway.apache.org/applib/layout/links http://causeway.apache.org/applib/layout/links/links.xsd http://causeway.apache.org/applib/layout/grid/bootstrap3 http://causeway.apache.org/applib/layout/grid/bootstrap3/bootstrap3.xsd">
-    <bs:row>
-        <bs:col span="12" unreferencedActions="true">
-            <cpt:domainObject />
-        </bs:col>
-    </bs:row>
-    <bs:row>
-        <bs:col span="12">
-            <bs:row>
-                <bs:col span="12">
-                    <cpt:fieldSet name=""
-                        unreferencedProperties="true"
-                        id="details" />
-                </bs:col>
-            </bs:row>
-        </bs:col>
-        <bs:col span="8">
-            <bs:tabGroup unreferencedCollections="true" />
-        </bs:col>
-    </bs:row>
-</bs:grid>
\ No newline at end of file
diff --git a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/helptree/HelpNodeVm-PAGE.svg b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/helptree/HelpNodeVm-PAGE.svg
new file mode 100644
index 0000000000..b788c7ff81
--- /dev/null
+++ b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/helptree/HelpNodeVm-PAGE.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
+     viewBox="0 0 58 58" style="enable-background:new 0 0 58 58;" xml:space="preserve">
+<polygon style="fill:#EDEADA;" points="51.5,14 37.5,0 6.5,0 6.5,58 51.5,58 "/>
+<g>
+	<path style="fill:#CEC9AE;" d="M16.5,23h25c0.552,0,1-0.447,1-1s-0.448-1-1-1h-25c-0.552,0-1,0.447-1,1S15.948,23,16.5,23z"/>
+	<path style="fill:#CEC9AE;" d="M16.5,15h10c0.552,0,1-0.447,1-1s-0.448-1-1-1h-10c-0.552,0-1,0.447-1,1S15.948,15,16.5,15z"/>
+	<path style="fill:#CEC9AE;" d="M41.5,29h-25c-0.552,0-1,0.447-1,1s0.448,1,1,1h25c0.552,0,1-0.447,1-1S42.052,29,41.5,29z"/>
+	<path style="fill:#CEC9AE;" d="M41.5,37h-25c-0.552,0-1,0.447-1,1s0.448,1,1,1h25c0.552,0,1-0.447,1-1S42.052,37,41.5,37z"/>
+	<path style="fill:#CEC9AE;" d="M41.5,45h-25c-0.552,0-1,0.447-1,1s0.448,1,1,1h25c0.552,0,1-0.447,1-1S42.052,45,41.5,45z"/>
+</g>
+<polygon style="fill:#CEC9AE;" points="37.5,0 37.5,14 51.5,14 "/>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/helptree/HelpNodeVm-TOPIC.svg b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/helptree/HelpNodeVm-TOPIC.svg
new file mode 100644
index 0000000000..5f1cdc96fd
--- /dev/null
+++ b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/helptree/HelpNodeVm-TOPIC.svg
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
+     viewBox="0 0 58 58" style="enable-background:new 0 0 58 58;" xml:space="preserve">
+<path style="fill:#EFCE4A;" d="M55.981,54.5H2.019C0.904,54.5,0,53.596,0,52.481V20.5h58v31.981C58,53.596,57.096,54.5,55.981,54.5z
+	"/>
+<path style="fill:#EBBA16;" d="M26.019,11.5V5.519C26.019,4.404,25.115,3.5,24,3.5H2.019C0.904,3.5,0,4.404,0,5.519V10.5v10h58
+	v-6.981c0-1.115-0.904-2.019-2.019-2.019H26.019z"/>
+<g>
+	<path style="fill:#EB7937;" d="M18,32.5h14c0.552,0,1-0.447,1-1s-0.448-1-1-1H18c-0.552,0-1,0.447-1,1S17.448,32.5,18,32.5z"/>
+	<path style="fill:#EB7937;" d="M18,38.5h22c0.552,0,1-0.447,1-1s-0.448-1-1-1H18c-0.552,0-1,0.447-1,1S17.448,38.5,18,38.5z"/>
+	<path style="fill:#EB7937;" d="M40,42.5H18c-0.552,0-1,0.447-1,1s0.448,1,1,1h22c0.552,0,1-0.447,1-1S40.552,42.5,40,42.5z"/>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/helptree/HelpNodeVm.java b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/helptree/HelpNodeVm.java
new file mode 100644
index 0000000000..0e1f428900
--- /dev/null
+++ b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/helptree/HelpNodeVm.java
@@ -0,0 +1,127 @@
+/*
+ *  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.
+ */
+package org.apache.causeway.extensions.docgen.helptree;
+
+import java.util.Optional;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.apache.causeway.applib.ViewModel;
+import org.apache.causeway.applib.annotation.DomainObject;
+import org.apache.causeway.applib.annotation.DomainObjectLayout;
+import org.apache.causeway.applib.annotation.LabelPosition;
+import org.apache.causeway.applib.annotation.Nature;
+import org.apache.causeway.applib.annotation.Navigable;
+import org.apache.causeway.applib.annotation.ObjectSupport;
+import org.apache.causeway.applib.annotation.Programmatic;
+import org.apache.causeway.applib.annotation.Property;
+import org.apache.causeway.applib.annotation.PropertyLayout;
+import org.apache.causeway.applib.annotation.Where;
+import org.apache.causeway.applib.graph.tree.TreeNode;
+import org.apache.causeway.applib.graph.tree.TreePath;
+import org.apache.causeway.extensions.docgen.CausewayModuleExtDocgen;
+import org.apache.causeway.extensions.docgen.applib.HelpNode;
+import org.apache.causeway.extensions.docgen.applib.HelpNode.HelpTopic;
+import org.apache.causeway.valuetypes.asciidoc.applib.value.AsciiDoc;
+
+import lombok.Getter;
+import lombok.val;
+import lombok.extern.log4j.Log4j2;
+
+@Named(CausewayModuleExtDocgen.NAMESPACE + ".HelpNodeVm")
+@DomainObject(
+        nature=Nature.VIEW_MODEL)
+@DomainObjectLayout(
+        named = "Application Help")
+@Log4j2
+public class HelpNodeVm implements ViewModel {
+
+    public final static String PATH_DELIMITER = "|"; // required to be URL-safe
+
+    public static HelpNodeVm forRootTopic(final HelpTopic rootTopic) {
+        return new HelpNodeVm(rootTopic, rootTopic);
+    }
+
+    @Getter @Programmatic
+    private final HelpTopic rootTopic;
+
+    @Getter @Programmatic
+    private final HelpNode helpNode;
+
+    @Inject
+    public HelpNodeVm(final HelpTopic rootTopic, final String rootPathMemento) {
+        this(rootTopic, TreePath.parse(rootPathMemento, PATH_DELIMITER));
+    }
+
+    HelpNodeVm(final HelpTopic rootTopic, final TreePath treePath) {
+        this(rootTopic, rootTopic
+                .lookup(treePath)
+                .orElseGet(()->{
+                    log.warn("could not resolve help node {}", treePath);
+                    return rootTopic;
+                }));
+    }
+
+    HelpNodeVm(final HelpTopic rootTopic, final HelpNode helpNode) {
+        this.rootTopic = rootTopic;
+        this.helpNode = helpNode;
+    }
+
+    @ObjectSupport public String title() {
+        return helpNode.getTitle();
+    }
+
+    @ObjectSupport public String iconName() {
+        val type = helpNode.getHelpNodeType();
+        return type!=null ? type.name() : "";
+    }
+
+    @Property
+    @PropertyLayout(labelPosition = LabelPosition.NONE, fieldSetId = "tree", sequence = "1")
+    public TreeNode<HelpNodeVm> getTree() {
+        final TreeNode<HelpNodeVm> tree = TreeNode.lazy(HelpNodeVm.forRootTopic(rootTopic), HelpTreeAdapter.class);
+
+        // expand the current node
+        helpNode.getPath().streamUpTheHierarchyStartingAtSelf()
+            .forEach(tree::expand);
+
+        return tree;
+    }
+
+    @Property
+    @PropertyLayout(navigable=Navigable.PARENT, hidden=Where.EVERYWHERE, fieldSetId = "detail", sequence = "1")
+    public HelpNodeVm getParent() {
+        return Optional.ofNullable(helpNode.getPath().getParentIfAny())
+                .map(parentPath->new HelpNodeVm(rootTopic, parentPath.toString()))
+                .orElse(null);
+    }
+
+    @Property
+    @PropertyLayout(labelPosition = LabelPosition.NONE, fieldSetId = "detail", sequence = "2")
+    @Getter(lazy = true)
+    private final AsciiDoc helpContent = helpNode.getContent();
+
+
+    @Override
+    public String viewModelMemento() {
+        return helpNode.getPath().stringify(PATH_DELIMITER);
+    }
+
+}
diff --git a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/helptree/HelpNodeVm.layout.xml b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/helptree/HelpNodeVm.layout.xml
new file mode 100644
index 0000000000..7253e3f50a
--- /dev/null
+++ b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/helptree/HelpNodeVm.layout.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!-- 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. -->
+<bs3:grid
+        xsi:schemaLocation="http://causeway.apache.org/applib/layout/component http://causeway.apache.org/applib/layout/component/component.xsd   http://causeway.apache.org/applib/layout/grid/bootstrap3 http://causeway.apache.org/applib/layout/grid/bootstrap3/bootstrap3.xsd"
+        xmlns:bs3="http://causeway.apache.org/applib/layout/grid/bootstrap3"
+        xmlns:cpt="http://causeway.apache.org/applib/layout/component"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+
+    <bs3:row>
+        <bs3:col span="10" unreferencedActions="true">
+            <cpt:domainObject />
+        </bs3:col>
+        <bs3:col span="2">
+            <cpt:fieldSet name="" id="sources" />
+        </bs3:col>
+    </bs3:row>
+
+    <bs3:row>
+        <bs3:col span="3">
+            <cpt:fieldSet name="Index" id="tree"/>
+        </bs3:col>
+        <bs3:col span="9">
+            <cpt:fieldSet name="" id="detail"/>
+            <cpt:fieldSet name="Other" id="other" unreferencedProperties="true"/>
+        </bs3:col>
+    </bs3:row>
+    <bs3:row>
+        <bs3:col span="12" unreferencedCollections="true"/>
+    </bs3:row>
+</bs3:grid>
diff --git a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/CausewayModuleExtDocgen.java b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/helptree/HelpTreeAdapter.java
similarity index 53%
copy from extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/CausewayModuleExtDocgen.java
copy to extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/helptree/HelpTreeAdapter.java
index e3fb969bce..733035ef96 100644
--- a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/CausewayModuleExtDocgen.java
+++ b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/helptree/HelpTreeAdapter.java
@@ -16,25 +16,29 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.causeway.extensions.docgen;
+package org.apache.causeway.extensions.docgen.helptree;
 
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Import;
+import java.util.Optional;
+import java.util.stream.Stream;
 
-import org.apache.causeway.extensions.docgen.help.DocumentationServiceDefault;
-import org.apache.causeway.extensions.docgen.menu.DocumentationMenu;
+import org.apache.causeway.applib.graph.tree.TreeAdapter;
 
-/**
- * Adds the {@link DocumentationMenu} with its auto-configured menu entries.
- * @since 2.0 {@index}
- */
-@Configuration
-@Import({
-    DocumentationMenu.class,
-    DocumentationServiceDefault.class,
-})
-public class CausewayModuleExtDocgen {
+public class HelpTreeAdapter implements TreeAdapter<HelpNodeVm> {
+
+    @Override
+    public Optional<HelpNodeVm> parentOf(final HelpNodeVm value) {
+        return Optional.ofNullable(value.getParent());
+    }
+
+    @Override
+    public int childCountOf(final HelpNodeVm value) {
+        return value.getHelpNode().childCount();
+    }
 
-    public static final String NAMESPACE = "causeway.ext.docgen";
+    @Override
+    public Stream<HelpNodeVm> childrenOf(final HelpNodeVm value) {
+        return value.getHelpNode().streamChildNodes()
+                .map(childNode->new HelpNodeVm(value.getRootTopic(), childNode));
+    }
 
 }
diff --git a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/menu/DocumentationMenu.java b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/menu/DocumentationMenu.java
index 83b92a5719..58517941b7 100644
--- a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/menu/DocumentationMenu.java
+++ b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/menu/DocumentationMenu.java
@@ -31,16 +31,17 @@ import org.apache.causeway.applib.annotation.NatureOfService;
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.annotation.SemanticsOf;
 import org.apache.causeway.extensions.docgen.CausewayModuleExtDocgen;
-import org.apache.causeway.extensions.docgen.help.DocumentationService;
+import org.apache.causeway.extensions.docgen.applib.HelpNode.HelpTopic;
+import org.apache.causeway.extensions.docgen.helptree.HelpNodeVm;
 
 import lombok.RequiredArgsConstructor;
 
 /**
- * Provides entries for a <i>Documentation</i> sub-menu section utilizing the {@link DocumentationService}.
+ * Provides entries for a <i>Documentation</i> sub-menu section.
  * <p>
- * Currently there is only one, namely (<i>help</i>).
+ * Currently there is only one, namely (<i>help</i>), utilizing the {@link HelpTopic}.
  *
- * @see DocumentationService
+ * @see HelpTopic
  * @since 2.x {@index}
  */
 @Named(CausewayModuleExtDocgen.NAMESPACE + ".DocumentationMenu")
@@ -54,8 +55,9 @@ public class DocumentationMenu {
 
     public static abstract class ActionDomainEvent<T> extends CausewayModuleApplib.ActionDomainEvent<T> {}
 
-    private final DocumentationService documentationService;
+    private final HelpTopic rootHelpTopic;
 
+    /** Returns a view-model that represents the application's primary help page. */
     @Action(
             domainEvent = help.ActionDomainEvent.class,
             semantics = SemanticsOf.NON_IDEMPOTENT //disable client-side caching
@@ -68,8 +70,8 @@ public class DocumentationMenu {
 
         public class ActionDomainEvent extends DocumentationMenu.ActionDomainEvent<help> {}
 
-        @MemberSupport public Object act() {
-            return documentationService.getHelp();
+        @MemberSupport public HelpNodeVm act() {
+            return HelpNodeVm.forRootTopic(rootHelpTopic);
         }
 
     }
diff --git a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/help/DocumentationServiceDefault.java b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/topics/welcome/WelcomeHelpPage.java
similarity index 93%
rename from extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/help/DocumentationServiceDefault.java
rename to extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/topics/welcome/WelcomeHelpPage.java
index 843366d171..b1d49e3e11 100644
--- a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/help/DocumentationServiceDefault.java
+++ b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/topics/welcome/WelcomeHelpPage.java
@@ -16,22 +16,18 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.causeway.extensions.docgen.help;
+package org.apache.causeway.extensions.docgen.topics.welcome;
 
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
 
-import javax.annotation.Priority;
 import javax.inject.Inject;
 import javax.inject.Named;
 
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.stereotype.Service;
+import org.springframework.stereotype.Component;
 
-import org.apache.causeway.applib.ViewModel;
 import org.apache.causeway.applib.annotation.DomainObject;
-import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.layout.component.ActionLayoutData;
 import org.apache.causeway.applib.layout.component.CollectionLayoutData;
 import org.apache.causeway.applib.layout.component.FieldSet;
@@ -54,29 +50,39 @@ import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
 import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
 import org.apache.causeway.extensions.docgen.CausewayModuleExtDocgen;
+import org.apache.causeway.extensions.docgen.applib.HelpPage;
+import org.apache.causeway.valuetypes.asciidoc.applib.value.AsciiDoc;
 
-import lombok.Getter;
 import lombok.RequiredArgsConstructor;
 
-@Service
-@Named(CausewayModuleExtDocgen.NAMESPACE + ".DocumentationServiceDefault")
-@Priority(PriorityPrecedence.MIDPOINT)
-@Qualifier("Default")
+@Component
+@Named(CausewayModuleExtDocgen.NAMESPACE + ".WelcomeHelpPage")
 @RequiredArgsConstructor(onConstructor_ = {@Inject})
-//@Log4j2
-public class DocumentationServiceDefault implements DocumentationService {
+public class WelcomeHelpPage implements HelpPage {
 
     private final SpecificationLoader specificationLoader;
     private final MenuBarsService menuBarsService;
     private final HomePageResolverService homePageResolverService;
     private final TranslationService translationService;
 
-    @Getter(onMethod_={@Override}, lazy = true)
-    private final ViewModel help = new DefaultHelpVm(this, "Application Help");
+    @Override
+    public String getTitle() {
+        return "Welcome";
+    }
+
+    @Override
+    public AsciiDoc getContent() {
+        // uses a HTML passthrough block (https://docs.asciidoctor.org/asciidoc/latest/pass/pass-block/)
+        return AsciiDoc.valueOf(
+                "== Welcome\n\n"
+                + "++++\n"
+                + getDocumentationAsHtml()
+                + "++++\n");
+    }
 
     // -- HELPER
 
-    String getDocumentationAsHtml() {
+    private String getDocumentationAsHtml() {
         final StringBuilder html = new StringBuilder();
         Object homePage = homePageResolverService.getHomePage();
         if (homePage != null) {
@@ -236,7 +242,8 @@ public class DocumentationServiceDefault implements DocumentationService {
                                                 html.append(String.format("<li><b>%s</b>: %s.",
                                                         member.getCanonicalFriendlyName(),
                                                         describedAs));
-                                                if (member.getElementType().getLogicalType().getCorrespondingClass().isAnnotationPresent(DomainObject.class)) {
+                                                if (member.getElementType().getLogicalType().getCorrespondingClass()
+                                                        .isAnnotationPresent(DomainObject.class)) {
                                                     html.append(String.format(" <i> See: <a href='#%s'>%s</a></i>",
                                                             member.getElementType().getLogicalTypeName(),
                                                             member.getElementType().getSingularName()));
@@ -271,4 +278,6 @@ public class DocumentationServiceDefault implements DocumentationService {
                 .orElse(null);
     }
 
+
 }
+