Building a Dynamic Artist Celebration Feature: Real-Time Content Updates Across Multi-Subdomain Event Sites
We recently implemented a dynamic artist celebration feature for the Queen of San Diego Rady Shell Events ecosystem. The challenge was to surface compelling artist content on individual event pages—updating three times daily until concert day—without requiring manual content updates or rebuilding static sites. Here's how we solved it using Google Apps Script, Python templating, and CloudFront cache invalidation.
The Problem: Static Sites Meet Dynamic Content
The Rady Shell events infrastructure consists of multiple static HTML sites hosted on S3, each behind a CloudFront distribution. Each event subdomain (e.g., event-name.queenofsandiego.com) is independently deployed. Adding a new content section that updates multiple times daily to celebrate each artist required a hybrid approach: we couldn't simply rebuild and redeploy HTML files three times per day, but we also needed the content to be fresh and engaging.
Architecture: Client-Side Content Injection via Google Apps Script
We chose a client-side injection model leveraging Google Apps Script as a lightweight backend service. Here's the layered architecture:
- Frontend: Static HTML event pages with a placeholder
<div id="artist-spotlight"></div>injected via Python templating - API Layer: Google Apps Script deployed as a web app, exposing an HTTP endpoint that returns artist celebration content as JSON
- Data Source: Google Sheet connected to Anthropic API for AI-generated artist spotlights
- Content Delivery: Client-side JavaScript fetches and renders the celebration section on page load
Implementation Details
Step 1: Python Template Injection
We modified /rady-shell-events/tools/render_event_sites.py to inject a standardized artist spotlight container into event HTML templates. The renderer now includes:
<section id="artist-spotlight" class="artist-celebration">
<div id="spotlight-loading">Loading artist celebration...</div>
</section>
<script src="/js/artist-spotlight.js"></script>
This approach allows us to deploy the section structure once while keeping content dynamic. The Python renderer iterates through all events in events.json, injecting this snippet into each event's HTML during the build process.
Step 2: Google Apps Script Backend
We created a new Google Apps Script project specifically for artist celebrations, deployed as a web app. Key components:
- New File:
ArtistCelebrationsService.gs— Contains the core celebration-fetching logic - Modified File:
Code.gs— Added routing logic to handle/artist-celebrationendpoint
The service architecture uses a Google Sheet as the source of truth, where each row contains an event date, artist name, and a "last_updated" timestamp. The Apps Script queries this sheet and calls the Anthropic API to generate fresh artist celebration content. Results are cached with a 12-hour TTL to avoid unnecessary API calls.
// Pseudocode structure of ArtistCelebrationsService.gs
function getArtistCelebration(eventDate) {
const sheet = SpreadsheetApp.openById(EVENTS_SHEET_ID);
const data = sheet.getRange("Artists!A:F").getValues();
// Find matching event
const event = data.find(row => row[0] === eventDate);
// Check cache; call Anthropic API if stale
const cached = getCachedCelebration(event.artistId);
if (cached && !isStale(cached)) {
return cached;
}
const celebration = callAnthropicAPI(event.artistName);
setCachedCelebration(event.artistId, celebration);
return celebration;
}
function doGet(e) {
if (e.parameter.path === '/artist-celebration') {
const eventDate = e.parameter.date;
const content = getArtistCelebration(eventDate);
return ContentService
.createTextOutput(JSON.stringify(content))
.setMimeType(ContentService.MimeType.JSON);
}
}
The deployment was created using the Apps Script CLI, and we stored the deployment ID for reference: this allows us to update the script without changing the endpoint URL.
Step 3: Client-Side Rendering
We created /rady-shell-events/assets/js/artist-spotlight.js, which runs on page load:
// Fetch from GAS endpoint and render
const eventDate = document.querySelector('[data-event-date]').dataset.eventDate;
fetch(`https://script.google.com/macros/d/{DEPLOYMENT_ID}/usercache/call?path=/artist-celebration&date=${eventDate}`)
.then(r => r.json())
.then(data => {
document.getElementById('artist-spotlight').innerHTML = `
<h3>${data.artistName}</h3>
<p>${data.celebration}</p>
<small>Updated: ${new Date(data.lastUpdated).toLocaleString()}</small>
`;
})
.catch(err => console.error('Artist celebration fetch failed', err));
Infrastructure Changes
S3 Buckets: Updated and Redeployed
After injecting the spotlight container, we uploaded updated HTML files to all event S3 buckets. The bucket naming convention follows qos-rady-shell-{event-slug}:
qos-rady-shell-events-main(primary events hub)qos-rady-shell-{specific-event}(individual event buckets)
We used a batch upload script to ensure all buckets were synchronized:
aws s3 sync ./dist/ s3://qos-rady-shell-events-main/ --delete
for event in $(aws s3 ls | grep qos-rady-shell | awk '{print $3}'); do
aws s3 sync ./dist/events/$event/ s3://$event/ --delete
done
CloudFront Cache Invalidation
Static content changes require cache invalidation. We identified all CloudFront distributions serving event subdomains and issued batch invalidations:
- Primary events distribution:
E{DIST_ID_MAIN} - Individual event distributions:
E{DIST_ID_EVENT_1},E{DIST_ID_EVENT_2}, etc.
After uploading HTML updates, we invalidated all affected distributions:
aws cloudfront create-invalidation --distribution-id E{DIST_ID} --paths "/*"
This ensures visitors receive the updated HTML with the new <div id="artist-spotlight"> container and the JavaScript loader script.
Why This Architecture?
Decou