AEM Sites Developer Professional common mistakes
AEM Sites Developer Professional · Understand the traps and how to avoid them.
Choosing "Language Copy" from the Sites console when the requirement says that edits made to the master locale must automatically flow to the other locales, then reporting that the copies drift out of sync over time.
Language Copy and Live Copy are adjacent actions in the same Create menu and both produce a new tree of pages, so candidates treat them as interchangeable and never look at the underlying mechanism (translation project / copy vs. MSM rollout configuration).
What to do instead
Language Copy produces a one-time copy (optionally via a translation workflow) with no ongoing relationship; only a Live Copy backed by a blueprint and rollout configurations keeps inheritance. When synchronization is required, answer Live Copy (or an MSM-based language copy created from a translation project with rollout configs) and be able to name the rollout trigger (activate/rollout/edit) that drives propagation.
Adds an author to content-authors and grants read/write on /content/<site>, then concludes that editable templates are broken because opening the template or saving a policy returns 403.
Candidates assume authoring permissions on /content are sufficient for anything an author touches; editable templates and their policies actually live under /conf, which has its own ACL set and its own group (template-authors/template-editors).
What to do instead
Grant the required ACLs on /conf//settings/wcm (read on templates and policies, plus write/jcr:modifyAccessControl where policy editing is needed) and add the user to the template author group. Verify with the user-admin permission tester on the concrete /conf path rather than assuming inheritance from /content.
Puts sample/default content into the editable template's structure node (jcr:content/structure/jcr:content/root/...) and expects each new page to start with independent, editable content.
In a static template, everything in the template component is effectively page content, so candidates carry that mental model over and forget that an editable template has two separate trees: structure (locked, shared by all pages) and initial (copied into each new page).
What to do instead
Anything that must be per-page and editable goes under initial; anything that must be identical and locked for every page (layout container, required header/footer component) goes under structure. Legacy static templates have no initial node at all, so check which template type the question describes before answering.
Creates a proxy component with only jcr:primaryType and sling:resourceType set, omits sling:resourceSuperType, and then debug why the component returns a script-not-found error and its dialog shows no inherited fields.
Candidates think copying a component's folder is what makes it a 'proxy', and forget that inheritance in Sling comes solely from sling:resourceSuperType - the dialog and HTL are resolved through the supertype chain, not through the folder location.
What to do instead
Set sling:resourceSuperType to the sling:resourceType of the core/foundation component being extended, keep only the overrides (HTL, _cq_dialog, _cq_editConfig) in the proxy, and validate with the resource type hierarchy view in the AEM console / the Sling Resource Resolver before blaming HTL.
Writes an HTL data-sly-use against a Java class that has no @Model(adaptables = Resource.class) annotation or is not exported as a package, and treats the resulting NullPointerException or 'Cannot adapt' error as an HTL syntax problem.
Candidates learn data-sly-use as an HTL feature and forget that it is an adapter lookup performed by the Sling Models/Scripting engine, so the failure is in the Java registration and bundle wiring, not in the markup.
What to do instead
Annotate the class with @Model(adaptables = Resource.class, adapters = .class) when exposing an interface, set defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL so missing properties do not fail the whole model, and ensure the bundle package is exported and the model is registered. Confirm by checking that the returned object is non-null before debugging ${model.property}.
Resolves a resource in a servlet/workflow, sets a property on the node, and reports that 'the save silently fails' because nothing persists in the repository.
In the Sling/JCR API the mutation lives only in the transient ResourceResolver/session layer until commit()/save() is called; no exception is thrown, so the omission is invisible unless you re-resolve the resource in a new request.
What to do instead
Mutate through the ResourceResolver API (ModifiableValueMap) and call resourceResolver.commit(), or session.save() for JCR-level changes, inside try/finally with the resolver/session closed; then verify persistence with a fresh resolver rather than the one that made the change.
Changes an OSGi configuration through the Web Console or a runtime config on author, then is surprised when the setting reverts after the next package deployment.
Configuration stored as sling:OsgiConfig nodes inside a content package wins over runtime edits on install and, depending on run-mode/merge semantics, deletes or overwrites the runtime values; candidates do not distinguish 'config in code' from 'config in the repository state'.
What to do instead
Decide the source of truth explicitly: keep the sling:OsgiConfig node in the package (under /apps or /conf) as the permanent configuration and change it there, or remove it from the package. Remember run-mode-specific folders (config.publish, config.author.dev) take precedence over the generic config folder, and check the configuration status/effective config before debugging the code.
Registers a client library with categories but references it with an absolute /etc/clientlibs/... URL in the component or template, then gets 404s on publish only.
Client library source folders live under /etc or /apps, which are not served to anonymous users on publish and are commonly blocked by the Dispatcher; candidates test only on author, where the path resolves, and never see the production behaviour.
What to do instead
Set allowProxy=true on the cq:ClientLibraryFolder and reference the library through its category (data-sly-call with the core page clientlib, or cq:includeClientLib with the category) so HTL emits the /etc.clientlibs//... proxy URL. Flush the client library (rebuild) after changing categories or dependencies, and re-test on a publish+Dispatcher setup.
Leaves /statfileslevel at 0 (or never sets it) and then expects that activating a single page invalidates only that page in the Dispatcher cache, while in practice the whole docroot is flushed.
Flush granularity in the Dispatcher is determined by where the .stat files are placed, which is what statfileslevel controls; candidates confuse this with the /invalidate or /filter sections, which govern what may be cached and how activation requests are accepted.
What to do instead
Set /statfileslevel to the depth that matches the content hierarchy (for /content// that means 2) so .stat files sit at the right level and per-page/level invalidation works. Verify by activating one page and confirming files under sibling paths remain, and remember statfileslevel only affects flush granularity, not caching eligibility.
Builds a content package, sees 'Package installed' in the log, and does not notice that the new /conf or /apps node is missing on the target instance.
A package only captures paths listed as elements in META-INF/vault/filter.xml; anything outside those roots is silently excluded, and filter mode (merge/replace/update) further determines whether an existing node is actually updated.
What to do instead
Add a (and any other needed root) to vault/filter.xml, choose the mode deliberately (merge for additions, replace when the subtree must exactly match the package), inspect the built zip's filter.xml and jcr_root before installing, and verify with a package 'dry run'/refresh after deploy.