In this post, we'll dive into how to use the power of Edge Delivery Services and Document Authoring to build a dynamic list of items from a spreadsheet located in DA.
There's a big difference between dumping a list of links onto a page and curating something actually usable. This project started as "we just need a list of URLs and descriptions," but quickly turned into solving a handful of real problems: keeping content maintainable, avoiding DOM bloat, and making filtering feel fast without overengineering it.
The result is the Reference List block; a data-driven, filterable card grid for Adobe Edge Delivery Services (EDS) that pulls content from a spreadsheet-backed JSON feed, supports category and tag filtering, and progressively loads results with polished animations.
Letting the Spreadsheet Drive Everything
The block is entirely data-driven. It fetches a JSON file generated by an EDS sheet at /en/reference-links.json. We decided to use a multi-sheet format; one sheet for reference items, one for categories:
{
"data": {
"data": [
{
"url": "https://developer.adobe.com/",
"image-url": "https://example.com/thumb.png",
"title": "Adobe Developer Documentation",
"description": "Official docs for building on Adobe platforms.",
"tag": "Documentation|Official",
"category": "Development"
}
]
},
"categories": {
"data": [{ "Category": "Development", "Description": "Dev tools and references" }]
}
}
The tags use pipes within the cell (Tag1 | Tag2), which keeps the authoring simple and predictable.
When you're creating this block you'll no doubt want to do some testing before pushing it live. If all you have is the populated list inside of DA, you will likely run into some cross-origin resource sharing (CORS) issues when testing locally. To fix this, simply create your own JSON file with some test entries that satisfy the same requirements as the DA sheet and enable local JSON usage.
const USE_LOCAL_JSON = false;
const LOCAL_JSON_PATH = '/tools/json/reference-list.json';
const LIVE_JSON_URL = '/en/reference-links.json';
Building Cards That Don't Break
Each reference item becomes a card with this DOM structure:
.reference-card
└── a.reference-link
├── div.reference-image-wrapper (optional)
│ └── img.reference-image
└── div.reference-content
├── h3.reference-title
├── p.reference-description (optional)
└── div.reference-tags
└── span.reference-tag (one per tag)
When building a block one of the most important things to consider is how easy an author can use it. To this end, we had to ensure that the list could handle different amounts of information and still function the same. Some have descriptions, tags, or neither. The rule was to only render what exists, but don't let the layout depend on any one piece being present. A few specific decisions:
-
Image is optional the image-url field (with a fallback to image) only renders if non-empty
-
Images use loading="lazy" so off-screen thumbnails don't block initial render
-
All links use rel="noopener noreferrer" to prevent tab-napping on external URLs
- This is usually done automagically by modern browsers, but better safe than sorry!
Filtering the List Without Rebuilding the World
When considering how to filter, things can sometimes get messy, especially when re-rendering on every interaction. To avoid this, we made sure to set each card's attributes from the get-go, that way it turns into more of a visibility-toggle instead.
card.setAttribute('data-category', ref.category);
card.setAttribute('data-tags', JSON.stringify(cardTags));
We also added a function, updateReferenceDisplay, to re-evaluate visibility on every interaction:
const categoryMatch = selectedCategory === 'all' || cardCategory === selectedCategory;
const tagMatch = cardTags.length === 0 || cardTags.some((tag) => activeTags.has(tag));
Tag matching uses OR logic, any selected tag qualifies a card. Active tags live in a Set, so add/remove and membership checks are all O(1). One subtle edge case: untagged items. Letting them always pass the tag filter meant they never disappear unexpectedly.
CSS Doing More Than Expected
While this isn't necessarily a blog about styling, I would be remiss if I didn't mention some of the decisions we made in that department. Especially regarding the "spotlight" effect when a card is hovered.
.reference-grid:has(.reference-card:hover) .reference-card:not(:hover) {
transform: scale(0.9);
opacity: 0.6;
}
The fade-in animation follows the same approach, so there's no coordination overhead between JS and styles:
.reference-card { opacity: 0; }
.reference-card.fade-in {
opacity: 1;
animation: fade-in-up 0.5s ease-out forwards;
}
@keyframes fade-in-up {
from { transform: translateY(20px); }
to { transform: translateY(0); }
}
Final Thoughts
This project is a strong example of how thoughtful architecture turns a common pattern into something genuinely scalable. By pushing responsibility into structured data and keeping the frontend focused on efficient rendering and state management, the Reference List block avoids the usual pitfalls of bloated DOMs and expensive re-renders.
The real value isn’t just in the filtering or presentation, but in the system itself. Authors get a predictable, low-friction way to manage content, while the implementation remains performant and easy to extend. It’s a pattern that holds up as content grows, not one that needs to be revisited every time it does.
See the reference page in action!
About The Author
Like what you heard? Have questions about what’s right for you? We’d love to talk! Contact Us