mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
feat(www): load _events mdx files on /events listing (#45176)
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Feature ## What is the current behavior? The /events page only loads events from the Notion "Developer Events" database and the Luma Community API. MDX files under `apps/www/_events/` (including webinars like agency-webinar, sentry, datadog, figma-make) are not surfaced on the listing, and past events could still appear until the moment they ended because the filter compared against `now()` rather than the current day. Addresses [DEBR-85](https://linear.app/supabase/issue/DEBR-85/events-page-powered-by-notion-page). ## What is the new behavior? - New `getMdxEvents()` reads `apps/www/_events/*.mdx`, parses frontmatter with `gray-matter`, and returns today-and-future events as `SupabaseEvent`s. - `/events` now merges Notion + mdx + Luma events. - Past events are hidden across all sources by comparing against the start of today (UTC) instead of `now()`, so events running today stay visible throughout the day. ## Additional context Links on mdx events point at the main_cta URL when it's an external \`http(s)\` URL, otherwise fall back to the built \`/events/{slug}\` page so on-demand recordings remain reachable. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Events can be sourced from MDX files and merged into site event listings. * Luma supports multiple calendars (community and hackathon) for richer feeds. * **Improvements** * Events now exclude anything before the start of the current UTC day. * Added a “Community Event” category filter and included it in counts. * Event title typography adjusted for improved readability. * “Hosted by” text now only shows when hosts exist; host fallbacks standardized. * **Chores** * Build env updated to include hackathon API key. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
e46ee776b4
commit
73286972fb
10 files changed
+257
-95
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { DEFAULT_META_DESCRIPTION } from '~/lib/constants'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
export interface LumaGeoAddressJson {
|
||||
city: string
|
||||
@@ -58,81 +58,90 @@ interface LumaResponse {
|
||||
next_cursor?: string
|
||||
}
|
||||
|
||||
export type LumaCalendar = 'community' | 'hackathon'
|
||||
|
||||
async function fetchLumaCalendar(
|
||||
apiKey: string,
|
||||
calendar: LumaCalendar,
|
||||
after: string | null,
|
||||
before: string | null
|
||||
) {
|
||||
const lumaUrl = new URL('https://public-api.lu.ma/public/v1/calendar/list-events')
|
||||
if (after) lumaUrl.searchParams.append('after', after)
|
||||
if (before) lumaUrl.searchParams.append('before', before)
|
||||
|
||||
const response = await fetch(lumaUrl.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'x-luma-api-key': apiKey,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Luma API error (${calendar}): ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data: LumaResponse = await response.json()
|
||||
|
||||
return data.entries
|
||||
.filter(({ event }) => event.visibility === 'public')
|
||||
.map(({ event }) => ({
|
||||
id: event.api_id,
|
||||
calendar,
|
||||
start_at: event.start_at,
|
||||
end_at: event.end_at,
|
||||
name: event.name,
|
||||
city: event.geo_address_json?.city,
|
||||
country: event.geo_address_json?.country,
|
||||
url: event.url,
|
||||
timezone: event.timezone,
|
||||
cover_url: event.cover_url,
|
||||
description: event.description,
|
||||
hosts: event.hosts || [],
|
||||
}))
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const lumaApiKey = process.env.LUMA_API_KEY
|
||||
const communityKey = process.env.LUMA_API_KEY
|
||||
const hackathonKey = process.env.LUMA_HACKATHONS_API_KEY
|
||||
|
||||
if (!lumaApiKey) {
|
||||
console.error('LUMA_API_KEY environment variable is not set')
|
||||
if (!communityKey && !hackathonKey) {
|
||||
console.error('No Luma API keys configured (LUMA_API_KEY / LUMA_HACKATHONS_API_KEY)')
|
||||
return NextResponse.json({ error: 'API configuration error' }, { status: 500 })
|
||||
}
|
||||
|
||||
// Extract query parameters from the request
|
||||
const { searchParams } = new URL(request.url)
|
||||
const after = searchParams.get('after')
|
||||
const before = searchParams.get('before')
|
||||
|
||||
// Build the Luma API URL with query parameters
|
||||
const lumaUrl = new URL('https://public-api.lu.ma/public/v1/calendar/list-events')
|
||||
const calendarFetches: Array<Promise<Awaited<ReturnType<typeof fetchLumaCalendar>>>> = []
|
||||
if (communityKey)
|
||||
calendarFetches.push(fetchLumaCalendar(communityKey, 'community', after, before))
|
||||
if (hackathonKey)
|
||||
calendarFetches.push(fetchLumaCalendar(hackathonKey, 'hackathon', after, before))
|
||||
|
||||
if (after) {
|
||||
lumaUrl.searchParams.append('after', after)
|
||||
}
|
||||
if (before) {
|
||||
lumaUrl.searchParams.append('before', before)
|
||||
}
|
||||
const results = await Promise.allSettled(calendarFetches)
|
||||
|
||||
// Fetch events from Luma API
|
||||
const response = await fetch(lumaUrl.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'x-luma-api-key': lumaApiKey,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Luma API error:', response.status, response.statusText)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Failed to fetch events from Luma',
|
||||
status: response.status,
|
||||
},
|
||||
{ status: response.status }
|
||||
)
|
||||
}
|
||||
|
||||
const data: LumaResponse = await response.json()
|
||||
|
||||
const launchWeekEvents = data.entries
|
||||
.filter(({ event }: { event: LumaPayloadEvent }) => event.visibility === 'public')
|
||||
.map(({ event }: { event: LumaPayloadEvent }) => ({
|
||||
id: event.api_id,
|
||||
start_at: event.start_at,
|
||||
end_at: event.end_at,
|
||||
name: event.name,
|
||||
city: event.geo_address_json?.city,
|
||||
country: event.geo_address_json?.country,
|
||||
url: event.url,
|
||||
timezone: event.timezone,
|
||||
cover_url: event.cover_url,
|
||||
description: event.description,
|
||||
hosts: event.hosts || [],
|
||||
}))
|
||||
const events = results
|
||||
.flatMap((result) => {
|
||||
if (result.status === 'fulfilled') return result.value
|
||||
Sentry.captureException(result.reason)
|
||||
console.error(result.reason)
|
||||
return []
|
||||
})
|
||||
.sort((a, b) => new Date(a.start_at).getTime() - new Date(b.start_at).getTime())
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
events: launchWeekEvents,
|
||||
total: launchWeekEvents.length,
|
||||
filters: {
|
||||
after,
|
||||
before,
|
||||
},
|
||||
events,
|
||||
total: events.length,
|
||||
filters: { after, before },
|
||||
})
|
||||
} catch (error) {
|
||||
Sentry.captureException(error)
|
||||
console.error('Error fetching meetups from Luma:', error)
|
||||
console.error('Error fetching events from Luma:', error)
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Internal server error',
|
||||
|
||||
@@ -20,9 +20,10 @@ const EventsContext = createContext<EventsContextValue | undefined>(undefined)
|
||||
interface EventsProviderProps {
|
||||
children: ReactNode
|
||||
notionEvents: SupabaseEvent[]
|
||||
mdxEvents: SupabaseEvent[]
|
||||
}
|
||||
|
||||
export function EventsProvider({ children, notionEvents }: EventsProviderProps) {
|
||||
export function EventsProvider({ children, notionEvents, mdxEvents }: EventsProviderProps) {
|
||||
const [lumaEvents, setLumaEvents] = useState<SupabaseEvent[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
@@ -42,9 +43,12 @@ export function EventsProvider({ children, notionEvents }: EventsProviderProps)
|
||||
|
||||
if (data.success) {
|
||||
const transformedEvents: SupabaseEvent[] = data.events.map((event: any) => {
|
||||
let categories: string[] = []
|
||||
const isMeetup = event.name.toLowerCase().includes('meetup')
|
||||
if (isMeetup) categories.push('meetup')
|
||||
// Categorize by the originating Luma calendar: events from the
|
||||
// Supabase Hackathons calendar → `hackathon`; everything from the
|
||||
// Supabase Community Events calendar → `community`.
|
||||
const categories: string[] = [
|
||||
event?.calendar === 'hackathon' ? 'hackathon' : 'community',
|
||||
]
|
||||
|
||||
const rawUrl = event?.url || ''
|
||||
let safeUrl: string | undefined
|
||||
@@ -73,7 +77,8 @@ export function EventsProvider({ children, notionEvents }: EventsProviderProps)
|
||||
location: new Intl.ListFormat('en', { style: 'narrow', type: 'unit' }).format(
|
||||
[event?.city, event?.country].filter(Boolean)
|
||||
),
|
||||
hosts: isMeetup || event?.hosts?.length === 0 ? [SUPABASE_HOST] : event?.hosts || [],
|
||||
// All Luma events are Supabase-hosted regardless of which calendar they're from.
|
||||
hosts: [SUPABASE_HOST],
|
||||
source: 'luma' as const,
|
||||
disable_page_build: true,
|
||||
link: safeUrl ? { href: safeUrl, target: '_blank' as const } : undefined,
|
||||
@@ -91,14 +96,17 @@ export function EventsProvider({ children, notionEvents }: EventsProviderProps)
|
||||
fetchLumaEvents()
|
||||
}, [])
|
||||
|
||||
// Merge Notion (server) + Luma (client) events, excluding past events
|
||||
// Merge Notion (server) + mdx (server) + Luma (client) events, showing only today and future.
|
||||
const allEvents = useMemo(() => {
|
||||
const now = new Date()
|
||||
return [...notionEvents, ...lumaEvents].filter((event) => {
|
||||
const startOfToday = new Date(
|
||||
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
|
||||
)
|
||||
return [...notionEvents, ...mdxEvents, ...lumaEvents].filter((event) => {
|
||||
const eventDate = new Date(event.end_date || event.date)
|
||||
return eventDate >= now
|
||||
return eventDate >= startOfToday
|
||||
})
|
||||
}, [notionEvents, lumaEvents])
|
||||
}, [notionEvents, mdxEvents, lumaEvents])
|
||||
|
||||
const categories = useMemo(() => {
|
||||
const counts: { [key: string]: number } = { all: 0 }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EventClientRenderer } from '~/components/Events/new/EventClientRenderer'
|
||||
import { breadcrumbs } from '~/lib/breadcrumbs'
|
||||
import { getNotionEvents } from '~/lib/events'
|
||||
import { getMdxEvents, getNotionEvents } from '~/lib/events'
|
||||
import { breadcrumbListSchema, serializeJsonLd } from '~/lib/json-ld'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
@@ -11,7 +11,10 @@ export const metadata: Metadata = {
|
||||
}
|
||||
|
||||
export default async function EventsPage() {
|
||||
const notionEvents = await getNotionEvents()
|
||||
const [notionEvents, mdxEvents] = await Promise.all([
|
||||
getNotionEvents(),
|
||||
Promise.resolve(getMdxEvents()),
|
||||
])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -21,7 +24,7 @@ export default async function EventsPage() {
|
||||
__html: serializeJsonLd(breadcrumbListSchema(breadcrumbs.eventsIndex)),
|
||||
}}
|
||||
/>
|
||||
<EventClientRenderer notionEvents={notionEvents} />
|
||||
<EventClientRenderer notionEvents={notionEvents} mdxEvents={mdxEvents} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -28,12 +28,14 @@ export function EventBanner() {
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p
|
||||
className="text-lg font-medium text-foreground-light"
|
||||
title={`Hosted by ${formatHosts(featuredEvent.hosts).fullList}`}
|
||||
>
|
||||
{formatHosts(featuredEvent.hosts).displayText}
|
||||
</p>
|
||||
{featuredEvent.hosts.length > 0 && (
|
||||
<p
|
||||
className="text-lg font-medium text-foreground-light"
|
||||
title={`Hosted by ${formatHosts(featuredEvent.hosts).fullList}`}
|
||||
>
|
||||
{formatHosts(featuredEvent.hosts).displayText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-foreground-light line-clamp-3 lg:line-clamp-4">
|
||||
|
||||
@@ -19,9 +19,15 @@ function EventBannerSection() {
|
||||
)
|
||||
}
|
||||
|
||||
export function EventClientRenderer({ notionEvents }: { notionEvents: SupabaseEvent[] }) {
|
||||
export function EventClientRenderer({
|
||||
notionEvents,
|
||||
mdxEvents,
|
||||
}: {
|
||||
notionEvents: SupabaseEvent[]
|
||||
mdxEvents: SupabaseEvent[]
|
||||
}) {
|
||||
return (
|
||||
<EventsProvider notionEvents={notionEvents}>
|
||||
<EventsProvider notionEvents={notionEvents} mdxEvents={mdxEvents}>
|
||||
<DefaultLayout className="flex flex-col">
|
||||
<EventsContainer className="border-x border-b py-8">
|
||||
<h1 className="h3 p-0! m-0!">
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import { Input_Shadcn_ as Input } from 'ui'
|
||||
import { SearchIcon } from 'lucide-react'
|
||||
import { Badge } from 'ui'
|
||||
import { useEvents } from '~/app/events/context'
|
||||
import { SearchIcon } from 'lucide-react'
|
||||
import { Badge, Input_Shadcn_ as Input } from 'ui'
|
||||
|
||||
const CATEGORIES_FILTERS = [
|
||||
{ name: 'All', value: 'all' },
|
||||
{ name: 'Conference', value: 'conference' },
|
||||
{ name: 'Community Event', value: 'community' },
|
||||
{ name: 'Meetup', value: 'meetup' },
|
||||
{ name: 'Workshop', value: 'workshop' },
|
||||
{ name: 'Hackathon', value: 'hackathon' },
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Badge, Button, cn } from 'ui'
|
||||
|
||||
const CATEGORIES_FILTERS = [
|
||||
{ name: 'All', value: 'all' },
|
||||
{ name: 'Community Event', value: 'community' },
|
||||
{ name: 'Meetup', value: 'meetup' },
|
||||
{ name: 'Conference', value: 'conference' },
|
||||
{ name: 'Workshop', value: 'workshop' },
|
||||
@@ -83,7 +84,7 @@ export function EventList() {
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="leading-3">{event.title}</h3>
|
||||
<h3 className="leading-snug">{event.title}</h3>
|
||||
{event.isSpeaking && (
|
||||
<Badge variant="success" className="flex items-center gap-1">
|
||||
Speaking
|
||||
@@ -107,18 +108,20 @@ export function EventList() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 items-center text-sm text-foreground-light">
|
||||
<div className="size-5 rounded-full border bg-linear-to-br from-background-surface-100 to-background-surface-200 relative">
|
||||
{event.hosts[0]?.avatar_url && (
|
||||
<img
|
||||
src={event.hosts[0].avatar_url}
|
||||
alt={event.hosts[0].name || 'Host image'}
|
||||
className="absolute inset-0 w-full h-full object-cover rounded-full"
|
||||
/>
|
||||
)}
|
||||
{event.hosts.length > 0 && (
|
||||
<div className="flex gap-2 items-center text-sm text-foreground-light">
|
||||
<div className="size-5 rounded-full border bg-linear-to-br from-background-surface-100 to-background-surface-200 relative">
|
||||
{event.hosts[0]?.avatar_url && (
|
||||
<img
|
||||
src={event.hosts[0].avatar_url}
|
||||
alt={event.hosts[0].name || 'Host image'}
|
||||
className="absolute inset-0 w-full h-full object-cover rounded-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
Hosted by {formatHosts(event.hosts).displayText}
|
||||
</div>
|
||||
Hosted by {formatHosts(event.hosts).displayText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 relative z-10">
|
||||
|
||||
+132
-2
@@ -16,9 +16,12 @@
|
||||
* Are you speaking at this event? -> multi_select
|
||||
* Participation -> multi_select
|
||||
*/
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import matter from 'gray-matter'
|
||||
import { queryDatabase } from 'lib/notion'
|
||||
|
||||
import { SUPABASE_HOST, SupabaseEvent } from './eventsTypes'
|
||||
import { EventHost, SUPABASE_HOST, SupabaseEvent } from './eventsTypes'
|
||||
|
||||
// The actual DB ID (child database inside the page)
|
||||
const NOTION_EVENTS_DB_ID_FALLBACK = '21b5004b775f8058872fe8fa81e2c7ac'
|
||||
@@ -70,6 +73,12 @@ function getMultiSelect(page: any, name: string): string[] {
|
||||
return prop.multi_select.map((s: any) => s.name)
|
||||
}
|
||||
|
||||
function getSelect(page: any, name: string): string {
|
||||
const prop = page.properties[name]
|
||||
if (!prop || prop.type !== 'select') return ''
|
||||
return prop.select?.name ?? ''
|
||||
}
|
||||
|
||||
// ─── Main fetch ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -107,6 +116,10 @@ export const getNotionEvents = async (): Promise<SupabaseEvent[]> => {
|
||||
const rawMeetingLink = getUrl(page, 'Book Meeting Link')
|
||||
const meetingLink = isSafeHttpUrl(rawMeetingLink) ? rawMeetingLink : ''
|
||||
const location = getRichText(page, 'Location')
|
||||
const notionType = getSelect(page, 'Type')
|
||||
// "Conference" events are third-party — we attend but don't host, so no
|
||||
// "Hosted by" line. "Supabase event" → host = Supabase.
|
||||
const isConferenceType = notionType.toLowerCase() === 'conference'
|
||||
const categories = ['conference']
|
||||
const speakingAnswers = getMultiSelect(page, 'Are you speaking at this event?')
|
||||
const isSpeaking = speakingAnswers.includes('Yes')
|
||||
@@ -126,7 +139,7 @@ export const getNotionEvents = async (): Promise<SupabaseEvent[]> => {
|
||||
categories,
|
||||
timezone: '',
|
||||
location,
|
||||
hosts: [SUPABASE_HOST],
|
||||
hosts: isConferenceType ? [] : [SUPABASE_HOST],
|
||||
source: 'notion',
|
||||
disable_page_build: true,
|
||||
isSpeaking,
|
||||
@@ -140,3 +153,120 @@ export const getNotionEvents = async (): Promise<SupabaseEvent[]> => {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// ─── MDX events ─────────────────────────────────────────────────────────────
|
||||
|
||||
const EVENTS_DIRECTORY = '_events'
|
||||
const FILENAME_DATE_PREFIX_LENGTH = 11
|
||||
|
||||
type MdxHost = { name?: string; avatar_url?: string }
|
||||
|
||||
type MdxFrontmatter = {
|
||||
title?: string
|
||||
subtitle?: string
|
||||
description?: string
|
||||
meta_description?: string
|
||||
type?: string
|
||||
date?: string
|
||||
end_date?: string
|
||||
timezone?: string
|
||||
categories?: string[]
|
||||
tags?: string[]
|
||||
onDemand?: boolean
|
||||
disable_page_build?: boolean
|
||||
hosts?: MdxHost[]
|
||||
main_cta?: { url?: string; target?: '_blank' | '_self'; label?: string }
|
||||
}
|
||||
|
||||
function mdxSlugFromFilename(filename: string): string {
|
||||
// Matches the slug produced by getAllPostSlugs for `_events` (keeps any leading
|
||||
// underscore from `YYYY-MM-DD__name.mdx`), so /events/{slug} lands on the built page.
|
||||
return filename.replace(/\.mdx$/, '').substring(FILENAME_DATE_PREFIX_LENGTH)
|
||||
}
|
||||
|
||||
function mdxHostsToEventHosts(hosts: MdxHost[] | undefined): EventHost[] {
|
||||
if (!hosts || hosts.length === 0) return [SUPABASE_HOST]
|
||||
return hosts.map((host, i) => ({
|
||||
id: `mdx-host-${i}`,
|
||||
email: '',
|
||||
name: host.name ?? null,
|
||||
first_name: host.name ?? null,
|
||||
last_name: null,
|
||||
avatar_url: host.avatar_url ?? '',
|
||||
}))
|
||||
}
|
||||
|
||||
function startOfTodayUtc(): Date {
|
||||
const now = new Date()
|
||||
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()))
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all events under `_events/` and return today-and-future events.
|
||||
* Past events are excluded (by end_date when present, otherwise start date).
|
||||
*/
|
||||
export const getMdxEvents = (): SupabaseEvent[] => {
|
||||
const postDirectory = path.join(process.cwd(), EVENTS_DIRECTORY)
|
||||
|
||||
let fileNames: string[]
|
||||
try {
|
||||
fileNames = fs.readdirSync(postDirectory)
|
||||
} catch (error) {
|
||||
console.error('Error reading _events directory:', error)
|
||||
return []
|
||||
}
|
||||
|
||||
const today = startOfTodayUtc()
|
||||
|
||||
return fileNames
|
||||
.filter((filename) => filename.endsWith('.mdx'))
|
||||
.map((filename): SupabaseEvent | null => {
|
||||
try {
|
||||
const fullPath = path.join(postDirectory, filename)
|
||||
const fileContents = fs.readFileSync(fullPath, 'utf8')
|
||||
const { data } = matter(fileContents) as unknown as { data: MdxFrontmatter }
|
||||
|
||||
if (!data.title || !data.date) return null
|
||||
|
||||
const eventEnd = new Date(data.end_date ?? data.date)
|
||||
if (eventEnd < today) return null
|
||||
|
||||
const slug = mdxSlugFromFilename(filename)
|
||||
const categories = data.categories ?? (data.type ? [data.type] : [])
|
||||
// Webinars don't show a "Hosted by" line — drop hosts entirely.
|
||||
const isWebinar = data.type === 'webinar' || categories.includes('webinar')
|
||||
const rawCtaUrl = data.main_cta?.url ?? ''
|
||||
const isExternalCta = /^https?:\/\//i.test(rawCtaUrl)
|
||||
const safeExternalCta = isExternalCta && isSafeHttpUrl(rawCtaUrl) ? rawCtaUrl : ''
|
||||
const internalPath = `/events/${slug}`
|
||||
const href = safeExternalCta || internalPath
|
||||
const target = safeExternalCta ? '_blank' : '_self'
|
||||
|
||||
return {
|
||||
slug,
|
||||
type: data.type ?? 'event',
|
||||
title: data.title,
|
||||
date: data.date,
|
||||
end_date: data.end_date,
|
||||
description: data.meta_description ?? data.description ?? data.subtitle ?? '',
|
||||
thumb: '',
|
||||
cover_url: '',
|
||||
path: internalPath,
|
||||
url: href,
|
||||
tags: data.tags ?? categories,
|
||||
categories,
|
||||
timezone: data.timezone ?? '',
|
||||
location: '',
|
||||
hosts: isWebinar ? [] : mdxHostsToEventHosts(data.hosts),
|
||||
source: 'mdx',
|
||||
onDemand: data.onDemand,
|
||||
disable_page_build: data.disable_page_build,
|
||||
link: { href, target, label: data.main_cta?.label },
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error parsing mdx event ${filename}:`, error)
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter((e): e is SupabaseEvent => e !== null)
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export interface SupabaseEvent {
|
||||
timezone: string
|
||||
location: string
|
||||
hosts: EventHost[]
|
||||
source: 'luma' | 'notion'
|
||||
source: 'luma' | 'notion' | 'mdx'
|
||||
end_date?: string
|
||||
onDemand?: boolean
|
||||
disable_page_build?: boolean
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"ASSET_CDN_S3_ENDPOINT",
|
||||
"SITE_NAME",
|
||||
"LUMA_API_KEY",
|
||||
"LUMA_HACKATHONS_API_KEY",
|
||||
"NEXT_RUNTIME",
|
||||
// These env vars are used by some community and marketing integrations
|
||||
"SENTRY_DSN_COMMUNITY",
|
||||
|
||||
Reference in new issue
Block a user