diff --git a/src/app/conf/_design-system/social-icon.tsx b/src/app/conf/_design-system/social-icon.tsx index 1cd3c83ea9..4f4d63a397 100644 --- a/src/app/conf/_design-system/social-icon.tsx +++ b/src/app/conf/_design-system/social-icon.tsx @@ -4,6 +4,7 @@ import { LinkedInFilledIcon, InstagramIcon, GlobeIcon, + GitHubIcon, } from "@/icons" export type SocialIconType = @@ -11,6 +12,7 @@ export type SocialIconType = | "linkedin" | "facebook" | "instagram" + | "github" | "website" export const SocialIconType = { @@ -22,6 +24,7 @@ export const SocialIconType = { all: [ "linkedin", "twitter", + "github", "instagram", "facebook", "website", @@ -42,6 +45,8 @@ export const SocialIcon = ({ type, ...rest }: SocialIconProps) => { return case "instagram": return + case "github": + return case "website": return default: @@ -59,6 +64,8 @@ export function urlForUser(type: SocialIconType, handleOrWebsite: string) { return `https://www.instagram.com/${handleOrWebsite}` case "facebook": return `https://www.facebook.com/${handleOrWebsite}` + case "github": + return `https://github.com/${handleOrWebsite}` case "website": return handleOrWebsite default: diff --git a/src/app/day/2026/singapore/page.tsx b/src/app/day/2026/singapore/page.tsx index 619ab1ee47..e1dd72d221 100644 --- a/src/app/day/2026/singapore/page.tsx +++ b/src/app/day/2026/singapore/page.tsx @@ -4,19 +4,14 @@ import { Button } from "@/app/conf/_design-system/button" import { Hero, HeroDateAndLocation } from "../components/hero" import { AboutSection } from "../components/about-section" import { WhyAttendSection } from "../components/why-attend-section" -import { - BecomeASpeakerSection, - CfpButton, -} from "../components/become-a-speaker" import { EventPartnersSection } from "../components/event-partners" -import { CtaCardSection } from "../components/cta-card-section" import { MarqueeRows } from "@/app/conf/2026/components/marquee-rows" import { PastSpeakersSection } from "../components/past-speakers" import { NavbarPlaceholder } from "../components/navbar" import { GallerySection } from "../../gallery-section" +import { ScheduleSection } from "./schedule-section" -const TICKET_LINK = - "https://portal.joinfost.io/event/future-of-software-technologies-singapore-2026/9521470b-6661-4c85-8594-b74d9d7cf2e3/graphql-day-at-fost-singapore" +const SCHEDULE_ANCHOR = "#schedule" const MARQUEE_ITEMS = [ ["SINGAPORE", "APRIL 2026", "GRAPHQL DAY", "FOST", "COMMUNITY", "APIs"], @@ -46,10 +41,12 @@ export default function SingaporePage() { location="Singapore" />
- -
@@ -60,28 +57,16 @@ export default function SingaporePage() { />
- - - - -
+ + ) diff --git a/src/app/day/2026/singapore/schedule-data.ts b/src/app/day/2026/singapore/schedule-data.ts new file mode 100644 index 0000000000..a96a23e8f2 --- /dev/null +++ b/src/app/day/2026/singapore/schedule-data.ts @@ -0,0 +1,130 @@ +import type { StaticImageData } from "next/image" + +import akshatSharmaAvatar from "./speakers/akshat-sharma.webp" +import michaelStaibAvatar from "./speakers/michael-staib.webp" +import pascalSennAvatar from "./speakers/pascal-senn.webp" + +export interface SingaporeSpeaker { + id: number + name: string + company: string + jobtitle: string + avatar: StaticImageData + socialurls: { service: string; url: string }[] +} + +export interface SingaporeSession { + id: number + uuid: string + title: string + /** ISO 8601 in venue local time, Asia/Singapore */ + start: string + /** ISO 8601 in venue local time, Asia/Singapore */ + end: string + /** Topic tags derived from the session description. */ + tags: string[] + /** HTML */ + description: string + venue: string + speakers: SingaporeSpeaker[] +} + +export const SINGAPORE_TIMEZONE = "Asia/Singapore" + +/** Color per topic, picked to read clearly against the cream/dark backgrounds. */ +export const tagColors: Record = { + Security: "#CC6BB0", + "Zero Trust": "#894545", + "Service Mesh": "#36C1A0", + "AI Agents": "#7e66cc", + Federation: "#FC8251", + "Public Sector": "#4e6e82", + "Schema Evolution": "#cbc749", + Observability: "#1a5b77", +} + +export const singaporeSessions: SingaporeSession[] = [ + { + id: 3224, + uuid: "80952503-07dd-4e31-acaf-b9e400f55126", + title: "Securing GraphQL at Scale with Zero Trust APIs", + start: "2026-04-15T15:55:00+08:00", + end: "2026-04-15T16:20:00+08:00", + tags: ["Security", "Zero Trust", "Service Mesh"], + description: + "

Modern microservice architectures often decentralize authentication and authorization, leading to inconsistent security policies and increased attack surfaces. GraphQL, while powerful, introduces unique risks such as over-fetching, query batching abuse, and introspection-based attacks that many teams underestimate.

\n

In this talk, I present a real-world case study of building a secure, identity-aware GraphQL gateway that enforces Zero Trust principles across distributed services. By integrating centralized identity management with Keycloak and leveraging service mesh technologies like Istio and Envoy, we created a unified layer for authentication, authorization, and traffic governance.

\n

The session will walk through practical challenges, including enforcing fine-grained access control, implementing query cost analysis, and mitigating abuse patterns in production. Attendees will gain insights into designing secure GraphQL APIs that scale without compromising performance or developer experience.

\n

This talk matters because API security is no longer optional. As organizations increasingly adopt GraphQL, understanding how to secure it in real-world systems is critical to preventing data leaks and maintaining trust.

\n", + venue: "Stage 3", + speakers: [ + { + id: 4557, + name: "Akshat Sharma", + company: "Deskree", + jobtitle: "Technology Advocate", + avatar: akshatSharmaAvatar, + socialurls: [ + { + service: "linkedin", + url: "https://www.linkedin.com/in/akshat-sharma11", + }, + ], + }, + ], + }, + { + id: 3225, + uuid: "2a24223a-16d0-40fa-821b-b91c491ff9a6", + title: "GraphQL as the Execution Layer for AI Agents", + start: "2026-04-15T16:20:00+08:00", + end: "2026-04-15T16:45:00+08:00", + tags: ["AI Agents", "Federation", "Public Sector"], + description: + "

Your next million API consumers won't be developers. They'll be AI agents. And they don't read documentation, parse hypermedia links, or guess which of your 200 REST endpoints returns the data they need.

\n

This talk examines what happens when autonomous AI agents become the primary consumers of your API layer. Drawing on real data from Singapore's public government APIs, I'll show how REST responses waste 30–60% of an agent's token budget on structural overhead, and how a typed, self-describing schema changes the equation entirely.

\n

We'll walk through the three properties that make an API truly agent-native: discoverability, precision, and composability. We'll look at what it would take to unify API estates like Singapore's 3,000+ government APIs across 75+ agencies into a single, self-describing surface. A pattern Gartner expects 30% of enterprises to adopt by 2027.

\n

You'll leave with a framework for what makes an API truly agent-native, why GraphQL's type system and federation model get you there, and how to start without a rewrite.

\n", + venue: "Stage 3", + speakers: [ + { + id: 2446, + name: "Pascal Senn", + company: "ChilliCream", + jobtitle: "Founder", + avatar: pascalSennAvatar, + socialurls: [ + { + service: "linkedin", + url: "https://www.linkedin.com/in/pascal-senn-90899a15a", + }, + { service: "github", url: "https://github.com/PascalSenn" }, + { service: "website", url: "https://chillicream.com" }, + ], + }, + ], + }, + { + id: 3226, + uuid: "02d9d427-d1ff-4f78-8d8c-243565d0f1cd", + title: + "Closing the Loop: How GraphQL Gives Coding Agents Eyes on What Actually Matters", + start: "2026-04-15T16:45:00+08:00", + end: "2026-04-15T17:10:00+08:00", + tags: ["AI Agents", "Schema Evolution", "Observability"], + description: + "

Coding agents are reshaping how we build software. Implementing features, refactoring systems, and shipping changes at a pace unthinkable 6 months ago. But to be successful with agents you need the right feedback loop. One that guides your agent to success, not into the spiral of death.

\n

Ask Claude to add a review system to your product API. Without knowing what's in use, it might reshape your types, move fields, and break your deployed clients because it is missing a crucial feedback loop of what's in use in your clients.

\n

GraphQL changes this. Every client operation explicitly declares the exact fields and types it needs. That gives you something rare: field-level usage data across your entire consumer base. Not endpoint hits, but actual demand, broken down to the individual field.

\n

When coding agents can access this data, they stop guessing. Evolve your schema grounded in reality, not assumptions.

\n

This talk shows how GraphQL's inherent usage visibility and the rise of coding agents create a feedback loop that didn't exist before. And why it matters for anyone building APIs that need to evolve fast.

\n", + venue: "Stage 3", + speakers: [ + { + id: 1881, + name: "Michael Staib", + company: "ChilliCream", + jobtitle: "Founder", + avatar: michaelStaibAvatar, + socialurls: [ + { + service: "linkedin", + url: "https://www.linkedin.com/in/michael-staib-31519571/", + }, + { service: "github", url: "https://github.com/michaelstaib" }, + { service: "website", url: "https://chillicream.com" }, + ], + }, + ], + }, +] diff --git a/src/app/day/2026/singapore/schedule-section.tsx b/src/app/day/2026/singapore/schedule-section.tsx new file mode 100644 index 0000000000..3e5229c083 --- /dev/null +++ b/src/app/day/2026/singapore/schedule-section.tsx @@ -0,0 +1,339 @@ +import Image from "next/image" +import clsx from "clsx" + +import { Tag } from "@/app/conf/_design-system/tag" +import { CalendarIcon } from "@/app/conf/_design-system/pixelarticons/calendar-icon" +import { PinIcon } from "@/app/conf/_design-system/pixelarticons/pin-icon" +import { StripesDecoration } from "@/app/conf/_design-system/stripes-decoration" +import { + SocialIcon, + SocialIconType, +} from "@/app/conf/_design-system/social-icon" +import { formatDescription } from "@/app/conf/2026/schedule/[id]/format-description" + +import { + SingaporeSession, + SingaporeSpeaker, + singaporeSessions, + tagColors, +} from "./schedule-data" + +const TIME_RANGE = new Intl.DateTimeFormat("en-US", { + hour: "2-digit", + minute: "2-digit", + hour12: false, + timeZone: "Asia/Singapore", +}) + +const DATE_FORMAT = new Intl.DateTimeFormat("en-US", { + day: "numeric", + month: "long", + timeZone: "Asia/Singapore", +}) + +export function ScheduleSection() { + return ( +
+
+
+
+
+

Schedule

+

+ All times in Singapore Time (SGT, UTC+8) +

+
+ + {singaporeSessions.map((session, i) => ( + + ))} +
+
+
+
+ ) +} + +function SessionBlock({ + session, + isFirst, +}: { + session: SingaporeSession + isFirst: boolean +}) { + // On xl+ with a single speaker we slot the card next to the last two + // paragraphs of the description so it sits in the bottom-right corner. + // Multi-speaker sessions keep the regular "speakers below" layout. + const sideSpeaker = session.speakers.length === 1 ? session.speakers[0] : null + + return ( +
+
+ + {session.description && ( + <> +
+ + + )} + {session.speakers.length > 0 && ( +
+
+ +
+ )} +
+ ) +} + +function SessionDescription({ + description, + sideSpeaker, +}: { + description: string + sideSpeaker: SingaporeSpeaker | null +}) { + const paragraphs = parseParagraphs(description) + const splitAt = + sideSpeaker && paragraphs.length >= 2 + ? paragraphs.length - 2 + : paragraphs.length + const lead = paragraphs.slice(0, splitAt) + const tail = paragraphs.slice(splitAt) + + return ( +
+ {lead.map((html, i) => ( +

+ ))} + {tail.length > 0 && ( +

+
+ {tail.map((html, i) => ( +

+ ))} +

+ {sideSpeaker && ( +
+ +
+ )} +
+ )} +
+ ) +} + +/** + * Split FOST description HTML (a sequence of `

...

` blocks) into the + * inner HTML of each paragraph so we can render them as real React `

` + * siblings — needed so we can splice the speaker card in alongside the last + * couple of paragraphs at xl+. + */ +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({ + session, + className, +}: { + session: SingaporeSession + className?: string +}) { + const start = new Date(session.start) + const end = new Date(session.end) + + return ( +

+

{session.title}

+
+
+
+ + +
+ {session.venue && ( +
+ + {session.venue} +
+ )} +
+ {session.tags.length > 0 && ( +
+ {session.tags.map(tag => ( + + {tag} + + ))} +
+ )} +
+
+ ) +} + +function SessionSpeakers({ + speakers, + className, +}: { + speakers: SingaporeSpeaker[] + className?: string +}) { + return ( +
*:not(:last-child)]:border-b-0", + className, + )} + > + {speakers.map((speaker, i) => ( + + ))} +
+ ) +} + +const STRIPE_VARIANTS: { mask: string; endColor: string }[] = [ + { + mask: "linear-gradient(120deg, hsl(var(--color-pri-base)) 0%, hsl(var(--color-pri-base)) 8%, transparent 35%, transparent)", + endColor: "hsl(var(--color-sec-base))", + }, + { + mask: "radial-gradient(circle at bottom right, hsl(var(--color-pri-base)) 0%, hsl(var(--color-pri-base)) 10%, transparent 40%, transparent)", + endColor: "hsl(var(--color-sec-darker))", + }, + { + mask: "linear-gradient(-40deg, hsl(var(--color-pri-base)) 0%, hsl(var(--color-pri-base)) 5%, transparent 40%, transparent)", + endColor: "hsl(var(--color-sec-light))", + }, +] + +function SpeakerCard({ + speaker, + index, +}: { + speaker: SingaporeSpeaker + index: number +}) { + const variant = STRIPE_VARIANTS[index % STRIPE_VARIANTS.length] + const subtitle = [ + speaker.company === "-" ? "" : speaker.company, + speaker.jobtitle, + ] + .filter(Boolean) + .join(", ") + + return ( +
+
+
+
+ +
+ +
+
+ +
+
+
{speaker.name}
+ {subtitle && ( +

+ {subtitle} +

+ )} +
+ {speaker.socialurls.length > 0 && ( + + )} +
+
+
+ ) +} + +function SpeakerSocialLinks({ + links, +}: { + links: SingaporeSpeaker["socialurls"] +}) { + const ordered = SocialIconType.all + .map(service => + links.find(l => l.service.toLowerCase() === service.toLowerCase()), + ) + .filter((x): x is { service: string; url: string } => !!x?.url) + + if (ordered.length === 0) return null + + return ( +
+ {ordered.map(social => ( + + + + ))} +
+ ) +} + +function Hr({ className }: { className?: string }) { + return ( +
+ ) +} diff --git a/src/app/day/2026/singapore/speakers/akshat-sharma.webp b/src/app/day/2026/singapore/speakers/akshat-sharma.webp new file mode 100644 index 0000000000..c56de12cce Binary files /dev/null and b/src/app/day/2026/singapore/speakers/akshat-sharma.webp differ diff --git a/src/app/day/2026/singapore/speakers/michael-staib.webp b/src/app/day/2026/singapore/speakers/michael-staib.webp new file mode 100644 index 0000000000..8306a11091 Binary files /dev/null and b/src/app/day/2026/singapore/speakers/michael-staib.webp differ diff --git a/src/app/day/2026/singapore/speakers/pascal-senn.webp b/src/app/day/2026/singapore/speakers/pascal-senn.webp new file mode 100644 index 0000000000..5e1ae5044c Binary files /dev/null and b/src/app/day/2026/singapore/speakers/pascal-senn.webp differ diff --git a/src/app/day/layout.tsx b/src/app/day/layout.tsx index 4ea5774034..84934d035f 100644 --- a/src/app/day/layout.tsx +++ b/src/app/day/layout.tsx @@ -30,8 +30,8 @@ export default function DayLayout({ { children: "All GraphQL Events", href: "/community/events/" }, { children: "GraphQLConf", href: "/conf/2026" }, { - children: "GraphQL Day Singapore", - href: "/day/2026/singapore", + children: "GraphQL Day NYC", + href: "/day/2026/nyc", }, ]} /> @@ -47,8 +47,8 @@ export default function DayLayout({ { children: "GraphQL", href: "/" }, { children: "GraphQLConf 2026", href: "/conf/2026" }, { - children: "Singapore Tickets", - href: "https://portal.joinfost.io/event/future-of-software-technologies-singapore-2026/9521470b-6661-4c85-8594-b74d9d7cf2e3/graphql-day-at-fost-singapore", + children: "Singapore Schedule", + href: "/day/2026/singapore#schedule", }, { children: "Code of Conduct",