aem create custom project tile header

AEM: How to create custom Project Tile

In this article we will look into the steps we need to go through to create a custom Project Tile. In case you are not familiar with AEM Projects, there is a good intro from Adobe.

Note: steps here were written for Adobe AEM 6.3.

Add And Render our AEM Project Tile

As a content for our custom Project Tile, we will display a number of not activated pages per market/locale in We-Retail website. We will start with creating our tile component.

<!--/* /apps/taradevko/components/activationStatistics/tile/tile.html */-->
<sly data-sly-use.init="init.js"/>
<sly data-sly-resource="${ @ path='/etc/projects/dashboard/gadgets/activationStatistics/jcr:content/default'}"></sly>
// /apps/taradevko/components/activationStatistics/tile/init.js
use(function () {
    request.setAttribute("projectLinkResource", resource);

As you can see, we are not doing here a lot. In init.js we set current resource as a projectLinkResource attribute. We will need this info later. Then, in tile.html we execute our js and include some resource. We want to reuse OOTB  component cq/gui/components/projects/admin/dashboard/tile which will render basic structure of our project tile, but to use it, we need to set projectLinkResource first. That’s why we kinda wrap it with our own component. This should make more sense after looking at our tile’s resource definition:

<!-- /etc/projects/dashboard/gadgets/activationStatistics/.content.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="" xmlns:jcr=""
    <jcr:content jcr:primaryType="nt:unstructured"
                 jcr:description="Activation statistics per market/locale"
                 jcr:title="Not Activated Pages Info"
        <image jcr:primaryType="nt:unstructured">
        <default jcr:primaryType="nt:unstructured"
                <layout jcr:primaryType="nt:unstructured" sling:resourceType="cq/gui/components/projects/admin/dashboard/layouts/tile"/>
            <header jcr:primaryType="nt:unstructured">
                <title jcr:primaryType="nt:unstructured" jcr:title="Not Activated Pages Info"/>
            <body jcr:primaryType="nt:unstructured" sling:resourceType="taradevko/components/activationStatistics/body"/>

Attribute allow stands for a number of tiles with given sling:resourceType to be allowed on the dashboard. Our resource contains additionally “default” resource, which has a type of the OOTB project tile component. Bellow it, we have layout definition, header, and body. In addition, under  /etc/projects/dashboard/gadgets/activationStatistics/_jcr_content/image we can add an image which will be shown on “Add Tile” screen as an icon of our own Tile.

Project Tile’s content

All the content specific to this tile type will reside in the body, so we need to create a component for rendering it.

<!--/* /apps/taradevko/components/activationStatistics/body/body.html */-->
<div class="cq-projects-AppDetails cq-projects-Pod-content""com.taradevko.aem.granite.ActivationStatistics">

    <section class="coral-light""${info.activationStatistics}">
        <h2 class="coral-Heading coral-Heading--2">${market}</h2>

            <coral-list-item data-sly-repeat.locale="${info.activationStatistics[market]}">${locale} -> ${info.activationStatistics[market][locale]}</coral-list-item>

Nothing fancy, just rendering our activation statistics which we get from Sling Model:

@Model(adaptables = SlingHttpServletRequest.class)
public class ActivationStatistics {

    private SlingHttpServletRequest request;

    private final IsPage isPage = new IsPage();
    private final NotActivated notActivated = new NotActivated();
    private final IsMarket isMarket = new IsMarket();
    private Map<String, Map<String, Integer>> activationStatistics;

    public void init() {
        //skipped permissions check
        ResourceResolver resourceResolver = request.getResourceResolver();

        String projectPath = request.getRequestPathInfo().getSuffix();

        Resource siteRootResource = resourceResolver.getResource(getContentPath(projectPath));
        activationStatistics = getActivationStatistics(siteRootResource);

    public Map<String, Map<String, Integer>> getActivationStatistics() {
        return activationStatistics;

    private Map<String, Map<String, Integer>> getActivationStatistics(final Resource siteRootResource) {
        final Map<String, Map<String, Integer>> results = new TreeMap<>();
        for (Resource market : siteRootResource.getChildren()) {
            if (isMarket.and(isPage).test(market)) {
                Map<String, Integer> marketStats = processMarket(market);
                results.put(market.getName(), marketStats);

        return results;

    private Map<String, Integer> processMarket(final Resource market) {
        final Map<String, Integer> results = new TreeMap<>();
        for (Resource locale : market.getChildren()) {
            if (isPage.test(locale)) {
                Integer localeStats = processPageTree(locale);
                results.put(locale.getName(), localeStats);

        return results;

    private Integer processPageTree(final Resource page) {
        Integer stats = 0;
        for (Resource child : page.getChildren()) {
            if (isPage.test(child)) {
                stats += processPageTree(child);

        if (notActivated.test(page)) {
            stats += 1;

        return stats;

    //ideally this should come from project settings.
    private String getContentPath(final String projectPath) {
        return projectPath.replace("/projects", "");

    class IsPage implements Predicate<Resource> {

        public boolean test(Resource resource) {
            return "cq:Page".equals(resource.adaptTo(ValueMap.class).get("jcr:primaryType"));

    class NotActivated implements Predicate<Resource> {

        public boolean test(Resource resource) {
            return !"Activate".equals(resource.getChild("jcr:content")

    class IsMarket implements Predicate<Resource> {

        public boolean test(Resource resource) {
            return resource.getName().length() == 2;

Here we collect a number of not activated pages per market/locale of We-Retail website. Note, while OOTB tiles have all logic in JSP files and it’s somewhat complex, we in our example removed many checks for the sake of simplicity.

Now we can deploy our example:

mvn clean install -PautoInstallPackage,autoInstallBundle

Finally, navigate to http://localhost:4502/projects/details.html/content/projects/we-retail and click Add button. You will see a screen like this:

aem projects custom tile add tile screenshot

There at the bottom in the middle, you see our custom Project Tile. Select it and submit. Now, after AEM redirect, you see Projects Dashboard and our tile on it:

aem projects dashboard with custom project tile

That’s it for our example.

What’s next?

If you need more examples – you can check next components/resources in the repository:

  1. /etc/projects/dashboard/gadgets – contains a definition of the tiles which are available on the Projects page.
  2. /libs/cq/gui/components/projects/admin/pod – components, which render gadgets from above. Mostly contains initialisation logic (as we have in taradevko/components/activationStatistics/tile/tile.html) and include of the default resource.
  3. /libs/cq/core/content/projects/dashboard/default – contains definition of the resources of OOTB tiles. Contains layout, header actions, footer link and some others.
  4. /libs/cq/gui/components/projects/admin/dashboard/tile – OOTB tile renderer (renders tile’s layout, header with actions, footer etc).
  5. /libs/cq/gui/components/projects/admin/dashboard/tiles – contains components for rendering specific content to OOTB tiles (like Project Info)


You can find the complete example in this repository.

Your thoughts are welcome