AEM components documentation (an alternative way)

August 28, 2018    aem tools components documentation markdown


Intro

One of the core things of any product is proper documentation. Documentation simplifies quite a few things such as the onboarding of a new member, saves your time in case someone wants to understand functionality you have implemented and secures project knowledge if someone leaves your company, etc. When working with Adobe Experience Manager, components’ documentation is a crucial thing, because it is very important for Content Managers to have complete and up-to-date components’ documentation.

You can say that there is a specific user story for a specific component, therefore we may assume that we already have certain documentation, BUT we have several issues here:

  • This documentation is not maintained by the developer, therefore some important points might be missed
  • This documentation might be maintained on several pages and different systems:
    • Jira
    • Confluence
    • ….

Conclusion: sometimes it is really hard to find the required information. * If you do not want your developers to maintain such documentation in the code base, they might be not very happy about it.

Nevertheless for some people the problem described above is not a problem and a further attempt to solve the problem might be overengineered, so let’s now move to the next section.

Note: In case it is not a real problem for you, I honestly hope that your developers use the helpPath attribute :)

Idea

From the developer’s perspective the idea is easy peasy lemon squeezy. We have an AEM instance, so why do we need to store documentation somewhere else if we can store it as content in AEM?

For me it sounds really cool. But now you might be asking yourself the following question: Hmm, how should we store documentation in AEM?

Part 1

The first step is to have a description of a component’s features/settings. This can be easily done with a README.MD file for every component, which can be placed in the component’s folder and carefully maintained by the developers.

Pros: * Maintained by developers (who actually implement this component or modify it) * The file will be stored under version control system, so it is easy to track changes * Easy to check during the code review that basic documentation was updated * Markdown is supported by all version control system providers, therefore it will also be rendered in the repository web view * Markdown is widely used, so it can be exported to other systems like Confluence

Cons: * Not all developers are happy to write documentation * Only developers can modify their documentation - this might be a problem if you have new well-written documentation

Implementation

We will need a Servlet to render a README.MD file:

@Component(service = Servlet.class,
        property = {
                Constants.SERVICE_DESCRIPTION + "=Markdown documentation servlet",
                "sling.servlet.methods=" + HttpConstants.METHOD_GET,
                "sling.servlet.resourceTypes=" + "sling/servlet/default",
                "sling.servlet.selectors=markdowndoc",
                "sling.servlet.extensions=html"
        })
public class MarkdownDocServlet extends SlingSafeMethodsServlet {

    private static final String README_MD = "README.md";

    private static final RequestDispatcherOptions OPTIONS = new RequestDispatcherOptions();
    private static final String DOCUMENTATION_PAGE_RESOURCE_TYPE = "aem-documentation-extension/components/page/document-page";

    {
        OPTIONS.setReplaceSelectors(StringUtils.EMPTY); // to remove 'markdowndoc' selector
        OPTIONS.setForceResourceType(DOCUMENTATION_PAGE_RESOURCE_TYPE);
    }

    @Override
    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException {

        final Resource resource = request.getResource();

        final Resource child = resource.getChild(README_MD);

        if (child != null) {

            final RequestDispatcher requestDispatcher = request.getRequestDispatcher(resource, OPTIONS);

            if (requestDispatcher != null) {
                response.setContentType("text/html; charset=UTF-8");
                requestDispatcher.forward(request, response);
            }
        } else {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
        }
    }
}

Simple servlet to dispatch rendering to the special component –> aem-documentation-extension/components/page/document-page.

View component:

<!DOCTYPE html>
<html lang="en">
<head data-sly-use.clientlib="${'/libs/granite/sightly/templates/clientlib.html'}">
    <link data-sly-call="${clientLib.css @ categories='coralui3,granite.ui.coral.foundation,granite.ui.shell,aem-documentation-extension'}"
          data-sly-unwrap/>
    <meta charset="UTF-8">
    <title>Components' Documentation page</title>
</head>
<body class="coral--light">

<div class="coral-Shell">
    <div class="foundation-content coral-Shell-content" role="main"
         data-sly-use.model="com.github.andreishilov.ade.core.models.DocumentPageModel">
        <div class="foundation-content-current foundation-layout-util-maximized-alt">
            <div class="foundation-layout-panel">
                <div class="foundation-layout-panel-header">
                    <div class="granite-actionbar">
                        <div class="granite-actionbar-centerwrapper">
                            <div class="granite-actionbar-center">
                                <span class="granite-title" role="heading"
                                      aria-level="1">${model.title @  i18n, locale=request.locale}</span>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="documentation-page">
                    <div class="documentation-page__content">
                        ${model.htmlMarkup @ context='html'}
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

Sling Model:

@Model(adaptables = {Resource.class})
public class DocumentPageModel {

    private static final Logger LOGGER = LoggerFactory.getLogger(DocumentPageModel.class);

    @Self
    private Resource resource;

    @SlingObject
    private ResourceResolver resourceResolver;

    @OSGiService
    private ReadmeService readmeService;

    private String htmlMarkup;

    private String title;

    @PostConstruct
    public void init() {
        title = resource.getValueMap().get(JcrConstants.JCR_TITLE, String.class);
        htmlMarkup = readmeService.getReadmeContent(resource.getPath(), resourceResolver);
    }

    public String getHtmlMarkup() {
        return htmlMarkup;
    }

    public String getTitle() {
        return title;
    }
}

Service:

@Component(service = ReadmeService.class)
public class ReadmeServiceImpl implements ReadmeService {

    private static final Logger LOGGER = LoggerFactory.getLogger(ReadmeServiceImpl.class);

    private static final String README_MD = "README.md";

    @Override
    public String getReadmeContent(final String componentPath, final ResourceResolver resourceResolver) {

        final Resource componentResource = resourceResolver.getResource(componentPath);


        final Resource readMeResource = componentResource.getChild(README_MD);

        if (readMeResource == null) {
            LOGGER.error("Readme.md resource is null, should be not null, checked at MarkdownDocServlet.java. Resource path = [{}]",
                    componentResource.getPath());
            return null;
        }

        try {
            final Node readMeNode = readMeResource.adaptTo(Node.class);

            if (readMeNode == null) {
                LOGGER.error("Could not adapt resource to Node.class. Resource path = [{}]", readMeResource.getPath());
                return null;
            }

            final Node readMeJcrContentNode = readMeNode.getNode(JCR_CONTENT);

            final InputStream inputStream = readMeJcrContentNode.getProperty(JCR_DATA).getBinary().getStream();

            return MarkDownUtils.markdownToHtml(IOUtils.toString(inputStream, "UTF-8"));

        } catch (RepositoryException | IOException e) {
            LOGGER.error(e.getMessage(), e);
            return e.getMessage() + ". Please checkout logs";
        }
    }
}

Good, now let’s create a component and a README.MD file for it.

File

Add a proper helpPath attribute:

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0"
          xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
          xmlns:granite="http://www.adobe.com/jcr/granite/1.0"
          jcr:primaryType="nt:unstructured"
          jcr:title="Markdown"
          helpPath="/apps/aem-documentation-extension/components/par/markdown.markdowndoc.html"
          sling:resourceType="cq/gui/components/authoring/dialog">
    <content>
    </content>
</jcr:root>

The full component can be found here

Awesome! It’s time for a demo!

Dialog
Documentation page

Part 2

Besides the component’s ‘How-To’ described in Part 1 it would be really useful to have a demo of this component with all available configurations.

I personally do not want to discuss a tree structure or where to place it, ie. I can only suggest what I would prefer. You then can choose any structure you want or the structure, that better suits your implementation.

The main idea is that we can finally use Content fragments !!!!

Implementation

Content fragments’ functionality also supports markdown syntax, therefore we can write notes in the same way we write the component’s README.MD file. Besides that a Content fragment can have variations and in our case it is very convenient, because we can create one Content fragment per component and as many variations as our component has possible configurations.

Structure

Variations

Last but not least, we need a component to select a content fragment and an applicable variation.

The code can be found here

Configured dialog

Final rendering

Conclusions

All of the above is just an example of how you can extend components’ documentation.

Generally it all depends on your particular case and you should be cautious. From my point of view this approach only makes sense if you have relatively many complex components and the content manager teams change frequently.

If you have simple components and a team of 2-5 content managers, there is no point to add additional complexity.




comments powered by Disqus