From b7eb0a30672ac786a51e6f3a261753392aad8903 Mon Sep 17 00:00:00 2001 From: myelinated-wackerow <263208946+myelinated-wackerow@users.noreply.github.com> Date: Tue, 3 Mar 2026 21:31:29 +0000 Subject: [PATCH] feat: add structured data for community hubs Add Service + Place + EventSeries JSON-LD schemas for each community hub to improve regional search discovery. Each hub gets three schema.org nodes in the @graph: Service (coworking), Place (location/address), and EventSeries (recurring schedule). Co-Authored-By: Claude Opus 4.6 Co-Authored-By: wackerow <54227730+wackerow@users.noreply.github.com> --- app/[locale]/community/events/page-jsonld.tsx | 89 +++++++++ src/data/community-hub-schemas.ts | 170 ++++++++++++++++++ src/data/community-hubs.ts | 1 - 3 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 src/data/community-hub-schemas.ts diff --git a/app/[locale]/community/events/page-jsonld.tsx b/app/[locale]/community/events/page-jsonld.tsx index 128ecb97536..03dab23711d 100644 --- a/app/[locale]/community/events/page-jsonld.tsx +++ b/app/[locale]/community/events/page-jsonld.tsx @@ -8,9 +8,93 @@ import { getLocaleYear } from "@/lib/utils/date" import { ethereumCommunityOrganization, ethereumFoundationOrganization, + ethereumFoundationReference, } from "@/lib/utils/jsonld" import { normalizeUrlForJsonLd } from "@/lib/utils/url" +import { communityHubSchemas } from "@/data/community-hub-schemas" +import communityHubs from "@/data/community-hubs" + +function buildHubSchemaNodes( + hub: (typeof communityHubs)[number], + description: string +) { + const schema = communityHubSchemas[hub.id] + if (!schema) return [] + + const placeId = `#hub-location-${hub.id}` + const seriesId = `#coworking-series-${hub.id}` + + const serviceNode = { + "@type": "Service" as const, + name: "Ethereum Community Coworking and Events", + description, + provider: ethereumFoundationReference, + areaServed: { + "@type": "City" as const, + name: hub.location, + }, + } + + const placeNode: Record = { + "@type": "Place" as const, + "@id": placeId, + name: schema.hubName ?? `Ethereum Community Hub (${hub.location})`, + } + + if (schema.address) { + placeNode.address = { + "@type": "PostalAddress" as const, + streetAddress: schema.address.streetAddress, + addressLocality: schema.address.addressLocality, + ...(schema.address.postalCode && { + postalCode: schema.address.postalCode, + }), + addressCountry: schema.address.addressCountry, + } + } + + if (schema.containedInPlace) { + placeNode.containedInPlace = { + "@type": "LocalBusiness" as const, + name: schema.containedInPlace.name, + ...(schema.containedInPlace.url && { + url: schema.containedInPlace.url, + }), + } + } + + const eventNode = { + "@type": ["EventSeries", "Event"] as const, + "@id": seriesId, + name: schema.eventSeriesName ?? "Open Ethereum Coworking Hours", + description: schema.eventDescription, + isAccessibleForFree: true, + url: hub.coworkingSignupUrl, + eventStatus: "https://schema.org/EventScheduled", + eventAttendanceMode: "https://schema.org/OfflineEventAttendanceMode", + organizer: ethereumFoundationReference, + location: { "@id": placeId }, + eventSchedule: { + "@type": "Schedule" as const, + ...(schema.schedule.startDate && { + startDate: schema.schedule.startDate, + }), + ...(schema.schedule.startTime && { + startTime: schema.schedule.startTime, + }), + ...(schema.schedule.endTime && { + endTime: schema.schedule.endTime, + }), + repeatFrequency: schema.schedule.repeatFrequency, + byDay: schema.schedule.byDay, + scheduleTimezone: schema.schedule.scheduleTimezone, + }, + } + + return [serviceNode, placeNode, eventNode] +} + export default async function EventsJsonLD({ locale, contributors, @@ -30,6 +114,10 @@ export default async function EventsJsonLD({ url: contributor.html_url, })) + const hubSchemaNodes = communityHubs.flatMap((hub) => + buildHubSchemaNodes(hub, t(hub.descriptionKey)) + ) + const jsonLd = { "@context": "https://schema.org", "@graph": [ @@ -118,6 +206,7 @@ export default async function EventsJsonLD({ publisher: ethereumFoundationOrganization, reviewedBy: ethereumFoundationOrganization, }, + ...hubSchemaNodes, ], } diff --git a/src/data/community-hub-schemas.ts b/src/data/community-hub-schemas.ts new file mode 100644 index 00000000000..47f1f61c4d9 --- /dev/null +++ b/src/data/community-hub-schemas.ts @@ -0,0 +1,170 @@ +const WEEKDAYS = [ + "https://schema.org/Monday", + "https://schema.org/Tuesday", + "https://schema.org/Wednesday", + "https://schema.org/Thursday", + "https://schema.org/Friday", +] as const + +export type CommunityHubSchemaData = { + /** Override for Place name. Default: "Ethereum Community Hub ({location})" */ + hubName?: string + /** Override for EventSeries name. Default: "Open Ethereum Coworking Hours" */ + eventSeriesName?: string + eventDescription: string + address?: { + streetAddress: string + addressLocality: string + postalCode?: string + addressCountry: string + } + containedInPlace?: { + name: string + url?: string + } + schedule: { + startDate?: string + startTime?: string + endTime?: string + repeatFrequency: string + byDay: string | readonly string[] + scheduleTimezone: string + } +} + +/** + * Structured data metadata for community hubs. + * Used to generate JSON-LD Service + Place + EventSeries schemas + * for regional search discovery. + * + * Keys must match hub IDs in community-hubs.ts. + * hub.location is used as areaServed city name (no need to duplicate here). + * hubName defaults to "Ethereum Community Hub ({location})" if omitted. + * eventSeriesName defaults to "Open Ethereum Coworking Hours" if omitted. + */ +export const communityHubSchemas: Record = { + london: { + eventDescription: + "Open community coworking for Ethereum builders at Encode Hub in London.", + address: { + streetAddress: "41 Pitfield St", + addressLocality: "London", + postalCode: "N1 6DA", + addressCountry: "GB", + }, + containedInPlace: { + name: "Encode Hub", + url: "https://hub.encode.club/", + }, + schedule: { + repeatFrequency: "P1D", + byDay: WEEKDAYS, + scheduleTimezone: "Europe/London", + }, + }, + + berlin: { + hubName: "Ethereum Foundation Office (Berlin)", + eventSeriesName: "Ethereum Community Hub Berlin -- Co-working Wednesdays", + eventDescription: + "Every Wednesday the Ethereum Foundation office opens for builders, researchers, creators, students, and explorers to co-work, connect, and collaborate.", + schedule: { + startTime: "10:00", + endTime: "20:00", + repeatFrequency: "P1W", + byDay: "https://schema.org/Wednesday", + scheduleTimezone: "Europe/Berlin", + }, + }, + + "hong-kong": { + eventDescription: + "Open community coworking for Ethereum builders at DoBe Hub in Hong Kong.", + address: { + streetAddress: "83 King Lam St", + addressLocality: "Cheung Sha Wan", + addressCountry: "HK", + }, + containedInPlace: { + name: "DoBe Hub", + }, + schedule: { + repeatFrequency: "P1D", + byDay: WEEKDAYS, + scheduleTimezone: "Asia/Hong_Kong", + }, + }, + + rome: { + eventDescription: "Open community coworking for Ethereum builders in Rome.", + address: { + streetAddress: "Largo Dino Frisullo", + addressLocality: "Rome", + addressCountry: "IT", + }, + schedule: { + repeatFrequency: "P1D", + byDay: WEEKDAYS, + scheduleTimezone: "Europe/Rome", + }, + }, + + dubai: { + eventDescription: + "Open community coworking for Ethereum builders at Hadron Founders Club in Dubai.", + address: { + streetAddress: "Warehouse 21-22, Al Qouz Industrial Third, Al Quoz", + addressLocality: "Dubai", + addressCountry: "AE", + }, + containedInPlace: { + name: "Hadron Founders Club", + url: "https://luma.com/HadronFC", + }, + schedule: { + repeatFrequency: "P1D", + byDay: WEEKDAYS, + scheduleTimezone: "Asia/Dubai", + }, + }, + + lagos: { + eventDescription: + "Open community coworking for Ethereum builders at Web3Bridge in Lagos.", + address: { + streetAddress: "25 Talabi Ademola Street, Abadek Avenue, Ogunlewe St", + addressLocality: "Igbogbo Ikorodu", + postalCode: "104102", + addressCountry: "NG", + }, + containedInPlace: { + name: "Web3Bridge", + url: "https://www.web3bridge.com/", + }, + schedule: { + repeatFrequency: "P1D", + byDay: WEEKDAYS, + scheduleTimezone: "Africa/Lagos", + }, + }, + + sf: { + eventDescription: + "Open community coworking for Ethereum builders at Frontier Tower in San Francisco.", + address: { + streetAddress: "995 Market St", + addressLocality: "San Francisco", + postalCode: "94103", + addressCountry: "US", + }, + containedInPlace: { + name: "Frontier Tower", + url: "https://frontiertower.io/", + }, + schedule: { + repeatFrequency: "P1D", + byDay: WEEKDAYS, + scheduleTimezone: "America/Los_Angeles", + }, + }, +} diff --git a/src/data/community-hubs.ts b/src/data/community-hubs.ts index 08a9fbeaf57..cffcfadbd87 100644 --- a/src/data/community-hubs.ts +++ b/src/data/community-hubs.ts @@ -28,7 +28,6 @@ const communityHubs: CommunityHub[] = [ coworkingSignupUrl: "https://forms.gle/bm78vRjZqvu45tsz5", meetupUrl: "https://luma.com/user/usr-ut3JGCXXuokkPdK", banner: HongKongHubBanner, - // TODO: Update brandColor: "bg-gradient-to-b from-[#A4FCF5]/5 to-[#A4FCF5]/10 dark:from-[#A4FCF5]/20 dark:to-[#A4FCF5]/10 border-[#A4FCF5]/20", },