Address bar featuring http://www (start of a URL)

AEM Content Fragments and URL Rewriting

Saulo Venancio, June 11, 2021

Have you ever wondered if you are using all the capabilities of AEM? Perhaps you are happy implementing basic components and templates. If that is enough for your application you can stop reading right here. But, if you're looking to do more, you have come to the right place, so continue reading!

AEM has evolved a lot in recent years. The “glorious” days of JSP development and jQuery alone are gone (if you are a developer you have your scars from that time). Here at 3|SHARE, we adapt to the changing times to provide our clients with the latest and greatest solutions.

AEM Content Fragments are a great example of this evolution, and the main topic of this article. In a recent project, we capitalized on this feature creating Content  Fragments that enable authors to create Authors Information, Calendar Events and User Walkthroughs.

AEM ships with a Content Services JSON exporter, which can be used by the UI to render content from AEM. But in our case, for the Calendar Event mentioned, there was one feature missing in the out-of-the-box content fragment exporter.

Although AEM allows you to set up URLs in the content fragment, the JSON exporter doesn’t do the URL rewriting for you.

Why do I need URL Rewriting for Content Fragments?

URL rewriting is a way of implementing URL mapping or routing within a web application. AEM websites usually have a base content path like /content/mywebsite/country_language. This is not optimal for a live website as it doesn't add any value in terms of SEO or for the users. In order to have better user-friendly URLs you'll need a URL Rewriting mechanism. Utilizing this, your website's URL could look like:  http://www.mysite.com/country_language.html.

However with the out-of-the-box JSON exporter from the AEM Content Services this URL rewriting doesn’t happen by default when the URL is a property of a Content Fragment. 

Building the solution

The most common solution for this issue would be to build a custom content fragment model based on the one shipped with AEM. Now imagine you have multiple types of content fragments exposed. You would need to create a new custom content fragment model for each type just to solve the issue resulting in many models and probably some duplicated code.

To solve this on our project, we decided to build a more solid and reusable solution leveraging the Sling Filters services. With Sling Filters, you can select the request's method, extensions and selectors to which the filter should be applied. By using these options you can explicitly target the Content Fragment JSON exporter service.

/* ... */
import org.apache.sling.engine.EngineConstants;
import org.osgi.service.component.annotations.ConfigurationPolicy;
/* ... */

@Component(
        service = Filter.class,
        immediate = true,
        configurationPolicy = ConfigurationPolicy.REQUIRE,
        property = {
                EngineConstants.SLING_FILTER_SCOPE + "=" + EngineConstants.FILTER_SCOPE_REQUEST,
                EngineConstants.SLING_FILTER_METHODS + "=GET",
                EngineConstants.SLING_FILTER_SELECTORS + "=model",
                EngineConstants.SLING_FILTER_EXTENSIONS + "=json"
        }
)
// ...

With this definition, our service class catches all the requests that are meant to be managed by the AEM JSON exporter. However we wanted to limit the scope of our custom filter so as to avoid an extra workload when it's not needed. To do this we included a couple of OSGi properties in order to restrict the filter for specific resource types and / or disable the filter when it's not needed.

@ObjectClassDefinition(name = "Content Fragment List Link Rewriter Filter",
            description = "Configuration for filter to extend Content Fragment List functionality to rewrite links")
    public @interface Config {
        @AttributeDefinition(name = "Enabled",
                description = "If this filter should not be active, rather try to delete this config. " +
                        "Only in cases where this cannot be easily accomplished uncheck this option to disable the filter.")
        boolean enabled() default false;

        @AttributeDefinition(name = "Resource Types",
                description = "Resource Types to be evaluated by the filter.")
        String[] resourceTypes();
    }

Having that set, we could incorporate the magic. A Filter can manipulate the content of the response. When exported via JSON, AEM content fragment models use a specific structure. We need to know that basic structure to manipulate it using the filter. Let’s see one example:

# http://localhost:4502/content/websites/my-site/global/en/services/calendar/jcr:content/root/responsivegrid/contentfragment.model.json
	
{
   "description":"",
   "title":"Teste Dezembro",
   "model":"my-site/models/calendar-event",
   ":items":{
      
   },
   ":type":"my-site/components/content/modules/contentfragment",
   ":itemsOrder":[
      
   ],
   "elements":{
      "description":{
         "value":"Teste dezembro",
         "title":"Event Description",
         "dataType":"string",
         ":type":"string",
         "multiValue":false
      },
      "type":{
         "value":"Blue Order Opening",
         "title":"Event Type",
         "dataType":"string",
         ":type":"string",
         "multiValue":false
      },
      "date":{
         "value":1608225300000,
         "title":"Event Date",
         "dataType":"calendar",
         ":type":"calendar",
         "multiValue":false
      },
      "link":{
         "value":"/content/websites/my-site/global/en",
         "title":"Event Link",
         "dataType":"string",
         ":type":"string",
         "multiValue":false
      }
   },
   "elementsOrder":[
      "description",
      "type",
      "date",
      "link"
   ]
}

The link property of the JSON object is the one that has our link set. Its value property needs rewriting.

"link":{
   "value":"/content/websites/my-site/global/en",
   "title":"Event Link",
   "dataType":"string",
   ":type":"string",
   "multiValue":false,
   "url":"/content/websites/my-site/global/en.html"
}

In the Filter, we have access to this content in raw format. To manipulate that we needed to convert the raw content (text) to JSON in our Java Class and traverse the whole json content to look for objects that have the link object and do our rewriting there.


package com.mysite.commons.utils.fuctions.impl;

import org.apache.sling.api.resource.ResourceResolver;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.mysite.commons.utils.fuctions.RewriterContentFragmentLink;

public class RewriterContentFragmentLinkImpl implements RewriterContentFragmentLink {

    private ResourceResolver resourceResolver;

    public RewriterContentFragmentLinkImpl(ResourceResolver resourceResolver) {
        this.resourceResolver = resourceResolver;
    }

    @Override
    public void rewriteLink(JsonElement jsonElement) {
        JsonObject elements = jsonElement.getAsJsonObject();
        JsonObject linkObject = elements.getAsJsonObject("link");
        if (linkObject != null) {
            JsonElement linkValue = linkObject.get("value");
            if (linkValue != null) {
                String link = linkValue.getAsString();
                linkObject.addProperty("url", resourceResolver.map(link));
            }
        }
    }
}

The rewriteLink method does the rewriting. You can see that we look for the link object and then get the value (name used by AEM) and rewrite it. To avoid problems with other out of the box usages of the content fragment exported JSON, we recommend adding a new property to the link object which called url.

We needed to make this work with a list of Content Fragment JSONs as well, in which case the JSON produced has a different structure. So, we worked on traversing the JSON tree and looking at where the link element is in the whole JSON structure. Recursive iteration is helpful there.

We created a JsonUtils class with this recursive function. It involves analysing the JSON structure, looking for our link objects in the whole structure and executing our functional class when that is found.

package com.mysite.commons.utils;

import java.util.Map;
import java.util.Set;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.mysite.commons.utils.fuctions.RewriterContentFragmentLink;

public final class JsonUtils {

    private JsonUtils() {}

    public static void applyFunctionByAttribute(JsonElement element, RewriterContentFragmentLink rewriter, String attributeName) {
        if (element instanceof JsonObject) {
            Set<Map.Entry<String, JsonElement>> entries = element.getAsJsonObject().entrySet();
            for (Map.Entry<String, JsonElement> entry : entries) {
                if (entry.getKey().equals(attributeName)) {
                    // apply function
                    rewriter.rewriteLink(entry.getValue());
                }
                else {
                    if (entry.getValue() instanceof JsonObject || entry.getValue() instanceof JsonArray) {
                        applyFunctionByAttribute(entry.getValue(), rewriter, attributeName);
                    }
                    else {
                        continue;
                    }
                }
            }
        }
        else if (element instanceof JsonArray) {
            JsonArray asJsonArray = element.getAsJsonArray();
            asJsonArray.forEach((object) -> {
                if (object instanceof JsonObject || object instanceof JsonArray) {
                    applyFunctionByAttribute(object, rewriter, attributeName);
                }
                return;
            });
        }
        return;
    }
}

It is a 28 line function that can be summarized in one line text, but can save dozens of hours of coding for the same functionality. This can be used for Content Fragment Lists, Content Fragments and virtually any component that extends the content fragments or content fragment list from AEM and exports them as a model.

Conclusion

As we covered in this article, Content Fragments are very versatile and can be used in many different ways, but they are currently not processed by AEM's URL Rewriter and that can be a showstopper. By leveraging Sling Filters, you can solve this problem in a very elegant way, making Core Fragments even more powerful, and further enhancing the capabilities of AEM.

Here you can find an example of a working project that you can download, review and reuse for your own purposes, courtesy of 3|SHARE. ;)
Content Fragments Link Rewriter repository

If your AEM project is not using all AEM capabilities and you are ready to start, reach out to 3|SHARE. Our experienced developers and architects are focused on using AEM to its fullest potential.

Topics: Adobe Experience Manager, Development, AEM, Content Fragments

Saulo Venancio

Saulo is a Senior AEM Developer at 3|SHARE who also has AEM Developer Certification and AEM Technical Architect Certification. He enjoys working with the other great developers at 3|SHARE and building upon his own expertise by learning new technologies to solve problems. When he’s not busy with that, he enjoys cooking and studying natural health sciences like Chinese medicine.