Generate URLs for Internal Links in Hippo

June 7th 2019 Hippo CMS Java

Hippo CMS treats internal links differently from external ones. Even when they are a part of an HTML field in a content document, they are stored as a reference to the referred content document and not as a regular hyperlink. When you're using the hst:html tag for rendering HTML fields in the template, it will take care of that automatically. But if you want to use the raw HTML string in some other way, the internal links inside it won't be valid:

<a href="sample-document">Internal link</a>
<a href="">External link</a>

In my case, I was linking to a content document which was published at /site/content/sample-document.html and /site/content/sample-document.html. This is correctly reflected in the URL when rendering the HTML using the hst:html tag:

<a href="/site/content/sample-document.html">Internal link</a>
<a href="">External link</a>

The sample-document value of the href attribute in the original HTML string is actually a unique name for the internal link in that specific HTML value. If you search for the content document in the Hippo Console, you will see that the node corresponding to that HTML field has a sub node with a matching name:

HTML field with an internal link in Hippo Console

This node points at the target of the internal link. To get the URL for it, the HstLinkCreator helper class needs to be used:

ContentDocument content = getHippoBeanForPath(documentPath, ContentDocument.class);
if (content != null) {
  HippoHtml hippoHtml = content.getContent();
  String htmlString = hippoHtml.getContent();

  HstRequestContext requestContext = request.getRequestContext();
  HstLinkCreator linkCreator = requestContext.getHstLinkCreator();

  // get child nodes corresponding to internal links
  List<Node> localLinkNodes = hippoHtml.getChildBeans(HippoFacetSelect.class)

  for (Node node : localLinkNodes) {
    try {
      // node name matches href attribute value
      String name = node.getName();
      // createAll returns links to all pages with the target content document
      List<HstLink> links = linkCreator.createAll(node, requestContext, true);
      if (!links.isEmpty()) {
        // generated URLs are ordered by their length, shortest first
        String url = links.get(0).toUrlForm(requestContext, true);
        htmlString = htmlString.replace("href=\"" + name + "\"", "href=\"" + url + "\"");
    } catch (RepositoryException e) {
      // skip node in case of failure

The code above replaces the invalid href attribute values with valid URLs:

<a href="http://localhost:9778/site/content/sample-document">Internal link</a>
<a href="">External link</a>

The last parameter of the createAll method determines whether the method returns URLs only for the mount corresponding to the current request or across all mounts on a host. Since I was using this code in a custom REST service hosted on its own mount, I had to include all mounts to get any URLs at all.

It's also worth mentioning that the built-in Content REST API also takes care of internal link generation:

  "id": "9caea793-42b1-4f3e-a8e5-d58f810ce2ef",
  "name": "another-sample-document",
  "displayName": "Another sample document",
  "type": "hippolocallinkurls:contentdocument",
  "locale": "en",
  "pubState": "published",
  "pubwfCreationDate": "2014-03-25T16:52:00.000+01:00",
  "pubwfLastModificationDate": "2019-05-23T09:37:27.527+02:00",
  "pubwfPublicationDate": "2019-05-23T09:37:43.364+02:00",
  "items": {
    "hippolocallinkurls:introduction": "Lorem Ipsum is simply dummy text of the printing and typesetting industry.",
    "hippolocallinkurls:title": "Another Lorem",
    "hippolocallinkurls:publicationdate": "2014-03-25T16:52:00.000+01:00",
    "hippolocallinkurls:content": {
      "type": "hippostd:html",
      "content": "<p><a data-hippo-link=\"sample-document\">Internal link</a></p>\n\n<p><a href=\"\">External link</a></p>",
      "links": {
        "sample-document": {
          "type": "local",
          "id": "64ab4648-0c20-40d2-9f18-d7a394f0334b",
          "url": "http://localhost:8080/site/api/documents/64ab4648-0c20-40d2-9f18-d7a394f0334b"

Instead of replacing them in the HTML string, it returns them in a separate links object. These can be mapped to the special data-hippo-link attribute of a elements. I only decided in favor of a custom REST service because of a rather limited querying support in the built-in API. If that's not a problem for your use case, the built-in APIs should work well enough for you.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Creative Commons License