May 13, 2020
Estimated Post Reading Time ~

Pages

While I have mentioned that pages are just page level components so most of the information on the Components page is the same here, there are a few things that are different in how they are used.

Templates

To create a page in AEM there has to be a template. The template tells AEM that a page component is allowed to be created, and where it can be created
<?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:description="Template for Sample applications."
jcr:primaryType="cq:Template"
jcr:title="Sample Admin Page"
allowedPaths="[/content/sample/en/.+]"
ranking="{Long}100">
<jcr:content jcr:primaryType="cq:PageContent"
sling:resourceType="sample/components/structure/admin-page"> <par jcr:primaryType="nt:unstructured"
sling:resourceType="wcm/foundation/components/parsys"/>
<node1 jcr:primaryType="nt:unsrtuctured" sling:resourceType="sample/components/content/admin"/>
</jcr:content>
</jcr:root>
The template has a few tasks.
  1. Tell AEM that a page type can be created in the sites.html console
  2. Tell AEM how to build the structure in the jcr:repository for the page so it is in a default state

Telling AEM about your page

As you can see in the code above, we have the fields jcr:title and jcr:description that will describe the template itself. You can also add an image called thumbnail.png below the node so that an image appears in AEM's page creation dialog
AEM Templates
The ranking defines the order that the template will appear in relation to the other templates available for that content location.
Obviously, you wouldn't want your page types being created in other applications, and this is where the allowedPaths property comes into play. It is an array so that you can add multiple locations. The values themselves are regex's so that you can define the different structures.
i.e imagine that the sites that we are going to build with the sample application will be /content/site1 and /content/site2 since we know what the structure of the site is we could either specify [/content/site1,/content/site2] however that is very restrictive and harder to maintain if we decide to add site3 later so instead you can add [/content/site*] which would mean anything under content starting with the site.
Again this is quite restrictive and would only allow the template to be created directly below a page starting with the site if we add a bit more regex we can allow it to be created anywhere below the site [/content/site*(/.*)?] will do the trick.
There are additional attributes that can be added to the template's root element
  • allowedParents
    • Defines what pages you can create a page of this template on. This is very similar to the cq:allowedTemplates section below.
  • allowedChildren
    • The opposite of allowedParents, it defines what child templates can be created under a page of this template

Telling AEM the default structure of the page

The nodes below the root define the structure that you will be created by default when you click create in the page creation console.
By default, this should set up any default nodes that you need i.e. any parsys fields or default components that are coded into your page.
There is also an additional item you can add to the jcr:content section other than the mandatory sling:resourceType of the page's component to control what templates are visible when creating new pages.
  • cq:allowedTemplates
    • Defines what templates can be created below this page
    • If set AEM will show these templates that have are in this list and have an allowedPath that fits this page, or no allowedPath.
      • Only templates that have both are shown. i.e. if a template has an allowedPath that is valid but is not in the list of templates in the cq:allowedTemplates, it will not be shown

Page Structure

I have mentioned inheritance on the components page, however, when it comes to pages it really comes into its own.
The example I am going to explain will have a base page with just header and footer and nothing else, that will be extended by a section page, which will be extended by a sub-section page.
I won't show the code for the templates as that is self-explanatory

Base Page

The base page wouldn't actually be a page you can create because it doesn't have the functionality to display anything and is just the boilerplate
I won't go into much explanation of the sightly structures as this will be discussed further in the sightly page.
The structure for this page will be
  • .content.xml
    • Page definition
  • _cq_dialog
    • Page dialog
  • base.html
    • Main page
  • partials
    • The substructure of the page
    • body
      • Body elements
    • foot
      • Footer elements
    • head
      • Header elements

.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"
jcr:primaryType="cq:Component"
jcr:title="Base Page"
sling:resourceSuperType="foundation/components/page"
componentGroup=".hidden"/>
There isn't anything special about the base page component, as this is the base its supertype is foundation/components/page which is the base page structure supplied by AEM. We don't have to extend the base page, it is just a standard that I have always done. Because we will have an html file named after this page, the base doesn't actually provide us with any functionality. Also, the foundation/components/page component is a jsp component so it can't be extended properly with sightly.
All pages should be in the componentGroup .hidden so that they are not shown in the component dropdown.

_cq_dialog.xml

As with all components, a page can have additional attributes, the difference with a page is that you are extending the default dialog for the pages, and it will just add a new tab into the default dialog. If you add a node that is the same as one of the default dialog nodes then you will overwrite the functionality of that particular tab.
The dialog is configured in the .content.xml file
<?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="nt:unstructured" jcr:title="Application Page"> <content jcr:primaryType="nt:unstructured">
<items jcr:primaryType="nt:unstructured">
<tabs jcr:primaryType="nt:unstructured">
<items jcr:primaryType="nt:unstructured">
<description
jcr:primaryType="nt:unstructured" jcr:title="Description" sling:resourceType="granite/ui/components/foundation/section" class="full-width"> <layout
jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns" margin="{Boolean}false" class="full-width" />
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/container" class="full-width"> <items jcr:primaryType="nt:unstructured">
<fieldset
jcr:primaryType="nt:unstructured" jcr:title="User Groups" sling:resourceType="granite/ui/components/foundation/form/fieldset" name="./aclGroup"> <layout
jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/layouts/fixedcolumns"/>
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/container"> <items jcr:primaryType="nt:unstructured">
<description jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/foundation/form/textfield" name="./sample:description" class="full-width"/>
</items>
</column>
</items>
</fieldset>
</items>
</column>
</items>
</description>
</items>
</tabs>
</items>
</content>
</jcr:root>
As you can see it is structured identically to a normal component dialog. You will notice that instead of just adding items to the dialog i have added a fieldset. The nice thing about a fieldset in a dialog is it groups the items, even if they are not in a multifield it will still put a divider between the items so they are visually separated. This dialog will add the description tab at the end of the existing page dialog, and because it is in the base, any page that extends this will also have the description tab.

base.html

The base.html is the initial file that will be used for the page, because it is named the same as the page component. Any page that extends this won't have to have an html file the name of it's page as AEM will go to it's parent and find it's base html
<html lang= "en" data-sly-use.userlocale= "sample.core.models.LocaleModel" data-sly-attribute.lang= "${userlocale.language}" >
<head>
<sly data-sly-include= "partials/head/head.html" data-sly-unwrap/>
</head>
<body class = "page" >
<sly data-sly-include= "partials/body/main.html" data-sly-unwrap/> <sly data-sly-include= "partials/foot/footlibs.html" data-sly-unwrap/>
</body>
</html>
As you can see it is very clean which makes management and extensibility very simple.
I did say I won't go much into sightly, however, I will mention a few things that are here.
<html lang="en" data-sly-use.userlocale="sample.core.models.LocaleModel" data-sly-attribute.lang="${userlocale.language}">
I will talk about data-sly-use later, but for now, you should know that the name after data-sly-use is the name that the object will be known as, and it will be an object of type sample.core.models.LocaleModel, as long as LocaleModel is either a model or a wcmuse class. I also use data-sly-attribute which replaces an existing attribute with a value if the value exists, in this case, I am changing the lang attribute which is defined as lang="en" with the value of user locale. language This is part of the internationalization of the page. The local model will be responsible for deciding what the user's locale is, whether that is from their profile, the location of the page, or the user's browser.
The other tag of interest is
<sly data-sly-include="partials/head/head.html" data-sly-unwrap/>
The data-sly-include attribute tells sightly that it needs to read the file mentioned and bring it into the page structure. While data-sly-unwrap specifies that the new content should replace the <sly> tag, in this case it is not really needed, however i keep it there to maintain standards and readability.

partials

As you saw in the base page, there isn't any content, all the content will be in the partials folder, this means that the child component has the ability to overwrite any part of the base page
head
The head folder is for all header information
head.html
This is the main file that makes up the header of the html page
<meta charset= "utf-8" data-sly-use.head= "head.js" >
<meta name= "viewport" content= "width=device-width" >
<title>${currentPage.title || currentPage.name}</title>
<meta name= "keywords" content= "${head.keywords}" />
<meta name= "description" content= "${properties.jcr:description}" />
<meta name= "appdescription" content= "${properties.sample:description}" />
<link rel= "apple-touch-icon" sizes= "180x180" href= "/etc/designs/sample/assets/img/favicons/apple-touch-icon.png" >
<link rel= "icon" type= "image/png" href= "/etc/designs/sample/assets/img/favicons/favicon-32x32.png" sizes= "32x32" >
<link rel= "icon" type= "image/png" href= "/etc/designs/sample/assets/img/favicons/favicon-16x16.png" sizes= "16x16" >
<link rel= "manifest" href= "/etc/designs/sample/assets/img/favicons/manifest.json" >
<link rel= "mask-icon" href= "/etc/designs/sample/assets/img/favicons/safari-pinned-tab.svg" color= "#5bbad5" >
<meta name= "theme-color" content= "#ffffff" >
<meta data-sly-test= "${wcmmode.edit || wcmmode.design}" data-sly-include= "/libs/wcm/core/components/init/init.jsp" data-sly-unwrap></meta>
<sly data-sly-include= "partials/head/headlibs.html" data-sly-unwrap/>
<sly data-sly-include= "partials/head/pagelibs.html" data-sly-unwrap/>
Pretty much boilerplate html for the header, but i have put a few extra bits in. you can see the data-sly-use.head is a js file. This will be processed as serverside javascript to get some values.
You can also see I am using a mix of variables, you get access to a few variables in your components by default, I am using currentPage, properties, and wcmmode which will be explained a bit more in the sightly page.
One thing of interest is:
<meta data-sly-test="${wcmmode.edit || wcmmode.design}" data-sly-include="/libs/wcm/core/components/init/init.jsp" data-sly-unwrap></meta>
This will include the init.jsp, only if you are in the wcmmode of edit or design. This will initialise some of the editing functions that are useful. Without this some of the editing functions around parsys won't work correctly as they haven't been initialised.
I am only including this while in edit and design mode as I don't like having anything in the HTML in the final view that is not needed there.
head.js
// Server-side JavaScript for the head.html logic
use(function () {
var WCMUtils = Packages.com.day.cq.wcm.commons.WCMUtils;
var resourceResolver = resource.getResourceResolver();
return {
keywords: WCMUtils.getKeywords(currentPage, false ),
favIcon: resourceResolver.getResource(currentDesign.getPath() + "/favicon.ico" ).getPath()
};
});
head.js is an example of a WCMUse class done in serverside javascript. In this case we use the WCMUtils to get the keywords and the current design's path. While this information is also available via the currentPage sightly object, it is easier to use a script to get the details.
headlibs.html
<!-- jQuery library -->
<sly data-sly-use.clientLib= "/libs/granite/sightly/templates/clientlib.html" data-sly-call= "${clientLib.js @ categories='cq.jquery'}" data-sly-unwrap/>
<script src= "//code.jquery.com/ui/1.11.4/jquery-ui.js" ></script>
<link rel= "stylesheet" href= "//code.jquery.com/ui/1.11.4/themes/black-tie/jquery-ui.css" >
<!-- Bootstrap -->
<script src= "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" ></script>
<sly data-sly-use.clientLib= "/libs/granite/sightly/templates/clientlib.html" data-sly-call= "${clientLib.css @ categories='sample.site'}" data-sly-unwrap/>
the headlibs.html file is where we define libraries commonly used by the pages, in this case i am importing a couple of client libs as well as some jquery and bootstrap libraries
<sly data-sly-use.clientLib="/libs/granite/sightly/templates/clientlib.html" data-sly-call="${clientLib.js @ categories='cq.jquery'}" data-sly-unwrap/>
Another important sightly use, in this case it is for clientlibs, the /libs/granite/sightly/templates/clientlib.html file defines some sightly templates used to import all of the client libs into a single file. as you can see in the above example we use a call to clientLib.js and clientLib.css which are separate templates within the html file. There is also a .all template so we could do clientLib.all and both the js and css would be imported. While it is available I do prefer to keep them separate so i can layout the html how i want.
pagelibs.html
pagelibs is actually an empty file, and this is so that any file that extends the base can keep all of it's functionality while adding more libraries to the head section of the html
body
The body folder contains all of the html content to make up the visual section of the page
<div class="page__main">>
<div data-sly-resource="${@path='header', resourceType='sample/components/content/applicationHeader'}"></div>
<sly data-sly-include="partials/body/main-content.html" data-sly-unwrap/>
</div>
Just like the rest of the html in the base, it uses a other resources to show information and includes a file that can be overwritten by later pages. The main-content.html like the pagelibs.html in the head section is an empty file.
In this file however we are adding a fixed component
<div data-sly-resource="${@path='header', resourceType='sample/components/content/applicationHeader'}"></div>
The data-sly-resource attribute tells sightly to replace this div with a new component. The path field specifies which sub-node is used to hold data for the component, while the resourceType specifies which component is to be used
foot
The foot section is the opposite of the head and is where we can put any javascript files that you want to load after the page is loaded.
<!--/* Include the site client libraries (loading only the JS in the footer, CSS was loaded in the header) */-->
<sly data-sly-use.clientLib="/libs/granite/sightly/templates/clientlib.html" data-sly-call="${clientLib.js @ categories='sample.site'}"data-sly-unwrap/>
<sly data-sly-include="partials/foot/pagelibs.html" data-sly-unwrap/>
again you can see I am bringing in the clientlib js for sample.site and adding another pagelibs.html for other components to overwrite
And that is it, you now have a base page that other pages can extend. As you can see this page won't do much if it was instantiated. It would load all the js/css code required, and add a header at the top of the page, but there is no area for an editor to add any new content so it wouldn't be very useful.

Application page

We are going to now build a new page type that an editor can add content to. It will use data from the dialog to display the page's description
We always have to have our .content.xml file to define the page
<?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"
jcr:primaryType="cq:Component"
jcr:title="Application Page"
sling:resourceSuperType="sample/components/structure/base"
componentGroup=".hidden"/>
You can see that the sling:resourceSuperType points to our base page as that is what we want to extend. We don't have an application.html as we want to use the core functionality of the base page and only overwrite small pieces

partials

We need to maintain the structure of any part of the page that we want to overwrite. Having a file named the same but in a different sub folder will not work.
head
in the base file we had pagelibs.html that was empty. While we could just overwrite the headlibs.html to make the header completely custom, we want to extend the page not overwrite it's base functions.
<sly data-sly-use.clientLib= "/libs/granite/sightly/templates/clientlib.html" data-sly-call= "${clientLib.css @ categories='sample.application.site'}" data-sly-unwrap/>
<script>
$.get( "/bin/sample/analytics" , function (data) {
if (data) {
(function (i, s, o, g, r, a, m) {
i[ 'GoogleAnalyticsObject' ] = r;
i[r] = i[r] || function () {
(i[r].q = i[r].q || []).push(arguments)
},
i[r].l = 1 * new Date();
a = s.createElement(o),
m = s.getElementsByTagName(o)[ 0 ];
a.async = 1 ;
a.src = g;
m.parentNode.insertBefore(a, m)
})(window, document, 'script' , '//www.google-analytics.com/analytics.js' , 'ga' );
ga( 'create' , String(data.trackingId), { 'siteSpeedSampleRate' : 100 }, {customUserId: data.userId});
ga( 'set' , 'dimension1' , data.userId);
ga( 'send' , 'pageview' );
}
});
</script>
<sly data-sly-include= "partials/head/itemlibs.html" data-sly-unwrap/>
For our application we want to add google analytics as well as adding any application specific clientlibs, as we expect this page to be extended we have also added a blank itemlibs.html that the sub pages can overwrite if they want
body
As with the header, we are overwriting the main-content.html file to add a bit more content
<div class="container ordering-container">
<p data-sly-test="${properties.sample.description}" >${properties.sample.description}</p> <div class="page__par" data-sly-resource="wcm/foundation/components/parsys"></div>
</div>
We are not doing much in the application's html, but you can see we are adding a parsys. There are multiple ways of adding parsys, I prefer the wcm parsys, however, you could just put "par" and you will get a parsys as well. Some people use the foundation/components/parsys, and in all reality, there isn't much difference between them and it is a personal choice on which one you use.
You will notice in the body we don't add an empty html file to be extended. We expect if someone is going to extend this page type then they will want to overwrite how the body is, and the parsys would be removed.
I have added the extra description from the page to show that any page properties are still available. One thing to note, when you are in a non page component the properties value is the properties of the component not the page. I am also using a data-sly-test attribute which will validate if the tag should be shown. In this case it will only be shown if the properties.sample:description has a value.
foot
Just like the rest we are adding more clientlibs into the footer in the pagelibs.html, as well as adding a new empty section for extension in the itemlibs.html
<!--/* Include the site client libraries (loading only the JS in the footer, CSS was loaded in the header) */-->
<sly data-sly-use.clientLib="/libs/granite/sightly/templates/clientlib.html" data-sly-call="${clientLib.js @ categories='sample.application.site'}" data-sly-unwrap/>
<sly data-sly-include="partials/foot/itemlibs.html" data-sly-unwrap/>
Again, it is surprising that we now have 2 page structures in a very small piece of code, and it allows us to create a new page that has content on it very simply

Content Page

The content page will be a specially structured page, while it will still have the ability to put additional content, it also contains some fixed content. The .content.xml file is again very simple
<?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"
jcr:primaryType= "cq:Component"
jcr:title= "Content Page"
sling:resourceSuperType= "sample/components/structure/application"
componentGroup= ".hidden" />

partials

We are still overriding some of the content from the application page, however in this case we don't have additional header and footer client libs so we are just going to overwrite the page content itself.
body
As we are overwriting how the application page works, we are going to put in place a new main-content.html file
<div class = "container ordering-container" >
<div data-sly-resource= "${@path='section1', resourceType='sample/components/content/sectionOne'}" ></div>
<div class = "page__par" data-sly-resource= "wcm/foundation/components/parsys" ></div>
<div data-sly-resource= "${@path='section2', resourceType='sample/components/content/sectionTwo'}" ></div>
<div class = "page__par" data-sly-resource= "{@path='par2', resourceType='wcm/foundation/components/parsys'}" ></div>
</div>
Again, not that complex and we have a different page. One thing you will notice, in this page we have 4 components, sectionOne, a parsys, sectionTwo and another parsys. While adding multiple parsys to a page might be a bit strange, i have done it to highlight the different syntax.
In the first parsys
<div class="page__par" data-sly-resource="wcm/foundation/components/parsys">></div>
In the basic parsys implementation, we don't specify a path, and it will default to the path "par", however, if you are adding 2, or just want to be specific they you can give it a path
<div class="page__par" data-sly-resource="{@path='section2', resourceType='wcm/foundation/components/parsys'}"></div>
you can see that when adding parameters to the detail you have to wrap it as a sightly command {} with an @ in front of the parameter name, you can have as many parameters as the resource needs, this is also possible with data-sly-use classes
So above we have seen how to really use component inheritance, as I mentioned, even though we are doing it here as a page, the same is possible with all components.


By aem4beginner

No comments:

Post a Comment

If you have any doubts or questions, please let us know.