diff --git a/next.config.js b/next.config.js index 10d19a2d85..55ac068367 100644 --- a/next.config.js +++ b/next.config.js @@ -85,6 +85,7 @@ export default withLess( return config }, + // Comment this out if you're working on OG images. output: "export", images: { loader: "custom", diff --git a/src/app/(development)/workroom/page.tsx b/src/app/(development)/workroom/page.tsx index 34b4f0d566..3df0ab62c2 100644 --- a/src/app/(development)/workroom/page.tsx +++ b/src/app/(development)/workroom/page.tsx @@ -1,11 +1,18 @@ -import { SpeakerOpengraphImage } from "@/app/conf/2025/components/speaker-opengraph-image" -import { SessionOpengraphImage } from "@/app/conf/2025/components/session-opengraph-image" +import { SpeakerOpengraphImage } from "@/app/conf/2025/components/og-images/speaker-opengraph-image" +import { SessionOpengraphImage } from "@/app/conf/2025/components/og-images/session-opengraph-image" +import { GenericOpengraphImage } from "@/app/conf/2025/components/og-images/generic-opengraph-image" import { SchedSpeaker } from "@/app/conf/2023/types" /** * This is cheaper than maintaining a Storybook config. */ export default function WorkroomPage() { + const dateAndLocation = { + date: "September 8-10", + year: "2025", + location: "Amsterdam, Netherlands", + } + const enisdenjo: SchedSpeaker = { name: "Denis Badurina", username: "enisdenjo", @@ -35,12 +42,7 @@ export default function WorkroomPage() { return (

SpeakerOpengraphImage

- +

ScheduleOpengraphImage / no speakers

ScheduleOpengraphImage / single speaker

@@ -63,9 +63,7 @@ export default function WorkroomPage() { event_type: "Keynote Sessions", event_subtype: "", }} - date="September 8-10" - year="2025" - location="Amsterdam, Netherlands" + {...dateAndLocation} />

ScheduleOpengraphImage / multiple speakers

@@ -76,9 +74,7 @@ export default function WorkroomPage() { event_type: "Developer Experience", event_subtype: "Backend", }} - date="September 8-10" - year="2025" - location="Amsterdam, Netherlands" + {...dateAndLocation} />

SpeakerOpengraphImage / very long title

@@ -107,10 +103,17 @@ export default function WorkroomPage() { event_type: "Keynote Sessions", event_subtype: "", }} - date="September 8-10" - year="2025" - location="Amsterdam, Netherlands" + {...dateAndLocation} /> + +

GenericOpengraphImage / GraphQLConf 2025

+ + +

GenericOpengraphImage / Sponsors

+
) } diff --git a/src/app/conf/2025/code-of-conduct/opengraph-image.tsx b/src/app/conf/2025/code-of-conduct/opengraph-image.tsx new file mode 100644 index 0000000000..84f732fc7c --- /dev/null +++ b/src/app/conf/2025/code-of-conduct/opengraph-image.tsx @@ -0,0 +1,10 @@ +import { SimpleOpengraphImage } from "../components/og-images/simple-opengraph-image" +export { + generateStaticParams, + contentType, + size, +} from "../components/og-images/simple-opengraph-image" + +export default SimpleOpengraphImage.bind(null, { + pageTitle: "Code of Conduct", +}) diff --git a/src/app/conf/2025/components/og-images/conference-opengraph-image-header.tsx b/src/app/conf/2025/components/og-images/conference-opengraph-image-header.tsx new file mode 100644 index 0000000000..1a35039192 --- /dev/null +++ b/src/app/conf/2025/components/og-images/conference-opengraph-image-header.tsx @@ -0,0 +1,171 @@ +import { CalendarIcon } from "@/app/conf/_design-system/pixelarticons/calendar-icon" +import { PinIcon } from "@/app/conf/_design-system/pixelarticons/pin-icon" + +import { GraphQLLogo } from "../graphql-conf-logo-link" +import { colors, fonts, RIGHT_COLUMN_WIDTH_PX } from "./speaker-opengraph-image" + +export const OG_IMAGE_HEADER_HEIGHT = 154 + +export function ConferenceOpengraphImageHeader({ + year, + date, + location, + style, +}: { + year: string + date: string + location: string + style?: React.CSSProperties +}) { + return ( +
+
+
+
+
+ + / +
+ GraphQLConf{" "} + {year} +
+
+
+
+
+ +
+
+
+ + + {date}, {year} + +
+
+ +
+
+ + + {location} + +
+
+
+
+ ) +} diff --git a/src/app/conf/2025/components/og-images/generic-opengraph-image.tsx b/src/app/conf/2025/components/og-images/generic-opengraph-image.tsx new file mode 100644 index 0000000000..821d1d1200 --- /dev/null +++ b/src/app/conf/2025/components/og-images/generic-opengraph-image.tsx @@ -0,0 +1,80 @@ +import { colors, fonts } from "./speaker-opengraph-image" +import { + ConferenceOpengraphImageHeader, + OG_IMAGE_HEADER_HEIGHT, +} from "./conference-opengraph-image-header" + +import graphqlLogoStripes from "./graphql-logo-stripes.png" + +export interface GenericOpengraphImageProps + extends React.HTMLAttributes { + date: string + year: string + location: string + pageTitle: string +} + +export function GenericOpengraphImage({ + date, + location, + year, + pageTitle, + ...rest +}: GenericOpengraphImageProps) { + const basePath = + `https://${process.env.VERCEL_URL}` || process.env.__NEXT_PRIVATE_ORIGIN + + const height = 630 + + return ( +
+ + +
+

+ {pageTitle} +

+ +
+
+ ) +} diff --git a/src/app/conf/2025/components/og-images/graphql-logo-stripes.png b/src/app/conf/2025/components/og-images/graphql-logo-stripes.png new file mode 100644 index 0000000000..31f190c214 Binary files /dev/null and b/src/app/conf/2025/components/og-images/graphql-logo-stripes.png differ diff --git a/src/app/conf/2025/components/og-images/normalize-protocol-relative-url.tsx b/src/app/conf/2025/components/og-images/normalize-protocol-relative-url.tsx new file mode 100644 index 0000000000..5d65ec3f49 --- /dev/null +++ b/src/app/conf/2025/components/og-images/normalize-protocol-relative-url.tsx @@ -0,0 +1,6 @@ +export function normalizeProtocolRelativeUrl(url: string) { + if (url.startsWith("//")) { + return `https:${url}` + } + return url +} diff --git a/src/app/conf/2025/components/og-images/opengraph-image-footer.tsx b/src/app/conf/2025/components/og-images/opengraph-image-footer.tsx new file mode 100644 index 0000000000..fc43d7115b --- /dev/null +++ b/src/app/conf/2025/components/og-images/opengraph-image-footer.tsx @@ -0,0 +1,34 @@ +import { colors, fonts } from "./speaker-opengraph-image" + +export function OpengraphImageFooter({ + children, +}: { + children: React.ReactNode +}) { + return ( + + ) +} diff --git a/src/app/conf/2025/components/session-opengraph-image.tsx b/src/app/conf/2025/components/og-images/session-opengraph-image.tsx similarity index 92% rename from src/app/conf/2025/components/session-opengraph-image.tsx rename to src/app/conf/2025/components/og-images/session-opengraph-image.tsx index 457fa1fcd9..a369833427 100644 --- a/src/app/conf/2025/components/session-opengraph-image.tsx +++ b/src/app/conf/2025/components/og-images/session-opengraph-image.tsx @@ -1,14 +1,11 @@ import type { SchedSpeaker, ScheduleSession } from "@/app/conf/2023/types" -import { - ConferenceOpengraphImageHeader, - normalizeProtocolRelativeUrl, - colors, - OpengraphImageFooter, - fonts, -} from "./speaker-opengraph-image" -import { getEventTitle } from "../utils" -import { formatSpeakerPosition } from "./format-speaker-position" -import { speakers as allSpeakers } from "../_data" +import { colors, fonts } from "./speaker-opengraph-image" +import { ConferenceOpengraphImageHeader } from "./conference-opengraph-image-header" +import { OpengraphImageFooter } from "./opengraph-image-footer" +import { normalizeProtocolRelativeUrl } from "./normalize-protocol-relative-url" +import { getEventTitle } from "../../utils" +import { formatSpeakerPosition } from "../format-speaker-position" +import { speakers as allSpeakers } from "../../_data" export interface SessionOpengraphImageProps extends React.HTMLAttributes { diff --git a/src/app/conf/2025/components/og-images/simple-opengraph-image.tsx b/src/app/conf/2025/components/og-images/simple-opengraph-image.tsx new file mode 100644 index 0000000000..bd086537b0 --- /dev/null +++ b/src/app/conf/2025/components/og-images/simple-opengraph-image.tsx @@ -0,0 +1,38 @@ +import { ImageResponse } from "next/og" + +import { loadFontsForOG } from "@/app/fonts/og/load-fonts-for-og" + +import { GenericOpengraphImage } from "./generic-opengraph-image" + +export const contentType = "image/png" +export const size = { + width: 1200, + height: 630, +} + +export function generateStaticParams() { + return [{}] +} + +export async function SimpleOpengraphImage({ + pageTitle, +}: { + pageTitle: string +}) { + const fonts = loadFontsForOG() + + return new ImageResponse( + ( + + ), + { + ...size, + fonts: await fonts, + }, + ) +} diff --git a/src/app/conf/2025/components/og-images/speaker-opengraph-image.tsx b/src/app/conf/2025/components/og-images/speaker-opengraph-image.tsx new file mode 100644 index 0000000000..16203bd041 --- /dev/null +++ b/src/app/conf/2025/components/og-images/speaker-opengraph-image.tsx @@ -0,0 +1,176 @@ +import type { SchedSpeaker } from "@/app/conf/2023/types" +import { formatSpeakerPosition } from "../format-speaker-position" +import { normalizeProtocolRelativeUrl } from "./normalize-protocol-relative-url" +import { OpengraphImageFooter } from "./opengraph-image-footer" +import { ConferenceOpengraphImageHeader } from "./conference-opengraph-image-header" + +/** + * We can't use CSS variables and Tailwind classes here because we're rendering an image. + */ +export const colors = { + neu0: "hsl(0 0% 100%)", + neu50: "hsl(75 57% 97%)", + neu100: "hsl(75 15% 95%)", + neu200: "hsl(77 14% 90%)", + neu300: "hsl(76 14% 85%)", + neu400: "hsl(77 14% 80%)", + neu500: "hsl(74 14% 70%)", + neu600: "hsl(76 15% 60%)", + neu700: "hsl(76 15% 40%)", + neu800: "hsl(77 14% 20%)", + neu900: "hsl(75 15% 5%)", + secLighter: "hsl(79 80% 90%)", + priBase: "hsl(319 100% 44.1%)", +} + +export const fonts = { + sans: "'Host Grotesk', var(--font-sans)", + mono: "'Commit Mono', var(--font-mono)", +} + +export const RIGHT_COLUMN_WIDTH_PX = 476 + +export interface SpeakerOpengraphImageProps + extends React.HTMLAttributes { + speaker: SchedSpeaker + date: string + year: string + location: string +} + +export function SpeakerOpengraphImage({ + speaker, + date, + year, + location, + ...rest +}: SpeakerOpengraphImageProps) { + return ( +
+ + +
+
+
+
+

+ {speaker.name} +

+ +
+ + {formatSpeakerPosition(speaker)} + +
+
+
+ + Speakers +
+ + {speaker.avatar && ( +
+ ` is crashing similarly to + // https://github.com/vercel/satori/issues/650. + // So we use `sepia` and `hue-rotate` to change + // the hue to around 79deg from the design system's + // secondary color (yellowish green). + filter: "sepia(1) hue-rotate(37.5deg)", + }} + width={RIGHT_COLUMN_WIDTH_PX} + height={RIGHT_COLUMN_WIDTH_PX} + /> +
+
+ )} +
+
+ ) +} diff --git a/src/app/conf/2025/components/speaker-opengraph-image.tsx b/src/app/conf/2025/components/speaker-opengraph-image.tsx deleted file mode 100644 index 51c8c1f95e..0000000000 --- a/src/app/conf/2025/components/speaker-opengraph-image.tsx +++ /dev/null @@ -1,374 +0,0 @@ -import type { SchedSpeaker } from "@/app/conf/2023/types" -import { CalendarIcon } from "../../_design-system/pixelarticons/calendar-icon" -import { PinIcon } from "../../_design-system/pixelarticons/pin-icon" -import { formatSpeakerPosition } from "./format-speaker-position" -import { GraphQLLogo } from "./graphql-conf-logo-link" - -export const colors = { - neu0: "hsl(0 0% 100%)", - neu50: "hsl(75 57% 97%)", - neu100: "hsl(75 15% 95%)", - neu200: "hsl(77 14% 90%)", - neu300: "hsl(76 14% 85%)", - neu400: "hsl(77 14% 80%)", - neu500: "hsl(74 14% 70%)", - neu600: "hsl(76 15% 60%)", - neu700: "hsl(76 15% 40%)", - neu800: "hsl(77 14% 20%)", - neu900: "hsl(75 15% 5%)", - secLighter: "hsl(79 80% 90%)", - priBase: "hsl(319 100% 44.1%)", -} - -export const fonts = { - sans: "'Host Grotesk', var(--font-sans)", - mono: "'Commit Mono', var(--font-mono)", -} - -const RIGHT_COLUMN_WIDTH_PX = 476 - -export interface SpeakerOpengraphImageProps - extends React.HTMLAttributes { - speaker: SchedSpeaker - date: string - year: string - location: string -} - -export function SpeakerOpengraphImage({ - speaker, - date, - year, - location, - ...rest -}: SpeakerOpengraphImageProps) { - return ( -
- - -
-
-
-
-

- {speaker.name} -

- -
- - {formatSpeakerPosition(speaker)} - -
-
-
- - Speakers -
- - {speaker.avatar && ( -
- ` is crashing similarly to - // https://github.com/vercel/satori/issues/650. - // So we use `sepia` and `hue-rotate` to change - // the hue to around 79deg from the design system's - // secondary color (yellowish green). - filter: "sepia(1) hue-rotate(37.5deg)", - }} - width={RIGHT_COLUMN_WIDTH_PX} - height={RIGHT_COLUMN_WIDTH_PX} - /> -
-
- )} -
-
- ) -} - -export function ConferenceOpengraphImageHeader({ - year, - date, - location, -}: { - year: string - date: string - location: string -}) { - return ( -
-
-
-
-
- - / -
- GraphQLConf{" "} - {year} -
-
-
-
-
- -
-
-
- - - {date}, {year} - -
-
- -
-
- - - {location} - -
-
-
-
- ) -} - -export function normalizeProtocolRelativeUrl(url: string) { - if (url.startsWith("//")) { - return `https:${url}` - } - return url -} - -export function OpengraphImageFooter({ - children, -}: { - children: React.ReactNode -}) { - return ( - - ) -} diff --git a/src/app/conf/2025/opengraph-image.tsx b/src/app/conf/2025/opengraph-image.tsx new file mode 100644 index 0000000000..6243f3534d --- /dev/null +++ b/src/app/conf/2025/opengraph-image.tsx @@ -0,0 +1,10 @@ +import { SimpleOpengraphImage } from "./components/og-images/simple-opengraph-image" +export { + generateStaticParams, + contentType, + size, +} from "./components/og-images/simple-opengraph-image" + +export default SimpleOpengraphImage.bind(null, { + pageTitle: "GraphQLConf 2025", +}) diff --git a/src/app/conf/2025/resources/opengraph-image.tsx b/src/app/conf/2025/resources/opengraph-image.tsx new file mode 100644 index 0000000000..6adec040fa --- /dev/null +++ b/src/app/conf/2025/resources/opengraph-image.tsx @@ -0,0 +1,10 @@ +import { SimpleOpengraphImage } from "../components/og-images/simple-opengraph-image" +export { + generateStaticParams, + contentType, + size, +} from "../components/og-images/simple-opengraph-image" + +export default SimpleOpengraphImage.bind(null, { + pageTitle: "Resources", +}) diff --git a/src/app/conf/2025/schedule/[id]/opengraph-image.tsx b/src/app/conf/2025/schedule/[id]/opengraph-image.tsx index c3ca880773..9b16215fd3 100644 --- a/src/app/conf/2025/schedule/[id]/opengraph-image.tsx +++ b/src/app/conf/2025/schedule/[id]/opengraph-image.tsx @@ -3,7 +3,7 @@ import { ImageResponse } from "next/og" import { loadFontsForOG } from "@/app/fonts/og/load-fonts-for-og" import { schedule } from "../../_data" -import { SessionOpengraphImage } from "../../components/session-opengraph-image" +import { SessionOpengraphImage } from "../../components/og-images/session-opengraph-image" export const contentType = "image/png" export const size = { diff --git a/src/app/conf/2025/schedule/opengraph-image.tsx b/src/app/conf/2025/schedule/opengraph-image.tsx new file mode 100644 index 0000000000..2dc7b26c9a --- /dev/null +++ b/src/app/conf/2025/schedule/opengraph-image.tsx @@ -0,0 +1,10 @@ +import { SimpleOpengraphImage } from "../components/og-images/simple-opengraph-image" +export { + generateStaticParams, + contentType, + size, +} from "../components/og-images/simple-opengraph-image" + +export default SimpleOpengraphImage.bind(null, { + pageTitle: "Schedule", +}) diff --git a/src/app/conf/2025/speakers/[id]/opengraph-image.tsx b/src/app/conf/2025/speakers/[id]/opengraph-image.tsx index b7dba4434f..185dd16930 100644 --- a/src/app/conf/2025/speakers/[id]/opengraph-image.tsx +++ b/src/app/conf/2025/speakers/[id]/opengraph-image.tsx @@ -3,7 +3,7 @@ import { ImageResponse } from "next/og" import { loadFontsForOG } from "@/app/fonts/og/load-fonts-for-og" import { speakers } from "../../_data" -import { SpeakerOpengraphImage } from "../../components/speaker-opengraph-image" +import { SpeakerOpengraphImage } from "../../components/og-images/speaker-opengraph-image" export const contentType = "image/png" export const size = { diff --git a/src/app/conf/2025/speakers/opengraph-image.tsx b/src/app/conf/2025/speakers/opengraph-image.tsx new file mode 100644 index 0000000000..435889efe5 --- /dev/null +++ b/src/app/conf/2025/speakers/opengraph-image.tsx @@ -0,0 +1,10 @@ +import { SimpleOpengraphImage } from "../components/og-images/simple-opengraph-image" +export { + generateStaticParams, + contentType, + size, +} from "../components/og-images/simple-opengraph-image" + +export default SimpleOpengraphImage.bind(null, { + pageTitle: "Speakers", +})