# SaaStock — MediaOps Playbook The single reference for how every photo and video shipped on saastock.com is named, captioned, converted, classified, schema-tagged, and cross-linked. Hand this to any developer or content operator building a new event gallery, and the result should be indistinguishable from the Dublin 2023 reference implementation at `/europe/2023/gallery`. This playbook extends [SearchOps](/agents/searchops/) — SearchOps governs the whole site's SEO/AEO/GEO stack; MediaOps governs the image- and video-specific stack inside it. --- ## 1. URL Architecture Every gallery lives under its parent event: ``` saastock.com/ ├── europe/2023/ │ ├── gallery/ ← Photo + video gallery │ │ ├── llms.txt ← Plain-text manifest for AI engines │ │ ├── photos/.webp ← Full-res (1600px max edge, q=82) │ │ ├── thumbs/.webp ← Thumb (600px max edge, q=78) │ │ └── video/ │ │ ├── .mp4 ← H.264 720p, AAC 96k, faststart │ │ ├── .webm ← VP9 720p, Opus 96k │ │ └── -poster.webp ``` Rules: - One gallery per event. Side events (Startup Day, NightStock, SaaS.City, Welcome Party) **are not separate URLs** — they're sub-sections inside the parent event's gallery, anchored at `#`. - File names are descriptive slugs (`expo-vanta-booth.webp`), never camera defaults (`DSC_0042.jpg`). File names are indexed by Google Images. --- ## 2. Phase taxonomy (the social-proof problem) Every photo carries a `phase` so the page can clearly separate the **pre-doors build-out** (empty booths, dark mainstage) from the **live event** (packed crowd, peak expo, panels in motion). Mixing those without context reads as "low attendance" to a casual scroller. | Phase | Badge text | Colour | Use for | |-----------|-----------------------------|-------------------|----------------------------------------------------------| | `live` | Live · during the event | Green `#0BA86A` | Audience seats filled, panel in motion, expo at peak | | `setup` | Pre-doors · sponsor booth | Amber `#C36A1A` | Sponsor booth shots captured before attendees arrived | | `bts` | Build-out | Slate `#3F4861` | Rigging, LED-wall alignment, empty stage waiting on chairs | | `signage` | On-site signage | Pink `#E91E8C` | Floor plans, partnership boards, future-edition booking | The page renders in this section order: **Live** → **Construction** (merged setup + bts) → **On-site signage** → **Startup Day / other side events**. Setup and bts share a single rendered section called "Construction" — the underlying `phase` distinction stays for badge colours and schema, but readers don't see two near-identical "empty venue" blocks back-to-back. **Hero anchor nav, not a separate section.** Section-skip links live inside the hero (right under the hero stat row) as short anchor pills — "Live during the event ↓" and "Construction ↓" — that scroll to the matching `id` on click. Do **not** add a standalone "Skip to a section / Live first, build-out clearly labelled" block below the hero — that's redundant. Keep the hero nav minimal (2 pills, not 5); the other sections are reachable by scrolling. **Badge placement:** phase badges and side-event badges render **below the image**, inline at the top of the figcaption — never as an absolute-positioned overlay on top of the photo. The image itself stays clean. --- ## 3. Side-event taxonomy (the two-place rule) When an event has side programs (Startup Day, NightStock, SaaS.City, Welcome Party, sponsor dinners), surface them in **two** places: 1. **`/europe//gallery`** — every side-event photo carries `sideEvent: { slug, label, venueName? }` in `gallery-data.ts`. The page renders them in their own `#` sub-section. `ImageObject.contentLocation` points at the side-event venue (e.g. Zendesk Dublin for Startup Day), not the main venue (RDS Simmonscourt). `ImageObject.about` includes a SubEvent referencing the parent Event's `@id`. 2. **`/europe/`** — a matching entry in `relatedEvents[]` in the event's `data.ts`, with `href: "/europe//gallery#"` and `image:` pointing at one feature photo. This renders as a clickable card in the "Side Events and Experiences" section. The lightbox doesn't auto-open on section anchors (only on photo-slug hashes), so clicking the side-events card scrolls the gallery to the correct sub-section. --- ## 4. Asset pipeline ### Photos - Source: any RAW or JPEG, any resolution. - Output: `cwebp -q 82 -m 6 -metadata none -resize 1600 0` for full, `-q 78 -m 6 -metadata none -resize 600 0` for thumb. - EXIF is stripped (`-metadata none`). - Native aspect ratios are preserved in the file (so the lightbox shows the full frame); we store `width` + `height` in the data file to prevent CLS. - **Grid display ratio is uniform** — every card in the gallery grid renders at `aspect-ratio: 4 / 3` with `object-fit: cover`. This prevents portrait + landscape shots from creating jagged rows with extra white space. The native aspect only matters in the lightbox, which sizes to the image. Pick the grid ratio per project (4:3 is the SaaStock default; square or 16:10 are reasonable alternatives) but apply it uniformly across every card on the same page. - The full WebP is what `` and `ImageObject.contentUrl` point at. The thumb is what `` and `thumbnailUrl` point at. `srcSet` lists both. ### Videos - Source: any `.mov`, `.mp4`, `.webm`. - Output: - `ffmpeg -c:v libx264 -preset slow -crf 24 -pix_fmt yuv420p -c:a aac -b:a 96k -movflags +faststart` → `.mp4` (Safari-safe). - `ffmpeg -c:v libvpx-vp9 -crf 34 -b:v 0 -row-mt 1 -tile-columns 2 -c:a libopus -b:a 96k` → `.webm` (Chrome/Firefox). - Scale: 720p (`scale=1280:-2` landscape, `scale=-2:1280` portrait). - Poster: extract a frame at 1s, save as `-poster.webp` at q=80. - `preload="none"` on every `