N
Naveenr.dev
Chapter 16
24 min read2026-06-30

SEO in EDS

SEO for EDS projects. Covers metadata, Open Graph and Twitter Card fields, helix-sitemap.yaml, helix-query.yaml, structured data, canonical URLs, hreflang, robots.txt, and search indexing checks.

Content Objective

This chapter covers:

  • How EDS generates page metadata and <head> tags from authored content
  • The metadata block — every supported field and what it renders
  • Open Graph (og:) and Twitter Card tags for social sharing
  • helix-sitemap.yaml — configuration, generation, and submission
  • helix-query.yaml — what it is, how it drives the Query API and search indexing
  • JSON-LD structured data — how to inject it via block JS or head.html
  • Canonical URLs in EDS — automatic and manual
  • hreflang for multi-locale projects
  • robots.txt configuration
  • Verifying your EDS pages are indexed correctly in Google Search Console

How EDS Handles SEO

EDS delivers pre-cached static HTML from the edge. From a search engine perspective, this is ideal:

  • Googlebot receives the fully rendered HTML with no JavaScript execution required
  • Page speed (which directly affects ranking) is a core EDS design goal
  • Metadata is in the <head> before the page body — no SPA rendering delay

However, SEO in EDS is authoring-driven. There is no automatic meta description generator, no default Open Graph image, no automatic structured data. Every SEO-relevant field must be either:

  1. Authored in the metadata block on each page, or
  2. Injected programmatically via scripts.js or block JS

The Metadata Block

The metadata block is a special key-value block that authors add to the bottom of every page. It generates <meta> tags, <title>, and other <head> content.

How to Author It

In DA (Document Authoring):

| Metadata    |                                                         |
|-------------|-----------------------------------------------------------|
| Title       | Abbott - Advancing Healthcare with Innovation              |
| Description | Discover Abbott's life-changing medical products and...    |
| Image       | [image]                                                   |
| Image Alt   | Abbott healthcare products overview                       |
| Robots      | index, follow                                             |
| Canonical   | https://www.abbott.com/products/                          |

In UE (xwalk) the metadata block uses the same key-value structure, configured in component-models.json.

What Each Field Generates

Metadata KeyGenerated HTML
Title<title>Your Title</title> and <meta property="og:title">
Description<meta name="description"> and <meta property="og:description">
Image<meta property="og:image"> and <meta name="twitter:image">
Image Alt<meta property="og:image:alt">
Robots<meta name="robots" content="index, follow">
Canonical<link rel="canonical" href="...">
TemplateBody class applied for CSS/JS selection
ThemeBody class for CSS theming
Author<meta name="author">
Publication Date<meta name="publication-date">
Twitter Card<meta name="twitter:card">
Twitter Site<meta name="twitter:site" content="@abbottglobal">

Complete Metadata Block Example

| Metadata           |                                                          |
|--------------------|----------------------------------------------------------|
| Title              | FreeStyle Libre — Abbott Continuous Glucose Monitor      |
| Description        | FreeStyle Libre is the world's most accurate CGM.        |
|                    | No fingerpricks. Wear up to 14 days.                     |
| Image              | [og-image-freestyle-libre.jpg]                           |
| Image Alt          | FreeStyle Libre CGM sensor on a person's arm             |
| Robots             | index, follow                                            |
| Canonical          | https://www.abbott.com/products/freestyle-libre/         |
| Author             | Abbott                                                   |
| Template           | product-detail                                           |
| Twitter Card       | summary_large_image                                      |
| Twitter Site       | @abbottglobal                                            |
| Publication Date   | 2026-01-15                                               |

Open Graph Configuration

Open Graph tags control how your page appears when shared on LinkedIn, Facebook, WhatsApp, Slack, and other platforms. They are generated from the metadata block automatically for Title, Description, and Image.

Full Open Graph Tag Set

EDS generates these automatically from metadata:

<meta property="og:type" content="website">
<meta property="og:title" content="FreeStyle Libre — Abbott CGM">
<meta property="og:description" content="...">
<meta property="og:image" content="https://main--eds-poc--org.aem.live/media/og-freestyle.jpg">
<meta property="og:image:alt" content="FreeStyle Libre CGM sensor">
<meta property="og:url" content="https://www.abbott.com/products/freestyle-libre/">
<meta property="og:site_name" content="Abbott">

Adding Custom Open Graph Fields

For article pages and product pages, add additional OG fields via scripts.js:

// scripts/scripts.js — add og:type per template
async function loadPage() {
  const template = getMetadata('template');
  const ogType = {
    'article': 'article',
    'product-detail': 'product',
    'default': 'website',
  }[template] || 'website';

  const ogTypeMeta = document.createElement('meta');
  ogTypeMeta.setAttribute('property', 'og:type');
  ogTypeMeta.content = ogType;
  document.head.append(ogTypeMeta);
}

The OG Image Size Requirement

Facebook and LinkedIn require OG images to be at least 1200×630 pixels for optimal display. Twitter uses 1200×600 pixels for summary_large_image.

Create a dedicated OG image per page or per content type — do not use your hero image directly unless it meets these dimensions. Store OG images in your project's /media/ folder so they flow through the EDS Media Bus.


Twitter Card Configuration

Twitter Cards control how pages appear in Twitter/X posts.

<!-- For pages with a large featured image -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@abbottglobal">
<meta name="twitter:title" content="...">
<meta name="twitter:description" content="...">
<meta name="twitter:image" content="...">
<meta name="twitter:image:alt" content="...">

<!-- For pages without a large image -->
<meta name="twitter:card" content="summary">

Set twitter:card in the metadata block. The twitter:site value is typically the same across all pages — add it to scripts.js as a global default:

// scripts/scripts.js
function addTwitterDefaults() {
  if (!document.querySelector('meta[name="twitter:site"]')) {
    const meta = document.createElement('meta');
    meta.name = 'twitter:site';
    meta.content = '@abbottglobal';
    document.head.append(meta);
  }
}

helix-sitemap.yaml — Generating a Sitemap

The helix-sitemap.yaml file configures the automatic sitemap generation for your EDS site.

Default helix-sitemap.yaml

# helix-sitemap.yaml (root of repository)
sitemaps:
  default:
    origin: https://www.abbott.com
    exclude:
      - /drafts/**
      - /404
      - /fragments/**
    hreflang:
      - en

Full Configuration Options

sitemaps:
  default:
    # The canonical domain for sitemap URLs
    origin: https://www.abbott.com

    # Exclude paths from the sitemap
    exclude:
      - /drafts/**
      - /fragments/**
      - /404
      - /test/**
      - /**/*.json    # Exclude JSON sheets

    # Include only specific paths (alternative to exclude)
    # include:
    #   - /products/**
    #   - /news/**

    # Language/region tags for multi-locale (see hreflang section)
    hreflang:
      - en
      - de
      - ja

  # Separate sitemap for products (optional — creates /products-sitemap.xml)
  products:
    origin: https://www.abbott.com
    include:
      - /products/**
    exclude:
      - /products/drafts/**

Where the Sitemap is Served

EDS automatically generates and serves your sitemap at:

https://www.abbott.com/sitemap.xml

For named sitemaps:

https://www.abbott.com/products-sitemap.xml

Submitting to Google Search Console

  1. Open Google Search Console
  2. Select your property
  3. Sitemaps → Enter sitemap.xml → Submit

Resubmit after major content changes. EDS regenerates the sitemap automatically as pages are previewed and published.


helix-query.yaml — Content Indexing

helix-query.yaml configures the EDS Query API, which indexes your page metadata and makes it queryable via JavaScript. This drives features like article listing, search results, and related content blocks.

Default helix-query.yaml

# helix-query.yaml (root of repository)
version: 1

indices:
  blog:
    include:
      - /blog/**
    exclude:
      - /blog/drafts/**
    target: /blog/query-index.json
    properties:
      title:
        select: head > meta[property="og:title"]
        value: |
          attribute(el, 'content')
      description:
        select: head > meta[name="description"]
        value: |
          attribute(el, 'content')
      image:
        select: head > meta[property="og:image"]
        value: |
          attribute(el, 'content')
      author:
        select: head > meta[name="author"]
        value: |
          attribute(el, 'content')
      date:
        select: head > meta[name="publication-date"]
        value: |
          attribute(el, 'content')
      category:
        select: head > meta[name="category"]
        value: |
          attribute(el, 'content')
      topics:
        select: head > meta[name="topics"]
        value: |
          attribute(el, 'content')

What helix-query.yaml Does

  1. EDS crawls all pages matching the include pattern
  2. For each page, it extracts the properties using the CSS selectors and expressions defined
  3. The result is written to query-index.json at the target path
  4. Your block JS fetches this JSON to build listing pages, search results, etc.

Using the Query Index in a Block

// blocks/article-list/article-list.js
export default async function decorate(block) {
  const response = await fetch('/blog/query-index.json');
  const { data } = await response.json();

  // Sort by date descending
  const articles = data
    .filter(item => item.date)
    .sort((a, b) => new Date(b.date) - new Date(a.date))
    .slice(0, 6);  // Latest 6 articles

  const ul = document.createElement('ul');
  ul.classList.add('article-list-items');

  articles.forEach(article => {
    const li = document.createElement('li');

    const link = document.createElement('a');
    link.href = article.path;

    const img = document.createElement('img');
    img.src = article.image;
    img.alt = article.title;
    img.loading = 'lazy';
    img.width = 400;
    img.height = 225;

    const title = document.createElement('h3');
    title.textContent = article.title;

    const meta = document.createElement('p');
    meta.classList.add('article-meta');
    meta.textContent = `${article.author} · ${formatDate(article.date)}`;

    link.append(img, title, meta);
    li.append(link);
    ul.append(li);
  });

  block.append(ul);
}

helix-query.yaml for Multiple Indices

indices:
  blog:
    include:
      - /blog/**
    target: /blog/query-index.json
    properties: # ...

  products:
    include:
      - /products/**
    exclude:
      - /products/drafts/**
    target: /products/query-index.json
    properties:
      title:
        select: head > meta[property="og:title"]
        value: attribute(el, 'content')
      category:
        select: head > meta[name="product-category"]
        value: attribute(el, 'content')
      sku:
        select: head > meta[name="product-sku"]
        value: attribute(el, 'content')

JSON-LD Structured Data

JSON-LD (JavaScript Object Notation for Linked Data) is structured data that Google uses to understand page content and generate rich results (FAQ boxes, product panels, breadcrumbs, article metadata in Search).

Adding JSON-LD to All Pages via scripts.js

// scripts/scripts.js
function addStructuredData() {
  const template = getMetadata('template') || 'website';

  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'WebPage',
    name: document.title,
    description: getMetadata('description'),
    url: document.querySelector('link[rel="canonical"]')?.href || window.location.href,
    publisher: {
      '@type': 'Organization',
      name: 'Abbott',
      logo: {
        '@type': 'ImageObject',
        url: 'https://www.abbott.com/icons/icon-512.png',
      },
    },
  };

  const script = document.createElement('script');
  script.type = 'application/ld+json';
  script.textContent = JSON.stringify(jsonLd);
  document.head.append(script);
}

JSON-LD for Article Pages

Add this in the article template or article block:

function addArticleStructuredData(block) {
  const headline = document.title;
  const description = getMetadata('description');
  const author = getMetadata('author');
  const datePublished = getMetadata('publication-date');
  const image = getMetadata('og:image');

  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline,
    description,
    author: {
      '@type': 'Person',
      name: author,
    },
    publisher: {
      '@type': 'Organization',
      name: 'Abbott',
      logo: {
        '@type': 'ImageObject',
        url: 'https://www.abbott.com/icons/icon-512.png',
      },
    },
    datePublished,
    dateModified: datePublished,
    image: {
      '@type': 'ImageObject',
      url: image,
    },
    mainEntityOfPage: {
      '@type': 'WebPage',
      '@id': window.location.href,
    },
  };

  const script = document.createElement('script');
  script.type = 'application/ld+json';
  script.textContent = JSON.stringify(jsonLd);
  document.head.append(script);
}

JSON-LD for Product Pages

function addProductStructuredData(product) {
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'MedicalDevice',
    name: product.name,
    description: product.description,
    brand: {
      '@type': 'Brand',
      name: 'Abbott',
    },
    url: window.location.href,
    image: product.image,
    // For regulated products, include regulatory status
    regulatoryStatus: product.regulatoryStatus,
  };

  const script = document.createElement('script');
  script.type = 'application/ld+json';
  script.textContent = JSON.stringify(jsonLd);
  document.head.append(script);
}

JSON-LD for FAQ Blocks

FAQ structured data generates FAQ rich results in Google Search:

// blocks/faq/faq.js
export default function decorate(block) {
  const faqs = [];
  const items = [...block.children];

  items.forEach(row => {
    const question = row.children[0]?.textContent?.trim();
    const answer = row.children[1]?.innerHTML;
    if (question && answer) {
      faqs.push({ question, answer: DOMPurify.sanitize(answer) });
    }
  });

  // Render the FAQ block
  renderFaqBlock(block, faqs);

  // Inject JSON-LD
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'FAQPage',
    mainEntity: faqs.map(faq => ({
      '@type': 'Question',
      name: faq.question,
      acceptedAnswer: {
        '@type': 'Answer',
        text: faq.answer,
      },
    })),
  };

  const script = document.createElement('script');
  script.type = 'application/ld+json';
  script.textContent = JSON.stringify(jsonLd);
  document.head.append(script);
}

Canonical URLs

A canonical URL tells Google which version of a page is the authoritative one, preventing duplicate content penalties.

Automatic Canonicals in EDS

EDS generates a canonical from the metadata block's Canonical field. If not provided, it defaults to the current page URL.

When to Set Canonical Explicitly

Scenario 1: Same content at multiple paths
  /products/freestyle-libre/
  /devices/cgm/freestyle-libre/  (same page, different path)
  → Set canonical on both to: https://www.abbott.com/products/freestyle-libre/

Scenario 2: Multi-locale hreflang pages
  /en/products/freestyle-libre/
  /de/produkte/freestyle-libre/
  → Each has its own canonical, but all have hreflang cross-references

Scenario 3: Preview URLs exposed to Google
  main--eds-poc--org.aem.page/products/freestyle-libre
  → MUST have canonical pointing to production domain
     to prevent preview URL from competing with production

Preventing Preview URL Indexing

EDS preview URLs (.aem.page) are publicly accessible. If Google crawls them, they can compete with your production URLs.

Two solutions:

Option 1: Add robots meta to all pages

// scripts/scripts.js — when running on non-production domain
if (window.location.hostname.endsWith('.aem.page')
    || window.location.hostname.endsWith('.aem.live')
    || window.location.hostname === 'localhost') {
  const robots = document.createElement('meta');
  robots.name = 'robots';
  robots.content = 'noindex, nofollow';
  document.head.append(robots);
}

Option 2: X-Robots-Tag via CDN Contact Adobe to configure the EDS CDN to add X-Robots-Tag: noindex to .aem.page and .aem.live responses while allowing production domain indexing.


hreflang for Multi-Locale

hreflang tells Google which page language/region variant to show to which users. Required for multi-locale EDS sites.

Author hreflang in the Metadata Block

| Metadata     |                                              |
|--------------|----------------------------------------------|
| Hreflang     | en:https://www.abbott.com/products/libre/    |
|              | de:https://www.abbott.de/produkte/libre/     |
|              | ja:https://www.abbott.co.jp/products/libre/  |
|              | x-default:https://www.abbott.com/products/libre/ |

Or add it programmatically in scripts.js:

// scripts/scripts.js
function addHreflangTags() {
  const hreflangMeta = getMetadata('hreflang');
  if (!hreflangMeta) return;

  hreflangMeta.split('\n').forEach(entry => {
    const [lang, href] = entry.split(':').map(s => s.trim());
    if (!lang || !href) return;

    const link = document.createElement('link');
    link.rel = 'alternate';
    link.hreflang = lang;
    link.href = href.startsWith('http') ? href : `https://${href}`;
    document.head.append(link);
  });
}

Rendered hreflang Output

<link rel="alternate" hreflang="en" href="https://www.abbott.com/products/libre/">
<link rel="alternate" hreflang="de" href="https://www.abbott.de/produkte/libre/">
<link rel="alternate" hreflang="ja" href="https://www.abbott.co.jp/products/libre/">
<link rel="alternate" hreflang="x-default" href="https://www.abbott.com/products/libre/">

robots.txt

EDS serves robots.txt from the root of your repository.

Standard EDS robots.txt

# /robots.txt
User-agent: *
Allow: /
Disallow: /drafts/
Disallow: /fragments/
Disallow: /tools/

Sitemap: https://www.abbott.com/sitemap.xml

Rules:

  • Allow: / — allow all crawling by default
  • Disallow drafts, fragments (internal), and tools paths
  • Include the Sitemap: directive pointing to your XML sitemap

Blocking Preview URLs

If your preview URL is main--eds-poc--org.aem.page, add a dedicated robots.txt there:

# robots.txt on aem.page subdomain
User-agent: *
Disallow: /

This requires a separate robots.txt file deployed to your preview — or relies on the noindex meta tag approach described earlier.


SEO Validation Checklist

Before launching a page or new section:

Page-Level Metadata:
[ ] <title> tag is present, unique, and under 60 characters
[ ] Meta description is present, unique, and 150–160 characters
[ ] Canonical URL is set (either automatic or explicit in metadata block)
[ ] OG image is 1200×630px minimum, present, and relevant
[ ] OG title and description match <title> and meta description
[ ] Twitter Card type is set (summary_large_image for rich image cards)

Structured Data:
[ ] Appropriate JSON-LD type for the page (Article, Product, FAQ, etc.)
[ ] JSON-LD validated at https://search.google.com/test/rich-results
[ ] No errors in structured data (required fields present)

Sitemap:
[ ] Page is not in the `exclude` list of helix-sitemap.yaml
[ ] Sitemap.xml is submitted to Google Search Console
[ ] Page appears in sitemap after Preview + Publish

Query Index:
[ ] helix-query.yaml includes this page's path
[ ] query-index.json is updated after publishing
[ ] Required properties (title, description, image, date) are populated

Crawling:
[ ] robots.txt allows crawling for the production domain
[ ] Production domain is NOT blocked
[ ] Preview URL has noindex meta tag or X-Robots-Tag: noindex
[ ] hreflang tags are correct for multi-locale pages

Google Search Console:
[ ] URL submitted for indexing after publish (URL Inspection tool)
[ ] No coverage errors for the URL
[ ] Rich result eligible (if structured data added)

Verifying with Google Search Console

After publishing a page:

  1. Open Google Search Console
  2. URL Inspection → Enter your page URL
  3. Click "Request Indexing"
  4. Check "View Crawled Page" → Coverage tab should show "Indexed"

For structured data:

  1. Rich Results Test: https://search.google.com/test/rich-results?url=https://www.abbott.com/products/libre/
  2. Should show detected items (Article, FAQPage, Product, etc.) with no errors

For Open Graph:

  1. Facebook Sharing Debugger: https://developers.facebook.com/tools/debug/
  2. Enter URL → Scrape Again to refresh cache
  3. Verify og:title, og:description, og:image preview are correct

Key Takeaways

  • SEO is authoring-driven in EDS — every page needs a metadata block with Title, Description, Image, and Canonical
  • OG image must be 1200×630px — create dedicated OG images, not hero crop-offs
  • helix-sitemap.yaml is the only configuration needed for sitemap generation — exclude drafts and fragments
  • helix-query.yaml indexes page metadata into a JSON file — used by listing blocks, search, and related content
  • JSON-LD goes in <head> via block JS or scripts.js — use Article, Product, FAQPage, WebPage as appropriate
  • Canonical must point to production domain — preview URLs (*.aem.page) must not be indexed
  • Block preview URLs from Google — use noindex meta tag in scripts.js when hostname is *.aem.page
  • hreflang requires explicit authoring — add it to metadata block or inject in scripts.js from page metadata
  • Validate structured data before launch — Google Rich Results Test catches errors before they affect ranking

Series Complete

This chapter completes the 16-chapter EDS series. The complete series covers everything a developer and architect needs to build, configure, secure, optimize, and maintain a production EDS project:

Foundations (1–5): What EDS is → Setup → Blocks → Folder structure → Debugging

Architecture (6–9): Capabilities → EDS vs AEM → Content storage → Limitations

Implementation (10–13): CSS architecture → UE annotations → DA vs UE → Performance

Production (14–16): Security → Accessibility → SEO

For reading paths by role:

  • AEM developer starting EDS: 1 → 2 → 3 → 7 → 8 → 10 → 11 → 4 → 5 → 14
  • Architect evaluating EDS: 7 → 9 → 6 → 8 → 12 → 1
  • Debugging an existing project: 5 → 3 → 4 → 13
  • Healthcare/regulated industry: 14 (Security) → 15 (Accessibility) → 16 (SEO) → then 1–9

Enjoyed this chapter?

Get an email when I publish the next chapter. No spam — just new technical deep-dives.

Comments

Share feedback or questions about this blog post.

No comments yet. Be the first to share your thoughts.