All agents

Photo & video gallery SEO · AEO · GEO

MediaOps

The single reference for how every SaaStock event gallery — photos, videos, floor maps — gets triaged, renamed, captioned, schema'd, cross-linked, and shipped. From raw camera files in Google Drive to live, schema-optimized gallery pages on saastock.com, including the sponsor booth ↔ event-page linkage.

ActiveUpdated 2026-05-20

Data assets

Source files this agent will draw from. Not indexed by search engines — internal working data only until the relevant pages ship.

  • MD

    SaaStock Gallery Page SEO Playbook (v2 working draft)

    Longer working-draft Markdown of the gallery playbook — covers the Google Drive triage workflow and the official-floor-map convention that the rendered MediaOps page above doesn't currently include. Treat the rendered page as the source of truth for shipped conventions; treat this MD as the wishlist of conventions we want to adopt next.

    Download (30 KB)

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 — 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/<slug>.webp   ← Full-res (1600px max edge, q=82)
│   │   ├── thumbs/<slug>.webp   ← Thumb (600px max edge, q=78)
│   │   └── video/
│   │       ├── <slug>.mp4        ← H.264 720p, AAC 96k, faststart
│   │       ├── <slug>.webm       ← VP9 720p, Opus 96k
│   │       └── <slug>-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 #<side-event-slug>.
  • 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.

PhaseBadge textColourUse for
liveLive · during the eventGreen #0BA86AAudience seats filled, panel in motion, expo at peak
setupPre-doors · sponsor boothAmber #C36A1ASponsor booth shots captured before attendees arrived
btsBuild-outSlate #3F4861Rigging, LED-wall alignment, empty stage waiting on chairs
signageOn-site signagePink #E91E8CFloor plans, partnership boards, future-edition booking

The page renders in this section order: LiveConstruction (merged setup + bts) → On-site signageStartup 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/<year>/gallery — every side-event photo carries sideEvent: { slug, label, venueName? } in gallery-data.ts. The page renders them in their own #<slug> 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/<year> — a matching entry in relatedEvents[] in the event's data.ts, with href: "/europe/<year>/gallery#<slug>" 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 <a href> and ImageObject.contentUrl point at. The thumb is what <img src> 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<slug>.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<slug>.webm (Chrome/Firefox).
    • Scale: 720p (scale=1280:-2 landscape, scale=-2:1280 portrait).
    • Poster: extract a frame at 1s, save as <slug>-poster.webp at q=80.
  • preload="none" on every <video> element except the hero/lead video.

Portrait video grids — always 3-up

Multiple portrait (9:16) videos shown together render in a 3-column grid on desktop / tablet, never 2-up. A 2-up portrait grid produces clips that are way too tall, dominate the viewport, and break the page rhythm. 3-up keeps each clip at a sensible scale and gets more content above the fold.

Why: Caught on /europe/2018/gallery — six portrait clips ran as a 2-up grid and read as "way too big." 3-up is the new floor.

How to apply:

  • Tailwind: grid grid-cols-3 gap-5 max-[640px]:grid-cols-1 for a portrait-video strip (drop straight to 1-up on narrow mobile; never go through a 2-up step).
  • Same rule for portrait-photo strips when they're shown as a dedicated "video strip"-style section. The main multi-column gallery grid already uses uniform 4:3 cards (see §4 Photos), which is unaffected.
  • Landscape videos can still render 2-up if the page only has two of them — landscape doesn't have the same vertical-dominance problem. But if you ever have ≥3 portrait clips, default to 3-up.

Gallery hero must be a photo, not a video (biggest stage + biggest audience)

The lead "feature" slot on every gallery page is a single landscape photo — the one frame that shows the biggest stage and the biggest audience in this gallery. Even when a landscape video is available, the photo beats the clip in the lead slot. The lead is a first impression; a still packed-house frame conveys "this is a serious event" instantly, while a video makes the reader wait for motion to interpret.

Why: First build of /europe/2018/gallery used the landscape LATKA Magazine seat-drop video (panning across empty chairs) as the lead. Even though it satisfied the no-portrait-hero rule, an empty-seats clip is the wrong first impression for "the first SaaStock to break 2,000 attendees." Swapped to the Rocket Fuel Stage balcony photo — packed audience, curved LED set, full venue context — and the page now opens with the right energy.

How to apply:

  • Pick the single landscape photo in the gallery that maximises stage size + audience density + venue context in one frame. Balcony / overhead shots usually win because they show the whole audience at once.
  • Render it as a large <figure> directly under the press-card row (same prominence the lead-video figure used to have).
  • The lead photo gets loading="eager" + fetchPriority="high" (it's the page's LCP candidate).
  • Click on the lead photo opens it at index 0 in the lightbox; the photo is excluded from the "Live" grid below so it isn't duplicated.
  • Videos — even strong landscape ones — move into the "More from the floor" grid below.

OG image and event-page hero should point at the same landscape photo (or a similarly-framed one from the gallery), so social previews and the event-page hero match the gallery's first impression.

Hero / lead-media orientation (no vertical hero, ever)

Never use a portrait/vertical (9:16) video or image as the page's lead/hero element on a wide-page layout. A 9:16 asset inside a wide container produces large dead bars on the left and right — bad first impression, wasted screen real-estate, and the actual content gets visually shrunk into a narrow centre column.

Why: Galleries and feature pages open on desktop/tablet wide layouts. The lead/hero slot has to fill that width. A vertical asset can fill the height but never the width — the result is a centred frame with empty rails on either side. This was caught on the first pass of /europe/2018/gallery, where a 9:16 dome clip ran as the lead and produced exactly that effect.

How to apply:

  • The lead media on any gallery, recap, or feature page must be landscape (≥ 16:9) — or square (1:1) only when no landscape asset exists.
  • Portrait assets belong inside the multi-column grid (where the uniform 4:3 card crop normalises them) or inside the lightbox after a click. Never at the top of the page on their own.
  • If the strongest "live" moment only exists in portrait, do not force it into the lead. Pick the next-best landscape clip — even a pre-doors / build-out / signage frame — and surface the portrait one inside the grid. Same rule for event-page heroes and landing-page hero bands.
  • The lead-video figure's frame uses aspectRatio: "16 / 9" (or wider). If your only candidate is portrait, change the lead — do not change the container aspect ratio to match the portrait asset.

File naming

Pattern: <section>-<subject>.webp. Section labels: stage, panel, audience, expo, team, venue, branding, bts, registration, networking, mainstage, <side-event-slug>. Subject is a descriptive slug (bootstrap-audience, founderpath-booth-wide).


5. Alt text and captions

Every alt follows the pattern:

<Context label> at SaaStock <Series> <Year> — <specific description of what's in the frame>

Context labels:

  • "Live during SaaStock Europe 2023"
  • "Before doors at SaaStock Europe 2023"
  • "Behind the scenes at SaaStock Europe 2023"
  • "On-site signage at SaaStock Europe 2023"
  • "Live at SaaStock Europe 2023 Startup Day" ← side events use this form

Rules:

  • Never start with "Image of" or "Photo of" — screen readers already announce the element type.
  • Include company names visible in the frame — these are the keywords people search for.
  • Include venue name (e.g. RDS Simmonscourt, Zendesk Dublin) — geo signal.
  • Include city — local SEO.
  • No duplicates across images on the same page.
  • Go longer than 125 chars if needed for accuracy. Google indexes the full alt text.

Captions (<figcaption>) are visible, not sr-only. They start with the same context label as the alt, then add one sentence of editorial detail the alt can't carry. Link sponsor names back to the event-page sponsor section (/europe/<year>#sponsors). Captions are crawled as regular text and carry significant weight.

Do not duplicate the alt into a title attribute on the same element. The playbook checklist explicitly removes redundant tooltips that don't help SEO.


6. Schema graph (one @graph, three top-level types)

Every gallery page emits exactly one <script type="application/ld+json"> block carrying a schema.org/@graph with three top-level entries:

  1. BreadcrumbList — Events → <Event short name> → Photo gallery.
  2. CollectionPage@id: <pageUrl>#gallery, isPartOf: { @id: <eventUrl>#event }, primaryImageOfPage: { @id: <pageUrl>#<heroSlug> }, plus keywords, inLanguage, and a SpeakableSpecification pointing at the "About" section CSS selector.
  3. ItemListnumberOfItems: <total>, itemListOrder: ItemListOrderAscending, with one ListItem per photo and video in the visual order they appear on the page. Each ListItem.item is a fully-populated ImageObject or VideoObject.

Cross-page entity bridge: the main event page (/europe/<year>/page.tsx) must carry @id: <eventUrl>#event on its Event schema. The gallery uses that @id to link every image to the event in the knowledge graph.

ImageObject required fields

  • @type: ImageObject
  • @id: <pageUrl>#<slug>
  • contentUrl, thumbnailUrl, name (= alt), description (= caption)
  • width, height
  • dateCreated — ISO date. Live/signage = mid-event day. Setup/bts = day before. Use the specific day where known.
  • representativeOfPage: true on the hero image, omit on the rest.
  • isPartOf: { @id: <eventUrl>#event }
  • creator = SaaStock Organization. copyrightHolder = SaaStock. license = site root or /terms/. creditText: "SaaStock".
  • contentLocation — Place + PostalAddress. Side-event photos point at the side-event venue (e.g. Zendesk Dublin), not the main venue.
  • about — when the photo features a sponsor booth, add an Organization entry pointing at the sponsor. When the photo is from a side event, also add an Event entry referencing the parent @id via superEvent.

VideoObject required fields

  • @type: VideoObject, @id: <pageUrl>#<slug>
  • name (= alt), description (= caption), thumbnailUrl (the poster), contentUrl (the MP4)
  • embedUrl (= page URL), uploadDate (event end date), dateCreated (the day filmed)
  • duration — ISO 8601 (PT28S)
  • creator, copyrightHolder, license, creditText, isPartOf, contentLocation — same as ImageObject

Skipped on purpose

  • WebP <picture> with JPEG fallback — cargo-cult in 2026. WebP is supported by every modern browser and crawler. Don't double storage.
  • Separate gallery-sitemap.xml — use <image:image> extensions inside the main sitemap entry for the gallery URL. One less file.
  • Per-photo individual route pages (/gallery/<slug>/) — overhead for marginal long-tail; revisit if traffic data justifies.

7. Lightbox

Every gallery is rendered with a full-screen lightbox. Click delegation: anchors carrying data-lightbox-index are intercepted by a client component (lightbox.tsx). Modifier-clicks (cmd/ctrl/shift, middle-click) bypass the lightbox so "open in new tab" still works.

Behaviour:

  • Centered media (max-height calc(100vh - 180px), max-width min(1280px, 92vw)), with aspect-ratio preserved.
  • Arrow keys (and on-screen chevron buttons) traverse all items in visual page order — lead video → live photos → setup → bts → signage → side-event photos → other videos. Wraps around the ends.
  • Home / End / PageUp / PageDown supported.
  • Esc closes. Clicks outside the media close.
  • Focus is moved into the overlay on open; returns to the original trigger on close.
  • Body scroll is locked while open.
  • Videos in the lightbox autoplay with controls.

URL hash deep-linking: opening /europe/<year>/gallery#<slug> auto-opens the lightbox at that slug. This is what powers the sponsor-booth deep-link from the event page. A hash that matches a section anchor (e.g. #startup-day) does not open the lightbox — the page just scrolls to the section.

Anchors still link to the raw WebP for crawlers and no-JS users.


8. Sponsor booth ↔ event-page linkage

Booth photos are the single highest-ROI image type for backlinks. The wiring:

  1. In gallery-data.ts — every sponsor-booth photo has aboutOrg: { name, url } and gets a corresponding entry in sponsorBoothPhotoBySlug (lowercased key → photo slug).
  2. In <event>/data.ts — sponsor rows can include { name, url, boothPhotoSlug }. The renderer reads boothPhotoSlug and looks up the thumb at /europe/<year>/gallery/thumbs/<slug>.webp.
  3. In event-page.tsx — the sponsor section renders a flat name list (no tier headings) followed by a separate "Booth Inspiration" subsection. The Booth Inspiration grid shows a 4:3 thumbnail per sponsor that has a booth photo, with the sponsor's name and a clickable card linking to /europe/<year>/gallery#<booth-photo-slug>. The lightbox auto-opens to that photo on arrival.

Tier headings ("Confirmed via booth", "Startup Day partners", "Silver", "Partners") were removed from the rendered output per Nathan's preference. The tier data is still in the source of truth — data.sponsors — so it can drive schema or other contexts, but the rendered page surfaces one flat list of names + the booth strip.

Why this matters for credibility: the reader sees "Vanta sponsored — and here's the actual booth they paid to build." That's miles more convincing than a list of names alone.


9. llms.txt route

Every gallery exposes /europe/<year>/gallery/llms.txt as a plain-text manifest:

  • Header with canonical URL + last-updated date
  • "What is this page?" paragraph naming the venue, dates, and total photo / video count
  • Explicit instruction: "Do NOT cite pre-doors booth shots or build-out shots as evidence of attendance."
  • Phase index — count by phase with one-line lede
  • Per-phase photo manifest: - <full URL> — <caption>
  • Video manifest with orientation + duration
  • Cross-links: event recap, speakers, talks pages

10. Checklist before publishing a gallery page

  • Every photo has a phase tag matching what's actually in the frame.
  • Side-event photos carry sideEvent; the recap page has a matching relatedEvents[] card.
  • All file names are semantic slugs, not DSC_*.jpg.
  • Alt text follows the "<Context label> at SaaStock <Series> <Year> — …" pattern; no duplicates.
  • <figcaption> on every image, visible, with editorial detail the alt can't carry.
  • <picture>-with-JPEG-fallback is not present. WebP only.
  • First image is loading="eager" + fetchPriority="high"; rest are loading="lazy".
  • Uniform grid aspect ratio across every card on the page (4:3 default, object-fit: cover) — portrait and landscape shots never mix in the same row with extra white space. width/height props + CSS aspect-ratio set — no CLS.
  • Badges below the image, not as absolute-positioned overlay on the photo. Phase pill always; side-event pill when applicable.
  • Schema is a single @graph block: BreadcrumbList + CollectionPage + ItemList.
  • Main event page has @id on its Event schema and the gallery references it.
  • Every ImageObject has dateCreated, creator, copyrightHolder, license, creditText, contentLocation.
  • Sponsor-booth photos have about: { @type: Organization, name, url }.
  • Side-event photos have contentLocation pointing at the side-event venue, and about includes a SubEvent.
  • Videos carry VideoObject with duration in ISO 8601.
  • Sitemap has <image:image> children on the gallery URL.
  • /europe/<year>/gallery/llms.txt returns 200 and lists every photo.
  • Lightbox: arrow keys cycle all items in visual order, Esc closes, modifier-clicks pass through.
  • URL hash #<slug> auto-opens the lightbox; #<section-id> scrolls only.
  • Sponsor section is flat (no tier headings) with a "Booth Inspiration" strip beneath listing every sponsor with a booth photo.
  • Recap page's "Side Events and Experiences" section has a card for every side event, linking to gallery#<slug>.
  • Press / high-res CTA scoped to booths, venue, build-out — not audience candids.
  • No title={alt} redundant tooltips.

11. Reference implementation

The Dublin 2023 build is the canonical reference:

  • Page: apps/web/app/europe/2023/gallery/page.tsx
  • Data: apps/web/app/europe/2023/gallery/gallery-data.ts
  • Lightbox: apps/web/app/europe/2023/gallery/lightbox.tsx
  • llms.txt: apps/web/app/europe/2023/gallery/llms.txt/route.ts
  • Sponsor cross-link: apps/web/app/europe/2023/data.ts (sponsors[].boothPhotoSlug) + apps/web/components/event-page.tsx (renderer)
  • Side-event card: apps/web/app/europe/2023/data.ts (relatedEvents[].href = "/europe/2023/gallery#startup-day")

When in doubt, clone Dublin 2023's structure and substitute the new event's data.


12. Local chapters (SaaStock Local)

City chapters publish at /local/<city-slug>/. Per-event galleries publish at /local/<city-slug>/gallery/ — every URL/phase/schema rule in this playbook applies unchanged. Where flagship-event galleries cover one event with many side events, a local-chapter gallery is the inverse: one chapter URL with many events across years, surfaced as time-anchored sub-sections inside the gallery.

URL architecture for local chapters

saastock.com/local/<city-slug>/
├── gallery/                          ← One gallery per city (all events combined)
│   ├── llms.txt
│   ├── photos/<slug>.webp            ← 1600px, q=82
│   ├── thumbs/<slug>.webp            ← 600px, q=78
│   └── (video/ if present)

Sub-sections: photos for a specific evening are anchored at #<event-slug> (e.g. #q2-2024, #september-19-2024) — same shape as the side-event anchors on flagship galleries, swapping side-event for event-date. For a chapter with only one documented event, the gallery can ship without sub-sections and use the Live → Construction → Signage section order directly (Amsterdam Q2 2024 is the reference).

Two-place rule for local events

Same wiring as flagship side events, applied to chapter events instead:

  1. /local/<city>/gallery — every photo of a chapter event carries the event date (used in ImageObject.dateCreated) and the venue (used in ImageObject.contentLocation). Photos hosted by a third-party SaaS office (e.g. Personio's Amsterdam office for Amsterdam Q2 2024) point contentLocation at the host venue — that's the geo-signal Google Images indexes.

  2. /local/<city> — every event in registry.ts that has a gallery carries galleryUrl: "/local/<city>/gallery" (optionally with a #<event-slug> fragment when the gallery is multi-event) and a galleryHeroThumb pointing at one feature photo. The chapter page renders this as a clickable card under the events-history table and adds an image field on the event's schema.org node.

Hero photo selection for chapter galleries

The gallery's hero (the photo that gets representativeOfPage: true on its ImageObject and is the OG image for /local/<city>/gallery) is the most representative live shot:

  • A handshake, a featured speaker on stage, or a crowd-and-stage wide. Not the empty venue, not a sponsor-logo close-up.

Chapter-page hero photo (LocalCity.chapterHeroPhoto) — full-bleed header

The chapter page (/local/<city>/) opens with a full-bleed packed-room hero that mirrors the SaaStock.com homepage. The chapterHeroPhoto.src is absolute-positioned behind a dark navy gradient (rgba(22,14,65,0.92) → 0.18), with the white H1, SaaStock Local eyebrow, and openingParagraph overlaid in the foreground. A back-link to /local/ sits top-left in white; a "Pictured: …" credit sits bottom-right and, when deepLinkUrl is set, links into the gallery lightbox at that exact slug.

Page-order rationale: the first thing a visitor lands on must answer "is this a real community?" — and a packed-room photo answers that in one second. The events-history table and AEO/SEO content sit below the trust-building content (hero → leaders → highlight video → speakers → photo strip).

Picking this image well is what makes the chapter page feel like a community, not a directory entry — so the rule is deliberate:

Pick the single photo that shows the biggest audience from a delivered event. Specifically:

  • The most heads visible in frame (≥20 ideally; the back rows extending into the room beats a tight close-up of the front row).
  • The stage and speakers visible too — so the photo reads as "real event happened here" not "stock photo of a crowd in a room".
  • At least one geo-anchor in frame when available: a hoodie/sign/window with a recognisable city marker (e.g. "Rusty Gold Motorshop Amsterdam", a London skyline through the window, the Lisbon hillside) — these turn a generic audience shot into one Google Images can place geographically.
  • The host venue's branding or wordmark also acceptable when no city marker is visible.
  • Clean composition: no plants or attendees' shoulders bisecting the frame.

What to avoid:

  • Empty room or sparsely-seated shots — these read as "low attendance". Always pick from the live phase.
  • Close-up portraits — they don't show scale.
  • Heavily backlit or low-light shots — the hero needs to render cleanly at fetchPriority="high" without a colour-cast or graininess.

The photo is the value of chapterHeroPhoto.src in registry.ts and deepLinkUrl should open the gallery lightbox at that exact slug. Eager-loaded (loading="eager" + fetchPriority="high"), rendered object-cover inside a min-h-[min(72vh,680px)] hero section. Landscape and portrait sources both work, but landscape is preferred because the H1 sits in the lower-left. This image is the canonical "social proof" asset for the chapter — if the chapter has multiple delivered events, pick the biggest-audience shot from any of them and caption it with the event date so the photo is honestly dated.

Chapters without a chapterHeroPhoto yet render the hero as a navy block with the gradient overlay and the white H1/lede — graceful degradation, no broken layout. Ship chapterHeroPhoto in the same PR that processes the first photo gallery for the chapter.

Chapter leads are always Featured Speakers

The Featured Speakers grid is rendered from a merged list — leaders prepended to speakers — with leader cards getting a "Chapter lead · SaaStock Local <City>" subtext instead of the "Spoke at SaaStock Local <City>" used for invited speakers. This is intentional duplicate placement: the leader already has a card up top, and they appear again as a speaker card below. Reasoning:

  • Leaders run the show at every chapter event — they're on stage by definition, so excluding them from "featured speakers" would be inaccurate.
  • The duplicate placement compounds the lead's visibility on the page, which is the single biggest lever for keeping leads engaged and hosting future events. (Per Nathan: "boosts their energy and gets them excited about hosting future events.")

To wire this in registry.ts, add a photo block to the leader entry — { src, alt, deepLinkUrl } — pointing at a gallery thumb where the leader is recognisably on stage or running the room (e.g. opening the welcome, introducing a panel, holding the mic). Avoid candid social shots — the leader photo should mirror the energy of the speaker photos beside it.

If a leader is also listed in speakers (e.g. a co-host who later spoke on stage), the renderer dedupes by name so they only show up once — the leader entry wins because the "Chapter lead" subtext outranks the "Spoke at" subtext.

Hub-page card image (LocalCity.cardImage)

The /local/ hub renders every published chapter in a "Featured chapters" grid. The whole card — image, name, country, leader, events-hosted count — is one <Link href="/local/<slug>/">. Every Phase-1 chapter that has a delivered-event gallery must ship a cardImage — a text-only card on the hub reads as "chapter doesn't exist" to a casual scroller.

Selection rules (similar to chapterHeroPhoto, but tighter crop because the card is 4:3 not 3:2):

  • Reuse the chapter hero photo's thumb where possible — same shot, same framing, no extra curation overhead.
  • Live phase only; empty-room and pre-doors shots are disqualified.
  • Must read at 320px wide. Faces / a panel slide / a packed room beats a wide skyline.
  • Avoid backlit silhouettes — the card sits on white and contrast collapses.

Wiring:

cardImage: {
  src: "/local/<slug>/gallery/thumbs/<chosen-slug>.webp",
  alt: "Live at SaaStock Local <City> <Year> — <city + venue/moment>"
}

Renders aspectRatio: 4 / 3, object-fit: cover, loading="lazy". No nested <a>s — the parent <Link> already carries the click. Chapters without a gallery yet ship without cardImage and render text-only; add the field in the same PR that processes the first photo gallery.

CTA block + WhatsApp sponsor CTA + AI Growth Summit Local rebrand

Brand transition language. SaaStock Local is being rebranded to AI Growth Summit Local in 2026. The H1 stays "SaaStock Local <City>" (SEO-anchored), but the Attend CTA refers to "AI Growth Summit Local <City> (formerly SaaStock Local)" — that's how visitors land on the new brand without losing the chapter's canonical name. Never describe chapter events as "free" — they are paid events; any older copy that implied free attendance must be rewritten.

Attend / Lead-or-sponsor CTAs are wired off LocalCity.nextEventUrl (Luma/Tito/etc.). When set:

  • Attend body: "Join the next AI Growth Summit Local <City> event (formerly SaaStock Local)."
  • Lead-or-sponsor body: "If you want to lead or sponsor, the best way to do it is to come to the event and talk to us in person — or message Nathan directly on WhatsApp."
  • Attend CTA: "Click here to attend →" → nextEventUrl (target="_blank" rel="noopener noreferrer").
  • Lead-or-sponsor CTAs: the same "Click here to attend →" AND a green WhatsApp button beside it.

When nextEventUrl is unset, the Attend block falls back to "Get notified →" /subscribe/ but the Lead-or-sponsor block still surfaces the green WhatsApp button as the primary CTA — sponsors don't wait for a chapter to have a Luma listing.

WhatsApp button (WHATSAPP_URL constant, single top-of-file source of truth — currently https://wa.link/y82o72 resolving to Nathan's US +1 703-431-2709). Surfaces in two places:

  1. Leader + WhatsApp paired row directly under the hero — 2-column 50/50 grid. Left cell holds the city-leader card(s); right cell is the pale-background WhatsApp sponsor card with the one-line pitch and a green WhatsApp button anchored bottom-left. Stacks to 1 column under 720px. The pairing surfaces "who runs this chapter" and "how to sponsor" in the same eye-line, so the sponsorship CTA has immediate context (a sponsor sees a real human running the chapter, not a faceless contact form).
  2. Lead-or-sponsor CTA block — green button beside "Click here to attend".

Both placements link to the same URL. Green is the WhatsApp brand (#25D366 → hover #1ebe5b), always target="_blank". Routing changes are a one-line constant edit — no per-city data work.

Sponsors: per-event row + all-time social-proof wall

Each LocalEvent carries a sponsors: string[] of company names. The chapter page renders these in two complementary places:

  1. Per-event — a Sponsors column inside the events-history table. Each event row gets a wrap of pale pill chips, one per company in LocalEvent.sponsors. Empty events render an em-dash.

  2. All-time — a standalone "All-time sponsors" section above the CTAs, aggregating every sponsor that has ever sponsored a chapter event. The heading reads "Thank you to our sponsors." — gratitude, not a counter (e.g. "3 companies have backed…" reads as small while "Thank you to our sponsors" reads as warm at any list length). Wall sorting: most repeat appearances first → most-recent year → alphabetical. Each chip carries the sponsor name + a year-tag showing every year they backed an event + an ×N multiplier when they appeared more than once.

The point of the all-time wall is social proof that compounds: the list only grows over time. Three sponsors at year one becomes ten at year three. The longer the wall, the harder it is for a new potential sponsor to say no. The wall is purely derived from LocalEvent.sponsors data — adding a sponsor to a new event row automatically updates the wall.

When the wall reaches ~20+ sponsors and reads cluttered as text-only chips, upgrade to a logo wall (separate work — SVG logos under public/local/<slug>/sponsors/<slug>.svg and a renderer change). For chapters with 4–15 sponsors, text chips read cleaner than logos and ship faster.

Per-event highlight reels (LocalEvent.highlightVideo)

When an event has a 60–120s highlight reel, drop it under apps/web/public/local/<slug>/gallery/video/<event-slug>.{mp4,webm} with a matching <event-slug>-poster.webp (1600px, q=82, frame from the live phase). Wire it through the matching LocalEvent.highlightVideo block:

{
  ...,
  highlightVideo: {
    src: "/local/<slug>/gallery/video/<event-slug>.mp4",
    srcWebm: "/local/<slug>/gallery/video/<event-slug>.webm",   // optional
    poster: "/local/<slug>/gallery/video/<event-slug>-poster.webp",
    alt: "<screen-reader / og:video description>",
    caption: "<editorial one-liner under the player>",
    duration: "1:24"                                            // optional, "M:SS" or "H:MM:SS"
  }
}

The chapter page renders a "Highlight reels" section directly under the events-history table — one <video controls preload="none" poster=...> card per event with video, in a repeat(auto-fit, minmax(360px, 1fr)) grid at 16:9. The section only renders when at least one event has video. Never autoplay; the play button is the CTA.

Reel must be a highlight, not a full talk. Full talk uploads belong on YouTube and ship as their own /talks/<slug> pages — they are not the same artifact. Cap MP4 at ~10–15 MB; bigger reels go to a CDN/YouTube and embed via a separate workflow.

The renderer emits a VideoObject on the event's schema.org Event.subjectOfcontentUrl, thumbnailUrl, uploadDate (= event date), and ISO-8601 duration (1:24PT1M24S). Don't add the video to the gallery's existing ItemList schema; reels are an event artifact, not a photo.

Reference: Amsterdam Q2 2024

  • Page: apps/web/app/local/amsterdam/gallery/page.tsx
  • Data: apps/web/app/local/amsterdam/gallery/gallery-data.ts
  • Lightbox: apps/web/app/local/amsterdam/gallery/lightbox.tsx (clone of the Dublin 2023 lightbox; refactor to a shared component once a 3rd chapter ships)
  • llms.txt: apps/web/app/local/amsterdam/gallery/llms.txt/route.ts
  • City-page cross-link: apps/web/app/local/registry.ts (LocalEvent.galleryUrl + LocalEvent.galleryHeroThumb) + apps/web/app/local/[slug]/page.tsx (Gallery column + card strip).
  • Hub card image: LocalCity.cardImage on the Amsterdam registry entry, pointing at audience-wide-from-back.webp (same shot's thumb as chapterHeroPhoto).

49 photos, hosted at Personio Amsterdam, April 25, 2024. Two operator talks (Ferdinand Goetzen / Joran Hofman; Koen Stam / Lepaya) plus reception, audience, and a SaaStock Dublin ticket giveaway. No side events, no videos. Hero is panel-ferdinand-joran-handshake (live, end-of-talk handshake — the single shot that signals "this event happened" without any framing).