Building Headless Applications with Edge Delivery Services

Author: Tom Cranstoun
Edge Delivery Services typically works best for static content delivery, but what if you need something more dynamic? This guide shows how to build a headless Single Page Application while keeping the performance benefits that make Edge Delivery Services attractive. Keeping everything framework-less.

The Challenge with Traditional SPAs

Single Page Applications often sacrifice the perfect Lighthouse scores that Edge Delivery Services delivers straight away. You gain enhanced user interactions but lose those fast initial load times. The solution lies in a hybrid approach that gives you both benefits.

Here's the architectural insight - keep your home, campaign, and landing pages as pure Edge Delivery Services implementations. These pages maintain those perfect 100/100/100/100 Lighthouse scores. Then use SPA functionality for secondary pages where interactivity matters more than initial load performance.

The code for this blog is here: https://github.com/ddttom/spa-with-eds

Building a Content-First Slideshow

Let's build something practical - a slideshow application that shows these principles in action. Traditional CMS implementations use complex image pools and extendable components, which make life difficult for content authors. We'll create something that puts the author's experience first.

The specification focuses on simplicity. Each slide exists as its own page, and authors create individual pages with images and content. The application extracts metadata from each page and displays slides in a continuous scrolling format. Each slide links to its full page for detailed content.

This approach transforms a typically difficult authoring experience into something straightforward. Authors work with familiar page creation tools rather than wrestling with component configurations.

Setting Up the Content Structure

Start by creating a folder structure in your Google Drive. You'll need a slides folder containing your individual slide pages - I used my home town :- york-minster, the-shambles, and city-walls. An example is below, note the metadata. it is used to create the query-index.

Add a query-index spreadsheet to this folder., the query-index sheet should follow this layout, to match the metadata on the page:


Each page follows a simple layout with a title, hero image, descriptive text, and metadata. The query-index spreadsheet acts as your data source. Edge Delivery Services automatically populates it with page information, creating a JSON endpoint at /slides/query-index.json. The json is created for published pages, this means that while you are working on them, they are not visible to the viewers of the site.

Remember to publish the pages if you cannot see the json.

The JSON-Powered Architecture

When you preview and publish your pages, Edge Delivery Services generates clean JSON data:

{
  total: 5,
  offset: 0,
  limit: 5,
  data: [
    {
      path: "/slides/york-minster",
      title: "York Minster",
      image: "/slides/media_188fa5bcd003e5a2d56e7ad3ca233300c9e52f1e5.png",
      description: "A magnificent Gothic cathedral...",
      lastModified: "1719573871"
    }
    // ... more slides
  ]
}

This is the excel sheet which informs the json:

This JSON becomes the backbone of your SPA, providing all the data needed to build the interface without complex API integrations. We are building a tutorial so I have not parameterized the path, in production you would do this.

Building the Application Logic

The implementation starts with a simple fetch to retrieve slide data:

async function fetchSlides() {
  const response = await fetch("/slides/query-index.json");
  const json = await response.json();
  const slides = [];
  for (const slide of json.data) {
    if (window.innerWidth > 799) {
      slide.html = await fetchSlideHtml(slide.path);
    } else {
      slide.html = null;
    }
    slides.push(slide);
  }
  return slides;
}

Notice the responsive design consideration - we only fetch full HTML content for desktop users, keeping mobile performance optimal. This mobile-first, desktop-enriched approach ensures fast loading on all devices.

Using Plain HTML Fragments

Edge Delivery Services provides a powerful feature through the .plain.html endpoint. Appending this to any page path returns just the content without headers, footers, or JavaScript:

async function fetchSlideHtml(path) {
  try {
    const response = await fetch(`${path}.plain.html`);
    if (!response.ok) {
      throw new Error(`Failed to fetch HTML for slide: ${path}`);
    }
    return await response.text();
  } catch (error) {
    console.error(error);
    return null;
  }
}

This technique lets you extract and reuse content from existing pages, treating the DOM as a first-class citizen - a core principle of Edge Delivery Services.

Implementing Lazy Loading

Performance remains crucial even in an SPA context. The implementation uses Intersection Observer for intelligent image loading:

const observer = new IntersectionObserver(

  (entries, observer) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        const slideItem = entry.target;
        const imageUrl = slideItem.dataset.bg;
        setSlideBackground(slideItem, imageUrl);
        observer.unobserve(slideItem);
      }
    });
  },
  { rootMargin: "100px" }
);

Images load just before they enter the viewport, maintaining smooth scrolling while reducing initial page weight.

Progressive Enhancement in Action

The application adapts its behaviour based on device capabilities:

async function createSlideItem(slideData, index) {
  const slideItem = document.createElement("div");
  // Basic content for all devices

  slideItem.innerHTML = `
    <div class="text-container">
      <h2>${title}</h2>
      <p><strong>${description}</strong></p>
    </div>
  `;
  // Enhanced content for desktop

  if (slideData.html) {
    const supportingText = fetchSupportingText(slideData.html);
    if (supportingText) {
      textContainer.insertAdjacentHTML('beforeend', 
        `<p class="supporting-text">${supportingText}</p>`
      );
    }
  }
  return slideItem;
}

Mobile users get the essential experience, while desktop users receive enriched content with supporting text extracted from the full pages.

Creating Interactive Overlays

User interaction opens detailed panels without navigation:

async function createPanel(slideData) {
  let html = slideData.html;
  if (!html) {
    html = await fetchSlideHtml(slideData.path);
  }
  const panel = document.createElement('div');
  panel.classList.add('slide-panel');
  panel.innerHTML = `
    <div class="slide-panel-content">
      <div class="slide-panel-body"></div>
    </div>
    <button class="slide-panel-close" aria-label="Close panel">&times;</button>
  `;
  panel.querySelector('.slide-panel-body').innerHTML = html;
  document.body.appendChild(panel);
}

This pattern keeps users engaged without full page loads, maintaining context while providing detailed information on demand.

Performance Optimisation Strategies

Despite the complex functionality, the application maintains excellent performance through several techniques.

Smart image loading constructs optimised URLs based on browser capabilities. The code checks for WebP support and adjusts accordingly, using query parameters to request appropriately sized images.

Conditional content loading means mobile devices skip loading detailed HTML until specifically requested. This reduces data usage and improves speed without limiting functionality - users can still access full content when they want it.

DOM parsing extracts content directly from HTML rather than requiring complex data structures. A simple DOMParser pulls out the first paragraph after each heading, providing supporting text without additional API calls or data transformation.

The Complete Implementation

Build the container page in the document store

The full implementation combines these concepts into a cohesive application that loads quickly, responds smoothly, and provides rich interactivity. The architecture achieves several key goals - maintaining excellent Lighthouse scores despite SPA functionality, creating a content author-friendly system, implementing progressive enhancement for different devices, establishing efficient data loading patterns, and maintaining clean separation between content and presentation.

Phew, that is complicated; by breaking it into smaller pieces, I hope you see that the design fits together and you will be able to build SPA Applications with query-index.json and .plain.html

We kept excellent lighthouse scores despite using lots of fetch and stitching together. Edge Delivery Services is a beautiful tool.

The finished Spa App is here Five things to do in York, England (allabout.network)

The code is here https://github.com/ddttom/spa-with-eds

Moving Forward

This headless approach to Edge Delivery Services opens new possibilities. You can extend these patterns to build product catalogues with filtering, interactive galleries, data visualisations, and dashboard interfaces.

The next evolution involves converting this vanilla JavaScript implementation to React, Vue and Adobe Spectrum Components. We use these architectures to add structure while maintaining the performance characteristics that make Edge Delivery Services compelling.

Edge Delivery Services isn't just for static pages. With thoughtful architecture and modern JavaScript techniques, you can build rich, interactive experiences without sacrificing the platform's core strengths.

I will create other posts which take this Json and its content to other frameworks:

<hr/>

REACT

https://allabout.network/blogs/ddt/integrations/building-a-react-app-with-edge-delivery-services

<hr/>

VUE

https://allabout.network/blogs/ddt/integrations/building-a-vuejs-app-with-edge-delivery-services

<hr/>

EDS

And as a reminder, using raw EDS, EDS for the Win! Five things to do in York, England (allabout.network)

<hr/>

When using these other frameworks, now you know how to create the Json just use this as the url https://allabout.network/slides/query-index.json

Found this helpful? Star the https://github.com/ddttom/spa-with-eds on GitHub!

/fragments/ddt/proposition

Related Articles

path=*
path=*
Back to Top