Production deployment with an AEM Publish service

In this tutorial, you will set up a local environment to simulate content being distributed from an Author instance to a Publish instance. You will also generate production build of a React App configured to consume content from the AEM Publish environment using the GraphQL APIs. Along the way, you will learn how to effectively use environment variables and how to update the AEM CORS configurations.


This tutorial is part of a multi-part tutorial. It is assumed that the steps outlined in the previous parts have been completed.


Learn how to:

  • Understand the AEM Author and Publish architecture.
  • Learn best practices for managing environment variables.
  • Learn how to properly configure AEM for Cross-Origin resource sharing (CORS).

Author Publish deployment pattern

A full AEM environment is made up of an Author, Publish, and Dispatcher. The Author service is where internal users create, manage, and preview content. The Publish service is considered the “Live” environment and is typically what end users interact with. Content, after being edited and approved on the Author service, is distributed to the Publish service.

The most common deployment pattern with AEM headless applications is to have the production version of the application connect to an AEM Publish service.

High-Level Deployment Pattern

The diagram above depicts this common deployment pattern.

  1. A Content author uses the AEM author service to create, edit, and manage content.
  2. The Content author and other internal users can preview the content directly on the Author service. A Preview version of the application can be set up that connects to the Author service.
  3. Once content has been approved, it can be published to the AEM Publish service.
  4. End users interact with the Production version of the application. The Production application connects to the Publish service and use the GraphQL APIs to request and consume content.

The tutorial simulates the above deployment by adding an AEM Publish instance to the current setup. In previous chapters the React App acted as a preview by connecting directly to the Author instance. A production build of the React App will be deployed to a static Node.js server that connects to the new Publish instance.

In the end, three local servers will be running:

  • http://localhost:4502 - Author instance
  • http://localhost:4503 - Publish Instance
  • http://localhost:5000 - React App in production mode, connecting to the Publish instance.

Install AEM SDK - Publish mode

Currently we have a running instance of the SDK in Author mode. The SDK can also be started in Publish mode to simulate an AEM Publish environment.

A more detailed guide for setting up a local development environment can be found here.

  1. On your local file system, create a dedicated folder to install the Publish instance, i.e named ~/aem-sdk/publish.

  2. Copy the Quickstart jar file used for the Author instance in previous chapters and paste it in the publish directory. Alternatively navigate to the Software Distribution Portal and download the latest SDK and extract the Quickstart jar file.

  3. Rename the jar file to aem-publish-p4503.jar.

    The publish string specifies that the Quickstart jar starts in Publish mode. The p4503 specifies that the Quickstart server runs on port 4503.

  4. Open a new terminal window and navigate to the folder that contains the jar file. Install and start the AEM instance:

    $ cd ~/aem-sdk/publish
    $ java -jar aem-publish-p4503.jar
  5. Provide an admin password as admin. Any admin password is acceptable, however it is recommended to use the default for local development to avoid extra configurations.

  6. When the AEM instance has finished installing, a new browser window will open at http://localhost:4503/content.html

    It is expected to return a 404 Not Found page. This is a brand new AEM instance and no content has been installed.

Install sample content and GraphQL endpoints

Just like on the Author instance, the Publish instance needs to have the GraphQL endpoints enabled and needs sample content. Next, install the WKND Reference Site on the Publish instance.

  1. Download the latest compiled AEM Package for WKND Site:


    Make sure to download the standard version compatible with AEM as a Cloud Service and not the classic version.

  2. Log in to the Publish instance by navigating directly to: http://localhost:4503/libs/granite/core/content/login.html with the user name admin and password admin.

  3. Next, navigate to Package Manager at http://localhost:4503/crx/packmgr/index.jsp.

  4. Click Upload Package and choose the WKND package downloaded in the prior step. Click Install to install the package.

  5. After installing the package, the WKND reference site is now available at http://localhost:4503/content/wknd/us/en.html.

  6. Sign out as the admin user by clicking the “Sign out” button in the menu bar.

    WKND Sign-out Reference site

    Unlike the AEM Author instance, the AEM Publish instances default to anonymous read-only access. We want to simulate the experience of an anonymous user when running the React application.

Update Environment variables to point the Publish instance

Next, update the environment variables used by the React application to point to the Publish instance. The React App should only connect to the Publish instance in production mode.

Next, add a new file .env.production.local to simulate the production experience.

  1. Open the WKND GraphQL React app in your IDE.

  2. Beneath aem-guides-wknd-graphql/react-app, add a file named .env.production.local.

  3. Populate .env.production.local with the following:


    Add new environment variable file

    Using environment variables makes it easy to toggle the GraphQL endpoint between an Author or Publish environment without adding extra logic inside the application code. More information about custom environment variables for React can be found here.


    Observe that no authentication information is included since Publish environments provide anonymous access to content by default.

Deploy a static Node server

The React app can be started by using the webpack server, but this is for development only. Next, simulate a production deployment by using serve to host a production build of the React app using Node.js.

  1. Open a new terminal window and navigate to the aem-guides-wknd-graphql/react-app directory

    $ cd aem-guides-wknd-graphql/react-app
  2. Install serve with the following command:

    $ npm install serve --save-dev
  3. Open the file package.json at react-app/package.json. Add a script named serve:

     "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "test": "react-scripts test",
        "eject": "react-scripts eject",
    +   "serve": "npm run build && serve -s build"

    The serve script performs two actions. First, a production build of the React App is generated. Second, the Node.js server starts and uses the production build.

  4. Return to the terminal and enter the command to start the static server:

    $ npm run serve
    │                                                    │
    │   Serving!                                         │
    │                                                    │
    │   - Local:            http://localhost:5000        │
    │   - On Your Network:   │
    │                                                    │
    │   Copied local address to clipboard!               │
    │                                                    │
  5. Open a new browser and navigate to http://localhost:5000/. You should see the React App being served.

    React App Served

    Notice that the GraphQL query is working on the home page. Inspect the XHR request using your developer tools. Observe that the GraphQL POST is to the Publish instance at http://localhost:4503/content/graphql/global/endpoint.json.

    However, all the images are broken on the home page!

  6. Click into one of the Adventure Detail pages.

    Adventure Detail Error

    Observe that a GraphQL error is thrown for adventureContributor. In the next exercises, the broken images and the adventureContributor issues are fixed.

Absolute Image references

The images appear broken because the <img src attribute is set to a relative path and ends up pointing to the Node static server at http://localhost:5000/. Instead these images should point to the AEM Publish instance. There are several potential solutions to this. When using the webpack dev server the file react-app/src/setupProxy.js set up a proxy between the webpack server and the AEM author instance for any requests to /content. A proxy configuration can be used in a production environment but must be configured at the web server level. For example, Apache’s proxy module.

The App could be updated to include an absolute URL using the REACT_APP_HOST_URI environment variable. Instead, let’s use a feature of AEM’s GraphQL API to request an absolute URL to the image.

  1. Stop the Node.js server.

  2. Return to the IDE and open the file Adventures.js at react-app/src/components/Adventures.js.

  3. Add the _publishUrl property to the ImageRef within the allAdventuresQuery:

    const allAdventuresQuery = `
        adventureList {
        items {
            adventurePrimaryImage {
            ... on ImageRef {
    +           _publishUrl

    _publishUrl and _authorUrl are values built in to the ImageRef object to make it easier to include absolute urls.

  4. Repeat the above steps to modify the query used in the filterQuery(activity) function to include the _publishUrl property.

  5. Modify the AdventureItem component at function AdventureItem(props) to reference the _publishUrl instead of the _path property when constructing the <img src=''> tag:

    - <img className="adventure-item-image" src={props.adventurePrimaryImage._path} alt={props.adventureTitle}/>
    + <img className="adventure-item-image" src={props.adventurePrimaryImage._publishUrl} alt={props.adventureTitle}/>
  6. Open the file AdventureDetail.js at react-app/src/components/AdventureDetail.js.

  7. Repeat the same steps to modify the GraphQL query and add the _publishUrl property for the Adventure

     adventureByPath (_path: "${_path}") {
        item {
            adventurePrimaryImage {
                ... on ImageRef {
    +           _publishUrl
            adventureDescription {
            adventureItinerary {
            adventureContributor {
                pictureReference {
                    ...on ImageRef {
    +                   _publishUrl
  8. Modify the two <img> tags for the Adventure Primary Image and the Contributor Picture reference in AdventureDetail.js:

    /* AdventureDetail.js */
    <img className="adventure-detail-primaryimage"
    -       src={adventureData.adventurePrimaryImage._path} 
    +       src={adventureData.adventurePrimaryImage._publishUrl} 
    pictureReference =  <img className="contributor-image" 
    -                        src={props.pictureReference._path}
    +                        src={props.pictureReference._publishUrl} 
                             alt={props.fullName} />
  9. Return to the terminal and start the static server:

    $ npm run serve
  10. Navigate to http://localhost:5000/ and observe that images appear and that the <img src''> attribute points to http://localhost:4503.

    Broken Images fixed

Simulate content publishing

Recall that a GraphQL error is thrown for adventureContributor when an Adventure Details page is requested. The Contributor Content Fragment Model does not yet exist on the Publish instance. Updates made to the Adventure Content Fragment Model are also not available on the Publish instance. These changes were made directly to the Author instance and need to be distributed to the Publish instance.

This is something to consider when rolling out new updates to an application that relies on updates to a Content Fragment or a Content Fragment Model.

Next, lets simulate content publishing between the local Author and Publish instances.

  1. Start the Author instance (if not already started) and navigate to Package Manager at http://localhost:4502/crx/packmgr/index.jsp

  2. Download the package and install it using Package Manager.

    This package installs a configuration that enables the Author instance to publish content to the Publish instance. Manual steps for this configuraiton can be found here.


    In an AEM as a Cloud Service environment the Author tier is automatically set up to distribute content to the Publish tier.

  3. From the AEM Start menu, navigate to Tools > Assets > Content Fragment Models.

  4. Click into the WKND Site folder.

  5. Select all three models and click Publish:

    Publish Content Fragment Models

    A confirmation dialog appears, click Publish.

  6. Navigate to the Bali Surf Camp Content Fragment at http://localhost:4502/editor.html/content/dam/wknd/en/adventures/bali-surf-camp/bali-surf-camp.

  7. Click the Publish button in the top menu bar.

    Click Publish Button in Content Fragment Editor

  8. The Publish wizard shows any dependent assets that should be published. In this case, the referenced fragment stacey-roswells is listed and several images are also referenced. The referenced assets are published along with the fragment.

    Referenced Assets to publish

    Click the Publish button again to publish the Content Fragment and dependent assets.

  9. Return to the React App running at http://localhost:5000/. You can now click into the Bali Surf Camp to see the adventure details.

  10. Switch back to the AEM Author instance at http://localhost:4502/editor.html/content/dam/wknd/en/adventures/bali-surf-camp/bali-surf-camp and update the Title of the fragment. Save & Close the fragment. Then publish the fragment.

  11. Return to http://localhost:5000/adventure:/content/dam/wknd/en/adventures/bali-surf-camp/bali-surf-camp and observe the published changes.

    Bali Surf Camp Publish Update

Update CORs configuration

AEM is secure by default and does not allow non-AEM web properties to make client-side calls. AEM’s Cross-Origin Resource Sharing (CORS) configuration can allow specific domains to make calls to AEM.

Next, experiment with the CORS configuration of the AEM Publish instance.

  1. Return to the terminal window where the React App is running with the command npm run serve:

    │                                                    │
    │   Serving!                                         │
    │                                                    │
    │   - Local:            http://localhost:5000        │
    │   - On Your Network:   │
    │                                                    │
    │   Copied local address to clipboard!               │
    │                                                    │

    Observe that two URLs are provided. One using localhost and another using the local network IP address.

  2. Navigate to the address starting with http://192.168.86.XXX:5000. The address will be slightly different for each local computer. Observe that there is a CORS error when fetching the data. This is because the current CORS configuration is only allowing requests from localhost.

    CORS error

    Next, update the AEM Publish CORS configuration to allow requests from the network IP address.

  3. Navigate to http://localhost:4503/content/wknd/us/en/errors/sign-in.html and sign in with the user name admin and password admin.

  4. Navigate to http://localhost:4503/system/console/configMgr and find the WKND GraphQL configuration at com.adobe.granite.cors.impl.CORSPolicyImpl~wknd-graphql.

  5. Update the Allowed Origins field to include the network IP address:

    Update CORS configuration

    It is also possible to include a regular expression to allow all requests from a specific sub domain. Save the changes.

  6. Search for Apache Sling Referrer Filter and review the configuration. The Allow Empty configuration is also needed to enable GraphQL requests from an external domain.

    Sling Referrer Filter

    These have been configured as part of the WKND reference site. You can view the full set of OSGi configurations via the GitHub repository.


    OSGi configurations are managed in an AEM project that is committed to source control. An AEM Project can be deployed to AEM as Cloud Service environments using Cloud Manager. The AEM Project Archetype can help generate a project for a specific implementation.

  7. Return to the React App starting with http://192.168.86.XXX:5000 and observe that the application no longer throws a CORS error.

    CORS Error corrected


Congratulations! You’ve now simulated a full production deployment using an AEM Publish environment. You also learned how to use the CORS configuration in AEM.

Other Resources

For more details about Content Fragments and GraphQL see the following resources:

On this page