Building a Daily Artist Celebration System for Event Pages: Google Apps Script + S3 + CloudFront Pipeline
Overview: Inspiration and Goals
The core premise for this feature came from a simple but powerful insight: Nike doesn't sell shoes by showing shoes—they celebrate athletes. Similarly, for Queen of San Diego's Rady Shell event series, we wanted to shift focus from event logistics to artist celebration. The goal was to create dynamic, frequently-updated artist spotlights that would refresh three times daily until concert day, building emotional connection between audiences and performers.
This required building a pipeline that integrated Google Apps Script (GAS) as a backend service, Python-based static site generation, AWS S3 for hosting, and CloudFront for global distribution with cache invalidation.
Technical Architecture
Component Stack
- Data Source: Google Sheets (artist information and celebration content)
- Backend API: Google Apps Script (ArtistCelebrationsService.gs)
- Static Generation: Python (render_event_sites.py, inject_artist_spotlight.py)
- Hosting: AWS S3 (per-event subdomain buckets)
- CDN: CloudFront distributions with cache invalidation
- DNS: Route53 (event subdomain routing)
Implementation Details
1. Google Apps Script Backend (ArtistCelebrationsService.gs)
We created a new service file that handles artist celebration data retrieval and formatting:
// ArtistCelebrationsService.gs
// Fetches artist celebration content from Google Sheet
// Returns formatted HTML-ready content for injection into event pages
function getArtistCelebration(artistName, eventDate) {
// Query sheet for today's celebration entry
// Return structured data with celebration text, images, metadata
}
Why this approach: Storing celebration content in Google Sheets meant non-technical staff (Carole and team) could update artist spotlights without touching code. The GAS backend abstracts the sheet structure from the frontend, allowing content changes without redeployment.
The service exposes an HTTP endpoint via a new GAS deployment. We registered this as a custom route in Code.gs to handle requests like /artist-celebration?event=artist-name.
2. Content Injection Pipeline (inject_artist_spotlight.py)
A new Python script was created to inject the artist celebration HTML into existing event page templates:
# inject_artist_spotlight.py
# Purpose: Insert artist celebration section into rendered event HTML files
# Workflow:
# 1. Read rendered event HTML from render_event_sites.py output
# 2. Query GAS endpoint for current artist celebration data
# 3. Inject HTML into appropriate location (before footer, after event details)
# 4. Write updated HTML back to file
# 5. Prepare files for S3 upload
for event_file in event_html_files:
# Load rendered HTML
soup = BeautifulSoup(open(event_file), 'html.parser')
# Fetch artist celebration from GAS
celebration_data = fetch_from_gas(event_id)
# Find injection point (class="event-content" or similar)
injection_point = soup.find('div', class_='event-content')
# Insert celebration section with styling
spotlight_html = build_celebration_section(celebration_data)
injection_point.append(BeautifulSoup(spotlight_html, 'html.parser'))
# Write modified HTML
write_updated_html(event_file, soup)
Design decision: Rather than modifying the core site renderer (render_event_sites.py), we created a separate injection step. This maintains separation of concerns—the renderer handles static content structure, while the injection script handles dynamic celebration content. This allows the celebration system to be updated independently without touching the event template.
3. Static Site Renderer Integration (render_event_sites.py)
Modified the existing renderer to prepare injection points for the artist celebration section:
# render_event_sites.py modifications
# Added a placeholder div with consistent class names that inject_artist_spotlight.py targets
# Ensures celebration section will render in correct location with proper styling
template_updates = {
'event_content_wrapper': 'div class="event-content"',
'artist_spotlight_anchor': 'id="artist-celebration-inject"',
'styling_classes': 'artist-celebration artist-celebration--active'
}
Infrastructure and Deployment
S3 Bucket Organization
Event pages are hosted in per-subdomain S3 buckets following this pattern:
s3://queenofsandiego-events-[event-name]/
├── index.html (with injected artist spotlight)
├── assets/
├── css/
└── img/
Example buckets updated:
- s3://queenofsandiego-events-artist1/
- s3://queenofsandiego-events-artist2/
- s3://queenofsandiego-events-artist3/
Upload workflow: After injection, updated HTML files were uploaded to each corresponding bucket using batch operations, preserving cache control headers to allow CloudFront invalidation to take effect immediately.
CloudFront Cache Invalidation
Since the artist celebration content updates three times daily (morning, afternoon, evening), we needed aggressive cache invalidation:
#!/bin/bash
# Invalidate CloudFront distributions for all event subdomains
# Triggered by automation (cron job or manual deployment)
DISTRIBUTIONS=(
"E1ABC123DEF456" # artist1.queenofsandiego.com
"E2XYZ789GHI012" # artist2.queenofsandiego.com
"E3QRS345TUV678" # artist3.queenofsandiego.com
)
for dist_id in "${DISTRIBUTIONS[@]}"; do
aws cloudfront create-invalidation \
--distribution-id "$dist_id" \
--paths "/*"
done
Why full path invalidation: We invalidate /* rather than specific paths to ensure the injected celebration section reaches all users immediately. Three daily updates meant we couldn't rely on long TTLs.
Google Apps Script Deployment
The ArtistCelebrationsService was deployed as a new standalone deployment within the existing GAS project:
// Code.gs router additions
function doGet(e) {
const path = e.parameter.action || '';
if (path === 'artist-celebration') {
const eventId = e.parameter.event;
const celebration = ArtistCelebrationsService.getArtistCelebration(eventId);
return ContentService.createTextOutput(JSON.stringify(celebration))
.setMimeType(ContentService.MimeType.JSON);
}
// ... existing routes
}
The live deployment URL (found via clasp deployments) was registered in the injection script's configuration to ensure it pulls the latest celebration data at build time.
Key Design Decisions
Static Generation + Dynamic Backend (Hybrid Approach)
Rather than making event pages fully dynamic (which would add latency and complexity), we chose a hybrid model: static