diff --git a/frontend/src/app/organizations/[organizationKey]/layout.tsx b/frontend/src/app/organizations/[organizationKey]/layout.tsx index e466c8463b..bd595f7954 100644 --- a/frontend/src/app/organizations/[organizationKey]/layout.tsx +++ b/frontend/src/app/organizations/[organizationKey]/layout.tsx @@ -1,7 +1,11 @@ import { Metadata } from 'next' +import Script from 'next/script' import React from 'react' import { apolloClient } from 'server/apolloClient' -import { GET_ORGANIZATION_METADATA } from 'server/queries/organizationQueries' +import { + GET_ORGANIZATION_METADATA, + GET_ORGANIZATION_DATA, +} from 'server/queries/organizationQueries' import { generateSeoMetadata } from 'utils/metaconfig' export async function generateMetadata({ @@ -28,6 +32,85 @@ export async function generateMetadata({ : null } -export default function OrganizationDetailsLayout({ children }: { children: React.ReactNode }) { - return children +async function generateOrganizationStructuredData(organizationKey: string) { + // https://developers.google.com/search/docs/appearance/structured-data/organization#structured-data-type-definitions + + const { data } = await apolloClient.query({ + query: GET_ORGANIZATION_DATA, + variables: { + login: organizationKey, + }, + }) + + const organization = data?.organization + if (!organization) return null + + return { + '@context': 'https://schema.org' as const, + '@type': 'Organization' as const, + ...(organization.email && { + contactPoint: { + '@type': 'ContactPoint' as const, + email: organization.email, + }, + }), + description: organization.description, + email: organization.email, + foundingDate: organization.createdAt, + keywords: [ + organization.name, + organization.login, + 'cybersecurity', + 'open source', + 'OWASP', + ].filter(Boolean), + ...(organization.location && { + location: { + '@type': 'Place' as const, + name: organization.location, + }, + }), + ...(organization.avatarUrl && { + logo: { + '@type': 'ImageObject' as const, + url: organization.avatarUrl, + }, + }), + ...(organization.login.toLowerCase() !== 'owasp' && { + memberOf: { + '@type': 'Organization' as const, + name: 'OWASP Foundation', + url: 'https://owasp.org', + }, + }), + name: organization.name || organization.login, + sameAs: [organization.url].filter(Boolean), + url: `https://nest.owasp.org/organizations/${organizationKey}`, + } +} + +export default async function OrganizationDetailsLayout({ + children, + params, +}: Readonly<{ + children: Readonly + params: Promise<{ organizationKey: string }> +}>) { + const { organizationKey } = await params + const structuredData = await generateOrganizationStructuredData(organizationKey) + + return ( + <> + {structuredData && ( +