Author the header page

Before developing the header block, first author its content in the Universal Editor to have something to develop against.

The header content lives in a dedicated AEM page named nav.

Default header page

To author the header:

  1. Open the nav page in the Universal Editor

  2. Replace the default button with an Image block containing the WKND logo

  3. Update the navigation menu in the Text block by:

    • Adding your desired navigation links
    • Creating sub-navigation items where needed
    • Setting all links to the home page (/) for now

Author header block in Universal Editor

Publish to preview

With the Header page updated, publish the page to preview.

Since the header content lives on its own page (the nav page), you must publish that page specifically for header changes to take effect. Publishing other pages that use the header will not update the header content on Edge Delivery Services.

Block HTML

To begin block development, start by reviewing the DOM structure exposed by the Edge Delivery Services preview. The DOM is enhanced with JavaScript and styled with CSS, providing the foundation for building and customizing the block.

Because the header is loaded as a fragment, we need to examine the HTML returned by the XHR request after it’s been injected into the DOM and decorated via loadFragment(). This can be done by inspecting the DOM in the browser’s developer tools.

The following is the header page’s HTML after it has been loaded using the provided header.js and injected into the DOM:

<header class="header-wrapper">
  <div class="header block" data-block-name="header" data-block-status="loaded">
    <div class="nav-wrapper">
      <nav id="nav" aria-expanded="true">
        <div class="nav-hamburger">
          <button type="button" aria-controls="nav" aria-label="Close navigation">
            <span class="nav-hamburger-icon"></span>
          </button>
        </div>
        <div class="section nav-brand" data-section-status="loaded" style="">
          <div class="default-content-wrapper">
            <p class="">
              <a href="#" title="Button" class="">Button</a>
            </p>
          </div>
        </div>
        <div class="section nav-sections" data-section-status="loaded" style="">
          <div class="default-content-wrapper">
            <ul>
              <li aria-expanded="false">Examples</li>
              <li aria-expanded="false">Getting Started</li>
              <li aria-expanded="false">Documentation</li>
            </ul>
          </div>
        </div>
        <div class="section nav-tools" data-section-status="loaded" style="">
          <div class="default-content-wrapper">
            <p>
              <span class="icon icon-search">
                <img data-icon-name="search" src="/icons/search.svg" alt="" loading="lazy">
              </span>
            </p>
          </div>
        </div>
      </nav>
    </div>
  </div>
</header>

To find and inspect the page’s <header> element in the web browser’s developer tools.

Header DOM

Block JavaScript

The /blocks/header/header.js file from the AEM Boilerplate XWalk project template provides JavaScript for navigation, including dropdown menus and a responsive mobile view.

While the header.js script is often heavily customized to match a site’s design, it’s essential to retain the first lines in decorate(), which retrieve and process the header page fragment.

/blocks/header/header.js

export default async function decorate(block) {
  // load nav as fragment
  const navMeta = getMetadata('nav');
  const navPath = navMeta ? new URL(navMeta, window.location).pathname : '/nav';
  const fragment = await loadFragment(navPath);
  ...

The remaining code can be modified to fit your project’s needs.

Depending on the header requirements, the boilerplate code can be adjusted or removed. In this tutorial, we’ll use the provided code and enhance it by adding a hyperlink around the first authored image, linking it to the site’s home page.

The template’s code processes the header page fragment, assuming it consists of three sections in the following order:

  1. Brand section – Contains the logo and is styled with the .nav-brand class.
  2. Sections section – Defines the site’s main menu and is styled with .nav-sections.
  3. Tools section – Includes elements like search, login/logout, and profile, styled with .nav-tools.

To hyperlink the logo image to the home page, we update the block JavaScript as follows:

The updated code wrapping the logo image with a link to the site’s homepage (/) is shown below:

/blocks/header/header.js

export default async function decorate(block) {

  ...
  const navBrand = nav.querySelector('.nav-brand');

  // WKND: Turn the picture (image) into a linked site logo
  const logo = navBrand.querySelector('picture');

  if (logo) {
    // Replace the first section's contents with the authored image wrapped with a link to '/'
    navBrand.innerHTML = `<a href="/" aria-label="Home" title="Home" class="home">${logo.outerHTML}</a>`;
    // Make sure the logo is not lazy loaded as it's above the fold and can affect page load speed
    navBrand.querySelector('img').settAttribute('loading', 'eager');
  }

  const navSections = nav.querySelector('.nav-sections');
  if (navSections) {
    // WKND: Remove Edge Delivery Services button containers and buttons from the nav sections links
    navSections.querySelectorAll('.button-container, .button').forEach((button) => {
      button.classList = '';
    });

    ...
  }
  ...
}

Below is the original header.js generated from the template:

/blocks/header/header.js

export default async function decorate(block) {
  ...
  const navBrand = nav.querySelector('.nav-brand');
  const brandLink = navBrand.querySelector('.button');
  if (brandLink) {
    brandLink.className = '';
    brandLink.closest('.button-container').className = '';
  }

  const navSections = nav.querySelector('.nav-sections');
  if (navSections) {
    navSections.querySelectorAll(':scope .default-content-wrapper > ul > li').forEach((navSection) => {
      if (navSection.querySelector('ul')) navSection.classList.add('nav-drop');
      navSection.addEventListener('click', () => {
        if (isDesktop.matches) {
          const expanded = navSection.getAttribute('aria-expanded') === 'true';
          toggleAllNavSections(navSections);
          navSection.setAttribute('aria-expanded', expanded ? 'false' : 'true');
        }
      });
    });
  }
  ...
}

Block CSS

Update the /blocks/header/header.css to style it in accordance to WKND’s brand.

We will add the custom CSS to the bottom of header.css to make the tutorial changes easier to see and understand. While these styles could be integrated directly into the template’s CSS rules, keeping them separate helps illustrate what was modified.

Since we are adding our new rules after the original set, we’ll wrap them with a header .header.block nav CSS selector to ensure they take precedence over the template rules.

/blocks/header/header.css

/* /blocks/header/header.css */

... Existing CSS generated by the template ...

/* Add the following CSS to the end of the header.css */

/**
* WKND customizations to the header
*
* These overrides can be incorporated into the provided CSS,
* however they are included discretely in thus tutorial for clarity and ease of addition.
*
* Because these are added discretely
* - They are added to the bottom to override previous styles.
* - They are wrapped in a header .header.block nav selector to ensure they have more specificity than the provided CSS.
*
**/

header .header.block nav {
  /* Set the height of the logo image.
     Chrome natively sets the width based on the images aspect ratio */
  .nav-brand img {
    height: calc(var(--nav-height) * .75);
    width: auto;
    margin-top: 5px;
  }

  .nav-sections {
    /* Update menu items display properties */
    a {
      text-transform: uppercase;
      background-color: transparent;
      color: var(--text-color);
      font-weight: 500;
      font-size: var(--body-font-size-s);

      &:hover {
        background-color: auto;
      }
    }

    /* Adjust some spacing and positioning of the dropdown nav */
    .nav-drop {
      &::after {
        transform: translateY(-50%) rotate(135deg);
      }

      &[aria-expanded='true']::after {
        transform: translateY(50%) rotate(-45deg);
      }

      & > ul {
        top: 2rem;
        left: -1rem;
       }
    }
  }

Development preview

As the CSS and JavaScript are developed, the AEM CLI’s local development environment hot-reloads the changes, allowing for quick and easy visualization of how code impacts the block. Hover over the CTA and verify the teaser’s image zooms in and out.

Local development preview of header using CSS and JS

Lint your code

Make sure to frequently lint your code changes to keep it clean and consistent. Regular linting helps catch issues early, reducing overall development time. Remember, you cannot merge your development work into the main branch until all linting issues are resolved!

# ~/Code/aem-wknd-eds-ue

$ npm run lint

Preview in Universal Editor

To view changes in AEM’s Universal Editor, add, commit, and push them to the Git repository branch used by the Universal Editor. Doing so ensures that the block implementation does not disrupt the authoring experience.

# ~/Code/aem-wknd-eds-ue

$ git add .
$ git commit -m "CSS and JavaScript implementation for Header block"
# JSON files are compiled automatically and added to the commit via a Husky pre-commit hook
$ git push origin header-and-footer

Now, the changes are visible in the Universal Editor when using the ?ref=header-and-footer query parameter.

Header in Universal Editor

Like the header, the footer content is authored on a dedicated AEM page - in this case, the Footer page (footer). The footer follows the same pattern of being loaded as a fragment and decorated with CSS and JavaScript.

The footer should be implemented with a three-column layout containing:

  • A left column featuring a promotion (image and text)
  • A middle column with navigation links
  • A right column with social media links
  • A row at the bottom spanning all three columns with the copyright

Footer previews

Use columns block in the Footer page to create the three-column effect.

Column 1Column 2Column 3
ImageHeading 3Heading 3
TextList of linksList of links

Header DOM

The CSS below styles the footer block with a three-column layout, consistent spacing, and typography. The footer implementation uses only the JavaScript provided by the template.

/blocks/footer/footer.css


/* /blocks/footer/footer.css */

footer {
  background-color: var(--light-color);

  .block.footer {
    border-top: solid 1px var(--dark-color);
    font-size: var(--body-font-size-s);

    a {
      all: unset;

      &:hover {
        text-decoration: underline;
        cursor: pointer;
      }
    }

    img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      border: solid 1px white;
    }

    p {
      margin: 0;
    }

    ul {
      list-style: none;
      padding: 0;
      margin: 0;

      li {
        padding-left: .5rem;
      }
    }

    & > div {
      margin: auto;
      max-width: 1200px;
    }

    .columns > div {
      gap: 5rem;
      align-items: flex-start;

      & > div:first-child {
        flex: 2;
      }
    }

    .default-content-wrapper {
      padding-top: 2rem;
      margin-top: 2rem;
      font-style: italic;
      text-align: right;
    }
  }
}

@media (width >= 900px) {
  footer .block.footer > div {
    padding: 40px 32px 24px;
  }
}

Congratulations!

You’ve now explored how headers and footers are managed and developed in Edge Delivery Services and Universal Editor. You’ve learned how they are:

  • Authored on dedicated AEM pages separate from the main content
  • Loaded asynchronously as fragments to enable independent updates
  • Decorated with JavaScript and CSS to create responsive navigation experiences
  • Integrated seamlessly with the Universal Editor for easy content management

This pattern provides a flexible and maintainable approach for implementing site-wide navigation components.

For more best practices and advanced techniques, check out the Universal Editor documentation.

Experience Manager


The Perfect Blend: A New Era of Collaboration with AEM and Workfront

Adobe Customer Success Webinars

Wednesday, Apr 2, 5:00 PM UTC

Explore how Adobe Experience Manager and Workfront integrate to help teams move from ideation to delivery without the usual bottlenecks, ensuring content is organized, on-brand, and ready to go live faster.

Register

Connect with Experience League at Summit!

Get front-row access to top sessions, hands-on activities, and networking—wherever you are!

Learn more