Simple Markdown Documentation For Your AEM Components | Perficient Digital

Simple Markdown Documentation For Your AEM Components

Documentation Documentation Documentation. I believe documentation is one of the most important aspects of delivering a product, or in this case, an AEM component.

If you aren’t already, you should use the Granite UI widget fieldDescription property to add helpful tooltips to your dialog fields. You can also use the Granite Alert Widget for quick in-line authoring tips.

Here is an example:

Markdown Servlet

In some cases, you might need to add more documentation about the component, whether it’s technical or otherwise. For that, we will piggyback off Dan’s post and create a simple servlet using flexmark. That servlet will accept the file path from the request suffix and render that file as HTML for us to view.

Update: I found out that Andrei Shilov had a very similar idea and wrote about it here.

The Steps:

First, add the following two dependencies to your maven project:

<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.11.3</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>com.vladsch.flexmark</groupId>
    <artifactId>flexmark-osgi</artifactId>
    <version>0.34.56</version>
    <scope>provided</scope>
</dependency>

Then embed both of them as bundles in your content-package-maven-plugin like so:

<plugin>
    <groupId>com.day.jcr.vault</groupId>
    <artifactId>content-package-maven-plugin</artifactId>
    <extensions>true</extensions>
    <configuration>
        ....
        <embeddeds>
            ...
            <embedded>
                <groupId>org.jsoup</groupId>
                <artifactId>jsoup</artifactId>
                <target>/apps/aem-markdown/install</target> <!-- Change this to your project install path -->
            </embedded>
            <embedded>
                <groupId>com.vladsch.flexmark</groupId>
                <artifactId>flexmark-osgi</artifactId>
                <target>/apps/aem-markdown/install</target> <!-- Change this to your project install path -->
            </embedded>
        </embeddeds>
       ...
    </configuration>
</plugin>

You can also embed them using the maven-bundle-plugin <Embed-Dependency> as an alternative method.

Then add the following servlet code:

import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.options.MutableDataSet;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Optional;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import org.apache.commons.io.IOUtils;
import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.commons.JcrUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.request.RequestPathInfo;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component(
    service = Servlet.class,
    immediate = true,
    property = {
      "service.description=Markdown Servlet",
      "service.vendor=Ahmed Musallam",
      "sling.servlet.paths=/bin/markdown",
      "sling.servlet.extensions=html",
      "sling.servlet.methods=GET"
    })
public class MarkdownServlet extends SlingSafeMethodsServlet {

  private static final Logger log = LoggerFactory.getLogger(MarkdownServlet.class);
  private static final MutableDataSet options = new MutableDataSet();

  /*
   * both Parser and Renderer are thread-safe according to:
   * https://github.com/vsch/flexmark-java/blob/master/flexmark-util/src/main/java/com/vladsch/flexmark/util/IParse.java
   * https://github.com/vsch/flexmark-java/issues/111
   */
  private static final Parser parser = Parser.builder(options).build();
  private static final HtmlRenderer renderer = HtmlRenderer.builder(options).build();

  @Override
  protected void doGet(SlingHttpServletRequest slingRequest, SlingHttpServletResponse slingResponse)
      throws ServletException, IOException {

    // The markdown file path is the suffix of the request
    Resource mdFileResource =
        Optional.ofNullable(slingRequest)
            .map(SlingHttpServletRequest::getRequestPathInfo)
            .map(RequestPathInfo::getSuffixResource)
            .filter(resource -> JcrConstants.NT_FILE.equals(resource.getResourceType()))
            .orElse(null);

    if (mdFileResource == null) { // does not exist

      String path = Optional.ofNullable(slingRequest)
          .map(SlingHttpServletRequest::getRequestPathInfo)
          .map(RequestPathInfo::getSuffix)
          .orElse("");

      slingResponse.sendError(404, "Markdown File does not exits at: "+ path);
    } else { // does exist
      try {
        slingResponse.setContentType("text/html; charset=utf-8");
        InputStream in = JcrUtils.readFile(mdFileResource.adaptTo(Node.class));
        String markdownString = IOUtils.toString(in, Charset.defaultCharset());
        String renderedHtml =  renderer.render(parser.parse(markdownString));
        slingResponse.getWriter().write(renderedHtml);
      } catch (RepositoryException e) {
        throw new ServletException("I've wrapped the real error in a Servlet Exception. Keep scrolling down...", e);
      }
    }
  }

}

This servlet binds to the path /bin/markdown.html and uses the suffix as the path to the markdown file. For example, if my markdown file for the blockquote component was at path:/apps/my-com/components/general/blockquote/README.mdthen I could get the HTML version of it by requesting:/bin/markdown.html/apps/my-com/components/general/blockquote/README.md

Example Component with hepPath

You can use the servlet above with the component dialog helpPath property.

Here is an example:

The sample README.md file will look like this:

# Blockquote
A component that displays blockquote.

## Features
- Feature One
- Feature Two

## Edit Dialog Properties

1. `./quote` (richtext) The quote text
2. `./qouteTreatment` (select) The treatment class. 

    Options: `quote`, `quote blue` and `quote single-quote` 

3. `./citation` (textfield) the citation text for the quote.

## Special Authoring Documentation
There are no special authoring documentation for this component

After the helpPath update, the dialog help icon will now point to the README.md path:

 

And when you click on it, the following page opens:


This is more than enough for general documentation. And since Markdown is parsed to semantic HTML, it is easy and clear to read.

Things I want to explore further:

While this is a good and simple way to render markdown with minimal effort, we can clean it up further. Let’s turn it into a project, and put it on Github to integrate with your existing project. To do this, add the following features:

  1. Add a Granite Widget to display documentation markdown in a new tab on component dialogs. You can even pull the default README.mdautomatically.
  2. Create a special page to display the markdown and add a CSS library for aesthetics.

That’s it! Simple markdown in AEM! Subscribe to get more helpful tips and tricks.

Leave a Reply