For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt, and this page is available as Markdown at https://nhanbe.id.vn/aem/advanced/component-development.md.

AEM Component Development

Your syllabus has one line for this topic, and it hides a trap:

"Tell me a complex component you have developed recently."

That question is not asking you to define a component. It is asking you to prove you have built things. Interviewers use it as a lie detector. Someone who has only read about components describes one in general terms. Someone who has actually built one mentions the dialog structure, the multifield that fought them, and the thing that broke on publish.

So this file does three jobs.

First, it makes sure you genuinely understand how a component is assembled — every file, what it does, and why. That is sections 1 to 3.

Second, it gives you three complete components at three difficulty levels, fully coded:

Level Component Answer length What it proves Simple CTA link/button ~1 minute You know dialog basics and the classic traps Medium FAQ accordion ~2 minutes You can handle multifields, nested models and accessibility Complex Tabbed product listing with Load More ~3 minutes You think about queries, caching and architecture Interviewers often warm up with "start with something simple" before pushing to "now tell me the most complex one." Having a graded set means you are never caught either over-explaining a trivial component or under-explaining a hard one.

Third, it grounds every example in a real, coherent project so your stories hang together across all 40+ files of this repository.

A note on the project domain Every example in this repository is set on a global energy technology company's marketing site — the kind that sells transformers, HVDC systems, grid automation and power quality equipment to utilities and industrial customers.

This is a genuinely good domain to talk about in interviews, because it naturally justifies the things AEM interviewers care about:

A large, deep product catalogue — hundreds of product category pages, which forces derived listings rather than hand-authored ones. Many countries and languages — URLs like /us/en/... and /in/en/... alongside a global site, which is a natural MSM and language-copy conversation. Technical documentation and PDF downloads — a real integration story. SEO-driven content like FAQ sections, because industrial buyers search technical questions. High but not consumer-scale traffic, so caching matters without the story becoming implausible. Important: never name the client in an interview, and this repository never does. "A global energy technology company" or "an industrial energy client" is the correct level of detail. Naming clients is a professionalism red flag, and interviewers do notice.

⚠️ Read this before using any story in this file These stories are shapes, not scripts. Interviewers at companies like Publicis Sapient and Valtech will drill three or four levels into anything you say. If you claim you fixed a caching problem, the next four questions will be about hit ratios, statfiles, and what you measured.

So fill these shapes with things you can actually defend. Support experience counts — "I traced a stale-content issue to a blocked replication queue" is a real, honest, technical story. A POC you built counts. WKND tutorial work counts. A borrowed story you cannot defend does more damage than having no story at all.

  1. Introduction 1.1 What is a component, really? Most people answer with "a reusable piece of a page." True, but shallow, and it tells an interviewer nothing.

Here is a more useful definition:

A component is a folder under /apps containing everything needed to render one type of content and to let an author edit it.

Read that again, because both halves matter.

"Everything needed to render" — the HTL file, and usually a Sling Model holding the logic.

"And to let an author edit it" — the dialog. This is what makes AEM a CMS rather than a templating engine. Without a dialog you have hardcoded HTML. With one, a content author changes the page without a developer.

That second half is where most interview follow-ups live, and it is the part beginners consistently underestimate.

1.2 How a component connects to content Component development only makes sense if you remember how Sling works, so let's connect back to file 01.

Recall the chain: URL → Resource → Resource Type → Script.

A component is the "Resource Type → Script" half. When an author drags a CTA onto a product page, AEM creates a node and sets one property:

/content/energy/global/en/products-and-solutions/transformers/jcr:content/root/container/cta ├── sling:resourceType = "energy/components/cta" ← points at your component ├── linkText = "Read the Press Release" └── linkUrl = "/content/energy/global/en/news/press-releases/2026/grid-upgrade"Copy Copy That sling:resourceType is the entire link between content and code. Sling reads it, goes to /apps/energy/components/cta, finds cta.html, and runs it.

So a component is just a folder that a content node points at. Once that lands, a lot of AEM stops being mysterious — including why a component "disappears" when someone renames a folder, and why copying content between projects breaks rendering.

1.3 Why we build custom components at all Adobe ships Core Components — around thirty ready-made components covering text, image, teaser, carousel, tabs, accordion, breadcrumb, navigation, forms and more. They are accessible, tested, and Adobe maintains them.

So the honest question in a modern interview is not "how do you build a component" but "when should you build one at all?"

The answer that scores well:

"My default is to use a Core Component and extend it, not build from scratch. They're accessible, tested, and Adobe keeps them updated — that's free maintenance I don't want to give up. I build custom only when the requirement genuinely has no Core Component equivalent, like a listing that derives its cards from the page tree. And even when I extend, I do it through a proxy so we control the version."

That one answer tells the interviewer you have worked on a real, maintained project rather than a tutorial.

1.4 The decision tree Here is what I would actually reason through, and it makes a good whiteboard answer:

Does a Core Component roughly do this already?

→ Yes, and I only need it to look different → Style System. No code at all. → Yes, but I need one or two extra fields → Extend via sling:resourceSuperType, add to the dialog via the Resource Merger. → Yes, but the markup is fundamentally wrong for our design system → Extend and override only the HTL. → No, this is genuinely new behaviour → Build custom, but still follow Core Component patterns.

Notice how far down "build custom" sits. That ordering is the answer.

1.5 A project description to adapt "On my current project — a global energy technology company's marketing site — we have around 45 components. About 25 are proxies to Core Components with small dialog additions, so we wrote almost no code for those. Around 10 are Core Components with custom HTL because the design system needed different markup. And about 8 are genuinely custom, mostly listings that derive their content from the page tree rather than being hand-authored, plus the components that integrate with our document publishing system."

That is strong because it shows proportion. A candidate who says "we built 45 custom components" is telling the interviewer their project has a maintenance problem.

  1. Core Concepts 2.1 The anatomy of a component — every file explained Let's open a real component folder and go through it file by file. You should be able to draw this from memory.

/apps/energy/components/cta/ │ ├── .content.xml ← the component definition itself ├── cta.html ← the HTL script that renders it │ ├── _cq_dialog/ │ └── .content.xml ← the author dialog (Touch UI) │ ├── _cq_editConfig.xml ← how it behaves in the page editor │ ├── _cq_template/ │ └── .content.xml ← default content when first dropped │ └── clientlibs/ ← optional CSS/JS just for this componentCopy Copy A note on the underscore naming. In the repository these nodes are called cq:dialog, cq:editConfig and cq:template. But a colon is not a legal filename character, so FileVault — the tool that serialises the repository to disk — writes them as _cq_dialog, _cq_editConfig, _cq_template.

Same node, different spelling depending on whether you are looking at CRXDE or your IDE. This confuses people constantly, and knowing it is a small credibility signal.

2.2 .content.xml — the component definition This file is what says "this folder is a component." Without it, the folder is just a folder.

<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="Call to Action" jcr:description="A single link styled as a button or arrow link" componentGroup="Energy - Content"/>Copy Copy Let me explain each property, and — more usefully — what breaks if you get it wrong.

jcr:primaryType="cq:Component" — this is what makes it a component. Missing or wrong, and AEM does not recognise the folder at all. It will never appear anywhere in the UI.

jcr:title — the name authors see in the component browser. Make it human-readable; authors are not developers.

jcr:description — the tooltip. Usually skipped, and authors quietly suffer for it.

componentGroup — which group the component appears under in the component browser. This one has behaviour worth knowing: if componentGroup is empty or set to .hidden, the component does not appear in the component browser at all.

That is deliberate, not a bug. It is how you hide components meant only for internal use — for example the individual item inside a carousel. An author should never drag a "carousel item" onto a page; only the carousel should create them.

This is a real interview question: "Your component doesn't show up in the component browser. Why?" Two answers: componentGroup is empty or .hidden, or the component is not in the allowed list of the template's policy. Give both and you have answered completely.

sling:resourceSuperType — the inheritance pointer, covered next. It is the single most important concept in modern component development.

2.3 The proxy component pattern — the most important concept here This gets its own section because it comes up in every AEM interview involving Core Components, and getting it right marks you as current rather than someone who learned AEM in 2016.

Start with the problem it solves.

Core Components live outside your project — their code sits under /apps/core/wcm/components/.... Now imagine an author drags Adobe's Accordion directly onto a page. The content node gets:

sling:resourceType = "core/wcm/components/accordion/v1/accordion"Copy Copy That resource type is now baked into hundreds of content nodes across your site. Three real problems follow:

One — you cannot customise anything. If the design system needs different markup or one extra field, you have nowhere to put it. You would have to edit Adobe's component, which lives outside your project.

Two — you do not control version upgrades. When Core Components ship v2 of the accordion, your content still says v1. Moving to v2 means rewriting every affected content node.

Three — your content depends on Adobe's internal paths. That is coupling you never want.

The solution is the proxy component. You create a tiny component in your own project that inherits:

/apps/energy/components/accordion/.content.xml

<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="Accordion" componentGroup="Energy - Content" sling:resourceSuperType="core/wcm/components/accordion/v1/accordion"/>Copy Copy That is the entire file. No HTL. No dialog. No Java. Just a pointer.

Now authors use your accordion, content stores energy/components/accordion, and look at what you gained:

Selective override. Want different markup? Add an accordion.html in your folder and Sling uses yours — but everything you did not override still comes from the Core Component.

Version control. Upgrading is a one-line change in the proxy. Not a single content node changes.

Decoupling. Your content depends only on your own paths.

The interview answer:

"A proxy component is a component in our own project whose only real content is a sling:resourceSuperType pointing at a Core Component — essentially an empty shell. We always do this for control. Content nodes then reference our resource type instead of Adobe's, so we can override the HTL or part of the dialog later without touching content, and upgrading Core Components from v1 to v2 becomes a one-line change in the proxy rather than a content migration across hundreds of nodes. Adobe's own documentation recommends never using Core Components directly."

How inheritance resolves. When Sling needs accordion.html for resource type energy/components/accordion:

  1. /apps/energy/components/accordion/accordion.html ← yours, if it exists
  2. → not found, so follow sling:resourceSuperType
  3. /apps/core/wcm/components/accordion/v1/accordion/accordion.html ← Adobe'sCopy Copy The same fallback applies to the dialog, the edit config, everything. That is why a proxy with a single .content.xml works perfectly — it all falls through to the parent.

2.4 The HTL file — how the script gets found The rendering script is an HTL file, and there is a naming rule that catches everyone once:

The HTL file must be named after the component's folder.

So /apps/energy/components/cta/ needs cta.html. Not index.html, not main.html.

Why? Go back to script resolution in file 01. One rule was "a script named after the last segment of the resource type." The resource type is energy/components/cta, last segment cta, so Sling looks for cta.html. It is the same rule you already learned, applied to components — not a special component rule.

2.5 The dialog — where authors actually work The dialog is the form an author fills in. It lives at _cq_dialog/.content.xml, written as a tree of Granite UI resource types.

Be honest with yourself: dialog XML is verbose and nobody memorises it. What interviewers test is whether you understand its structure and the handful of properties that cause bugs.

The structural skeleton. Every Touch UI dialog nests the same way:

cq:dialog └── content (container) └── items └── tabs (optional) └── items └── properties (one tab) └── items └── columns (layout) └── items └── your actual fieldsCopy Copy That is a lot of nesting for a text field, but there is a rhythm: container → items → container → items, repeating. Once you see it, you can read any dialog.

Now the properties that actually cause bugs.

name="./title" — the ./ prefix is not optional.

This is the single most common dialog bug in AEM, and it is asked about constantly.

./ means "relative to the current component's node." Without it, the value is written to the parent node instead. The dialog appears to save — no error, no warning — but your Sling Model reads nothing, because the property landed one level up.

So when asked "the author fills the dialog and clicks save, but the value doesn't appear", the first check is a missing ./.

required="{Boolean}true" — note the {Boolean} type hint. XML attributes are strings by default. {Boolean} tells FileVault to store a real boolean. Write required="true" without it and you store the string "true", which some Granite fields ignore. Same for {Long} on numbers.

fieldLabel and fieldDescription — the label and help text. Authors judge your component almost entirely on these.

emptyText — placeholder shown when empty.

The field types worth knowing by name:

What you need Resource type (under granite/ui/components/coral/foundation/form/ unless noted) Single line of text textfield Multiple lines textarea Rich text editor cq/gui/components/authoring/dialog/richtext Pick a page or asset pathfield Dropdown select Checkbox checkbox On/off toggle switch Number numberfield Date datepicker Radio buttons radiogroup Hidden value hidden Upload a file fileupload Repeatable set of fields multifield Tag picker cq/gui/components/coral/common/form/tagfield Colour picker colorfield 2.6 Multifields — the one authors always ask for A multifield lets an author add a repeating set of values — a list of links, a set of FAQ questions, a group of specifications. It appears in almost every project, and in interviews, because there are two kinds and candidates mix them up.

Simple multifield — one value per row.

Values are stored as a String array on one property.

Copy Copy Stored as:

applications = ["Utilities", "Railways", "Data Centers"] ← one multi-value propertyCopy Copy Read in Java as String[] or List.

Composite multifield — several fields per row.

Each row becomes its own child node.

Copy Copy Stored as:

faqs/ ├── item0/ │ ├── question = "What is a transformer in electricity?" │ └── answer = "

A transformer changes the voltage level...

" └── item1/ ├── question = "What do electrical transformers do?" └── answer = "

They transfer electrical energy between circuits...

"Copy Copy Read with @ChildResource into a List.

The three things that make composite multifields work, and that people forget:

composite="{Boolean}true" on the multifield. Without it, only the first field saves. The field node must be a container, not a form field. name="./faqs" goes on the container, and inner fields use plain relative names. The paths combine. The interview question: "Difference between simple and composite multifield?"

"A simple multifield stores a multi-value string property on the component node — one value per row. A composite sets composite=true and wraps its fields in a container, so each row becomes a child node like faqs/item0, faqs/item1, each holding several properties. I read the simple one as a String[], and the composite one with @ChildResource into a list of a nested model class."

2.7 cq:editConfig — behaviour in the editor This controls what happens in the page editor, not on the published page. It is optional, but there are things you can only do here.

<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" jcr:primaryType="cq:EditConfig" cq:actions="[text:CTA,-,edit,copymove,delete,insert]" cq:dialogMode="floating" cq:layout="editbar"> <cq:listeners jcr:primaryType="cq:EditListenersConfig" afteredit="REFRESH_SELF"/> </jcr:root>Copy Copy cq:actions — which buttons appear on the toolbar.

cq:listeners — what happens after an edit. REFRESH_PAGE reloads the whole page; REFRESH_SELF reloads just this component.

This matters for a real reason. By default AEM refreshes only the component. Fast, but if your component's change affects something outside itself — say a filter driving a list below it — the page looks wrong until the author reloads manually. afteredit="REFRESH_PAGE" fixes it.

That is genuinely good to mention in an interview, because it shows you have watched real authors use your component.

cq:dropTargets — lets an author drag an asset from the DAM onto the component. propertyName says where the path is written, accept restricts MIME types.

cq:inplaceEditing — lets the author edit text directly on the page instead of opening the dialog.

2.8 cq:template — default content When an author drops your component, sometimes you want sensible defaults rather than an empty box.

<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" jcr:primaryType="nt:unstructured" linkText="Explore more" style="arrow"/>Copy Copy These properties are copied onto the new node the moment the component is added.

Do not confuse this with a page template. Same word, completely different thing. cq:template inside a component is default content for that component. A page template — file 03 — defines a whole page's structure. Interviewers occasionally use this overlap as a trick question.

2.9 Component-level clientlibs You can put a clientlibs folder inside a component so its CSS and JS travel with it. Full detail is in file 04, but the relevant idea:

The advantage is cohesion — everything about the component lives in one folder, and deleting the component removes its styles too.

The catch is that a component clientlib still has to be loaded by something. It does not load automatically just because the component is on the page. Either the page component includes it by category, or you embed it into a site-wide bundle.

This trips people up: they put CSS in a component clientlib, see nothing, and assume it is broken. It is not broken — nobody asked for it.

2.10 Types of component Not every component is a box on a page. Categorising them shows architectural awareness.

Content components — the ordinary kind. CTA, text, image, teaser.

Container components — components holding other components. cq:isContainer="{Boolean}true", renders a parsys inside. Tabs and accordions are usually containers.

Structure components — the page component itself, header, footer. Placed by the template, not by authors.

Utility / hidden components — things like a carousel's individual item. They exist so a container can create them. That is exactly what componentGroup=".hidden" is for.

  1. Internal Working 3.1 What happens when an author drops a component Understanding this sequence explains several bugs at once.

sequenceDiagram participant A as Author (browser) participant E as Page Editor participant P as Sling Post Servlet participant J as JCR participant S as Sling render

A->>E: drags "CTA" into a container
E->>P: POST to the container path
P->>J: create node "cta"
P->>J: set sling:resourceType = energy/components/cta
P->>J: copy properties from cq:template
J-->>E: node created
E->>S: request the component HTML
S->>S: resolve resource type → find cta.html
S-->>E: rendered markup
E-->>A: component appears on the page

⛶ Fullscreen Now the author opens the dialog and saves:

sequenceDiagram participant A as Author participant D as Dialog participant P as Sling Post Servlet participant J as JCR A->>D: opens dialog, types link text D->>P: POST ./linkText=Explore more to the component node P->>J: write property "linkText" on that node J-->>P: saved P-->>D: success D->>A: dialog closes, component re-renders ⛶ Fullscreen Here is the insight worth carrying away: notice that you never wrote a servlet to save that dialog. AEM did not generate one either.

The Sling Post Servlet — from file 01 — handles it. It is Sling's built-in handler for POST to a repository path, and its default behaviour is "take the form fields and write them as properties on the node at this path."

That is why ./ matters so much. The POST goes to the component's node path, and ./linkText means "the property linkText on the node I'm posting to." Drop the ./ and the field name is addressed differently, so the value lands somewhere you did not intend.

This connects three things you now know: dialogs are HTML forms, the Sling Post Servlet writes them to the repository, and ./ is the addressing scheme. Explain it that way and it sounds like understanding, not memorisation.

3.2 How the resource super type chain resolves Let's make this precise, because interviewers push on it.

Say you have:

/apps/energy/components/accordion sling:resourceSuperType = "core/wcm/components/accordion/v1/accordion"Copy Copy and your folder contains only .content.xml and a custom accordion.html.

HTL script — yours is found immediately and used. Dialog — not found in your folder, so Sling follows sling:resourceSuperType and uses Adobe's. Edit config — same, Adobe's is used.

So you get Adobe's dialog and editing behaviour with your own markup. Selective override. That is the whole point.

The follow-up is almost always: "How do you add one extra field to an inherited dialog without copying the whole thing?"

The Sling Resource Merger — from file 01. You create a dialog containing only the new field, plus sling:orderBefore to position it. Sling merges your partial dialog with the inherited one instead of replacing it.

Copy Copy You can also remove an inherited field with sling:hideResource="{Boolean}true", or hide properties with sling:hideProperties.

Naming the Resource Merger and giving these three properties is a strong intermediate answer.

3.3 The decoration wrapper — where those extra divs come from You write clean HTL, and the rendered page has

elements you never wrote.

Those come from the decoration tag. AEM wraps each component in a div so the page editor has something to attach its toolbar and click handlers to.

Usually fine. But sometimes it breaks your CSS — particularly with flexbox and grid, where an unexpected wrapper changes the layout entirely.

Three ways to control it:

Per include, in HTL:

Copy Copy Always, on the component: cq:noDecoration="{Boolean}true" in .content.xml.

Change the tag rather than remove it:

Copy Copy That last one matters when the wrapper must be a valid child — inside a

    , a
    is invalid HTML but an
  • is correct.

    A caution worth mentioning: removing decoration entirely makes a component harder to select in the editor, because the editor loses the element it attaches to. Usually the better answer is changing the tag name, not removing the wrapper.

    1. Important Interview Questions 4.1 Basic Q1. What is a component in AEM?

    A component is a folder under /apps that contains everything needed to render one type of content and to let authors edit it: an HTL script, a dialog, and usually a Sling Model behind it. The component node has the type cq:Component. Content is linked to it through the sling:resourceType property, so every node that names the component is rendered by it.

    Cross: Where do they live? · What node type? (cq:Component) · What links content to a component?

    Q2. What files make up a component?

    A component folder usually contains .content.xml, which defines the component and its properties; an HTL file named after the folder, which renders it; and _cq_dialog, the form authors use to configure it. Optional parts include _cq_editConfig for editor behaviour, _cq_template for default values when the component is added, and a clientlib for its CSS and JavaScript. Strictly speaking, only .content.xml is mandatory.

    Cross: Why _cq_dialog on disk but cq:dialog in CRXDE? · Which are mandatory? (strictly, only .content.xml) · What if there is no dialog? (authors can't configure it)

    Q3. Why must the HTL file be named after the folder?

    Because of Sling's script resolution rules. One rule looks for a script named after the last segment of the resource type. For energy/components/cta, the last segment is cta, so Sling looks for cta.html in that folder. If the file has any other name, Sling will not find it unless another rule matches, such as a selector-specific script like print.html.

    Cross: What if you name it something else? (not found, unless another rule matches) · Can a component have several HTL files? (yes — selector-based, like print.html)

    Q4. What is componentGroup?

    componentGroup sets the group under which a component appears in the page editor's component browser, such as "Energy – Content". If the value is .hidden, the component never appears there, which is useful for helper components such as accordion items that should only be created by their parent. Note that a component can also be missing because the template's policy does not allow it.

    Cross: Why hide one deliberately? (helper components like accordion items) · Is that the only reason it might not appear? (no — template policy is the other)

    Q5. What is sling:resourceSuperType?

    sling:resourceSuperType is the inheritance pointer between components. When Sling cannot find a script, dialog or other file in a component, it looks in the component named by this property, and continues up the chain through as many levels as exist. This is what makes the proxy pattern possible: a proxy inherits everything from a Core Component and overrides only what it needs.

    Cross: Difference from sling:resourceType? · Can you inherit through several levels? (yes) · What does it enable? (the proxy pattern)

    Q6. What is cq:dialog?

    cq:dialog is the Touch UI dialog — the form authors fill in to configure a component. It is built from Granite UI field types arranged in containers, tabs and items, and each field writes its value to a property on the component's content node. It replaced the old Classic UI dialog, and in editable templates the old design dialog has been replaced by policies.

    Cross: What was the old one? (dialog, Classic UI — deprecated) · What is cq:design_dialog? (design dialog for static templates; replaced by policies in editable templates)

    Q7. What does ./ mean in name="./linkText"?

    It tells the Sling POST Servlet to store the value relative to the component's own content node, so ./linkText becomes the property linkText on that node. Without ./, the value is written somewhere else and the component silently reads nothing. The same servlet also understands suffixes such as @TypeHint to set a property's type and @Delete to remove a property.

    Cross: What processes that POST? (Sling Post Servlet) · What is @TypeHint? · How do you delete a property? (@Delete)

    Q8. What is a Core Component?

    Core Components are Adobe's library of ready-made, production-quality components, such as Title, Text, Image, Teaser and Container. They are accessible, well tested, versioned and released independently of AEM itself. Projects should not use them directly in content; instead, each one is wrapped in a project-specific proxy component that inherits from it.

    Cross: Where do they live? · Why version folders like v1, v2? · Should you use them directly? (no — proxy them)

    Q9. What is cq:editConfig for?

    cq:editConfig controls how a component behaves inside the page editor. It configures toolbar actions, listeners such as REFRESH_PAGE after an edit, drop targets for dragging assets onto the component, and in-place editing. It has no effect on the published page, and it is optional — most simple components do not need one.

    Cross: Mandatory? (no) · What does REFRESH_PAGE do and when do you need it? · What is cq:dropTargets?

    Q10. What is cq:template inside a component?

    cq:template inside a component holds default property values that are copied onto the new content node when an author first adds the component to a page. For example, it can pre-fill a heading level or a placeholder title. It is stored as _cq_template in the component folder and is completely unrelated to page templates.

    Cross: How is that different from a page template? (completely different thing) · Where does it live? (_cq_template in the component folder)

    4.2 Intermediate Q11. What is a proxy component and why do we use it?

    A proxy component is a small component in your project that inherits from a Core Component through sling:resourceSuperType, and content refers to the proxy instead of Adobe's path. There are three reasons to use one. You can selectively override parts, such as the HTL or the dialog. You control which version of the Core Component is used, because upgrading means changing one pointer. And your content is decoupled from Adobe's internal paths, so an upgrade never requires rewriting content.

    Cross: What's actually in the file? · How do you upgrade v1 to v2? · What if you skip the proxy?

    Q12. How do you add one field to an inherited dialog without copying it?

    Use the Sling Resource Merger. Create a dialog in your component that contains only the new field, and position it with sling:orderBefore; everything else is inherited from the parent dialog. You can remove an inherited field with sling:hideResource. This is better than copying the dialog, because a copy stops receiving Adobe's improvements and has to be maintained by hand.

    Cross: Which mount path? (/mnt/overlay) · How do you reorder? · Why not just copy the dialog? (you stop receiving Adobe's improvements)

    Q13. What is the difference between a simple and a composite multifield?

    A simple multifield repeats a single field, and its values are stored as one multi-value property on the component node — for example, a list of tags. A composite multifield repeats a group of fields, such as a question and an answer, and stores each entry as a child node named item0, item1 and so on. It needs composite="{Boolean}true"; without it, only the first field of each entry is saved. In a Sling Model you read the first with @ValueMapValue as an array or list, and the second with @ChildResource.

    Cross: How do you read each in a model? · What are the child nodes called? (item0, item1, …) · What breaks without composite=true? (only the first field saves)

    Q14. What is the difference between extending and overlaying a component?

    Extending means creating a new component that inherits from another through sling:resourceSuperType; the change affects only your new component. Overlaying means placing a file under /apps at the same relative path as one in /libs, which replaces the product version everywhere. Extend by default, and overlay only when you must customise AEM's own authoring interface, because every overlay has to be checked on each upgrade.

    Cross: When would you ever overlay? (adding a tab to page properties) · Which is safer on upgrade? · How does the Resource Merger relate to each?

    Q15. Where does component logic go?

    Component logic belongs in a Sling Model in the core bundle, not in the HTL. HTL is deliberately restricted — it cannot run arbitrary Java — which keeps templates simple and secure. The HTL calls the model with data-sly-use, and the model contains the logic in a plain Java class that can be unit tested with AEM Mocks.

    Cross: Why can't you write logic in HTL? (by design, for security and testability) · What came before Sling Models? (WCMUsePojo, then JSP) · How do you unit test it?

    Q16. Why do extra div elements appear around my component?

    They are decoration tags, which AEM adds around each component so the editor can select it and so the Style System has an element to apply classes to. You can change the element with cq:htmlTag or decorationTagName — for example to li inside a list — or remove it with cq:noDecoration or decoration=false. Removing it entirely makes the component harder to select in the editor, so it is rarely the right choice.

    Cross: Why not always disable it? (harder to select in the editor) · When would you change the tag? (inside a

      )

      Q17. How do you make a component available on a page?

      Two layers must both allow it. First, the component needs a visible componentGroup, so it is not hidden. Second, the template's policy on the container where the author wants to place it must list the component, or its group, as allowed. The policy is the deciding layer: if it does not allow the component, authors never see it, regardless of anything in the component itself.

      Cross: Where are policies stored? (under /conf//settings/wcm/policies) · Can two templates allow different components? (yes — each has its own policies) · What controls which templates can be used under a page? (cq:allowedTemplates)

      Q18. What is cq:isContainer?

      cq:isContainer marks a component as one that can hold other components, so the editor treats it as a drop zone. Examples are the Layout Container, Tabs and Accordion. What authors can place inside it is controlled by a policy on the container, and the container renders its children with data-sly-resource over its child resources.

      Cross: Example? (Layout Container, tabs, accordion) · How do you restrict what goes inside? (a policy on the container) · How do children render? (data-sly-resource over child resources)

      Q19. How does in-place editing work?

      In-place editing lets authors edit text directly on the page instead of in a dialog. It is configured in cq:editConfig under cq:inplaceEditing, with an editor type such as text for rich text, or title or plaintext for simple text. The value is saved through the same Sling POST Servlet as a dialog. You might disable it for rich text that must follow strict formatting rules.

      Cross: How does the value save? (same Sling Post Servlet) · Why might you disable it? (rich text with strict formatting rules)

      Q20. How do you version a component?

      Follow the Core Components convention: each version lives in its own folder, such as v1 and v2, and the proxy component points at the version you want. Existing content refers to the proxy, so it keeps working. You never change a released version in place, because existing content depends on its markup and properties; a breaking change becomes a new version, with a content migration if pages should move to it.

      Cross: Why not edit in place? (you'd break existing content) · How do you migrate content between versions?

      4.3 Advanced Q21. How would you build a listing component whose items come from the page tree rather than being hand-authored?

      Work through four decisions.

      Traverse or query. If the items live under one known parent page, traverse its children: it needs no index and is predictable. If they can be anywhere and are selected by tag, you must query, which means confirming an Oak index exists and always setting a limit. Where the work happens. Build the list once in @PostConstruct, not in a getter that HTL might call repeatedly inside a loop. How much renders on the server. For a long list, render the first batch on the server so the page is fast and crawlable, and load the rest through a servlet. Keep that servlet cacheable by registering it by resource type with a selector and passing the batch number as a suffix, not a query parameter. Invalidation. A derived listing goes stale when a child page is published, so add a flush rule that invalidates the listing page when its children are activated. Cross: Query or traverse — how do you decide? · Is your query indexed and how do you know? · Why a suffix and not a query parameter? · What happens when an author adds a new product?

      Q22. A component works on author but is blank on publish. How do you debug it?

      Work through these checks in order:

      Read error.log on publish first. A model using the optional injection strategy renders nothing instead of throwing, so the real error may only appear in the log. Confirm that everything the component references was activated — assets, fragments and, for listings, the child pages themselves. Check that the anonymous user can read every path the component reads. Check that the component's clientlib is loading, including allowProxy on the clientlib folder. Check whether the model depends on something that only exists on author. Cross: Why does OPTIONAL hide the problem? · How do you test as anonymous? (private window, direct publish URL) · What does the dispatcher have to do with it?

      Q23. How do you make a component accessible?

      Use semantic HTML rather than generic div elements, correct heading levels, and alt text supplied through a dialog field. Add ARIA attributes only where native semantics are not enough, make every interactive element usable with the keyboard, and ensure sufficient colour contrast. Extending Core Components also helps, because they are built to WCAG standards, so you inherit that work. Test with tools such as axe and Lighthouse, and by navigating with the keyboard alone.

      Cross: How do you test? (axe, Lighthouse, keyboard-only) · Whose job is alt text? (developer provides the field, author fills it, component handles it being empty) · What is a decorative image?

      Q24. How do you handle a component that must render differently on mobile?

      Prefer CSS and responsive design. If the markup genuinely has to differ, use the Style System or an author-controlled option — never server-side user-agent detection. Device detection on the server breaks dispatcher caching: either every device variant needs its own cache entry, or one variant is cached and served to everyone.

      Cross: Why is server-side device detection dangerous with a cache? · What is the Style System? · How do responsive images work in AEM?

      Q25. How do you unit test a component?

      Test the Sling Model, because that is where the logic lives; HTL itself is not practical to unit test. Use JUnit 5 with AEM Mocks: load test content from a JSON file into AemContext, register the model and any services it needs, adapt a resource to the model, and assert on its output. Services the model depends on can be replaced with mocks using context.registerService.

      Cross: What is AemContext? · How do you mock an injected OSGi service? (context.registerService) · What does Cloud Manager's quality gate check?

      Q26. How would you let authors change a component's appearance without new components?

      Use the Style System. Style options are defined in the component's policy, the author picks one from the component's toolbar, and AEM adds the corresponding CSS class to the component's wrapper element. No code change or new component is needed, and the available styles are controlled centrally per template rather than per component instance.

      Cross: Where are styles defined? (in the policy) · How is the class applied? (on the decoration wrapper) · Why better than a dialog dropdown? (controlled centrally per template, no code)

      Q27. What is the Sling Model Exporter and why does it matter here?

      The Sling Model Exporter serialises a Sling Model to JSON, available at .model.json, by adding @Exporter(name = "jackson", extensions = "json") to the model. It is how a component's authored content reaches a single-page application or a mobile app. Unlike Content Fragments delivered through GraphQL, it follows the structure of the page and its components.

      Cross: Which selector and extension does it use? (model and json) · How does it relate to the SPA Editor? (the SPA Editor renders pages from this JSON) · How does it differ from Content Fragments with GraphQL? (page-shaped output versus structured content)

      Q28. How do you handle validation in a dialog?

      Granite UI provides basic validation on fields, such as required="{Boolean}true". For anything more, register a custom validator in JavaScript with the Granite UI validation registry, targeting a field by a validation attribute or a selector. The script is placed in a clientlib with the category cq.authoring.dialog so it loads only in the editor. A custom validator can also compare two fields.

      Cross: Where does the JavaScript live? (a clientlib with the category cq.authoring.dialog) · How do you show the error message? (return it from the validator function) · Can you validate across two fields? (yes, with a custom validator)

      4.4 Scenario based Q29. An author saves the dialog, but nothing appears on the page. What do you check?

      Check, in order: a field name missing its ./ prefix; the model reading a property with a different name; an optional injection hiding a failure; and the page being served from the dispatcher cache. Then open the node in CRXDE Lite. That single check splits the problem cleanly: if the property is missing, the dialog did not save it; if it is there, the component is not reading it.

      Q30. A component works on one template but not on another. Why?

      Almost certainly because of the template's policy. Each template has its own policies, and on the second template the container's policy does not list this component, or its group, as allowed. Check the policy for that container in the template editor rather than changing the component.

      Q31. After upgrading Core Components, some pages broke. What went wrong?

      The content referred to Adobe's resource type directly instead of a proxy component, so it was tied to a specific version, and the upgrade changed the markup or property names. The immediate fix is to roll back the version or migrate the affected content. The long-term fix is to introduce proxy components and point the content at them, so future upgrades are a deliberate change to one pointer.

      Q32. Authors say a component is confusing to use. How do you improve it?

      Improve the dialog rather than dismiss the feedback. Add help text with fieldDescription, group related fields into clear tabs, show examples with emptyText placeholders, provide sensible defaults through cq:template, and mark genuinely required fields as required, so authors get immediate feedback instead of a blank component on the page.

      Q33. The same component must look different for three product lines. How do you build it?

      Use the Style System with different style definitions in each template's policy: one component, three policies. Do not build three separate components, which triples the maintenance, and do not add a "brand" dropdown to the dialog, which spreads a styling decision into every content instance.

      Q34. A component has made the page very slow. How do you fix it?

      Measure first, then fix the cause. The usual culprits are a synchronous external API call during rendering, a query without an index, or an expensive getter called repeatedly from an HTL loop. The fixes are, respectively, to cache the external data or load it client-side, to add an index, and to do the work once in @PostConstruct.

      4.5 Production support Q35. A component appears in the component browser, but authors cannot drop it onto the page. Why?

      Either the policy of the container they are dropping into does not allow that component, or the target area is not actually a container. Check the container's policy in the template editor; the component browser shows every component allowed anywhere on the page, not only in a specific container.

      Q36. A component renders, but without any styling. What do you check?

      Check that the component's clientlib is actually loaded on the page. The usual causes are that the clientlib's category is not included by the page or template, that allowProxy is not set to true on the clientlib folder (so the browser cannot fetch it from /etc.clientlibs on publish), or a typo in the category name.

      Q37. A component still shows old content after an author edits it. Why?

      On publish, the page is probably being served from the dispatcher cache and has not been invalidated. In the editor, the component may need REFRESH_PAGE in its cq:editConfig listeners, because by default the editor refreshes only the component itself and can show a stale fragment when the change affects other parts of the page.

      Q38. A component's dialog will not open. Where do you start?

      Open the browser console first. The usual causes are malformed dialog XML, a field with an incorrect sling:resourceType, or a JavaScript error in a custom validator clientlib that stops the dialog from initialising.

      Q39. A component works locally but not on the server. What do you check?

      Check that the package's filter.xml actually includes the component's path, so it was deployed at all, and that the Java bundle deployed and is Active in the Felix console. A path missing from the filter is easy to overlook, because it works locally where the files were created by hand or by an earlier build.

      1. Cross Questions — how this topic gets drilled Thread A — from "tell me about a component you built" What did the dialog look like? → Multifield? → Simple or composite? → How did you read it in the model? → What node structure resulted? → What if the author adds fifty rows? → How did you validate? → What happened when a field was empty?

      Thread B — from "do you use Core Components?" Which ones? → Directly or proxied? → What is a proxy? → What's in the file? → How do you override just the markup? → How do you add one dialog field? → What is the Resource Merger? → How do you hide an inherited field? → How would you upgrade v1 to v2?

      Thread C — from "how does a dialog save data?" What handles the POST? → What is the Sling Post Servlet? → What does ./ mean? → What happens without it? → What is @TypeHint? → How do you delete a property? → How does a composite multifield become child nodes?

      Thread D — from "how do you decide to build a custom component?" Why not Core Components? → What can the Style System do instead? → Where are styles defined? → What if the markup must differ, not just CSS? → Extend or overlay? → Which is safer on upgrade?

      1. Best Interview Answers 6.1 "Walk me through how you build a component" — about 2 minutes "I'd start by asking whether I should build one at all. My first check is whether a Core Component already does most of it, because extending one gives me accessibility and Adobe's ongoing maintenance for free.

      Assuming I do need one, I create a folder under /apps//components/. The .content.xml marks it as a cq:Component with a title, description and componentGroup — and if it's a helper component authors shouldn't place directly, I set the group to .hidden.

      If I'm extending a Core Component, that same file gets sling:resourceSuperType, and often that's the entire component — a proxy with no other files, inheriting everything.

      The rendering script is an HTL file named after the folder, because Sling's script resolution looks for a script matching the last segment of the resource type. The HTL stays presentation-only; logic goes into a Sling Model in the core bundle that HTL calls with data-sly-use, which keeps it unit-testable.

      The dialog goes in _cq_dialog as Granite UI resource types — containers and items nested down to the fields. The detail I'm careful about is the ./ prefix on every field name, because without it the Sling Post Servlet writes the value to the parent node and the component silently reads nothing.

      Then optionally _cq_editConfig — I'll add REFRESH_PAGE if editing this component changes something outside itself — and _cq_template for sensible defaults.

      Finally a unit test on the Sling Model with AEM Mocks, because Cloud Manager's quality gate checks coverage on new code."

      6.2 The graded answer — simple, medium, complex Interviewers frequently ask for a component story twice: "give me a simple one first", then "now the most complex thing you've built." Here are all three, at the right length.

      Simple — about 1 minute

      "The simplest one I own is our CTA component. It's a single link that renders either as a button or as an arrow link — the pattern you see all over the site as 'Read the Press Release' or 'Explore more' with a chevron.

      Three dialog fields: link text, a pathfield for the target, and a checkbox for opening in a new tab. Plus a style dropdown for button versus arrow link.

      It sounds trivial but it has three details I'd call out. The link can point at an internal page or a PDF in our document publishing system, so the model resolves internal paths through resourceResolver.map() to get the short URL, and leaves external ones alone. Second, when the author ticks 'open in new tab' the model emits rel="noopener noreferrer" alongside target="_blank", because without it the opened page can manipulate window.opener. Third, the checkbox needed an uncheckedValue, because otherwise unticking it deletes the property rather than setting it false, which isn't the same thing to the model.

      The value of building it was consistency — it replaced about six hand-coded link variants across the site with one component the design system controls."

      Medium — about 2 minutes

      "A step up is our FAQ accordion. Product category pages have FAQ sections — things like 'What is a transformer in electricity?' — and they're written by the content team primarily for SEO, because industrial buyers search technical questions.

      The dialog is a composite multifield: each row has a question as a textfield and an answer as rich text. Composite matters here — with composite=true and the field node as a container, each row becomes a child node like faqs/item0 with both properties on it, rather than a flat string array. In the model I read those with @ChildResource into a list of a small nested model class.

      Two things made it more interesting than it looks. First, accessibility — an accordion has a real ARIA contract. Each header is a button with aria-expanded and aria-controls, each panel has an id and role="region" with aria-labelledby back to the button. That means generating stable unique IDs, and they have to stay unique when there are two accordions on one page, so I derived them from the component's own path rather than a counter.

      Second, SEO — since these exist for search, I emit FAQPage structured data as JSON-LD alongside the markup, so the questions can appear as rich results. That needed the answer's rich text stripped of HTML for the structured data while keeping it for display.

      The result was that FAQ content started appearing as expandable results in search, which was the whole point of the requirement."

      Complex — about 3 minutes. This is section 7's flagship story; see below.

      6.3 "Tell me about a complex component you developed recently" — the full answer The structure that works: requirement → what made it complex → approach → the hard part → outcome.

      The critical move is "what made it complex." Do not describe a component with lots of fields — that is tedious, not complex. Name a genuine technical challenge: an integration, a caching conflict, a performance problem, or an authoring constraint fighting the technical design.

      Here it is delivered in full:

      "The most involved one is our product category listing — the component behind the main Products and Solutions landing page.

      The requirement looked simple: show product categories as cards, grouped into three tabs — products and systems, software, and solutions — with a Load More button because there are more than twenty categories in some tabs.

      What made it complex was that the cards can't be hand-authored. We have hundreds of product category pages and the portfolio changes constantly. If an author had to add a card manually every time marketing launched a category, it would be permanently out of date. So the cards are derived from the page tree — each card is a child page under a configurable root, with its title, description and thumbnail read from that page's properties, filtered by tag.

      That created three problems that fought each other.

      First, the query. My initial version ran a repository-wide QueryBuilder query filtered by tag. It worked locally on a small content set and was slow on stage. Checking the Query Performance tool showed it wasn't hitting an index cleanly and Oak was traversing. I ended up restricting the search to a known path so the traversal was bounded, added a proper limit, and where a genuine tag query was needed we deployed a custom index. The lesson I'd give anyone is that a query that works on a laptop tells you nothing.

      Second, the Load More. The obvious implementation fetches the next batch with a query parameter — something like ?offset=20. But query parameters aren't part of the URL path, so the dispatcher won't cache those responses, and every Load More click would go through to publish. Instead I registered a servlet by resource type with a cards selector and passed the batch number as a suffix — so the URL is a stable path that the dispatcher caches like any other file. Same data, but cacheable.

      Third, invalidation. Because the listing is derived, publishing a new product page doesn't tell the listing page's cache anything — the listing stays stale until something else flushes it. We handled that with a flush rule so activating a child page invalidates the parent listing.

      The hardest part was actually the first batch. For SEO and first paint, the initial set of cards has to be in the server-rendered HTML — a crawler won't click Load More. But the later batches come from the servlet. So the same card markup had to render two ways, from HTL server-side and from the JSON response client-side, and they had to be identical or the grid visibly broke at the boundary. I solved it by having the servlet return the rendered HTML fragment rather than raw JSON, so there was exactly one template for a card.

      The outcome was that the listing page stayed fully dispatcher-cacheable including the Load More responses, the page stopped going stale when the portfolio changed, and the content team stopped raising tickets to add cards — the page just reflects whatever they publish."

      Why this answer works. It names a specific, believable requirement. It identifies genuine trade-offs rather than listing features. It shows architectural judgment — the suffix-versus-query-parameter decision is exactly the thinking file 01 taught. It includes a mistake the candidate learned from, which reads as honest. And every sentence invites a follow-up you can answer.

      1. Real Project Examples Three stories, one per level, all from the same coherent project. Pick the ones you can defend and make them yours.

      Story 1 (Simple) — the CTA component Requirement. Product and news pages needed consistent call-to-action links — "Read the Press Release", "Read the Customer Success Story", "Explore more" — some rendered as buttons, some as arrow links.

      Why it was worth building. Before it existed, these were hand-coded inside other components and inside rich text, so there were about six visual variants and none of them matched the design system exactly. Accessibility was inconsistent too — some were

      elements with click handlers rather than real links, so keyboard users couldn't reach them.

      Approach. One component, four dialog fields: link text, target path, open-in-new-tab, and a style dropdown for button versus arrow. The model resolves internal paths through resourceResolver.map() so links render as short URLs rather than /content/... paths, and leaves external URLs untouched. Links to the document publishing system open in a new tab by default and get a file-type indicator.

      The details that mattered. Always a real element, never a div with a click handler. rel="noopener noreferrer" whenever target="_blank". And uncheckedValue on the checkbox, because without it unticking the box deletes the property rather than setting it false — which the model can't distinguish from "never configured."

      Result. Six variants collapsed into one component. Accessibility audit findings on link semantics went to zero, and the design team could change CTA styling in one place.

      Story 2 (Medium) — the FAQ accordion Requirement. Product category pages needed FAQ sections. The content team writes them for SEO, because industrial buyers search technical questions like "what does an electrical transformer do."

      What made it more than trivial. Three things: a composite multifield with rich-text answers, a real accessibility contract, and structured data for search.

      Approach. A composite multifield where each row is a question textfield and a rich-text answer. Read in the model with @ChildResource into a list of a nested model class.

      The hard parts.

      Accessibility. An accordion has a defined ARIA pattern — each header is a

</jcr:root>Copy Copy The three details to point at in an interview:

Every name starts with ./. The select field has an node where each child has text and value, with selected="{Boolean}true" marking the default. And the checkbox has both value and uncheckedValue — without uncheckedValue, unticking the box removes the property entirely rather than setting it to false, and a missing property is not the same as false to your model.

8A.3 Sling Model core/src/main/java/com/energy/core/models/CtaModel.java

package com.energy.core.models;

import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.models.annotations.DefaultInjectionStrategy; import org.apache.sling.models.annotations.Model; import org.apache.sling.models.annotations.injectorspecific.SlingObject; import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

import javax.annotation.PostConstruct; import org.apache.commons.lang3.StringUtils;

@Model( adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL ) public class CtaModel {

@ValueMapValue
private String linkText;

@ValueMapValue
private String linkUrl;

@ValueMapValue
private String style;

@ValueMapValue
private boolean newTab;

@SlingObject
private ResourceResolver resourceResolver;

private String resolvedUrl;
private boolean openInNewTab;

@PostConstruct
protected void init() {
    this.resolvedUrl   = resolveUrl(linkUrl);
    this.openInNewTab  = newTab || isDocumentLink(linkUrl);
}

/**
 * Internal /content paths are shortened via resourceResolver.map() so the
 * rendered link matches the public URL. External URLs pass through unchanged.
 */
private String resolveUrl(String url) {
    if (StringUtils.isBlank(url)) {
        return null;
    }
    if (url.startsWith("/content")) {
        String mapped = resourceResolver.map(url);
        // Internal pages need the .html extension; assets already have one
        return url.startsWith("/content/dam") ? mapped : mapped + ".html";
    }
    return url;
}

private boolean isDocumentLink(String url) {
    if (StringUtils.isBlank(url)) {
        return false;
    }
    String lower = url.toLowerCase();
    return lower.endsWith(".pdf") || lower.endsWith(".docx") || lower.endsWith(".xlsx");
}

public String getLinkText() {
    return StringUtils.defaultString(linkText);
}

public String getLinkUrl() {
    return resolvedUrl;
}

public String getStyle() {
    return StringUtils.defaultIfBlank(style, "arrow");
}

public String getTarget() {
    return openInNewTab ? "_blank" : null;
}

/**
 * Security: target="_blank" without rel="noopener" lets the opened page
 * manipulate this one through window.opener.
 */
public String getRel() {
    return openInNewTab ? "noopener noreferrer" : null;
}

/** Render nothing at all unless we have both a label and a destination. */
public boolean isReady() {
    return StringUtils.isNotBlank(linkText) && StringUtils.isNotBlank(resolvedUrl);
}

}Copy Copy 8A.4 HTL /apps/energy/components/cta/cta.html

${cta.linkText}

Copy Copy Two details worth explaining. Returning null from getTarget() and getRel() means HTL omits the attribute entirely rather than rendering target="" — cleaner markup and valid HTML. And the arrow icon is aria-hidden="true" because it is decorative; a screen reader announcing "right arrow" after every link is noise.

8B. MEDIUM — the FAQ accordion 8B.1 Dialog with a composite multifield /apps/energy/components/faq/_cq_dialog/.content.xml (fields only, skeleton omitted for brevity)

Copy Copy 8B.2 The nested row model core/src/main/java/com/energy/core/models/FaqItemModel.java

package com.energy.core.models;

import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.resource.Resource; import org.apache.sling.models.annotations.DefaultInjectionStrategy; import org.apache.sling.models.annotations.Model; import org.apache.sling.models.annotations.injectorspecific.SlingObject; import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

@Model( adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL ) public class FaqItemModel {

@ValueMapValue
private String question;

@ValueMapValue
private String answer;

@SlingObject
private Resource resource;

public String getQuestion() {
    return StringUtils.defaultString(question);
}

public String getAnswer() {
    return StringUtils.defaultString(answer);
}

/**
 * IDs are derived from this row's own repository path, not a counter.
 *
 * Why: a counter restarts at 0 for every accordion, so two accordions on
 * one page would both emit id="faq-0" -- duplicate IDs break the
 * aria-controls / aria-labelledby relationships and are invalid HTML.
 * The resource path is unique per row across the whole page.
 */
public String getId() {
    return "faq-" + Math.abs(resource.getPath().hashCode());
}

public String getButtonId() {
    return getId() + "-button";
}

public String getPanelId() {
    return getId() + "-panel";
}

/** Plain text version for JSON-LD, which must not contain markup. */
public String getPlainAnswer() {
    return getAnswer().replaceAll("<[^>]+>", "").trim();
}

public boolean isValid() {
    return StringUtils.isNotBlank(question) && StringUtils.isNotBlank(answer);
}

}Copy Copy 8B.3 The parent model core/src/main/java/com/energy/core/models/FaqModel.java

package com.energy.core.models;

import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.resource.Resource; import org.apache.sling.models.annotations.DefaultInjectionStrategy; import org.apache.sling.models.annotations.Model; import org.apache.sling.models.annotations.injectorspecific.ChildResource; import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

import javax.annotation.PostConstruct; import java.util.Collections; import java.util.List; import java.util.stream.Collectors;

@Model( adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL ) public class FaqModel {

private static final ObjectMapper MAPPER = new ObjectMapper();

@ValueMapValue
private String heading;

@ValueMapValue
private String headingLevel;

@ValueMapValue
private boolean emitStructuredData;

// Each multifield row is a child node under "faqs",
// so we inject them as a list of the nested model.
@ChildResource
private List<FaqItemModel> faqs;

private List<FaqItemModel> validFaqs;
private String structuredData;

@PostConstruct
protected void init() {
    // Build ONCE here. HTL may call getFaqs() repeatedly inside a loop.
    this.validFaqs = (faqs == null)
            ? Collections.emptyList()
            : faqs.stream()
                  .filter(FaqItemModel::isValid)
                  .collect(Collectors.toList());

    this.structuredData = emitStructuredData ? buildJsonLd() : null;
}

/**
 * Builds schema.org FAQPage JSON-LD so questions can appear as rich results.
 *
 * Built with Jackson rather than string concatenation on purpose: author
 * answers routinely contain quotes and apostrophes, and hand-built JSON
 * breaks the moment one appears.
 */
private String buildJsonLd() {
    if (validFaqs.isEmpty()) {
        return null;
    }
    try {
        ObjectNode root = MAPPER.createObjectNode();
        root.put("@context", "https://schema.org");
        root.put("@type", "FAQPage");

        ArrayNode entities = root.putArray("mainEntity");
        for (FaqItemModel faq : validFaqs) {
            ObjectNode q = entities.addObject();
            q.put("@type", "Question");
            q.put("name", faq.getQuestion());

            ObjectNode a = q.putObject("acceptedAnswer");
            a.put("@type", "Answer");
            a.put("text", faq.getPlainAnswer());
        }
        return MAPPER.writeValueAsString(root);
    } catch (JsonProcessingException e) {
        // Never let SEO markup break the page
        return null;
    }
}

public String getHeading() {
    return StringUtils.defaultString(heading);
}

public String getHeadingLevel() {
    return StringUtils.defaultIfBlank(headingLevel, "h2");
}

public List<FaqItemModel> getFaqs() {
    return validFaqs;
}

public String getStructuredData() {
    return structuredData;
}

public boolean isReady() {
    return !validFaqs.isEmpty();
}

}Copy Copy 8B.4 HTL with the full ARIA contract /apps/energy/components/faq/faq.html

<sly data-sly-element="${faq.headingLevel}" data-sly-test="${faq.heading}">
    <span class="cmp-faq__heading">${faq.heading}</span>
</sly>

<dl class="cmp-faq__list">
    <sly data-sly-list.item="${faq.faqs}">

        <dt class="cmp-faq__question">
            <button class="cmp-faq__button"
                    type="button"
                    id="${item.buttonId}"
                    aria-expanded="false"
                    aria-controls="${item.panelId}">
                <span class="cmp-faq__question-text">${item.question}</span>
                <span class="cmp-faq__indicator" aria-hidden="true"></span>
            </button>
        </dt>

        <dd class="cmp-faq__answer"
            id="${item.panelId}"
            role="region"
            aria-labelledby="${item.buttonId}"
            hidden>
            ${item.answer @ context='html'}
        </dd>

    </sly>
</dl>

Copy Copy The accessibility contract, spelled out — this is what you explain in the interview:

Attribute Why it's there

}Copy Copy 8C.2 The servlet — why a selector and a suffix, not a query parameter package com.energy.core.servlets;

import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.servlets.SlingSafeMethodsServlet; import org.apache.sling.servlets.annotations.SlingServletResourceTypes; import org.osgi.service.component.annotations.Component;

import javax.servlet.Servlet; import java.io.IOException;

/**

SlingSafeMethodsServlet because this is read-only — it only handles GET and HEAD. Extending SlingAllMethodsServlet when you only serve GET is a code-review comment, because you are advertising POST/PUT/DELETE you never implemented. (Full detail in file 07.)

Registered by resource type, not by path. A path-bound servlet needs whitelisting in the dispatcher and bypasses ACLs on the resource. Resource-type binding means the URL is a real content path, so permissions apply naturally.

The batch number is clamped. parseBatch never trusts the suffix — an unbounded value from a URL is how someone makes your server build a 10,000-item list.

8C.3 The Load More markup

<div class="cmp-listing__grid" data-listing-grid>
    <sly data-sly-list.card="${listing.cards}"
         data-sly-resource="${card.path @ resourceType='energy/components/card'}"/>
</div>

<button class="cmp-listing__more"
        type="button"
        data-sly-test="${listing.hasMore}"
        data-load-more>
    Load more
</button>

<!-- Announces new results to screen readers when the grid updates -->
<div class="sr-only" role="status" aria-live="polite" data-listing-status></div>
Copy Copy The aria-live region is the detail worth mentioning. When Load More injects cards, a sighted user sees them appear. A screen reader user gets nothing unless you announce it. One role="status" element that you update with "12 more results loaded" makes the whole pattern accessible, and almost nobody thinks of it.

8C.4 Unit test — testing what actually breaks package com.energy.core.models;

import io.wcm.testing.mock.aem.junit5.AemContext; import io.wcm.testing.mock.aem.junit5.AemContextExtension; import org.apache.sling.api.resource.Resource; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith;

import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(AemContextExtension.class) class FaqModelTest {

private final AemContext context = new AemContext();

@BeforeEach
void setUp() {
    context.load().json("/faq/content.json", "/content");
    context.addModelsForClasses(FaqModel.class, FaqItemModel.class);
}

@Test
void returnsEmptyListWhenNoQuestionsAuthored() {
    FaqModel model = adapt("/content/faq-empty");
    assertNotNull(model);
    assertTrue(model.getFaqs().isEmpty(), "must return empty list, never null");
    assertFalse(model.isReady());
}

@Test
void skipsRowsWhereAuthorLeftTheAnswerBlank() {
    // Authors WILL do this. It is the most common real-world case.
    FaqModel model = adapt("/content/faq-partial");
    assertEquals(1, model.getFaqs().size(),
            "rows with a blank answer must be filtered out");
}

@Test
void generatesUniqueIdsAcrossTwoAccordionsOnOnePage() {
    // The bug that a counter-based ID would cause.
    FaqModel first  = adapt("/content/page/first-faq");
    FaqModel second = adapt("/content/page/second-faq");

    String firstId  = first.getFaqs().get(0).getPanelId();
    String secondId = second.getFaqs().get(0).getPanelId();

    assertNotEquals(firstId, secondId,
            "two accordions on one page must not emit duplicate IDs");
}

@Test
void structuredDataIsOmittedWhenDisabled() {
    FaqModel model = adapt("/content/faq-no-seo");
    assertNull(model.getStructuredData());
}

private FaqModel adapt(String path) {
    Resource resource = context.resourceResolver().getResource(path);
    assertNotNull(resource, "test content missing at " + path);
    return resource.adaptTo(FaqModel.class);
}

}Copy Copy Note what is being tested. Not "does the getter return the field" — that tests nothing. These cover the cases authors actually create: a blank component, a half-filled row, two accordions on one page. Those are the paths that break in production, and testing them is what makes a test worth writing. Say that in an interview and it separates you immediately.

  1. Common Mistakes The mistake What actually happens The fix Missing ./ in a dialog field name Value written to the parent node; component silently reads nothing Always name="./propertyName" Forgetting composite="{Boolean}true" Only the first field in each row saves Add it, and make the field node a container required="true" without {Boolean} Stored as a string; some fields ignore it Use {Boolean}true Checkbox with no uncheckedValue Unticking deletes the property rather than setting false Set both value and uncheckedValue HTL file not named after the folder Sling never finds the script Name it exactly after the folder Using a Core Component directly Adobe's resource type baked into content; upgrades become migrations Always create a proxy Overlaying /libs instead of extending Changes behaviour globally; breaks on upgrade Use sling:resourceSuperType Business logic in HTL Untestable, and HTL can't express it well anyway Sling Model Expensive work in a getter Called repeatedly from HTL loops @PostConstruct, store in a field Returning null from a getter HTL renders "null", or you need guards everywhere Empty string / empty list Counter-based IDs in a repeating component Duplicate IDs when two instances are on one page Derive from the resource path Unbounded listChildren() Builds a huge list; can take the instance down Hard ceiling constant, always Trusting a value from a suffix or parameter Someone requests batch 99999 Parse and clamp Query parameters for pagination Dispatcher won't cache; every click hits publish Selector plus suffix No data-sly-test guard Unconfigured components render empty divs that break layouts Guard the outer element Empty componentGroup by accident Component never appears in the browser Set a real group, or .hidden deliberately Building a component for a styling variant Component sprawl and duplicated maintenance Style System No fieldDescription Authors misuse the component and raise tickets Help text on every non-obvious field
  2. Best Practices On deciding what to build. Default to Core Components. Extend through a proxy. Reach for the Style System before building a variant. Build fully custom only when nothing fits — and expect to justify it.

On structure. One component, one job. Fifteen dialog fields across six tabs usually means two components. Keep the folder self-contained.

On the dialog. Every field gets a fieldLabel and, unless obvious, a fieldDescription. Group into tabs past roughly six fields. Mark genuinely required fields required. Restrict pathfields with rootPath so authors cannot pick something nonsensical.

On the model. Adapt from Resource unless you genuinely need request-scoped data — it is easier to test and reusable outside a request. Use OPTIONAL injection. Never return null. Derived work in @PostConstruct. Always put a hard ceiling on anything that builds a list.

On HTL. Presentation only. Always guard the outer element with data-sly-test. Correct escaping context every time — the wrong context is as dangerous as none. Add an editor placeholder for the empty state.

On accessibility. Semantic elements, correct heading levels, an alt-text field on every image, keyboard support on anything interactive, and an aria-live region for anything that updates content dynamically. Inheriting from Core Components gives you a large head start.

On performance. Lazy-load below-the-fold images. Never block on an external call during render. Think about whether a component clientlib belongs in a shared bundle instead.

  1. Debugging Tips Start by splitting the problem in half. Almost every component bug is either "the data isn't there" or "the data is there but isn't rendering." Open CRXDE and look at the component's node. Property missing → dialog problem. Property present → model or HTL problem. That one check saves enormous time and is a genuinely good thing to describe in an interview.

When the dialog isn't saving: check the node in CRXDE — did the property land on the parent? That is a missing ./. Watch the POST in the browser network tab to see exactly what field names are sent. Check the console for JS errors preventing submission.

When the dialog won't open: browser console first. Malformed dialog XML, a wrong sling:resourceType on a field, or a broken custom validator clientlib.

When the component renders nothing: turn on the Developer layer in the page editor — it shows the resource type and resolved script, which immediately tells you whether Sling even found your HTL. Check /system/console/status-adapters for your model's adapter. Output ${model} temporarily — an object reference means it resolved, nothing means the adaptation returned null.

When it works on author but not publish: was everything it references activated; test as anonymous in a private window against the publish URL directly, which separates permissions from caching; check allowProxy on the clientlib.

Console What it tells you Developer layer in the editor Resource type and resolved script per component /system/console/status-adapters Whether your Sling Model adapter registered /system/console/servletresolver What a URL actually resolves to /crx/de Whether the property is on the node, and where Browser network tab Exactly what the dialog POSTed 12. Performance Optimization In the model. Work once in @PostConstruct. A getter called from inside data-sly-list runs every iteration — a repository lookup inside a fifty-item loop is fifty lookups.

In listings. Prefer walking a known path over a repository-wide query. If you must query, confirm it is indexed and always set a limit. Always put a hard ceiling in code as well, because a misconfigured root path should degrade, not kill the instance.

In external calls. Never synchronously during render without a timeout. Cache in the OSGi service, or move the fetch client-side.

In images. Serve renditions, never the original. loading="lazy" below the fold.

In clientlibs. A component clientlib shipping on every page is worse than putting the CSS in the site bundle. Consider how many pages actually use the component.

In caching. The architectural one. A component rendering per-visitor data server-side makes the whole page uncacheable. Isolate personalisation — client-side fetch, or Sling Dynamic Include — so the rest stays cached. And when you do fetch client-side, use a selector and suffix so that response is cacheable too.

  1. Real Production Scenarios

  2. Dialog saves but nothing renders. Check the node in CRXDE. Property on the parent → missing ./. Property present → model or HTL.

  3. Only the first field of each multifield row saves. composite="{Boolean}true" missing, or the field node isn't a container.

  4. Component missing from the browser. Empty or .hidden componentGroup, or the template policy doesn't allow it.

  5. Visible but can't be dropped in. The container's policy doesn't allow it, or the target isn't a container.

  6. Renders unstyled. Clientlib not loaded, allowProxy not true, or a category typo.

  7. Extra divs breaking the layout. Decoration wrapper. Change it with decorationTagName, or decoration=false accepting harder selection in the editor.

  8. Editor shows stale content after an edit. Add afteredit="REFRESH_PAGE" if the edit affects anything outside the component.

  9. Core Components upgrade broke pages. Content references Adobe's resource type directly. Migrate content and introduce proxies.

  10. Component throws when a field is empty. Injection strategy is REQUIRED, or a getter doesn't null-check.

  11. Page slow after adding one component. Profile. Usually a blocking external call, an unindexed query, or an expensive getter in a loop.

  12. Unticking a checkbox doesn't disable the feature. No uncheckedValue, so the property is deleted rather than set false — and a missing property may not equal false in your logic.

  13. Two accordions on one page, and clicking one opens the other. Duplicate IDs from counter-based generation. Derive IDs from the resource path.

  14. Load More button hammers publish. Pagination passed as a query parameter, so the dispatcher never caches it. Move to a selector plus suffix.

  15. Derived listing shows stale content after publishing a new page. The listing page's cache doesn't know a child changed. Add a flush rule invalidating the parent on child activation.

  16. Authors can select the wrong kind of asset. The pathfield has no rootPath restriction.

  17. Works locally but not on the server. Check the package filter includes the component path, and that the bundle deployed and is ACTIVE.

  18. Rich text renders escaped HTML tags on the page. Missing @ context='html' on that value.

  19. target="_blank" links flagged in a security review. Missing rel="noopener noreferrer".

  20. Screen reader users can't tell that Load More did anything. No aria-live region announcing the update.

  21. JSON-LD structured data invalid in Search Console. Built by string concatenation and an author's answer contained a quote. Build it with a serialiser.

  22. Follow-up Questions How many components does your project have? How many are custom versus Core Component proxies? Who writes the dialogs — you, or a dedicated author-experience person? How do you handle a design change affecting twenty components? What's your test coverage on models? Have you migrated content between component versions? What's the most complex dialog you've built? How do you decide between a dialog field and a policy setting? What would you do differently if you built that component again? That last one deserves a genuine answer. For the listing story: "I'd have designed the Load More around a selector and suffix from day one. We built it with a query parameter, shipped it, and then spent a sprint undoing the caching problem we'd created in week one."

  23. Comparison Tables Core Component (proxied) versus fully custom

Core Component (proxied) Fully custom Build effort Very low High Accessibility Built in, WCAG tested Yours to implement Maintenance Adobe maintains it You maintain it Upgrades Change the proxy Manual Flexibility Limited to what it supports Total When to choose Almost always Only when nothing fits Extend versus Overlay

Extend Overlay Mechanism sling:resourceSuperType Same path under /apps as /libs Scope Just your new component Global Upgrade risk Low High Typical use Customising a Core Component Customising the AEM UI itself Simple versus Composite Multifield

Simple Composite composite property Not set {Boolean}true Fields per row One Several Stored as Multi-value string property Child nodes item0, item1, … field node type A form field A container Read with String[] / List @ChildResource List Dialog field versus Policy setting

Dialog field Policy setting Who changes it Content author, per instance Template author, once Example Link text, heading Allowed styles, allowed components Varies per Component instance Template Rule of thumb Content varies → dialog Configuration is consistent → policy The three difficulty levels, side by side

Simple (CTA) Medium (FAQ) Complex (Listing) Dialog 4 plain fields Composite multifield Multifield + tag picker + config Model ~60 lines Parent + nested model Derived data, bounded traversal Extra pieces None JSON-LD, ARIA contract Servlet, caching strategy, flush rule Key risk Missing uncheckedValue Duplicate IDs Unindexed query, uncacheable pagination Answer length ~1 min ~2 min ~3 min 16. Memory Tricks Component files: "Define, Render, Edit, Default" — .content.xml defines, HTL renders, _cq_dialog edits, _cq_template defaults.

The ./ rule: "No dot-slash, no data."

Proxy pattern: "Point, don't paste."

Multifield: "Composite means children."

Component visibility needs two yeses: "Group and policy — both must say yes."

Naming: "The script wears the folder's name."

Where logic lives: "HTL shows, Java thinks."

Pagination: "A dot is cached, a question mark is not." (from file 01 — selector and suffix cache, query parameters do not)

IDs in repeating components: "Path, not counter."

  1. Revision Notes A component is a folder under /apps that content points at via sling:resourceType. Files: .content.xml (cq:Component), .html, _cq_dialog, _cq_editConfig, _cq_template, clientlibs. cq on disk, cq: in CRXDE. HTL must be named after the folder — Sling script resolution, not a special rule. componentGroup empty or .hidden → not in the browser. Policy must also allow it. Proxy pattern: sling:resourceSuperType to a Core Component. Selective override, version control, decoupled content. Always do this. ./ on every dialog field name. Sling Post Servlet handles the save; nobody writes a servlet for dialogs. Simple multifield → multi-value string property. Composite (composite=true, field is a container) → child nodes item0, item1. Read with @ChildResource. Checkbox needs value and uncheckedValue. {Boolean} and {Long} type hints matter. cq:editConfig: cq:actions, cq:listeners (REFRESH_PAGE), cq:dropTargets, cq:inplaceEditing. Editor only. Decoration wrapper: decoration=false, cq:noDecoration, decorationTagName. Add one field to an inherited dialog → Sling Resource Merger, sling:orderBefore; remove with sling:hideResource. Logic in the Sling Model. @PostConstruct for derived values. Never return null. Hard ceiling on any list. Derived listings: traverse a known path over a repo-wide query; index and limit if you must query; hard cap in code. Pagination must use a selector and suffix, never a query parameter, or the dispatcher can't cache it. IDs in repeating components come from the resource path, not a counter. Style System for appearance variants — not new components. Test the model on the empty and partial cases authors actually create.
  2. Cheat Sheet Component folder

/apps//components// .content.xml cq:Component + jcr:title + componentGroup [+ sling:resourceSuperType] .html HTL — must match the folder name _cq_dialog/.content.xml _cq_editConfig.xml _cq_template/.content.xml clientlibs/Copy Copy .content.xml properties

jcr:primaryType="cq:Component" makes it a component jcr:title name shown to authors jcr:description tooltip componentGroup browser group; ".hidden" hides it sling:resourceSuperType inherit from another component cq:isContainer="{Boolean}true" can hold child components cq:noDecoration="{Boolean}true" no wrapper divCopy Copy Dialog skeleton

cq:dialog (sling:resourceType="cq/gui/components/authoring/dialog") └ content → granite/ui/components/coral/foundation/container └ items └ tabs → .../foundation/tabs └ items └ (jcr:title) → .../foundation/container └ items └ columns → .../foundation/fixedcolumns └ items └ column → .../foundation/container └ items └ Copy Copy Field resource types (prefix granite/ui/components/coral/foundation/form/)

textfield · textarea · pathfield · select · checkbox · switch numberfield · datepicker · radiogroup · hidden · fileupload multifield · colorfield

cq/gui/components/authoring/dialog/richtext rich text cq/gui/components/coral/common/form/tagfield tag pickerCopy Copy Field properties

name="./property" ALWAYS the ./ prefix fieldLabel the label fieldDescription help text required="{Boolean}true" mandatory emptyText placeholder value / uncheckedValue checkboxes — set BOTH rootPath restrict a pathfield composite="{Boolean}true" composite multifield selected="{Boolean}true" default option in a selectCopy Copy Resource Merger

sling:orderBefore="fieldName" position a merged field sling:hideResource="{Boolean}true" remove an inherited field sling:hideProperties="[prop1,prop2]" hide inherited propertiesCopy Copy cq:editConfig

cq:actions="[text:Label,-,edit,copymove,delete,insert]" cq:dialogMode="floating|inline|auto" cq:listeners → afteredit / afterinsert = "REFRESH_PAGE" | "REFRESH_SELF" cq:dropTargets → accept, groups, propertyName cq:inplaceEditing → editorType="text|title|plaintext"Copy Copy HTL essentials

data-sly-use.model="com.x.Model" bind a Sling Model data-sly-test="${model.ready}" conditional render data-sly-list.item="${model.items}" loop data-sly-element="${model.headingLevel}" dynamic tag name data-sly-resource="${path @ resourceType='...', decoration=false}" ${value @ context='html'} rich text ${value @ context='uri'} URLs ${value @ context='unsafe'} ONLY for serialiser-produced JSON-LDCopy Copy Accessibility contract for an accordion