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