Edge Delivery Services · 22 of 29
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.
This chapter covers:
<head> tags from authored contentog:) and Twitter Card tags for social sharinghelix-sitemap.yaml — configuration, generation, and submissionhelix-query.yaml — what it is, how it drives the Query API and search indexinghead.htmlhreflang for multi-locale projectsrobots.txt configurationEDS delivers pre-cached static HTML from the edge. From a search engine perspective, this is ideal:
<head> before the page body — no SPA rendering delayHowever, 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:
scripts.js or block JSThe 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.
In DA (Document Authoring):
| Metadata | |
|-------------|-----------------------------------------------------------|
| Title | abc - Advancing Healthcare with Innovation |
| Description | Discover abc's life-changing medical products and... |
| Image | [image] |
| Image Alt | abc healthcare products overview |
| Robots | index, follow |
| Canonical | https://www.abc.com/products/ |
In UE (xwalk) the metadata block uses the same key-value structure, configured in component-models.json.
| 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="@abcglobal"> |
| Metadata | |
|--------------------|----------------------------------------------------------|
| Title | FreeStyle Libre — abc 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.abc.com/products/freestyle-libre/ |
| Author | abc |
| Template | product-detail |
| Twitter Card | summary_large_image |
| Twitter Site | @abcglobal |
| Publication Date | 2026-01-15 |
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.
EDS generates these automatically from metadata:
<meta property="og:type" content="website">
<meta property="og:title" content="FreeStyle Libre — abc 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.abc.com/products/freestyle-libre/">
<meta property="og:site_name" content="abc">
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);
}
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 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="@abcglobal">
<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 = '@abcglobal';
document.head.append(meta);
}
}
The helix-sitemap.yaml file configures the automatic sitemap generation for your EDS site.
# helix-sitemap.yaml (root of repository)
sitemaps:
default:
origin: https://www.abc.com
exclude:
- /drafts/**
- /404
- /fragments/**
hreflang:
- en
sitemaps:
default:
# The canonical domain for sitemap URLs
origin: https://www.abc.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.abc.com
include:
- /products/**
exclude:
- /products/drafts/**
EDS automatically generates and serves your sitemap at:
https://www.abc.com/sitemap.xml
For named sitemaps:
https://www.abc.com/products-sitemap.xml
sitemap.xml → SubmitResubmit after major content changes. EDS regenerates the sitemap automatically as pages are previewed and published.
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.
# 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')
include patternquery-index.json at the target path// 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);
}
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 (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).
// 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: 'abc',
logo: {
'@type': 'ImageObject',
url: 'https://www.abc.com/icons/icon-512.png',
},
},
};
const script = document.createElement('script');
script.type = 'application/ld+json';
script.textContent = JSON.stringify(jsonLd);
document.head.append(script);
}
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: 'abc',
logo: {
'@type': 'ImageObject',
url: 'https://www.abc.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);
}
function addProductStructuredData(product) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'MedicalDevice',
name: product.name,
description: product.description,
brand: {
'@type': 'Brand',
name: 'abc',
},
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);
}
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);
}
A canonical URL tells Google which version of a page is the authoritative one, preventing duplicate content penalties.
EDS generates a canonical from the metadata block's Canonical field. If not provided, it defaults to the current page URL.
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.abc.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
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 tells Google which page language/region variant to show to which users. Required for multi-locale EDS sites.
| Metadata | |
|--------------|----------------------------------------------|
| Hreflang | en:https://www.abc.com/products/libre/ |
| | de:https://www.abc.de/produkte/libre/ |
| | ja:https://www.abc.co.jp/products/libre/ |
| | x-default:https://www.abc.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);
});
}
<link rel="alternate" hreflang="en" href="https://www.abc.com/products/libre/">
<link rel="alternate" hreflang="de" href="https://www.abc.de/produkte/libre/">
<link rel="alternate" hreflang="ja" href="https://www.abc.co.jp/products/libre/">
<link rel="alternate" hreflang="x-default" href="https://www.abc.com/products/libre/">
EDS serves robots.txt from the root of your repository.
# /robots.txt
User-agent: *
Allow: /
Disallow: /drafts/
Disallow: /fragments/
Disallow: /tools/
Sitemap: https://www.abc.com/sitemap.xml
Rules:
Allow: / — allow all crawling by defaultSitemap: directive pointing to your XML sitemapIf 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.
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)
After publishing a page:
For structured data:
https://search.google.com/test/rich-results?url=https://www.abc.com/products/libre/For Open Graph:
https://developers.facebook.com/tools/debug/<head> via block JS or scripts.js — use Article, Product, FAQPage, WebPage as appropriatenoindex meta tag in scripts.js when hostname is *.aem.pagescripts.js from page metadataChapters 1–22 cover the main arc: what EDS is, how to build and author blocks, how it compares to AEM, and the production concerns — performance, security, accessibility, and SEO.
The remaining chapters go deeper on specific topics from the POC. The next chapter, Universal Editor Routing and Local Development, covers what to reuse from AEM's Universal Editor routing setup in a pure EDS project and what to ignore.
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.