
Searching Aem Documentation
- 26 installs
- 45 repo stars
- Updated August 4, 2026
- adobe/helix-website
searching-aem-documentation is a Claude Code skill that searches the aem.live documentation and blog corpus to find implementation guidance for AEM Edge Delivery Services features.
About
searching-aem-documentation runs a local script that searches the complete aem.live docs and blog corpus for AEM Edge Delivery Services topics. It returns ranked results with titles, snippets, and relevance scores so a developer reads only the most relevant pages with WebFetch. It saves context when a basic web search does not surface the right AEM documentation.
- Searches the full aem.live documentation and blog corpus with a local relevance-scoring script
- Returns ranked JSON results (title, description, snippet, relevanceScore) so only relevant pages are read
- Flags deprecated features and caches index files locally for 24 hours
Searching Aem Documentation by the numbers
- 26 all-time installs (skills.sh)
- Ranked #958 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
searching aem documentation capabilities & compatibility
- Capabilities
- docs search · documentation retrieval
- Use cases
- documentation · research · web search
What searching aem documentation says it does
This skill helps you efficiently search the complete aem.live documentation (docs and blog posts) without wasting context on irrelevant pages.
**Primary**: Full documentation (docpages-index.json) - 150+ pages with complete content
Index files are cached locally for 24 hours in `.claude/skills/docs-search/.cache/`.
npx skills add https://github.com/adobe/helix-website --skill searching-aem-documentationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 26 |
|---|---|
| repo stars | ★ 45 |
| Last updated | August 4, 2026 |
| Repository | adobe/helix-website ↗ |
What it does
Search aem.live documentation for how to implement an AEM Edge Delivery Services feature without wasting context.
Who is it for?
Finding official aem.live docs on a feature when a plain web search does not surface relevant pages
Skip if: Finding reusable block code (use block-collection-and-party) or non-AEM web development topics
When should I use this skill?
You need aem.live documentation on a feature and a basic web search did not return relevant results
What you get
A ranked list of the most relevant aem.live doc and blog pages to read with WebFetch
- Ranked JSON search results (path, title, description, snippet, relevanceScore) pointing to aem.live pages
By the numbers
- Searches 150+ documentation pages
- Title match scores 10 points, description 5, content 1 per relevance rule
- Caches index files for 24 hours
Files
Searching AEM Documentation
Overview
This skill helps you efficiently search the complete aem.live documentation (docs and blog posts) without wasting context on irrelevant pages. Use the provided search script to find relevant documentation pages, then use WebFetch to read the full content of the most relevant results.
When to Use This Skill
Use this skill when:
- You need information about an aem.live feature or concept
- You've already looked at the project codebase for context
- You've tried a basic web search but didn't find relevant aem.live documentation
- You need technical guidance on implementing aem.live features
- You're looking for best practices or examples from the official docs
Do NOT use this skill when:
- You need reusable code snippets or block examples (use
block-collection-and-partyinstead) - You already know the specific documentation URL
- You're looking for general web development information (not aem.live specific)
How to Use This Skill
Step 1: Identify Keywords
Determine 1-3 specific keywords related to what you're searching for. Be specific rather than general.
Good keywords:
- "block decoration"
- "metadata"
- "universal editor"
- "sidekick plugin"
Poor keywords:
- "aem" (too generic, filtered as stop word)
- "how to build website" (too broad)
- "the" (stop word)
Step 2: Run the Search Script
Execute the search script from the project root:
node .claude/skills/docs-search/scripts/search.js [--all] <keyword1> [keyword2] [...]Options:
--all: Return all matching results (default: limit to 10 most relevant)- Without
--all: Returns top 10 results
Examples:
# Search for block decoration info
node .claude/skills/docs-search/scripts/search.js block decoration
# Search for metadata with all results
node .claude/skills/docs-search/scripts/search.js --all metadata
# Multi-word search
node .claude/skills/docs-search/scripts/search.js universal editor blocksStep 3: Review Search Results
The script returns JSON with the following structure:
[
{
"path": "/developer/markup-sections-blocks",
"title": "Markup, Sections, Blocks, and Auto Blocking",
"description": "To design websites and create functionality, developers use the markup and DOM...",
"snippet": "Markup, Sections, Blocks, and Auto Blocking\n\nTo design websites...",
"type": "doc",
"deprecation": null,
"relevanceScore": 141
}
]Field Explanations:
path: URL path to the documentation pagetitle: Page titledescription: Brief summary (usually ~150 chars) - use this for quick contextsnippet: Relevant excerpt from the page content showing keyword contexttype: "doc" or "blog"deprecation: Warning message if feature is deprecated (or null)relevanceScore: Relevance score (higher = more relevant)
Important Notes:
- Results are sorted by relevance (highest first)
- Deprecated pages have reduced relevance scores but still appear in results
- The
descriptionfield provides the best quick summary of the page - The
snippetshows keyword context but may not be comprehensive
Step 4: Use WebFetch for Full Content
The search results give you an overview. To get detailed information, use WebFetch to read the full page:
WebFetch: https://www.aem.live{path}Best Practice: Start with the top 2-3 most relevant results, read them fully with WebFetch, then decide if you need more.
Step 5: Alert User to Deprecations
If any results have a deprecation field with content, inform the user that the feature is deprecated and include the deprecation message. Suggest they look at higher-ranked (non-deprecated) alternatives.
Search Behavior Details
What Gets Searched
1. Primary: Full documentation (docpages-index.json) - 150+ pages with complete content 2. Secondary: Blog posts (query-index.json) - Only searched if < 5 doc results found
Stop Words (Automatically Filtered)
These common words are ignored in searches:
- Articles: the, a, an
- Conjunctions: and, or, but
- Prepositions: in, on, at, to, for, of, with, by
- AEM-specific: aem, cms, edge, delivery, services
Relevance Scoring
- Title match: 10 points per occurrence
- Description match: 5 points per occurrence
- Content match: 1 point per occurrence
- Multi-keyword bonus: 1.5x multiplier if multiple keywords match
- Deprecation penalty: 0.5x multiplier (score halved) for deprecated features
Caching
Index files are cached locally for 24 hours in .claude/skills/docs-search/.cache/. This speeds up subsequent searches.
Examples
Example 1: Finding Block Documentation
User Request: "How do I decorate blocks in aem.live?"
Good Approach: 1. Search: node .claude/skills/docs-search/scripts/search.js block decoration 2. Review top 3 results 3. WebFetch the most relevant: https://www.aem.live/developer/markup-sections-blocks 4. Read full content and provide answer
Poor Approach:
- Using WebSearch instead (wastes time on irrelevant results)
- Not using the search script (might miss the best documentation page)
Example 2: Learning About Metadata
User Request: "I need to add metadata to my pages"
Good Approach: 1. Search: node .claude/skills/docs-search/scripts/search.js metadata 2. Notice top result is "/docs/bulk-metadata" (score: 63) 3. Also see "/docs/metadata" (score: 30) 4. WebFetch both to understand page-level vs bulk metadata 5. Provide comprehensive answer with both approaches
Poor Approach:
- Only reading the first result and missing bulk metadata option
- Not using
--allwhen you need comprehensive coverage
Example 3: Deprecated Feature Warning
User Request: "How do I use folder mapping?"
Search Results:
[
{
"path": "/developer/authoring-path-mapping",
"title": "Path mapping for AEM authoring",
"relevanceScore": 51,
"deprecation": null
},
{
"path": "/developer/folder-mapping",
"title": "Folder Mapping",
"relevanceScore": 34.5,
"deprecation": "Please contact us if you have a use case for folder mapping..."
}
]Good Response: "I found information about folder mapping, but this feature is deprecated. The deprecation notice says: 'Please contact us if you have a use case for folder mapping...'. The current recommended approach is Path mapping for AEM authoring (the top result). Let me read that documentation for you instead."
Poor Response:
- Ignoring the deprecation warning and implementing the deprecated feature
- Not mentioning the better alternative
Related Skills
- block-collection-and-party: Use when you need reusable code examples or block implementations
- building-blocks: Use when creating new blocks from scratch
- content-modeling: Use when designing content models for blocks
Important Reminders
1. Always check for deprecation warnings and alert the user 2. Use WebFetch to read full pages - search results are just for finding the right pages 3. Start with top 2-3 results before expanding search 4. The description field is your friend - it's usually well-written and concise 5. Don't rely solely on snippets - they're for context, not comprehensive information
{
"type": "module"
}
#!/usr/bin/env node
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import https from 'https';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const CACHE_DIR = path.join(__dirname, '..', '.cache');
const CACHE_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours
// Stop words to ignore in searches
const STOP_WORDS = new Set([
'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by',
'aem', 'cms', 'edge', 'delivery', 'services'
]);
// Scoring weights
const SCORE_WEIGHTS = {
TITLE: 10,
DESCRIPTION: 5,
CONTENT: 1,
MULTI_KEYWORD_MULTIPLIER: 1.5,
DEPRECATION_PENALTY: 0.5 // Multiply score by 0.5 if deprecated
};
/**
* Fetch JSON from URL with caching
*/
async function fetchWithCache(url, cacheFileName) {
const cacheFilePath = path.join(CACHE_DIR, cacheFileName);
// Check if cache exists and is fresh
if (fs.existsSync(cacheFilePath)) {
const stats = fs.statSync(cacheFilePath);
const age = Date.now() - stats.mtimeMs;
if (age < CACHE_DURATION_MS) {
const cached = fs.readFileSync(cacheFilePath, 'utf8');
return JSON.parse(cached);
}
}
// Fetch fresh data
return new Promise((resolve, reject) => {
https.get(url, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => {
try {
const json = JSON.parse(data);
// Cache the result
fs.mkdirSync(CACHE_DIR, { recursive: true });
fs.writeFileSync(cacheFilePath, data, 'utf8');
resolve(json);
} catch (err) {
reject(err);
}
});
}).on('error', reject);
});
}
/**
* Filter keywords to remove stop words
*/
function filterKeywords(keywords) {
return keywords
.map(k => k.toLowerCase().trim())
.filter(k => k.length > 0 && !STOP_WORDS.has(k));
}
/**
* Count keyword occurrences in text (case-insensitive)
*/
function countMatches(text, keyword) {
if (!text) return 0;
const regex = new RegExp(keyword, 'gi');
const matches = text.match(regex);
return matches ? matches.length : 0;
}
/**
* Clean up content noise from the beginning of text
*/
function cleanContent(text) {
if (!text) return '';
// Remove common noise patterns at the start
let cleaned = text
.replace(/^style\s+content\s+/i, '')
.replace(/^\s*\n+/, '')
.trim();
return cleaned;
}
/**
* Extract full sentences around keyword match
*/
function extractSnippet(text, keyword) {
if (!text) return '';
// Clean the text first
text = cleanContent(text);
const regex = new RegExp(keyword, 'i');
const match = text.match(regex);
if (!match) {
// No match - return first sentence(s) up to ~200 chars
return extractFirstSentences(text, 200);
}
const matchIndex = match.index;
// Find sentence boundaries around the match
// Look backwards for sentence start (. ! ? or start of text)
let sentenceStart = 0;
for (let i = matchIndex - 1; i >= 0; i--) {
if (text[i] === '.' || text[i] === '!' || text[i] === '?') {
sentenceStart = i + 1;
break;
}
}
// Look forwards for sentence end (. ! ? or end of text)
let sentenceEnd = text.length;
for (let i = matchIndex; i < text.length; i++) {
if (text[i] === '.' || text[i] === '!' || text[i] === '?') {
sentenceEnd = i + 1;
break;
}
}
let snippet = text.substring(sentenceStart, sentenceEnd).trim();
// If snippet is too short, try to include next sentence
if (snippet.length < 100 && sentenceEnd < text.length) {
let nextSentenceEnd = sentenceEnd;
for (let i = sentenceEnd + 1; i < text.length; i++) {
if (text[i] === '.' || text[i] === '!' || text[i] === '?') {
nextSentenceEnd = i + 1;
break;
}
}
snippet = text.substring(sentenceStart, nextSentenceEnd).trim();
}
// If snippet is too long, truncate at ~300 chars on sentence boundary
if (snippet.length > 300) {
snippet = snippet.substring(0, 300);
const lastPeriod = snippet.lastIndexOf('.');
if (lastPeriod > 100) {
snippet = snippet.substring(0, lastPeriod + 1);
} else {
snippet += '...';
}
}
return snippet;
}
/**
* Extract first few sentences up to maxLength
*/
function extractFirstSentences(text, maxLength = 200) {
text = cleanContent(text);
let end = 0;
let lastSentenceEnd = 0;
for (let i = 0; i < text.length && i < maxLength + 100; i++) {
if (text[i] === '.' || text[i] === '!' || text[i] === '?') {
end = i + 1;
if (end <= maxLength) {
lastSentenceEnd = end;
} else {
break;
}
}
}
if (lastSentenceEnd === 0) {
// No sentence found, just truncate
return text.substring(0, maxLength) + (text.length > maxLength ? '...' : '');
}
return text.substring(0, lastSentenceEnd).trim();
}
/**
* Calculate relevance score for a document
*/
function calculateScore(doc, keywords) {
let score = 0;
let matchedKeywords = 0;
for (const keyword of keywords) {
let keywordMatched = false;
// Title matches
const titleMatches = countMatches(doc.title, keyword);
if (titleMatches > 0) {
score += titleMatches * SCORE_WEIGHTS.TITLE;
keywordMatched = true;
}
// Description matches
const descMatches = countMatches(doc.description, keyword);
if (descMatches > 0) {
score += descMatches * SCORE_WEIGHTS.DESCRIPTION;
keywordMatched = true;
}
// Content matches (if available)
if (doc.content) {
const contentMatches = countMatches(doc.content, keyword);
if (contentMatches > 0) {
score += contentMatches * SCORE_WEIGHTS.CONTENT;
keywordMatched = true;
}
}
if (keywordMatched) matchedKeywords++;
}
// Multi-keyword bonus
if (matchedKeywords > 1) {
score *= SCORE_WEIGHTS.MULTI_KEYWORD_MULTIPLIER;
}
// Deprecation penalty
if (doc.deprecation && doc.deprecation.length > 0) {
score *= SCORE_WEIGHTS.DEPRECATION_PENALTY;
}
return score;
}
/**
* Search documents
*/
function searchDocuments(docs, keywords, type = 'doc') {
const results = [];
for (const doc of docs) {
const score = calculateScore(doc, keywords);
// Only include if score >= 1 (at least one keyword match)
if (score >= 1) {
// Extract snippet from content with keyword context
let snippet = '';
if (doc.content) {
for (const keyword of keywords) {
if (countMatches(doc.content, keyword) > 0) {
snippet = extractSnippet(doc.content, keyword);
break;
}
}
}
// Fallback to first sentences if no keyword match in content
if (!snippet && doc.content) {
snippet = extractFirstSentences(doc.content, 200);
}
results.push({
path: doc.path,
title: doc.title,
description: doc.description || '',
snippet,
type,
deprecation: doc.deprecation || null,
relevanceScore: score
});
}
}
return results.sort((a, b) => b.relevanceScore - a.relevanceScore);
}
/**
* Main search function
*/
async function search(keywords, limit = 10) {
// Filter keywords
const filteredKeywords = filterKeywords(keywords);
if (filteredKeywords.length === 0) {
console.error('No valid keywords provided after filtering stop words');
process.exit(1);
}
// Fetch indexes
const [docpagesIndex, queryIndex] = await Promise.all([
fetchWithCache('https://www.aem.live/docpages-index.json', 'docpages-index.json'),
fetchWithCache('https://www.aem.live/query-index.json', 'query-index.json')
]);
// Create a map of deprecation warnings from query-index
const deprecationMap = new Map();
for (const doc of queryIndex.data) {
if (doc.deprecation && doc.deprecation.length > 0) {
deprecationMap.set(doc.path, doc.deprecation);
}
}
// Search docpages first and merge deprecation data
let docpagesData = docpagesIndex.data.map(doc => ({
...doc,
deprecation: deprecationMap.get(doc.path) || doc.deprecation || null
}));
let results = searchDocuments(docpagesData, filteredKeywords, 'doc');
// If less than 5 results, search blog posts too
if (results.length < 5) {
const blogPosts = queryIndex.data.filter(doc => doc.path.startsWith('/blog/'));
const blogResults = searchDocuments(blogPosts, filteredKeywords, 'blog');
results = [...results, ...blogResults].sort((a, b) => b.relevanceScore - a.relevanceScore);
}
// Limit results if specified
if (limit > 0) {
results = results.slice(0, limit);
}
return results;
}
/**
* CLI entry point
*/
async function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error('Usage: search.js [--all] <keyword1> [keyword2] [keyword3] ...');
console.error('Example: search.js block decoration');
console.error(' search.js --all metadata');
console.error('');
console.error('Options:');
console.error(' --all Return all results (default: limit to 10)');
process.exit(1);
}
// Check for --all flag
let limit = 10;
let keywords = args;
if (args[0] === '--all') {
limit = 0; // 0 means no limit
keywords = args.slice(1);
}
if (keywords.length === 0) {
console.error('No keywords provided');
process.exit(1);
}
try {
const results = await search(keywords, limit);
console.log(JSON.stringify(results, null, 2));
} catch (error) {
console.error('Search error:', error.message);
process.exit(1);
}
}
main();