Skip to content

Latest commit

 

History

176 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Adobe Labs

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.

Environments

Documentation

aem-boilerplate

Before using the aem-boilerplate, we recommend you to go through the documentation on https://www.aem.live/docs/ and more specifically:

  1. Developer Tutorial
  2. The Anatomy of a Project
  3. Web Performance
  4. Markup, Sections, Blocks, and Auto Blocking

Quick Start

npm i
npm start

Local development

  1. Install the AEM CLI: npm install -g @adobe/aem-cli
  2. Start AEM Proxy: npm start (opens your browser at http://localhost:3000)
  3. Open the adobe-labs-website directory in your favorite IDE and start coding

Code Quality

To run ESLint and Stylelint:

npm run lint

Creating blocks

A 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.

Add the block code

  1. Create blocks/<block-name>/ with a CSS file, and a JS file if the block needs decoration.
  2. 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.
  3. 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.
  4. Run npm start and place the block on a preview page to verify. Inspect http://localhost:3000/<path>.plain.html if the authored markup is unclear.

See The Anatomy of a Project and Exploring blocks.

Make the block available to authors

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.
  1. Create a document named after the block in the blocks folder.
  2. For each variant, add an H2 (the Library label), then a block table with dummy content. Use header options for visual variants.
  3. Optionally add a Library Metadata table after a sample block so Library shows an info icon with that description. Wrap a heading with the block in library-container-start / library-container-end if the heading should insert with the block.
  4. Preview the document so Library can read it. Publish if authors on the live site should see it.
  5. Add a row on the blocks spreadsheet:
    • name: the label shown in Library → Blocks
    • path: https://content.da.live/adobe/adobe-labs-website/docs/library/blocks/<block-name> (use content.da.live, not da.live)
  6. Preview the spreadsheet (publish if live authors need it).
  7. In DA, open Library → Blocks and confirm the new name, with nested H2 variants.

Library setup

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.

content-grid (query-driven)

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 category as an array or comma-separated list so the Category filter can match
  • Add isVideo from meta[name="isvideo"] so the play icon can follow page metadata outside /sneaks/
  • Add imageAspect from meta[name="image-aspect"] so card frames follow page metadata Image 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).

content-grid (query-driven)

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 category as an array or comma-separated list so the Category filter can match
  • Add isVideo from meta[name="isvideo"] so the play icon can follow page metadata outside /sneaks/
  • Add imageAspect from meta[name="image-aspect"] so card frames follow page metadata Image 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).

Hero (first section)

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.

AEM editing

  1. Insert a section break after the previous section.
  2. Add a Section Metadata table in that section.
  3. Set Style to one surface (same pattern as full-bleed):
    • section-rounded-default — default surface
    • section-rounded-blue — blue surface
    • section-rounded-pink — pink surface
    • section-rounded-orange — orange surface

Keep the hero in the first section. Do not put a rounded Style on the hero section.

On the homepage, keep Manifesto (grid-line-content) in its own last main section.

Spacing rules

A section-rounded-* section gets vertical padding and z-index: 1.

  • The first default section in a group, and every color section, get a start corner radius.
  • A color section that follows a rounded section, or a default section that follows a color section, overlaps the section before it. The offset is --section-margin-negative-offset: -(radius + --section-space-between). A default hero is not overlapped.
  • Adjacent default sections appear as one continuous card. The next section cancels the flex gap and start padding. The last default section before a different surface keeps an end radius.
  • When motion is opted in (prefers-reduced-motion: no-preference), section overlays load Lenis for smooth scrolling on a fine pointer. A coarse pointer skips Lenis. When that browser can run scroll-driven animations, the cover dim, hero-copy fade, and page-header fade run in CSS and the page does not load GSAP. Browsers without that support keep those fades on GSAP. A rounded section overlays whatever section is immediately before it (page header, hero, or another rounded card). Rounded cards pin, lag, and dim once the incoming card reaches COVER_START_VH of the viewport (0.6 by default). Inner content recedes with that cover, from rest, so the outgoing card does not reverse as the next one overtakes it. A page header or default hero does not pin: its content keeps scrolling, just slower, while the first rounded section covers it. The overlay starts dimming when that overlap begins and eases to full strength as the incoming section covers it. Adjacent default sections do not slow. Full-screen heroes pin in place without shifting under the nav. A pinned card stays parked in the viewport for the rest of the page, and the page-header wrapper stays stuck under the nav after it fades. Focusing a control that a card, the sticky header, or that fade still covers scrolls the page until the control is fully in view (WCAG 2.2 SC 2.4.11). Nothing is taken out of the tab order, made inert, or hidden with visibility, so every control stays reachable; fades use opacity rather than GSAP's autoAlpha for the same reason. Lenis additionally forces scroll-behavior: auto while it runs, since native smooth scrolling fights it on the skip link, in-page anchors, and hash deep links.
  • The last rounded section on the page gets an end radius and uses --section-padding-block-end-last. A page with one rounded section gets all four corners.
  • After a full-screen hero, the next rounded section overlaps the hero by -(radius + --section-space-between).
  • The footer overlaps the last section by the section radius only when that section is rounded (section-rounded-*). Footer inner padding grows by that amount so links stay clickable (.footer uses z-index: 0). Footer start padding increases at 64rem and above. When the last section is not rounded, it uses --section-padding-block-end-last and the footer sits below it. The Adobe logo sticks to the bottom of the viewport and rises from below it as the footer content scrolls past: .footer__logo-image translates from 50% down to rest as --footer-logo-entry-progress runs from -100 to 0. When motion is opted in, .footer__inner sticks to the bottom behind the last rounded card, clips, and translates its content as the card uncovers it. The menu stays in the tab order so a keyboard user can reach it; focusing a control the card still covers scrolls the card off it (WCAG 2.2 SC 2.4.11). After the menu is in, the logo sticky takes over. Reduced motion keeps the radius peek.
  • The research index (template: research, body.research) uses a larger --section-space-between, then a larger value at 48rem and above. Set that template on the research index only.
  • Article pages (template: article) use the default surface only. --section-padding-block-end is --s2a-spacing-2xl, then --s2a-spacing-4xl at 90rem (1440px). The last rounded section still uses --section-padding-block-end-last.

Testing

To run tests:

npm test

Code Guidelines

Typography

Three font stacks live on :root in styles/styles.css.

Use Custom property Face
Headings --heading-font-family Adobe Clean Display Black
UI and default body --body-font-family Adobe Clean
Article body --serif-font-family Adobe Clean Spectrum Serif

h1–h6 and any class that contains heading- take size, line height, letter spacing, and weight from the s2a heading tokens in styles/styles.css. Use those elements or a heading-* class when the design calls for a heading. Article pages keep that heading scale. Serif applies to p, ul, and ol inside body.article .default-content-wrapper (16px/20px below 64rem, then 20px/26px).

Grayscale font smoothing

Display Black and other heavy weights (700–900) render thicker than the design frames on macOS when the browser uses subpixel antialiasing. Grayscale smoothing matches the frames:

-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;

Both properties inherit. The shared heading rule and .label already set them, so a heading element or heading-* class needs no extra declarations.

Add both declarations on the same rule that sets the heavy face when that element sits outside h1–h6, [class*="heading-"], and .label:

  • --heading-font-family, --s2a-font-weight-heading, or --typography-font-weight-heading-2 (Display Black, 900). See the manifesto paragraphs in grid-line-content, abstract numbers, the hero, and lead-in headlines.
  • --s2a-font-weight-adobe-clean-black (900) on --body-font-family. See the header wordmark.
  • Label and button text: --s2a-font-weight-label, .button, and .action-button.

Regular body copy (--s2a-font-weight-body, 400) and article serif paragraphs and lists keep the browser’s subpixel smoothing.

CSS

For 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:

  1. 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.
  2. 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.

JS

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.

Writing Block Tests

See the write-block-tests skill for instructions and guidelines on writing unit tests for blocks.

Vendoring third-party JS libraries

This project has no bundler on the request path, so npm packages cannot be imported by name at runtime, and production code should not load libraries from a public CDN. Third-party libraries are vendored instead: a thin re-export under deps/<library>/src is bundled once at development time with esbuild into a self-contained ESM file at deps/<library>/dist, then committed and imported by relative path. This matches the pattern aemsites/author-kit uses to ship Lit without a build system.

Updating the vendored Lenis library

Lenis (smooth scrolling used by section overlays) is vendored this way. After bumping the version in package.json:

npm install
npm run build:lenis

That writes deps/lenis/dist/index.js and deps/lenis/dist/lenis.css. Commit those files with the version change.

When a feature needs Lenis, import the committed dist file and load the stylesheet at that point. Do not add Lenis to scripts.js or head.html. Section overlays do this from scripts/section-scroll/init.js when motion is opted in, and skip it entirely on touch.

Lenis sets scrollTop from its own loop, so it fights anything else that animates the scroll position. styles/features/section-scroll.css turns off the native scroll-behavior: smooth while Lenis is active, and programmatic scrolls that run before Lenis attaches — the hash deep link in loadLazy — jump instantly so Lenis cannot take over mid-flight and strand them short of the target. In-page anchors (the skip link, content-grid pagers) are handled in capture: the native hash jump is prevented, Lenis smooth-scrolls to the target's in-flow offset (sticky offsetTop is the pinned box, so a Previous pager would stop short of the section top), focus moves to the destination heading so Tab continues in that section, and history.pushState updates the URL. A native hash click would otherwise be undone on the next animation frame, so the first click looks like a no-op.

import Lenis from '../../deps/lenis/dist/index.js';
import { loadCSS } from '../../scripts/aem.js';

loadCSS(`${window.hlx.codeBasePath}/deps/lenis/dist/lenis.css`);

Updating the vendored GSAP library

GSAP plus ScrollTrigger (scroll-driven section parallax) are vendored the same way. After bumping the version in package.json:

npm install
npm run build:gsap

That writes deps/gsap/dist/index.js. Commit that file with the version change.

When a feature needs GSAP, import the committed dist file at the point of use. Do not add GSAP to head.html, and do not import it from scripts.js. Section overlays keep every GSAP import inside scripts/section-scroll/section-motion.js, which scripts/section-scroll/init.js dynamic-imports only once motion is opted in — so the bundle is never reachable from a static import chain.

import { gsap, ScrollTrigger } from '../../deps/gsap/dist/index.js';

Anything driven from gsap.ticker — Lenis is, in scripts/section-scroll/init.js — needs gsap.ticker.lagSmoothing(0), or a slow frame lets the ticker jump time forward and desyncs it from the real scroll position. That setting is global to GSAP, so restore the stock lagSmoothing(500, 33) on teardown instead of leaving every later animation on the page without it.

scripts.js may still emit a guarded <link rel="modulepreload"> for the bundle, as loadLazy does for section overlays. A dynamic import() inside a module cannot be requested until that module's own imports have resolved, so a vendored bundle behind one starts downloading several round trips late. The hint starts the download early without placing the bundle in any import graph, and it must carry the same guard as the import it warms — otherwise it becomes an eager load for requests that never use it.

Query Indexes

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.xml is 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 noindex robots 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.

Metadata

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.

Article headings keep the site h1–h6 / heading-* tokens (s2a heading-1 through heading-6). Default-content paragraphs and lists use Adobe Clean Spectrum Serif at 16px/20px below 64rem (1024px), then 20px/26px. Font stacks and grayscale smoothing rules are in Typography.

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.

Dataset download

Article pages show a Download action next to Copy link when the page Metadata table includes a Download Link row. After preview, that value is available as meta[name="download-link"].

Add a PDF from the DA media folder

  1. Upload the PDF under /media in Document Authoring.
  2. Preview and publish the file so Edge Delivery can serve it.
  3. In the article Metadata table, add Download Link and paste either:
    • The site path, for example /media/c4611-sample-explain.pdf
    • The DA media URL, for example https://da.live/media#/adobe/adobe-labs-website/media/c4611-sample-explain.pdf

The site rewrites DA media and content URLs for this project (adobe/adobe-labs-website) to the same-origin file path. Visitors download the file from the site; they do not land on da.live, which is the authoring app.

You can also paste a public file URL (for example an Adobe-hosted PDF).

How AEM handles media

  • Images and short videos go through Media Bus when you paste them into a document.
  • PDF and SVG files are content files. They follow the normal preview and publish lifecycle and are not stored on Media Bus.
  • Document Authoring can upload JPG, PNG, GIF, SVG, PDF, and MP4. See Adding media.
  • ZIP is not a DA media type. Host the ZIP (AEM Assets or another public URL) and paste that URL into Download Link.
  • Do not drop a ZIP or PDF into the article body as if it were an image.

Feedback

Article pages include a Feedback action with Copy link. It opens a message to labs@adobe.com. The subject is the Title row from the page Metadata table. After preview, AEM publishes that row as og:title. When Title is empty, the link has no subject.

Full-bleed images in articles

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.

Author byline images

Article pages show a "Words by:" byline built in JS from the author metadata (not a block — see buildArticleAuthorMeta in scripts/utils/utils.js). To make an author's photo appear, upload it to media/authors/<slugified-name>.png in Document Authoring and preview it. The filename must exactly match the slugified form of the name authored in the author field — lowercase, spaces and other non-alphanumeric characters replaced with hyphens (for example, an author named "Richard Example" needs richard-example.png).

  • No author metadata → the byline falls back to "Adobe Labs" and its logo (media/authors/adobe-labs.png).
  • A named author with no matching image uploaded → the name shows as plain text; no broken-image icon, no layout shift.
  • A named author with a matching image → the photo shows next to their name.

DA rewrites uploaded filenames to a hash on publish, but requesting the original filename redirects to the hashed asset once the file has been previewed — that's what makes the slug-based lookup work without a per-author authoring field.

Table of Contents

Article pages can list opted-in sections. Insert an empty Table of Contents block where the list should appear. The table header is the block name.

On each section that should appear, add a Section Metadata row named Table of Contents. The cell value is the label in the list. The block links that label to the section heading. If a section has both rows, the Table of Contents value is the label. If no section has either row, the block removes itself.

Buttons

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.

Adding a button in AEM

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.

Adding a button in JavaScript

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.

Disabled buttons as links

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.

About

Adobe Labs Website

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

Generated from adobe/aem-boilerplate