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 submissionhelix-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
hreflangfor multi-locale projectsrobots.txtconfiguration- 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:
- Authored in the metadata block on each page, or
- Injected programmatically via
scripts.jsor 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 Key | Generated 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="..."> |
Template | Body class applied for CSS/JS selection |
Theme | Body 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
- Open Google Search Console
- Select your property
- 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
- EDS crawls all pages matching the
includepattern - For each page, it extracts the properties using the CSS selectors and expressions defined
- The result is written to
query-index.jsonat thetargetpath - 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:
- Open Google Search Console
- URL Inspection → Enter your page URL
- Click "Request Indexing"
- Check "View Crawled Page" → Coverage tab should show "Indexed"
For structured data:
- Rich Results Test:
https://search.google.com/test/rich-results?url=https://www.abbott.com/products/libre/ - Should show detected items (Article, FAQPage, Product, etc.) with no errors
For Open Graph:
- Facebook Sharing Debugger:
https://developers.facebook.com/tools/debug/ - Enter URL → Scrape Again to refresh cache
- 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
noindexmeta tag inscripts.jswhen hostname is*.aem.page - hreflang requires explicit authoring — add it to metadata block or inject in
scripts.jsfrom 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.