AEM Maven Project Including React SPA Part I

At the time of this writing, the AEM project archetype doesn’t allow you to create a single site project containing both the generic and react frontend modules. Typically, there will be a mix of SPA and non SPA content pages in a site project. This post documents how I was able to set this up for Adobe Experience Manager version 6.5. using aem-project-archetype-23.

Requirements

  • Java 1.8 or Java 11 (AEM 6.5+ only)
  • Apache Maven (3.3.9+)
  • Add the adobe-public profile to user Maven settings, e.g., .m2/settings.xml.
  • This tutorial is written with the assumption that an instance of AEM is setup for local development.

Setup

Create two projects with identical properties except for the frontendModule property value. They will need to be generated in different folders since their appId’s will be the name of the sub folder created by Maven. Skip ahead to Part 2

Target Project “A” (frontendModule=general)

mvn -B archetype:generate \
 -D archetypeGroupId=com.adobe.granite.archetypes \
 -D archetypeArtifactId=aem-project-archetype \
 -D archetypeVersion=23 \
 -D aemVersion=6.5.0 \
 -D appTitle="myproject" \
 -D appId="myproject" \
 -D groupId="com.myproject" \
 -D frontendModule=general \
 -D includeExamples=n

Source Project “B” (frontendModule=react)

mvn -B archetype:generate \
 -D archetypeGroupId=com.adobe.granite.archetypes \
 -D archetypeArtifactId=aem-project-archetype \
 -D archetypeVersion=23 \
 -D aemVersion=6.5.0 \
 -D appTitle="myproject" \
 -D appId="myproject" \
 -D groupId="com.myproject" \
 -D frontendModule=react \
 -D includeExamples=n

Compare & Merge

Compare the two project directories. I used KDiff3 for this.

Using KDiff to compare projects

Inspect each of the files and manually merge from the project in folder B into folder A. For example, here I’m using VS Code’s file comparison utility.

Visual Studio Code file compare

Merge these changes from project-B/myproject/pom.xml into project-A/myproject/pom.xml.

Additionally, add the ui.frontend.react module property node.

pom.xml
<modules>
    ...
    <module>ui.frontend</module>
    <module>ui.frontend.react</module>
    ...
</modules>

<properties>
	...
	<spa.project.core.version>1.0.6</spa.project.core.version>
</properties>

...

<!-- SPA Project Core (includes hierarchy page model) -->
<dependency>
    <groupId>com.adobe.aem</groupId>
    <artifactId>spa.project.core.all</artifactId>
    <type>zip</type>
    <version>${spa.project.core.version}</version>
</dependency>
<dependency>
    <groupId>com.adobe.aem</groupId>
    <artifactId>spa.project.core.core</artifactId>
    <version>${spa.project.core.version}</version>
</dependency>

Note that the ellipsis ... in the code snippet examples are not a part of the actual code and is there only to denote code that is being skipped and not applicable to the example. Verify all merges with actual file comparisons, these code examples are a guide.

all

Merge all/pom.xml from project-B into project-A.

all/pom.xml
<embeddeds>
	...
	<embedded>
	    <groupId>com.adobe.aem</groupId>
	    <artifactId>spa.project.core.core</artifactId>
	    <target>/apps/myproject-vendor-packages/install</target>
	</embedded>
	...
<embeddeds>
<subPackages>
    <subPackage>
        <groupId>com.adobe.aem</groupId>
        <artifactId>spa.project.core.all</artifactId>
        <filter>true</filter>
    </subPackage>
</subPackages>

...

<dependencies>
	...
	<!-- SPA Project Core (includes hierarchy page) -->
	<dependency>
	    <groupId>com.adobe.aem</groupId>
	    <artifactId>spa.project.core.all</artifactId>
	    <type>zip</type>
	</dependency>
	<dependency>
	    <groupId>com.adobe.aem</groupId>
	    <artifactId>spa.project.core.core</artifactId>
	</dependency>
	...
<dependencies>
ui.apps
  • apps
    • myproject
      • components
        • page-spa
          • .content.xml
          • customfooterlibs.html
          • customheaderlibs.html

Copy the page component from project-B and rename it page-spa in project-A. e.g., ui.apps/src/main/content/jcr_root/apps/myproject/components/page-spa

Update the title in .content.xml

page-spa/.content.xml
jcr:title="myproject Page SPA"

The clientlib-react folder linked in customheaderlibs.html will be created when we build the project after all of the merges are completed using the ui.frontend.react/pom.xml configuration.

Update the spa component sling:resourceSuperType to proxy the page-spa component added above.

spa/.content.xml
sling:resourceSuperType="myproject/components/page-spa"

Merge ui.apps/pom.xml from project-B into project-A bringing in the spa specific configuration items.

ui.apps/pom.xml
<embeddeds>
    ...

    <embedded>
        <groupId>com.adobe.aem</groupId>
        <artifactId>spa.project.core.core</artifactId>
        <target>/apps/myproject/install</target>
    </embedded>
</embeddeds>
<subPackages>
    <subPackage>
        <groupId>com.adobe.aem</groupId>
        <artifactId>spa.project.core.all</artifactId>
    </subPackage>
</subPackages>

...

<!-- SPA Project Core (includes hierarchy page) -->
<dependency>
    <groupId>com.adobe.aem</groupId>
    <artifactId>spa.project.core.all</artifactId>
    <type>zip</type>
</dependency>
<dependency>
    <groupId>com.adobe.aem</groupId>
    <artifactId>spa.project.core.core</artifactId>
</dependency>
ui.content
  • conf
    • myproject
      • settings
        • wcm
          • policies

Merge .content.xml from project-B into project-A for the policies folder.

policies/.content.xml
<page jcr:primaryType="nt:unstructured">
	...

    <policy_spa
        jcr:primaryType="nt:unstructured"
        jcr:title="myproject SPA Page Policy"
        sling:resourceType="wcm/core/components/policy/policy"
        clientlibs="[myproject.react]">
        <jcr:content jcr:primaryType="nt:unstructured"/>
    </policy_spa>
</page>
<spa jcr:primaryType="nt:unstructured">
    <default
        jcr:primaryType="nt:unstructured"
        jcr:title="myproject App Policy"
        sling:resourceType="wcm/core/components/policy/policy"
        clientlibs="[wcm.foundation.components.page.responsive]"
        isRoot="{Boolean}true"
        structureDepth="3"
    />
</spa>
  • conf
    • myproject
      • settings
        • wcm
          • templates
            • spa-app-template
            • spa-page-template
            • .content.xml

Copy the spa-app-template and spa-page-template folders and files from project-B into project-A.

Update the sling:resourceType in the spa-page-template/structure, content to use the page-spa component. e.g.,

spa-page-template/structure/.content.xml
<jcr:content
    cq:deviceGroups="[mobile/groups/responsive]"
    cq:template="/conf/myproject/settings/wcm/templates/spa-page-template"
    jcr:primaryType="cq:PageContent"
    sling:resourceType="myproject/components/page-spa">

	...
</jcr:content>

Update the spa-page-template title.

spa-page-template/.content.xml
jcr:title="myproject Page SPA Template"

Merge the templates folder content nodes from project-B into project-A.

templates/.content.xml
  <spa-app-template />
  <spa-page-template />
  ...
</jcr:root>

spa content page

Create a folder named spa in ui.content/src/main/content/jcr_root/content/myproject/us/en

  • content
    • myproject
      • us
        • en
          • spa

Copy the content/myproject/us/en/home folder and files from project-B and paste into the en folder of project-A. Rename the folder spa and update the .content.xml document title and sling:resourceType properties as needed. e.g.,

jcr:title="myproject SPA Page"
sling:resourceType="myproject/components/page-spa
spa/.content.xml
<?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"
    jcr:primaryType="cq:Page">
    <jcr:content
        cq:lastModified="{Date}2018-10-04T09:50:29.650+02:00"
        cq:lastModifiedBy="admin"
        cq:template="/conf/myproject/settings/wcm/templates/spa-page-template"
        jcr:primaryType="cq:PageContent"
        jcr:title="myproject SPA Page"
        sling:resourceType="myproject/components/page-spa">
        <root
            jcr:primaryType="nt:unstructured"
            sling:resourceType="wcm/foundation/components/responsivegrid">
            <responsivegrid
                jcr:primaryType="nt:unstructured"
                sling:resourceType="wcm/foundation/components/responsivegrid">
                <text
                    jcr:primaryType="nt:unstructured"
                    sling:resourceType="myproject/components/text"
                    text="&lt;p>Hello World!&lt;/p>"
                    textIsRich="true">
                    <cq:responsive jcr:primaryType="nt:unstructured"/>
                </text>
            </responsivegrid>
        </root>
    </jcr:content>
</jcr:root>
ui.frontend

Optionally update the name so it’s format is consistent with the other modules.

ui.frontend/pom.xml
<name>myproject - UI Frontend</name>
ui.frontend.react

Copy the ui.frontend folder from project-B and rename it ui.frontend.react in project-A. e.g., myproject/ui.frontend.react

ui.frontend.react/package.json

Update "react-scripts": "~3.3.0" to "react-scripts": "^3.4.0"

This fixes npm start task TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. - per https://github.com/facebook/create-react-app/issues/8499

In the ui.frontend.react/pom.xml, update the artifactId and name properties.

Then replace the frontend-maven-plugin with the exec-maven-plugin and its executions so there will not be two copies of node and npm installed into the project.

ui.frontend.react/pom.xml
...

<!-- ====================================================================== -->
<!-- P R O J E C T  D E S C R I P T I O N                                   -->
<!-- ====================================================================== -->
<artifactId>myproject.ui.frontend.react</artifactId>
<packaging>pom</packaging>
<name>myproject - UI Frontend.React</name>

<!-- ====================================================================== -->
<!-- B U I L D   D E F I N I T I O N                                        -->
<!-- ====================================================================== -->
<build>
    <sourceDirectory>src/main/content/jcr_root</sourceDirectory>
    <plugins>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>exec-maven-plugin</artifactId>
            <executions>
                <execution>
                    <id>npm install (initialize)</id>
                    <goals>
                        <goal>exec</goal>
                    </goals>
                    <phase>initialize</phase>
                    <configuration>
                        <executable>npm</executable>
                        <arguments>
                            <argument>install</argument>
                        </arguments>
                    </configuration>
                </execution>
                <execution>
                    <id>npm install (clean)</id>
                    <goals>
                        <goal>exec</goal>
                    </goals>
                    <phase>pre-clean</phase>
                    <configuration>
                        <executable>npm</executable>
                        <arguments>
                            <argument>install</argument>
                        </arguments>
                    </configuration>
                </execution>

                <!--
                <execution>
                    <id>npm run test (test)</id>
                    <goals>
                        <goal>exec</goal>
                    </goals>
                    <phase>test</phase>
                    <configuration>
                        <executable>npm</executable>
                        <arguments>
                            <argument>run</argument>
                            <argument>test</argument>
                        </arguments>
                    </configuration>
                </execution>-->

                <execution>
                    <id>npm run build (compile)</id>
                    <goals>
                        <goal>exec</goal>
                    </goals>
                    <phase>compile</phase>
                    <configuration>
                        <executable>npm</executable>
                        <arguments>
                            <argument>run</argument>
                            <argument>build</argument>
                        </arguments>
                    </configuration>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-assembly-plugin</artifactId>
            <version>3.1.1</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>single</goal>
                    </goals>
                    <configuration>
                        <descriptors>
                            <descriptor>assembly.xml</descriptor>
                        </descriptors>
                        <appendAssemblyId>false</appendAssemblyId>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

...

Install

For frontend builds - If you already have node and npm installed, switching to node version 10 will likely prevent errors due to an environment change.

cd project-A/myproject

mvn -PautoInstallPackage clean install

Source Code

Part 1 of 2 in the AEM Maven Project Including React series.

AEM Maven Project Including React SPA Part II

comments powered by Disqus