diff --git a/src/app/day/2026/[city]/talks/[id]/opengraph-image.tsx b/src/app/day/2026/[city]/talks/[id]/opengraph-image.tsx new file mode 100644 index 0000000000..6664118168 --- /dev/null +++ b/src/app/day/2026/[city]/talks/[id]/opengraph-image.tsx @@ -0,0 +1,24 @@ +import { findDayTalk, getDayTalkParams } from "../../../day-events" +import { TalkOpengraphImage } from "../../../components/og-images/talk-opengraph-image" +export { + contentType, + size, +} from "../../../components/og-images/talk-opengraph-image" + +export const dynamicParams = false + +export function generateStaticParams() { + return getDayTalkParams() +} + +export default function Image({ + params, +}: { + params: { city: string; id: string } +}) { + const talk = findDayTalk(params.city, params.id) + if (!talk) { + throw new Error(`Talk not found: ${params.city}/${params.id}`) + } + return TalkOpengraphImage(talk) +} diff --git a/src/app/day/2026/[city]/talks/[id]/page.tsx b/src/app/day/2026/[city]/talks/[id]/page.tsx new file mode 100644 index 0000000000..289c997bf1 --- /dev/null +++ b/src/app/day/2026/[city]/talks/[id]/page.tsx @@ -0,0 +1,101 @@ +import { Metadata } from "next" +import { notFound } from "next/navigation" + +import { formatDescription } from "@/app/conf/2026/schedule/[id]/format-description" + +import { findDayTalk, getDayTalkParams } from "../../../day-events" +import { NavbarPlaceholder } from "../../../components/navbar" +import { BackLink } from "../../../components/schedule/back-link" +import { + Hr, + SessionHeader, + SessionSpeakers, +} from "../../../components/event-schedule-section" + +type TalkProps = { params: { city: string; id: string } } + +export const dynamicParams = false + +export function generateStaticParams() { + return getDayTalkParams() +} + +export function generateMetadata({ params }: TalkProps): Metadata { + const talk = findDayTalk(params.city, params.id) + if (!talk) notFound() + + const { event, session } = talk + const speakers = session.speakers.map(s => s.name).join(", ") + const description = session.description + .replace(/<[^>]+>/g, " ") + .replace(/&[lr]dquo;/g, '"') + .replace(/&/g, "&") + .replace(/\s+/g, " ") + .trim() + + return { + title: session.title, + description: speakers ? `${speakers}: ${description}` : description, + keywords: [ + event.city, + ...session.tags, + ...session.speakers.map(s => s.name), + ], + } +} + +export default function TalkPage({ params }: TalkProps) { + const talk = findDayTalk(params.city, params.id) + if (!talk) notFound() + + const { event, session } = talk + + return ( + <> + +
+
+
+
+
+ +
+ {session.title} + } + timezone={event.timezone} + tagColors={event.tagColors} + className="px-2 pb-8 sm:px-3 lg:pb-12" + /> + {session.description && ( + <> +
+
+ + )} + {session.speakers.length > 0 && ( + <> +
+ + + )} +
+
+
+
+ + ) +} diff --git a/src/app/day/2026/amsterdam/opengraph-image.tsx b/src/app/day/2026/amsterdam/opengraph-image.tsx index 37daf1f000..c1fb566b10 100644 --- a/src/app/day/2026/amsterdam/opengraph-image.tsx +++ b/src/app/day/2026/amsterdam/opengraph-image.tsx @@ -1,12 +1,9 @@ import { SimpleOpengraphImage } from "../components/og-images/simple-opengraph-image" +import { dayEvents } from "../day-events" export { generateStaticParams, contentType, size, } from "../components/og-images/simple-opengraph-image" -export default SimpleOpengraphImage.bind(null, { - pageTitle: "Amsterdam", - date: "Jun 9-10, 2026", - location: "Tolhuistuin, Amsterdam", -}) +export default SimpleOpengraphImage.bind(null, dayEvents.amsterdam) diff --git a/src/app/day/2026/amsterdam/page.tsx b/src/app/day/2026/amsterdam/page.tsx index e018cca67a..c0ff87ae39 100644 --- a/src/app/day/2026/amsterdam/page.tsx +++ b/src/app/day/2026/amsterdam/page.tsx @@ -63,6 +63,7 @@ export default function AmsterdamPage() {
= { } // UUID is omitted for this manually maintained schedule. -export const bengaluruSessions: Omit[] = [ +export const bengaluruSessions: EventSession[] = [ { id: 5001, + uuid: "1dcb246e-ccdd-43a3-a06b-8de9ff705d6a", title: "Closing the Loop: How GraphQL Gives Coding Agents Eyes on What Actually Matters", start: "2026-08-19T11:00:00+05:30", @@ -64,6 +65,7 @@ export const bengaluruSessions: Omit[] = [ }, { id: 5002, + uuid: "e270991e-889d-48ff-9cbd-3bc7662dcff6", title: "REST in Peace? GraphQL vs REST in the Age of Agentic AI", start: "2026-08-19T11:30:00+05:30", end: "2026-08-19T11:55:00+05:30", @@ -90,6 +92,7 @@ export const bengaluruSessions: Omit[] = [ }, { id: 5003, + uuid: "2c99072b-f5f7-4696-a29b-9be0b4bb92bc", title: "Zero-to-Federation in 60 Seconds: How a GraphQL Middleware Replaced 500+ Legacy APIs and Saved 3 Years of Migration", start: "2026-08-19T12:00:00+05:30", @@ -117,6 +120,7 @@ export const bengaluruSessions: Omit[] = [ }, { id: 5004, + uuid: "508b7b4f-245a-4163-9948-b3fe3a9521a3", title: "A Mock Is a Promise: Making GraphQL Mocks Enforceable", start: "2026-08-19T12:30:00+05:30", end: "2026-08-19T12:55:00+05:30", @@ -139,6 +143,7 @@ export const bengaluruSessions: Omit[] = [ }, { id: 5005, + uuid: "68f425cf-bb8d-403e-a269-c793246b83b2", title: "Human in the Loop: Governing API Evolution in the Age of Agents", start: "2026-08-19T14:00:00+05:30", end: "2026-08-19T14:25:00+05:30", @@ -166,6 +171,7 @@ export const bengaluruSessions: Omit[] = [ }, { id: 5006, + uuid: "3cac57d2-207b-45db-aec7-071ebcb9b23d", title: "Just Ask: Building a Natural Language GraphQL Agent from the Graph You Already Have (the Easy Way)", start: "2026-08-19T14:30:00+05:30", @@ -192,6 +198,7 @@ export const bengaluruSessions: Omit[] = [ }, { id: 5007, + uuid: "cddc951c-bb4f-49c5-bae6-93f522e931da", title: "Evolving GraphQL: Designing APIs That Deliver Insights with Intelligent Resolvers", start: "2026-08-19T15:00:00+05:30", @@ -231,6 +238,7 @@ export const bengaluruSessions: Omit[] = [ }, { id: 5008, + uuid: "bc314ed6-bba9-466a-bc8c-058fb819dbd8", title: "Ask, Don't Fetch: Why GraphQL + MCP Is the Native Language of AI Agents", start: "2026-08-19T16:00:00+05:30", @@ -267,6 +275,7 @@ export const bengaluruSessions: Omit[] = [ }, { id: 5009, + uuid: "b8cb6419-a6f7-4b6f-8acb-e929377d6855", title: "Streaming the Graph: Architecting Flow-Based Progressive Hydration in Android", start: "2026-08-19T16:30:00+05:30", @@ -295,6 +304,7 @@ export const bengaluruSessions: Omit[] = [ }, { id: 5010, + uuid: "5edfd37c-5ba1-4817-9105-e88bf871f83a", title: "Functional Testing of GraphQL APIs: Simple Checks That Catch Real Bugs", start: "2026-08-19T17:00:00+05:30", diff --git a/src/app/day/2026/bengaluru/speakers/akhilMuralidharan.jpg b/src/app/day/2026/bengaluru/speakers/akhilMuralidharan.jpg new file mode 100644 index 0000000000..8bc5bbbb31 Binary files /dev/null and b/src/app/day/2026/bengaluru/speakers/akhilMuralidharan.jpg differ diff --git a/src/app/day/2026/bengaluru/speakers/akhilMuralidharan.webp b/src/app/day/2026/bengaluru/speakers/akhilMuralidharan.webp deleted file mode 100644 index e50483b10b..0000000000 Binary files a/src/app/day/2026/bengaluru/speakers/akhilMuralidharan.webp and /dev/null differ diff --git a/src/app/day/2026/bengaluru/speakers/akshayShaju.jpg b/src/app/day/2026/bengaluru/speakers/akshayShaju.jpg new file mode 100644 index 0000000000..86c9c2d4b2 Binary files /dev/null and b/src/app/day/2026/bengaluru/speakers/akshayShaju.jpg differ diff --git a/src/app/day/2026/bengaluru/speakers/akshayShaju.webp b/src/app/day/2026/bengaluru/speakers/akshayShaju.webp deleted file mode 100644 index b7a0c8c53f..0000000000 Binary files a/src/app/day/2026/bengaluru/speakers/akshayShaju.webp and /dev/null differ diff --git a/src/app/day/2026/components/event-schedule-section.tsx b/src/app/day/2026/components/event-schedule-section.tsx index c0047f1650..8c3098668a 100644 --- a/src/app/day/2026/components/event-schedule-section.tsx +++ b/src/app/day/2026/components/event-schedule-section.tsx @@ -1,7 +1,6 @@ -"use client" - -import { useState } from "react" +import type { ReactNode } from "react" import Image from "next/image" +import NextLink from "next/link" import type { StaticImageData } from "next/image" import clsx from "clsx" @@ -13,7 +12,7 @@ import { SocialIcon, SocialIconType, } from "@/app/conf/_design-system/social-icon" -import { formatDescription } from "@/app/conf/2026/schedule/[id]/format-description" +import { SessionDescription } from "./session-description" export interface EventSpeaker { id: number @@ -26,7 +25,7 @@ export interface EventSpeaker { export interface EventSession { id: number - uuid?: string + uuid: string title: string /** ISO 8601 in venue local time */ start: string @@ -41,11 +40,14 @@ export interface EventSession { } export function EventScheduleSection({ + city, sessions, timezone, timezoneLabel, tagColors, }: { + /** URL segment under /day/2026/, used to link each talk. */ + city: string sessions: EventSession[] timezone: string timezoneLabel: string @@ -68,6 +70,7 @@ export function EventScheduleSection({ @@ -100,6 +105,13 @@ function SessionBlock({ /> + + {session.title} + + + } timezone={timezone} tagColors={tagColors} className="px-2 py-8 sm:px-3 lg:py-12" @@ -109,7 +121,9 @@ function SessionBlock({
+ } /> )} @@ -130,80 +144,15 @@ function SessionBlock({ ) } -function SessionDescription({ - description, - sideSpeaker, -}: { - description: string - sideSpeaker: EventSpeaker | null -}) { - const [expanded, setExpanded] = useState(false) - const paragraphs = parseParagraphs(description) - const hasMore = paragraphs.length > 1 - const visible = expanded ? paragraphs : paragraphs.slice(0, 1) - const splitAt = sideSpeaker ? Math.max(0, visible.length - 2) : visible.length - const lead = visible.slice(0, splitAt) - const tail = visible.slice(splitAt) - const lastInLead = tail.length === 0 ? lead.length - 1 : -1 - const lastInTail = tail.length - 1 - - const toggle = hasMore && ( - <> - {" "} - - - ) - - return ( -
- {lead.map((html, i) => ( -

- - {i === lastInLead && toggle} -

- ))} - {tail.length > 0 && ( -
-
- {tail.map((html, i) => ( -

- - {i === lastInTail && toggle} -

- ))} -
- {sideSpeaker && ( -
- -
- )} -
- )} -
- ) -} - -function parseParagraphs(html: string): string[] { - const formatted = formatDescription(html) - const matches = formatted.match(/

[\s\S]*?<\/p>/g) - if (!matches) return [formatted] - return matches.map(p => p.replace(/^

/, "").replace(/<\/p>$/, "")) -} - -function SessionHeader({ +export function SessionHeader({ session, + heading, timezone, tagColors, className, }: { session: EventSession + heading: ReactNode timezone: string tagColors: Record className?: string @@ -224,7 +173,7 @@ function SessionHeader({ return (

-

{session.title}

+ {heading}
@@ -259,7 +208,7 @@ function SessionHeader({ ) } -function SessionSpeakers({ +export function SessionSpeakers({ speakers, className, }: { @@ -389,7 +338,7 @@ function SpeakerSocialLinks({ links }: { links: EventSpeaker["socialurls"] }) { ) } -function Hr({ className }: { className?: string }) { +export function Hr({ className }: { className?: string }) { return (
{ - pageTitle: string + city: string date: string location: string } export function GenericDayOpengraphImage({ - pageTitle, + city, date, location, ...rest @@ -45,11 +45,7 @@ export function GenericDayOpengraphImage({ }} {...rest} > - +
GraphQL Day - at FOST {pageTitle} + at FOST {city}
- ), + , { ...size, fonts: await fonts, diff --git a/src/app/day/2026/components/og-images/talk-opengraph-image.tsx b/src/app/day/2026/components/og-images/talk-opengraph-image.tsx new file mode 100644 index 0000000000..395cef453c --- /dev/null +++ b/src/app/day/2026/components/og-images/talk-opengraph-image.tsx @@ -0,0 +1,164 @@ +import { readFileSync } from "node:fs" +import { join } from "node:path" +import { ImageResponse } from "next/og" +import type { StaticImageData } from "next/image" + +import { loadFontsForOG } from "@/app/fonts/og/load-fonts-for-og" + +import type { DayEvent } from "../../day-events" +import type { EventSession } from "../event-schedule-section" +import { + colors, + DayOpengraphImageHeader, + fonts, +} from "./day-opengraph-image-header" + +export const contentType = "image/png" +export const size = { + width: 1200, + height: 630, +} + +/** + * Satori needs the image bytes. Imported images point at the copy Next emits + * to `.next/static/media`, which exists when OG images are prerendered. + */ +function avatarDataUri(avatar: StaticImageData) { + const file = join( + process.cwd(), + ".next", + avatar.src.replace(/^\/_next\//, ""), + ) + const type = avatar.src.endsWith(".png") ? "png" : "jpeg" + return `data:image/${type};base64,${readFileSync(file).toString("base64")}` +} + +export async function TalkOpengraphImage({ + event, + session, +}: { + event: DayEvent + session: EventSession +}) { + const ogFonts = loadFontsForOG() + const { title, speakers } = session + + return new ImageResponse( + ( +
+ +
+
+ + GraphQL Day at FOST {event.city} + +

+ {title} +

+
+ +
+ {speakers.map(speaker => ( +
+ {speaker.avatar && ( + + )} +
+ 1 ? "32px" : "40px", + lineHeight: "1", + color: colors.neu900, + }} + > + {speaker.name} + + {speaker.company && speaker.company !== "-" && ( + + {speaker.company} + + )} +
+
+ ))} +
+
+
+ ), + { + ...size, + fonts: await ogFonts, + }, + ) +} diff --git a/src/app/day/2026/components/session-description.tsx b/src/app/day/2026/components/session-description.tsx new file mode 100644 index 0000000000..a31168af94 --- /dev/null +++ b/src/app/day/2026/components/session-description.tsx @@ -0,0 +1,73 @@ +"use client" + +import { useState, type ReactNode } from "react" + +import { formatDescription } from "@/app/conf/2026/schedule/[id]/format-description" + +export function SessionDescription({ + description, + aside, +}: { + description: string + /** Shown next to the last paragraphs on wide screens. */ + aside?: ReactNode +}) { + const [expanded, setExpanded] = useState(false) + const paragraphs = parseParagraphs(description) + const hasMore = paragraphs.length > 1 + const visible = expanded ? paragraphs : paragraphs.slice(0, 1) + const splitAt = aside ? Math.max(0, visible.length - 2) : visible.length + const lead = visible.slice(0, splitAt) + const tail = visible.slice(splitAt) + const lastInLead = tail.length === 0 ? lead.length - 1 : -1 + const lastInTail = tail.length - 1 + + const toggle = hasMore && ( + <> + {" "} + + + ) + + return ( +
+ {lead.map((html, i) => ( +

+ + {i === lastInLead && toggle} +

+ ))} + {tail.length > 0 && ( +
+
+ {tail.map((html, i) => ( +

+ + {i === lastInTail && toggle} +

+ ))} +
+ {aside && ( +
+ {aside} +
+ )} +
+ )} +
+ ) +} + +function parseParagraphs(html: string): string[] { + const formatted = formatDescription(html) + const matches = formatted.match(/

[\s\S]*?<\/p>/g) + if (!matches) return [formatted] + return matches.map(p => p.replace(/^

/, "").replace(/<\/p>$/, "")) +} diff --git a/src/app/day/2026/day-events.ts b/src/app/day/2026/day-events.ts new file mode 100644 index 0000000000..509d950cf0 --- /dev/null +++ b/src/app/day/2026/day-events.ts @@ -0,0 +1,115 @@ +import type { EventSession } from "./components/event-schedule-section" +import { + amsterdamSessions, + AMSTERDAM_TIMEZONE, + tagColors as amsterdamTagColors, +} from "./amsterdam/schedule-data" +import { + bengaluruSessions, + BENGALURU_TIMEZONE, + tagColors as bengaluruTagColors, +} from "./bengaluru/schedule-data" +import { + londonSessions, + LONDON_TIMEZONE, + tagColors as londonTagColors, +} from "./london/schedule-data" +import { + melbourneSessions, + MELBOURNE_TIMEZONE, + tagColors as melbourneTagColors, +} from "./melbourne/schedule-data" +import { + nycSessions, + NYC_TIMEZONE, + tagColors as nycTagColors, +} from "./nyc/schedule-data" +import { + singaporeSessions, + SINGAPORE_TIMEZONE, + tagColors as singaporeTagColors, +} from "./singapore/schedule-data" + +export interface DayEvent { + city: string + /** Short date shown in social previews. */ + date: string + location: string + timezone: string + sessions: EventSession[] + tagColors: Record +} + +/** Keyed by the city's URL segment under /day/2026/. */ +export const dayEvents = { + singapore: { + city: "Singapore", + date: "Apr 14-15, 2026", + location: "Singapore", + timezone: SINGAPORE_TIMEZONE, + sessions: singaporeSessions, + tagColors: singaporeTagColors, + }, + nyc: { + city: "NYC", + date: "May 13-14, 2026", + location: "Convene 360 Madison, New York", + timezone: NYC_TIMEZONE, + sessions: nycSessions, + tagColors: nycTagColors, + }, + amsterdam: { + city: "Amsterdam", + date: "Jun 9-10, 2026", + location: "Tolhuistuin, Amsterdam", + timezone: AMSTERDAM_TIMEZONE, + sessions: amsterdamSessions, + tagColors: amsterdamTagColors, + }, + bengaluru: { + city: "Bengaluru", + date: "Aug 19-20, 2026", + location: "Conrad Bengaluru, India", + timezone: BENGALURU_TIMEZONE, + sessions: bengaluruSessions, + tagColors: bengaluruTagColors, + }, + london: { + city: "London", + date: "Oct 1, 2026", + location: "Convene Sancroft, London", + timezone: LONDON_TIMEZONE, + sessions: londonSessions, + tagColors: londonTagColors, + }, + melbourne: { + city: "Melbourne", + date: "Oct 29, 2026", + location: "Melbourne, Australia", + timezone: MELBOURNE_TIMEZONE, + sessions: melbourneSessions, + tagColors: melbourneTagColors, + }, + paris: { + city: "Paris", + date: "Dec 1-3, 2026", + location: "CNIT Forest, Paris", + timezone: "Europe/Paris", + sessions: [], + tagColors: {}, + }, +} satisfies Record + +export type DayEventSlug = keyof typeof dayEvents + +export function findDayTalk(city: string, id: string) { + const event: DayEvent | undefined = dayEvents[city as DayEventSlug] + const session = event?.sessions.find(s => s.uuid === id) + return event && session ? { event, session } : undefined +} + +export function getDayTalkParams() { + return Object.entries(dayEvents).flatMap(([city, event]) => + event.sessions.map(s => ({ city, id: s.uuid })), + ) +} diff --git a/src/app/day/2026/london/opengraph-image.tsx b/src/app/day/2026/london/opengraph-image.tsx index 95918e1e46..76dccce6d7 100644 --- a/src/app/day/2026/london/opengraph-image.tsx +++ b/src/app/day/2026/london/opengraph-image.tsx @@ -1,12 +1,9 @@ import { SimpleOpengraphImage } from "../components/og-images/simple-opengraph-image" +import { dayEvents } from "../day-events" export { generateStaticParams, contentType, size, } from "../components/og-images/simple-opengraph-image" -export default SimpleOpengraphImage.bind(null, { - pageTitle: "London", - date: "Sep 30-Oct 1, 2026", - location: "Convene Sancroft, London", -}) +export default SimpleOpengraphImage.bind(null, dayEvents.london) diff --git a/src/app/day/2026/london/page.tsx b/src/app/day/2026/london/page.tsx index 0fcbcf70a7..209b458551 100644 --- a/src/app/day/2026/london/page.tsx +++ b/src/app/day/2026/london/page.tsx @@ -32,7 +32,7 @@ const MARQUEE_ITEMS = [ ] export const metadata: Metadata = { - title: "GraphQL Day @ FOST London — Sep 30-Oct 1", + title: "GraphQL Day @ FOST London — Oct 1", } export default function LondonPage() { @@ -42,8 +42,8 @@ export default function LondonPage() {

@@ -62,8 +62,12 @@ export default function LondonPage() {
- + = { export const melbourneSessions: EventSession[] = [ { id: 5001, + uuid: "64a4db37-cbc2-4726-8637-025e7efa4b49", title: "Federation for the JVM", start: "2026-10-29T11:00:00+11:00", end: "2026-10-29T11:25:00+11:00", @@ -48,6 +49,7 @@ export const melbourneSessions: EventSession[] = [ }, { id: 5002, + uuid: "2362071a-320d-448c-9384-f8dbe2e64a62", title: "Closing the Loop: How GraphQL Gives Coding Agents Eyes on What Actually Matters", start: "2026-10-29T11:30:00+11:00", @@ -76,6 +78,7 @@ export const melbourneSessions: EventSession[] = [ }, { id: 5003, + uuid: "0dfb8add-40b2-4f26-9c25-2f69616b9318", title: "Ask, Don't Fetch: Why GraphQL + MCP Is the Native Language of AI Agents", start: "2026-10-29T12:00:00+11:00", @@ -102,6 +105,7 @@ export const melbourneSessions: EventSession[] = [ }, { id: 5004, + uuid: "2ca7fe95-80c2-40d1-a2e9-f1ff3ea25dee", title: "Telling Your Success Story: Illustrating Graph Impact Through Observability", start: "2026-10-29T12:30:00+11:00", @@ -128,6 +132,7 @@ export const melbourneSessions: EventSession[] = [ }, { id: 5005, + uuid: "e3ff6be8-9b34-49e5-ba50-7deea5de55dd", title: "GraphQL in the Real World: The Security and Governance Playbook Nobody Gives You", start: "2026-10-29T14:00:00+11:00", @@ -154,6 +159,7 @@ export const melbourneSessions: EventSession[] = [ }, { id: 5006, + uuid: "44ca595b-2b0f-4b59-88e0-3d2d131ccab2", title: "Teach Your AI Agent GraphQL", start: "2026-10-29T14:30:00+11:00", end: "2026-10-29T14:55:00+11:00", @@ -176,6 +182,7 @@ export const melbourneSessions: EventSession[] = [ }, { id: 5007, + uuid: "05db7eb0-02d4-4348-a72f-deb01da4613a", title: "Beyond the Experience Graph: The Data Graph Meets AI", start: "2026-10-29T15:00:00+11:00", end: "2026-10-29T15:25:00+11:00", diff --git a/src/app/day/2026/nyc/opengraph-image.tsx b/src/app/day/2026/nyc/opengraph-image.tsx index 6ed5b759da..078937a4c0 100644 --- a/src/app/day/2026/nyc/opengraph-image.tsx +++ b/src/app/day/2026/nyc/opengraph-image.tsx @@ -1,12 +1,9 @@ import { SimpleOpengraphImage } from "../components/og-images/simple-opengraph-image" +import { dayEvents } from "../day-events" export { generateStaticParams, contentType, size, } from "../components/og-images/simple-opengraph-image" -export default SimpleOpengraphImage.bind(null, { - pageTitle: "NYC", - date: "May 13-14, 2026", - location: "Convene 360 Madison, New York", -}) +export default SimpleOpengraphImage.bind(null, dayEvents.nyc) diff --git a/src/app/day/2026/nyc/page.tsx b/src/app/day/2026/nyc/page.tsx index dfdfaf35b4..edef79c64f 100644 --- a/src/app/day/2026/nyc/page.tsx +++ b/src/app/day/2026/nyc/page.tsx @@ -63,6 +63,7 @@ export default function NYCPage() {