Goal
Adobe Experience Manager 2021.9.5899.20210929T093525Z-210800 (Sep 29, 2021)
Create a AEM React SPA supporting Vanity Urls. Otb Vanity Urls are added in Page Properties, saved as jcr:content/sling:vanityPath
For URL Shortening check this post
Demo | Package Install | Github
Configure Vanity URL
SPA Navigation with Vanity URL
Solution
1) Add the function getVanityUrls() to get the list of vanity urls using querybuilder before loading the app. VanityURLModelClient extending ModelClient was added as a workaround to not request model.json using the vanity url (eg. this is invalid request http://localhost:4502/eaem-home.model.json). Code in eaem-spa-vanity-urls\ui.frontend\src\index.js facilitates loading of vanity urls into the app...
import 'react-app-polyfill/stable'; import 'react-app-polyfill/ie9'; import 'custom-event-polyfill'; import { Constants, ModelManager } from '@adobe/aem-spa-page-model-manager'; import { createBrowserHistory } from 'history'; import React from 'react'; import { render } from 'react-dom'; import { Router } from 'react-router-dom'; import {ModelClient} from '@adobe/aem-spa-page-model-manager'; import App from './App'; import LocalDevModelClient from './LocalDevModelClient'; import './components/import-components'; import './index.css'; const getVanityUrls = async () => { const QUERY = "/bin/querybuilder.json?path=/content/eaem-spa-vanity-urls&property=jcr:content/sling:vanityPath&property.operation=exists" + "&p.hits=selective&p.properties=jcr:content/sling:vanityPath%20jcr:path&type=cq:Page"; const response = process.env.REACT_APP_PROXY_ENABLED ? await fetch(QUERY, { credentials: 'same-origin', headers: { 'Authorization': process.env.REACT_APP_AEM_AUTHORIZATION_HEADER } }): await fetch(QUERY); const data = (await response.json()).hits.reduce((current, next) => { return { ...current, ...{ [next["jcr:path"]]: next["jcr:content"]?.["sling:vanityPath"] } } }, {}); return data; }; class VanityURLModelClient extends ModelClient{ fetch(modelPath) { //if the path does not start with /content (page editing) or /conf (template editing) return empty model if (modelPath && !/^\/content|^\/conf/.test(modelPath)) { return Promise.resolve({}); }else{ return super.fetch(modelPath); } } } const modelManagerOptions = {}; if(process.env.REACT_APP_PROXY_ENABLED) { modelManagerOptions.modelClient = new LocalDevModelClient(process.env.REACT_APP_API_HOST); }else{ modelManagerOptions.modelClient = new VanityURLModelClient(process.env.REACT_APP_API_HOST); } const renderApp = (vanityUrls) => { ModelManager.initialize(modelManagerOptions).then(pageModel => { const history = createBrowserHistory(); render( <Router history={history}> <App vanityUrls={vanityUrls} history={history} cqChildren={pageModel[Constants.CHILDREN_PROP]} cqItems={pageModel[Constants.ITEMS_PROP]} cqItemsOrder={pageModel[Constants.ITEMS_ORDER_PROP]} cqPath={pageModel[Constants.PATH_PROP]} locationPathname={window.location.pathname} /> </Router>, document.getElementById('spa-root') ); }); }; document.addEventListener('DOMContentLoaded', () => { getVanityUrls().then((vanityUrls) => { renderApp(vanityUrls); }, (err) => { console.log("Error getting vanity urls", err); renderApp({}); }); });
2) Pass the page full url to vanity url mapping object loaded in step above to child pages in eaem-spa-vanity-urls\ui.frontend\src\App.js
import { Page, withModel } from '@adobe/aem-react-editable-components'; import React from 'react'; // This component is the application entry point class App extends Page { render() { const vanityUrls = this.props.vanityUrls; return ( <div> {this.childComponents} {this.childPages.map((childPage) => { return <React.Fragment> { React.cloneElement(childPage, { vanityUrls: vanityUrls })} </React.Fragment> })} </div> ); } } export default withModel(App);
3) Add the full url and vanity url in Route in ui.frontend\src\components\RouteHelper\RouteHelper.js. Vanity urls are added to Route only if the app is loaded Published mode (?wcmmode=disabled in author)...
import React, { Component } from 'react'; import { Route } from 'react-router-dom'; import { AuthoringUtils } from '@adobe/aem-spa-page-model-manager'; /** * Helper that facilitate the use of the {@link Route} component */ /** * Returns a composite component where a {@link Route} component wraps the provided component * * @param {React.Component} WrappedComponent - React component to be wrapped * @param {string} [extension=html] - extension used to identify a route amongst the tree of resource URLs * @returns {CompositeRoute} */ export const withRoute = (WrappedComponent, extension) => { return class CompositeRoute extends Component { render() { let routePath = this.props.cqPath; if (!routePath) { return <WrappedComponent {...this.props} />; } extension = extension || 'html'; let paths = ['(.*)' + routePath + '(.' + extension + ')?']; if(!AuthoringUtils.isInEditor()){ let vanityUrl = this.props.vanityUrls?.[routePath]; if(vanityUrl){ paths.push((!vanityUrl.startsWith("/") ? "/" : "") + vanityUrl) ; } } return ( <Route key={routePath} exact path={ paths } render={routeProps => { return <WrappedComponent {...this.props} {...routeProps} />; }} /> ); } }; };
SPA App Local Development
1) For deploying the app with React changes combined with AEM editor changes eg. using if(!AuthoringUtils.isInEditor()) the app needs to be built using npm run build and pushed to AEM using npm run sync
2) However if the changes are only in React scripts (with no AEM specific code), it's much easier to use npm run start and do hot deployment (the app running on localhost:3000 gets updated in browser when you save the script in editor). The model.json required for app is proxied from AEM. Check it in action
PUBLIC_URL=/
REACT_APP_PROXY_ENABLED=true
REACT_APP_PAGE_MODEL_PATH=/content/eaem-spa-vanity-urls/us/en.model.json
REACT_APP_API_HOST=http://localhost:4502
REACT_APP_AEM_AUTHORIZATION_HEADER='Basic YWRtaW46YWRtaW4='
REACT_APP_ROOT=/content/eaem-spa-vanity-urls/us/en/home.html
5) To start the local dev server npm run start
6) Since the model json request is CORS request from local page load to AEM, such requests are not allowed in modern browsers. Either configure CORS in AEM or since this is just for local development, use a browser profile with disabled web security....
a. Create a new profile in chrome...
b. Create a shortcut with target :
"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --profile-directory="Profile 1"
--user-data-dir="C:/Users/<user>/AppData/Local/Google/Chrome/User Data/Profile 1" --disable-web-security
c. Run the new shortcut to open a browser instance disabling CORS...
7) Open the disabled web security chrome instance and access url http://localhost:3000/content/eaem-spa-vanity-urls/us/en/home.html to load the SPA app (the app is now loading from react dev server on port 3000)
8) A sample AEM vanity url served from react dev server on port 3000 eg. http://localhost:3000/eaem-spa-cloud
We should not be using QueryBuilder servlet in production code, as noted at https://helpx.adobe.com/au/experience-manager/kb/Unclosed-ResourceResolver-warnng-at-com-day-cq-search-impl-builder-QueryBuilderImpl.html, since it could lead to memory leaks.
ReplyDeleteThis comment has been removed by the author.
ReplyDeletei am getting empty object in vanityUrl object. any help?
ReplyDelete