AEM React SPA with Server Side Rendering using Adobe IO

Introduction

Single page applications are not only in fashion but in demand as well. But, when added with server side rendering it becomes the most desired technology of the current time. It is not easy to get started with server side rendering as it includes some complexities, limitations and difficulties. No worries, with this blog you can understand everything you need to know to implement AEM application with React and SSR.

Getting Started

All you need is below software in order to kick off a sample project. I assume Java and Node (v16) is installed already in your system.

  1. AEM SDK
  2. Maven
  3. Docker

Generate Project Code

Set up AEM author and publish server as well as docker for dispatcher related stuff. Now lets create new project using below archetype command:

mvn -B org.apache.maven.plugins:maven-archetype-plugin:3.2.1:generate -D archetypeGroupId=com.adobe.aem -D archetypeArtifactId=aem-project-archetype -D archetypeVersion=42 -D appTitle=”SPA Starter Project” -D appId=”spa-starter-project” -D artifactId=”spa-starter.react” -D groupId=”com.adobe.aem.spa.starter” -D frontendModule=”react” -D aemVersion=”cloud” -D enableSSR=y

Always use latest archtype version to generate new project, enableSSR=y is required in order to generate project SSR enabled. This command will generate starter project code just confirm BUILD SUCCESS message. This code will need few modifications which will be described later in this blog.

Create an Adobe IO project

Created new Adobe IO project on developer console (https://developer.adobe.com/) using App builder template with I/O runtime

After that all you need is NAMESPACE and AUTH keys to be used in further deployment steps. I will recommend to install openwsk for debugging, and checking logs from server side response. To use wsk commands for your project you will need wsk configuration like below.

Download the credentials and copy values to wsk configuration file (c:\users\nitin\.wskprops)

to check wsk working properly type command wsk list and the output will be like this.

Final modifications to the OOTB code

This is most critical step. as it connects AEM deployment along with adobe io ssr action deployment. Below modifications are required in order to use mvn clean install -PautoInstallPackage to deploy code to local AEM server and adobe io ssr action to adobe io server.

  1. Update OSGi configuration (com.adobe.cq.remote.content.renderer.impl.factory.ConfigurationFactoryImpl~spa-starter-react.cfg.json) with generated NAMESPACE and AUTH values this change can be done to local OSGi console later as well but this makes easy to deploy along with code.

2. Update POM file with same credentials for the below two values

  • aio.runtime.namespace
  • aio.runtime.auth

3. Rename ssr actions common folder to zcommon

This step looks weird but this is a temporary fix by Adobe in order to generate SSR action without any error. Also make sure to rename all occurrences of this folder.

  • /actions/ssr/index.js
  • manifest.yml

All set, now you can hit maven build command and it should deploy adobe io action as well as complete code to your local AEM.

mvn clean install -PautoInstallPackage -PautoInstallPackagePublish

After that you can hit a page and check wsk logs to see server response. Make sure in console you see the ssr OOTB message hydrated react DOM.

wsk activation list

wsk activation get {activation-id}

Docker for local development

Using docker for local development makes it easy to debug and also helps to debug dispatcher configurations. It becomes very critical when we start implementing URL shortening for pages. Install docker for windows. (https://docs.docker.com/desktop/install/windows-install/)

Go to dispatcher tools folder which came with AEM SDK and to start dispatcher in docker run below command (make sure docker is running)

bin\docker_run C:\codes\spa-starter-react\dispatcher\src host.docker.internal:4503 8080
The above command uses path to your project dispatcher code and 4503 is port for your AEM publish server. Now port 8080 or you can 80 to make it more fun, also make changes to your host file in order to test custom domain. (C:\Windows\System32\drivers\etc\hosts) and add an entry something like

127.0.0.1 spa-starter.com

So now you can access publish pages with http://spa-starter.com/content/…..html (instead of localhost:8080/content/….html)

Make sure you have connected your AEM author and publish instances to achieve content published from author to publish. All you need to connect your local author to local publish replication:

In AEM author go to http://localhost:4502/etc/replication/agents.author/publish.html and do below two changes.

After that make sure to test the connection and it should say (Replication test succeeded).

Common Problems

  1. Request larger than allowed: This Happens when size of ssr json response go more than 1 MB and to fix it you can try to use compression or try to decrease level of depth in SPA root template.
  2. Page coming blank: This can occur when there is a mismatch in browser location and the path defined in JSON.
  3. URL shortening: This is most complex stuff to do with SPA and ssr as it has some really complex stuff to overlay. (next blog)

Invisible recaptcha to AEM

This post is about how to add invisible google recaptcha to your AEM site.

Get Started

First of all you need to register for keys

Go to: https://www.google.com/recaptcha/admin#list

keys

you will get secret key and site key that’s all we need from here. Select invisible and also a number of domains you can register for s single pair of keys.

Create recaptcha component

It depends on your requirement but I advise to include in form container.

Just create a recaptcha.jsp or html and include in form container so that it will be a part of form when you submit form data.

CaptchaHtmlYou should make site key configurable and get it here.

Below attribute will make recaptcha hidden if you remove it then checkbox will appear so thats upto you.

data-size="invisible"

Verify recaptcha response at server side

When you submit the form you will get the parameter g-recaptcha-response and all that is needed to check if captcha is valid.

public boolean isCaptchaValid(SlingHttpServletRequest request, SlingHttpServletResponse response) {
   try {
      final String postParameters = "secret=" + this.config.secretKey() + "&response="
            + getCaptchaResponse(request);
      final String message = proxyHttpConnectionProvider.doPostRequest(this.config.url() , postParameters);
      
      if (StringUtils.isEmpty(message)) {
         return false;
      }
      
      if(message.contains("true")) {
         return true;
      }
      return false;
   } catch (Exception e) {
      return false;
   }
}
private String getCaptchaResponse(SlingHttpServletRequest request) {
   Map<String, String[]> parameterMap = request.getParameterMap();
   for (Map.Entry<String, String[]> param : parameterMap.entrySet()) {
      if (param.getKey().equals("g-recaptcha-response")) {
         Object value = param.getValue();
         if (value instanceof String[]) {
            String[] valueArr = (String[]) value;
                   return valueArr[0];
         } else {
            return (String) value;
         }
      }
   }
   return null;
}

All you need to do is make a post request with below details. (This post request can differ as per your project Here I have used proxy settings.)

API Request

URL: https://www.google.com/recaptcha/api/siteverify

METHOD: POST

POST Parameter Description
secret Required. The shared key between your site and reCAPTCHA.
response Required. The user response token provided by reCAPTCHA, verifying the user on your site.
remoteip Optional. The user’s IP address.

API Response

The response is a JSON object:

{
  "success": true|false,
  "challenge_ts": timestamp,  // timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ)
  "hostname": string,         // the hostname of the site where the reCAPTCHA was solved
  "error-codes": [...]        // optional
}

AEM Best Code Practices

AEM related (Sightly, OSGi components, Sling models)

Sightly

  • Use of Sightly must be preferred over JSP
  • Avoid injecting CSS code in HTML Sightly templates
  • Use unsafe context sparingly
  • Use tag instead of data-sly-unwrap attribute (deprecated)
  • We should write a Sling Model to handle links handling
    • e.g. adding .html extension
  • Get rid of the custom TransformerFactory
  • Use data-sly-attribute.attributeName only when necessary (it is for removing the attribute from the element if the condition is false).
    Otherwise use normal html attributes.
  • ${variable != “”} can be replaced with ${variable}
  • Use component name when including the back-end file.
    • e.g. “data-sly-use.footer”
  • Don’t put images or other containers on the page when the data is empty (check with data-sly-test, it puts the container on the page only when the values exist).
  • If there is no logic around some property values (just a getter for the field), don’t access them through Java/Sling models. Use ${properties.propertyName} instead to get values directly from JCR when all you need is the unmodified value.

Sling Models

  • Prefer usage of Sling Models over Java Use API or Javascript Use API
  • Adapt from Request only when you really need, otherwise Resource
  • AEM components business logic implementation: avoid using WCMUsePojo. Use Sling Models instead.
  • Catch exceptions inside components business logic implementation
  • Use Sling models instead of WCMUsePojo
  • Use Optional as default injection strategy
    • @Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
    • Eliminates the need to put @Optional on each field separately
  • Getting the property values from JCR
    • Use @ValueMapValue annotation on a field with the same name as the property
    • If there is no logic around some property values (just a getter for the field), don’t access them through Java/Sling models
    • Use ${properties.propertyName} in HTML/Sightly instead to get values directly from JCR

Sling Servlets

  • Register Sling Servlets with resource type and not with resource paths

Commons

  • Avoid hard coding content structure

Node configNode = session.getNode(“/content/project/folder”);

  • Avoid hard coding of configurations
  • Avoid memory leaks by closing JCR Session and Sling Resource Resolver (Use Java 7 feature try-with-resource on ResourceResolver object)
  • Write operations on the repository should not be taken too easy
  • When doping large write repository operations, do a session.save() approx. every 1000 nodes
  • Avoid JCR queries whenever possible
  • Avoid unnecessary JCR observations, restrict as much as possible the scope of listeners
  • Avoid writing to the repository in JCR Observation or Sling event listeners. Collect the data and write outside of these handlers in a separate thread.
  • Do not use deprecated features, especially administrative ResourceResolver (use specific service user)
  • Avoid adding new dependencies, avoid embedding new compile jar dependencies
  • Always prefer working on Sling resources instead of JCR nodes/properties
  • Use Sling Resource and ValueMap API to read data from JCR
  • Implement proper abstractions (using adaptTo pattern) for String operations on paths in order to avoid excessive use of String operations

String[] segments = resource.getPath().split(“/”);
String settingsPath = “/” + StringUtils.join(segments,”/”,0,2) + “/settings/jcr:content”;
Resource settings = resourceResolver.get(settingsPath);
ValueMap vm = settings.adaptTo(ValueMap.class);
String language = vm.get(“language”);

vs

String language = “en”;
Settings settings = resource.adaptTo(Settings.class);
If (settings != null) {
language = settings.getLanguage();
}

  • Write adaptTo() calls with proper null checks or use ModelFactory instead!
    Log hygiene, avoid excessive use of LOGGER.error(); meaningful usage of error, warn, info, debug levels
  • Develop components with dispatcher in mind (caching and invalidation)
  • If we start using link rewriter then we need to use resourceResolver.map() for mapping links
  • error.log must be kept clean of errors/warnings
  • Use consistent naming conventions across all AEM components implementations (dialog input field name and descriptions, AEM content folder structure)
  • Use overrides as less possible, prefer extending a component with sling:resourceSuperType

OSGi components