Adobe Labs is the always-on public home that makes Adobe's AI-and-creativity innovation visible, continuous, and credible; the trusted, human-centered, evidence-led voice on creative work in the AI era.
This site is built using AEM with content managed via Document Authoring (DA). The codebase is based off of the aem-boilerplate.
- Preview: https://main--adobe-labs-website--adobe.aem.page/
- Live: https://main--adobe-labs-website--adobe.aem.live/
- Editing: https://da.live/#/adobe/adobe-labs-website/
Before using the aem-boilerplate, we recommend you to go through the documentation on https://www.aem.live/docs/ and more specifically:
- Developer Tutorial
- The Anatomy of a Project
- Web Performance
- Markup, Sections, Blocks, and Auto Blocking
npm i
npm start- Install the AEM CLI:
npm install -g @adobe/aem-cli - Start AEM Proxy:
npm start(opens your browser athttp://localhost:3000) - Open the
adobe-labs-websitedirectory in your favorite IDE and start coding
To run ESLint and Stylelint:
npm run lintA block is a named table in a document. Authors insert it; developers style and decorate it. The block name must match a folder under blocks/ in this repo (for example blocks/hero/ loads hero.css and hero.js). See Markup, Sections, Blocks, and Auto Blocking.
- Create
blocks/<block-name>/with a CSS file, and a JS file if the block needs decoration. - Scope CSS to
.block-name. Section wrappers use.block-name-wrapper/.block-name-container— do not put block layout CSS only on those unless you mean to style the section shell. - Authors can omit cells and add options in the table header, for example
grid-item (aspect-4/5). Options become extra classes on the block (aspect-4-5). Decorate defensively. - Run
npm startand place the block on a preview page to verify. Inspecthttp://localhost:3000/<path>.plain.htmlif the authored markup is unclear.
See The Anatomy of a Project and Exploring blocks.
Authors insert blocks from the Library in Document Authoring. That catalog lives in DA under /docs/library/, not in this repo. Code merges ship separately from content publish.
Library lists two kinds of variants:
- Content variants: an H2 above each sample table. The heading text is the sub-item name in Library.
- Visual variants: options in the table header, as in
grid-item (aspect-4/5)above. Section breaks are for page layout, not for grouping Library items.
- Create a document named after the block in the blocks folder.
- For each variant, add an H2 (the Library label), then a block table with dummy content. Use header options for visual variants.
- Optionally add a
Library Metadatatable after a sample block so Library shows an info icon with that description. Wrap a heading with the block inlibrary-container-start/library-container-endif the heading should insert with the block. - Preview the document so Library can read it. Publish if authors on the live site should see it.
- Add a row on the blocks spreadsheet:
name: the label shown in Library → Blockspath:https://content.da.live/adobe/adobe-labs-website/docs/library/blocks/<block-name>(usecontent.da.live, notda.live)
- Preview the spreadsheet (publish if live authors need it).
- In DA, open Library → Blocks and confirm the new name, with nested H2 variants.
Blocks, Templates, Placeholders, and Icons are registered on the library tab of the site config (for example Blocks → https://content.da.live/adobe/adobe-labs-website/docs/library/blocks.json). Put those rows on the library tab, not data. Edit this tab only when adding a new type of library, not for each block.
For Templates, Placeholders, and Icons, see Setup library.
The homepage Latest Content section uses content-grid with a key/value table:
| Field | Meaning |
|---|---|
| Content Type | All (default) fetches /content.json. A section name — Research, Workflows, Sneaks, Playground — fetches that folder’s content.json |
| Category | Optional. All or omitted means no filter. Otherwise matched against the index category field (array or comma-separated string) after trim + lowercase |
| Count | How many cards to show (defaults to 8) |
| Intro | Optional freeform first cell (heading, paragraph, links). Extra; does not count toward Count |
The block fetches the Content Type endpoint via dataStore, filters by Category after the fetch, and renders each hit as a grid-item. If nothing matches, the block and its .content-grid-wrapper are hidden (including authored Intro). An Intro cell, when authored, sits in column 1 at four columns and stacks full-width above the cards at three columns and one. Card image frames follow the index imageAspect value (1:1, 4:5, 3:2, 2:3; separators :, /, or - are fine). Missing or unknown values default to 1:1. Video cards get the play icon when the index has isVideo true, contentType is video, or the page lives under /sneaks/ (Sneaks are video unless isVideo is explicitly false). Card content-type labels (when show-content-type is set) come from the first path segment (Research, Workflows, Sneaks, Playground)—not the topic Category metadata.
Card subheads default to the publication date (Oct 21 this year, Oct 21, 2027 otherwise). Add subhead-description to the content-grid block header (content-grid (subhead-description)) to use the index description instead.
Standalone grid-item cards link when the Title cell is a link. Content-type labels on cards are off by default. Add show-content-type to the content-grid block header (content-grid (show-content-type)) to render each card’s .grid-item__content-type link. On a standalone grid-item, author a Content Type row (legacy Category still works).
Cards stay empty until indexed article pages exist. Index config lives at tools.aem.live (this repo does not contain helix-query.yaml).
Index properties (reindex after saving):
- Keep
title,image,description,publicationDate,robots - Add
categoryas an array or comma-separated list so the Category filter can match - Add
isVideofrommeta[name="isvideo"]so the play icon can follow page metadata outside/sneaks/ - Add
imageAspectfrommeta[name="image-aspect"]so card frames follow page metadataImage Aspect
On each Labs article in DA, put the page under /research, /workflows, /sneaks, or /playground, and author description, og:image, publication date, and Image Aspect (1:1, 4:5, 3:2, or 2:3). The block drops noindex pages and section index pages (/research/, /workflows/index, and the other known sections).
The homepage Latest Content section uses content-grid with a key/value table:
| Field | Meaning |
|---|---|
| Content Type | All (default) fetches /content.json. A section name — Research, Workflows, Sneaks, Playground — fetches that folder’s content.json |
| Category | Optional. All or omitted means no filter. Otherwise matched against the index category field (array or comma-separated string) after trim + lowercase |
| Count | How many cards to show (defaults to 8) |
| Intro | Optional freeform first cell (heading, paragraph, links). Extra; does not count toward Count |
The block fetches the Content Type endpoint via dataStore, filters by Category after the fetch, and renders each hit as a grid-item. If nothing matches, the block and its .content-grid-wrapper are hidden (including authored Intro). An Intro cell, when authored, sits in column 1 at four columns and stacks full-width above the cards at three columns and one. Card image frames follow the index imageAspect value (1:1, 4:5, 3:2, 2:3; separators :, /, or - are fine). Missing or unknown values default to 3:2. Video cards get the play icon when the index has isVideo true, contentType is video, or the page lives under /sneaks/ (Sneaks are video unless isVideo is explicitly false). Card section labels (when show-category is set) come from the first path segment.
Card subheads default to the publication date (Oct 21 this year, Oct 21, 2027 otherwise). Add subhead-description to the content-grid block header (content-grid (subhead-description)) to use the index description instead.
Standalone grid-item cards link when the Title cell is a link. Section labels on cards are off by default. Add show-category to the content-grid block header (content-grid (show-category)) to render each card’s .grid-item__category link.
Cards stay empty until indexed article pages exist. Index config lives at tools.aem.live (this repo does not contain helix-query.yaml).
Index properties (reindex after saving):
- Keep
title,image,description,publicationDate,robots - Add
categoryas an array or comma-separated list so the Category filter can match - Add
isVideofrommeta[name="isvideo"]so the play icon can follow page metadata outside/sneaks/ - Add
imageAspectfrommeta[name="image-aspect"]so card frames follow page metadataImage Aspect
On each Labs article in DA, put the page under /research, /workflows, /sneaks, or /playground, and author description, og:image, publication date, and Image Aspect (1:1, 4:5, 3:2, or 2:3). The block drops noindex pages and section index pages (/research/, /workflows/index, and the other known sections).
Keep the hero in its own first section. That is what makes the hero image load quickly.
The page already loads the first image in the first section right away, and AEM lazy-loads the rest. If the content grid, Explore, or article body sit in that same section, the page waits on them before it starts the hero.
In Document Authoring, insert a section break after the hero table. Paste the hero image as a normal picture — you do not need to set loading or fetchpriority. Adding fetchpriority="high" or a preload usually makes Lighthouse scores worse on Edge Delivery; see Adobe’s keeping-it-100 guidance.
To run tests:
npm testFor blocks and other custom classes, the preference is to use BEM style classes where possible.
Native nesting can be used for this project. When doing so, keep the following in mind in order to increase the support for some slightly older Safari versions:
- Use
&when referencing base elements, e.g..thing { & p { color: red; }}. This helps support Safari 16.5 through 17.1 that enforced a strict grammar rule. - If using
@supports, keep this as a root selector and not nested within other selectors, to avoid a Webkit bug that breaks all the other adjacent styles and CSS custom properties. Safari versions 16.5 through 18.1 were affected by this CSS nesting hoisting bug.
Make sure all code is documented with JSDOC style comments. Including functions, their parameters, and return values. Avoid an excessive amount of separate imported files, as each is an a network request since the JS is not compiled into a bundle.
See the write-block-tests skill for instructions and guidelines on writing unit tests for blocks.
The following query indexes are configured for this site.
The custom content.json indexes are used to render dynamic content, such as articles within the Content Grid.
- All pages: The
sitemap.xmlis configured to point to this./query-index.json - All content: Returns all types of single article content within specific directories (excludes index pages).
/content.json - Research content: Returns all research articles (excludes the index page).
/research/content.json - Workflows content: Returns all workflow articles (excludes the index page).
/workflows/content.json - Sneaks content: Returns all workflow articles (excludes the index page).
/sneaks/content.json - Playground content: Returns all workflow articles (excludes the index page).
/playground/content.json
Important development notes:
- The indexes are configured by admins using the AEM Index Admin Tool, not via the "retired" method of using a YAML file.
- Per AEM docs, sitemaps should automatically exclude
noindexrobots metadata. They are not automatically excluded from the query index JSON, so these must be filtered on the frontend. - Only published pages (and changes) will show in the query indexes.
Default metadata values are set via the root /metadata spreadsheet.
See AEM bulk metadata docs for more info.
Article detail pages (/research/*, /workflows/*, /sneaks/*, /playground/*) get template: article from that spreadsheet. The article pre-footer autoblock keys off this metadata, not a hardcoded path list. A page-level metadata block can still add or omit article for an exception.
Individual pages can then set metadata values via a metadata block, including overriding any of those default values.
See AEM metadata block docs for more info.
To use full-bleed default content in an article (for example a lone image), in the AEM editor use a section break and a Section Metadata block that includes "full-bleed" as a value for "Style". Keep that content in its own section.
The default .button class uses the Primary style. So far only the default/primary style is supported until others are needed.
Default buttons are dark-mode aware. The .button--static-white variant can be used for non-theme-aware buttons, like in the hero.
Follow AEM's Buttons docs.
The paragraph must contain only the link. decorateButtons then adds class button to the link and class button-wrapper to the paragraph.
.button-wrapper is a p, so it has the default paragraph margin.
You can use the same steps for a standalone button in default content and for a button in a block cell.
Note
The AEM docs incorrectly state that p > a (without <strong> or <em>). This is outdated, as standalone plain links do not receive the .button class. See decorateButtons.
Create a native button or a as needed and add class button to it.
Add extra classes for variants as needed, for example button--static-white.
decorateButtons runs before block JavaScript. For a disabled link that you create as a.button in block JS, set aria-disabled="true", set tabIndex = "-1", and call event.preventDefault() on click.