Skip to content

Worked examples

Four organisations, the configuration each one would write, and what an author sees as a result.

Every URL on this page is the URL the shipped extension builds from the configuration above it, held there by a test that reads this page and drives the real detector and engines with it. The hostnames are placeholders: swap yourcompany.com and yoursite for your own.

Your setup Read
One public site, an author instance per environment, a language root under /content Example 1
Several sites in one instance, a site on more than one domain, or public URLs that do not match the author path Example 2
A team with its own conventions in page properties, and pages built out of content fragments Example 3
A rollout where the configuration must not be visible outside the company Example 4

The first three are cumulative: example 2 assumes you have read example 1, and example 3 adds rules on top of either. Example 4 is about hosting rather than configuration, and stands on its own.

One public website. Four AEM author instances: production, staging, development, and whatever each developer runs locally. Content lives at /content/yoursite/en/... and the live site drops the /en. Roughly twenty editors, none of whom will ever open a YAML file.

The whole thing is one file.

environment-config.yaml
authors:
prod: [author.yourcompany.com]
stage: [author-stage.yourcompany.com]
dev: [author-dev.yourcompany.com]
local: [http://localhost:4502]
sites:
yoursite:
stripPath: /en
public:
prod: [www.yoursite.com]
stage: [stage.yoursite.com]
dev: [dev.yoursite.com]
local: [http://localhost:3000]
environments:
prod:
color: "#D32F2F"
stage:
color: "#FF8F00"
dev:
color: "#388E3C"
local:
color: "#1976D2"

The site key yoursite must be the folder name under /content. stripPath: /en is the segment that exists in the author path and not the public one. The local SDK is written http://localhost:4502 because an instance on http or a non-standard port needs its full origin.

The environments section is optional. Leave it out and colours are assigned in the order the environments appear under authors: red, orange, green, blue, purple, gold, teal. Listing production first therefore makes it red for free.

An editor opens https://author.yourcompany.com/editor.html/content/yoursite/en/products/kettles.html.

The toolbar renders immediately as a small collapsed rail carrying a red badge, which reads E: pro... collapsed and Env: prod expanded.

prodPage editor

  • View Livehttps://www.yoursite.com/products/kettles
  • Page Propertieshttps://author.yourcompany.com/mnt/overlay/wcm/core/content/sites/properties.html?item=/content/yoursite/en/products/kettles

The same editor on the live page at https://www.yoursite.com/products/kettles gets the return trip and three more.

prodPublished page

  • Edit Pagehttps://author.yourcompany.com/editor.html/content/yoursite/en/products/kettles.html
  • Page Propertieshttps://author.yourcompany.com/mnt/overlay/wcm/core/content/sites/properties.html?item=/content/yoursite/en/products/kettles
  • Open Enclosing Directoryhttps://author.yourcompany.com/sites.html/content/yoursite/en/products
  • Show Asset LinksAn edit link on every image that resolves to a DAM assetOverlays on the page itself

The defaults also put one message on a live page: an info row reading Example Meta Description: followed by whatever the page’s meta[name='description'] holds. It ships as an example most teams replace with a rule of their own.

Show Asset Links is the one row on that list that is not offered everywhere. It appears on a live page, while the per-user DAM overlay setting is on, and only when the environment the page matched has an author instance to link into. Being signed in to that author instance is not one of those conditions: it decides which images the overlays can resolve once the row is used, not whether the row is there.

Switch instance and only the badge and the destinations change.

On Badge View Live goes to
author-stage.yourcompany.com orange stage https://stage.yoursite.com/products/kettles
author-dev.yourcompany.com green dev https://dev.yoursite.com/products/kettles
localhost:4502 blue local http://localhost:3000/products/kettles

On the “view as published” preview at https://author.yourcompany.com/content/yoursite/en/products/kettles.html the defaults give three links rather than two: View Live and Page Properties as above, plus Edit Page. They add a warning as well, that the preview is missing ?wcmmode=disabled, which is clickable and adds it. That is a message rule, and example 3 is about writing your own.

stripPath is the setting people get wrong first. Set it to /en when the public URLs keep the language code and View Live lands on a 404. There is nothing to debug: open a page, press View Live, and the path tells you which segment is wrong.

Example 2 - Two sites in one instance, different URL shapes

Section titled “Example 2 - Two sites in one instance, different URL shapes”

One AEM instance holding two sites, and two environments. The main site answers on two production domains, the apex and the www host, because a rebrand left both live. The shop’s public URLs do not match its author path: content authored under /catalogue is published under /shop.

environment-config.yaml
authors:
prod: [author.yourcompany.com]
stage: [author-stage.yourcompany.com]
sites:
yoursite:
stripPath: /en
public:
prod: [www.yoursite.com, yoursite.com]
stage: [stage.yoursite.com]
yourshop:
stripPath: /en
pathTransforms:
- authorPattern: "^/catalogue"
liveReplacement: "/shop"
public:
prod: [shop.yoursite.com]
stage: [stage-shop.yoursite.com]

Both sites live in one authors section because they live in one AEM instance. The environment is a property of the instance, not of the site.

On https://author.yourcompany.com/editor.html/content/yoursite/en/about.html, View Live is not one link but one per configured domain, each labelled with the host it goes to.

prodPage editor, two public domains

  • View Livewww.yoursite.comhttps://www.yoursite.com/about
  • View Liveyoursite.comhttps://yoursite.com/about

The editor picks the one they mean rather than finding out afterwards which host they landed on. On the live site itself, the host being viewed is marked as the primary one.

On https://author.yourcompany.com/editor.html/content/yourshop/en/catalogue/kettles.html, View Live goes to https://shop.yoursite.com/shop/kettles. stripPath removed /en, then the transform rewrote /catalogue to /shop. The return trip works too: on https://shop.yoursite.com/shop/kettles, Edit Page goes back to https://author.yourcompany.com/editor.html/content/yourshop/en/catalogue/kettles.html.

An author path the transform does not match is published unchanged, so /content/yourshop/en/about is still at shop.yoursite.com/about.

Three things to check before you copy this

Section titled “Three things to check before you copy this”

A path transform is read in both directions. ^/catalogue to /shop maps out at any depth, and the return trip puts /catalogue back. A capture-group rule such as ^/catalogue/(.+)$ to /shop/$1 works both ways too: coming back, the live path is matched against /shop/$1 and the capture is written into /catalogue/.

Two live paths the return trip declines to rewrite, and it leaves each as it is rather than guessing. One that only shares the replacement prefix, so /shopping stays /shopping instead of coming back as /content/yourshop/en/catalogueping, which is not a page. One whose authorPattern does not spell out the text it matched, such as ^/(?:catalogue|store)/(.+)$, where the pattern cannot say which of the two the live path came from. In both cases Edit Page goes to the untransformed path, so open a live page and press it before you hand the configuration to anyone.

If your live site needs the .html, one transform puts it back.

environment-config.yaml
sites:
yoursite:
stripPath: /en
pathTransforms:
- authorPattern: "^/(.+)$"
liveReplacement: "/$1.html"
public:
prod: [www.yoursite.com]

/content/yoursite/en/products/kettles then goes to https://www.yoursite.com/products/kettles.html, and the site root is left as / rather than becoming /.html. The return trip is unaffected, because this capture group appends a suffix rather than rewriting the path.

Only the first transform whose authorPattern matches is applied, so no one path can pick up both this catch-all and the /catalogue to /shop swap above. A site that needs both has to express them in a single transform.

A site in the instance but not in the config gets a partial toolbar. Open a page under a /content folder with no entry in sites and the toolbar still appears, because the author hostname matched, and Page Properties still works. View Live simply is not there, because there is no public domain to send it to. That is the honest failure mode, but it looks like a missing feature to an editor, so list every site you expect people to open.

Example 3 - A team with conventions of its own

Section titled “Example 3 - A team with conventions of its own”

One site, one author instance, and an editorial team that already keeps things in page properties: a link to the campaign brief, a review date, the template the page was built from. Pages are assembled from content fragments held in the DAM.

Nothing here needs an AEM code change. The team’s existing conventions become toolbar rows.

Two files, added to whatever you already have.

link-matrix.yaml
author/pageEditor:
- label: "View Live"
urlTemplate: "{LIVE_URL}"
icon: "world"
- label: "Campaign brief"
urlTemplate: "{page.campaignBriefUrl}"
icon: "link"
- label: "Hero fragment: {page.heroFragmentPath}"
urlTemplate: "{instanceOrigin}/editor.html{page.heroFragmentPath}"
icon: "contentFragment"
message-matrix.yaml
author/pageEditor:
- message: "Template: {page.cq:template}"
messageType: "info"
assertion:
operator: "urlContains"
value: "/editor.html/"
- message: "Review due {page.reviewDate}"
messageType: "warning"
icon: "alert"
assertion:
operator: "urlContains"
value: "/editor.html/"
public/page:
- message: "This page is set to noindex."
messageType: "warning"
icon: "alert"
assertion:
operator: "elementAttributeContains"
value: "meta[name='robots']"
attributeName: "content"
text: "noindex"

{page.<name>} is the AEM page’s own property, read from the page node on the author instance. That is the constraint worth knowing up front: these rules resolve on author pages only, so one written under public/page will never appear. Property names carry their prefix, so cq:template is written {page.cq:template}.

On a page that carries campaignBriefUrl and heroFragmentPath:

prodPage editor, properties set

  • View Livehttps://www.yoursite.com/products/kettles
  • Campaign briefhttps://intranet.yourcompany.com/briefs/spring
  • Hero fragment: /content/dam/yoursite/fragments/spring-herohttps://author.yourcompany.com/editor.html/content/dam/yoursite/fragments/spring-hero

plus the message Template: /conf/yoursite/settings/wcm/templates/standard-page.

On a page that carries neither property, the same configuration shows only View Live and no messages. Not an empty row, not a broken link, not a label with a gap in it: the rules are simply not there.

That is the rule worth understanding before you write any of this. A link, message or condition naming a property the page does not carry is not rendered at all. A property an editor has cleared counts as absent, and a multi-value property resolves to its first value.

It is what makes one site-wide configuration safe. You can write a rule for a convention only half your pages follow, roll it out to everybody, and it stays silent on the other half.

The same rule has a consequence worth stating plainly: you cannot warn about a property being missing. The “Review due” rule above fires on pages that set reviewDate and stays silent on pages that do not, which is the opposite of a nag. For “somebody forgot to do X”, assert against the page instead, which is what the noindex rule does.

On the live site, the public/page rule reads the rendered page and shows an orange warning on anything still carrying noindex. It is the shape most useful to an editorial team: a fact about the published page, checked on the published page, in front of the person who can fix it.

The assertion operators cover the DOM (element exists, missing, contains text, attribute contains), the URL, query parameters, local and session storage, and JavaScript globals, and they combine with AND and OR. message-matrix.yaml in Downloads documents each one inline.

Content fragments, with no configuration at all

Section titled “Content fragments, with no configuration at all”

On any author page, the toolbar asks the instance which content fragments the page uses and adds a “Content fragments” row with a count. Expand it and each fragment is a link into its editor.

There is nothing to configure. A page that uses no fragments gets no row, rather than a permanent “Content fragments 0”. The lookup uses AEM’s QueryBuilder at /bin/querybuilder.json, and plenty of organisations restrict it for non-admin users; when that happens the row says “Could not load” rather than showing a wrong count, and stops asking for a while.

The lookup runs after the toolbar is already on screen, as do the page property lookups. The toolbar never blocks its render on a network request, so a slow or unreachable instance costs you rows, never the toolbar.

Example 4 - A package only your company can reach

Section titled “Example 4 - A package only your company can reach”

A bank with an AEM estate that is not reachable from the internet. The author instances sit on the corporate network, the publish tier is behind the CDN, and the security team’s first question about any browser extension is where the configuration comes from.

The answer is that it comes from wherever you put it, including a host nobody outside the company can resolve.

The estate itself is ordinary. The only thing worth noticing is that none of these hostnames has to be publicly resolvable.

environment-config.yaml
authors:
prod: [aem-author.corp.yourcompany.internal]
stage: [aem-author-stage.corp.yourcompany.internal]
sites:
yoursite:
stripPath: /en
public:
prod: [www.yoursite.com]
stage: [stage-www.corp.yourcompany.internal]
environments:
prod:
color: "#D32F2F"
stage:
color: "#FF8F00"

The package is the same four files plus a manifest, hosted on an internal static server:

yourcompany.aem-toolbar.json
{
"name": "Your Company AEM Toolbar Config",
"version": "1.0.0",
"description": "Toolbar configuration for our AEM environments",
"author": "Your Web Team",
"configs": {
"environmentConfig": "./environment-config.yaml",
"linkMatrix": "./link-matrix.yaml",
"messageMatrix": "./message-matrix.yaml",
"regionMap": "./region-map.yaml"
}
}

Served at https://aem-toolbar.corp.yourcompany.internal/config/yourcompany.aem-toolbar.json, a URL that resolves on the corporate network and nowhere else.

Every fetch of that package is made by the extension running in one employee’s browser, on their own machine, inside your network. There is no server of ours in the path, and no service that has to reach your host. If the browser can open the URL, the extension can fetch it; if the machine is off the VPN, the fetch fails and the previously stored configuration keeps working until it succeeds again.

The same is true of the hostnames inside the file. They are matched against the address bar of the page in front of the user and never sent anywhere, so an internal-only author hostname is matched exactly like a public one. That is why the stage site above can be published on an internal host: nothing needs to resolve it except the machine the editor is sitting at.

Two things still apply, and neither is negotiable:

  • HTTPS. The extension refuses a package served over plain HTTP, localhost excepted, because the package decides which sites the toolbar activates on. An internal certificate authority your machines already trust is fine.
  • No login in front of the files. The first import happens while the user is looking at the page, but the scheduled refresh is a plain fetch with no login flow. A package behind an SSO redirect imports once and then silently stops updating. If your intranet server can serve one path anonymously to anyone already inside the network, use that.

Relative config paths are what make the package portable. Because the manifest points at ./environment-config.yaml rather than an absolute URL, you can move the whole folder between hosts, or copy it to a second region, without editing anything inside it.

  • The configuration never leaves the machine it is loaded on, and neither does the list of hostnames in it.
  • The package is data. It is YAML and JSON, validated before it is stored, and one bad file rejects the whole import rather than applying half of it.
  • The only other outbound requests are to your own AEM instances, carrying the session the editor already has.
  • Nothing is sent to the extension’s author, ever. The privacy policy is the full statement.

Each example above is a set of files. Put them in a folder with a manifest whose name ends in .aem-toolbar.json, host that folder over HTTPS with no login in front of it, then send two links: the Chrome Web Store listing and the manifest URL. Clicking the manifest URL opens an import dialog in the page.

For a team or org walks the whole rollout, and Downloads has a sample package to start from.