diff --git a/src/hooks/useOpenGraph.ts b/src/hooks/useOpenGraph.ts new file mode 100644 index 00000000..5699c25c --- /dev/null +++ b/src/hooks/useOpenGraph.ts @@ -0,0 +1,78 @@ +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; +import { getBannerUrls } from '@/lib/cloudflareImageUtils'; + +interface OpenGraphOptions { + title: string; + description?: string; + imageUrl?: string; + imageId?: string; + type?: 'website' | 'article' | 'profile'; + enabled?: boolean; +} + +export function useOpenGraph({ + title, + description, + imageUrl, + imageId, + type = 'website', + enabled = true +}: OpenGraphOptions) { + const location = useLocation(); + const currentUrl = window.location.origin + location.pathname; + + useEffect(() => { + if (!enabled || !title) return; + + // Determine the image to use + let finalImageUrl = '/og-image.png'; + + if (imageId) { + const bannerUrls = getBannerUrls(imageId); + finalImageUrl = bannerUrls.desktop || imageUrl || '/og-image.png'; + } else if (imageUrl) { + finalImageUrl = imageUrl; + } + + // Convert relative URL to absolute for social media + if (finalImageUrl.startsWith('/')) { + finalImageUrl = window.location.origin + finalImageUrl; + } + + // Update or create meta tags + updateMetaTag('og:title', title); + updateMetaTag('og:description', description || 'Explore theme parks and roller coasters worldwide with ThrillWiki'); + updateMetaTag('og:image', finalImageUrl); + updateMetaTag('og:type', type); + updateMetaTag('og:url', currentUrl); + + // Twitter tags + updateMetaTag('twitter:title', title, 'name'); + updateMetaTag('twitter:description', description || 'Explore theme parks and roller coasters worldwide with ThrillWiki', 'name'); + updateMetaTag('twitter:image', finalImageUrl, 'name'); + + return () => { + updateMetaTag('og:title', 'ThrillWiki - Theme Park & Roller Coaster Database'); + updateMetaTag('og:description', 'Explore theme parks and roller coasters worldwide with ThrillWiki - the comprehensive database for enthusiasts'); + updateMetaTag('og:image', window.location.origin + '/og-image.png'); + updateMetaTag('og:type', 'website'); + + updateMetaTag('twitter:title', 'ThrillWiki - Theme Park & Roller Coaster Database', 'name'); + updateMetaTag('twitter:description', 'Explore theme parks and roller coasters worldwide with ThrillWiki - the comprehensive database for enthusiasts', 'name'); + updateMetaTag('twitter:image', window.location.origin + '/og-image.png', 'name'); + }; + }, [title, description, imageUrl, imageId, type, currentUrl, enabled]); +} + +function updateMetaTag(property: string, content: string, attributeName: 'property' | 'name' = 'property') { + let meta = document.querySelector(`meta[${attributeName}="${property}"]`); + + if (!meta) { + meta = document.createElement('meta'); + meta.setAttribute(attributeName, property); + document.head.appendChild(meta); + } + + meta.setAttribute('content', content); +} diff --git a/src/pages/BlogPost.tsx b/src/pages/BlogPost.tsx index f2a9a1d3..bcf05d1c 100644 --- a/src/pages/BlogPost.tsx +++ b/src/pages/BlogPost.tsx @@ -12,6 +12,7 @@ import { Skeleton } from '@/components/ui/skeleton'; import { Header } from '@/components/layout/Header'; import { Footer } from '@/components/layout/Footer'; import { useDocumentTitle } from '@/hooks/useDocumentTitle'; +import { useOpenGraph } from '@/hooks/useOpenGraph'; export default function BlogPost() { const { slug } = useParams<{ slug: string }>(); @@ -35,6 +36,16 @@ export default function BlogPost() { // Update document title when post changes useDocumentTitle(post?.title || 'Blog Post'); + + // Update Open Graph meta tags + useOpenGraph({ + title: post?.title || '', + description: post?.content?.substring(0, 160), + imageUrl: post?.featured_image_url, + imageId: post?.featured_image_id, + type: 'article', + enabled: !!post + }); useEffect(() => { if (slug) { diff --git a/src/pages/DesignerDetail.tsx b/src/pages/DesignerDetail.tsx index bd09075a..e8218551 100644 --- a/src/pages/DesignerDetail.tsx +++ b/src/pages/DesignerDetail.tsx @@ -25,6 +25,7 @@ import { EntityHistoryTabs } from '@/components/history/EntityHistoryTabs'; import { trackPageView } from '@/lib/viewTracking'; import { useAuthModal } from '@/hooks/useAuthModal'; import { useDocumentTitle } from '@/hooks/useDocumentTitle'; +import { useOpenGraph } from '@/hooks/useOpenGraph'; export default function DesignerDetail() { const { slug } = useParams<{ slug: string }>(); @@ -41,6 +42,16 @@ export default function DesignerDetail() { // Update document title when designer changes useDocumentTitle(designer?.name || 'Designer Details'); + + // Update Open Graph meta tags + useOpenGraph({ + title: designer?.name || '', + description: designer?.description || (designer ? `${designer.name} - Ride Designer${designer.headquarters_location ? ` based in ${designer.headquarters_location}` : ''}` : ''), + imageUrl: designer?.banner_image_url, + imageId: designer?.banner_image_id, + type: 'profile', + enabled: !!designer + }); useEffect(() => { if (slug) { diff --git a/src/pages/ManufacturerDetail.tsx b/src/pages/ManufacturerDetail.tsx index 90d45836..39257d2e 100644 --- a/src/pages/ManufacturerDetail.tsx +++ b/src/pages/ManufacturerDetail.tsx @@ -25,6 +25,7 @@ import { VersionIndicator } from '@/components/versioning/VersionIndicator'; import { EntityHistoryTabs } from '@/components/history/EntityHistoryTabs'; import { useAuthModal } from '@/hooks/useAuthModal'; import { useDocumentTitle } from '@/hooks/useDocumentTitle'; +import { useOpenGraph } from '@/hooks/useOpenGraph'; export default function ManufacturerDetail() { const { slug } = useParams<{ slug: string }>(); @@ -42,6 +43,16 @@ export default function ManufacturerDetail() { // Update document title when manufacturer changes useDocumentTitle(manufacturer?.name || 'Manufacturer Details'); + + // Update Open Graph meta tags + useOpenGraph({ + title: manufacturer?.name || '', + description: manufacturer?.description || (manufacturer ? `${manufacturer.name} - Ride Manufacturer${manufacturer.headquarters_location ? ` based in ${manufacturer.headquarters_location}` : ''}` : ''), + imageUrl: manufacturer?.banner_image_url, + imageId: manufacturer?.banner_image_id, + type: 'profile', + enabled: !!manufacturer + }); useEffect(() => { if (slug) { diff --git a/src/pages/OperatorDetail.tsx b/src/pages/OperatorDetail.tsx index 9ad211dd..fb78cbc1 100644 --- a/src/pages/OperatorDetail.tsx +++ b/src/pages/OperatorDetail.tsx @@ -26,6 +26,7 @@ import { VersionIndicator } from '@/components/versioning/VersionIndicator'; import { EntityHistoryTabs } from '@/components/history/EntityHistoryTabs'; import { useAuthModal } from '@/hooks/useAuthModal'; import { useDocumentTitle } from '@/hooks/useDocumentTitle'; +import { useOpenGraph } from '@/hooks/useOpenGraph'; export default function OperatorDetail() { const { slug } = useParams<{ slug: string }>(); @@ -45,6 +46,16 @@ export default function OperatorDetail() { // Update document title when operator changes useDocumentTitle(operator?.name || 'Operator Details'); + + // Update Open Graph meta tags + useOpenGraph({ + title: operator?.name || '', + description: operator?.description || (operator ? `${operator.name} - Park Operator${operator.headquarters_location ? ` based in ${operator.headquarters_location}` : ''}` : ''), + imageUrl: operator?.banner_image_url, + imageId: operator?.banner_image_id, + type: 'profile', + enabled: !!operator + }); useEffect(() => { if (slug) { diff --git a/src/pages/ParkDetail.tsx b/src/pages/ParkDetail.tsx index c0ee50b9..7590a1d0 100644 --- a/src/pages/ParkDetail.tsx +++ b/src/pages/ParkDetail.tsx @@ -30,6 +30,7 @@ import { VersionIndicator } from '@/components/versioning/VersionIndicator'; import { EntityHistoryTabs } from '@/components/history/EntityHistoryTabs'; import { useAuthModal } from '@/hooks/useAuthModal'; import { useDocumentTitle } from '@/hooks/useDocumentTitle'; +import { useOpenGraph } from '@/hooks/useOpenGraph'; export default function ParkDetail() { const { @@ -52,6 +53,16 @@ export default function ParkDetail() { // Update document title when park changes useDocumentTitle(park?.name || 'Park Details'); + // Update Open Graph meta tags + useOpenGraph({ + title: park?.name || '', + description: park?.description || (park ? `${park.name} - A theme park${park.location ? ` in ${park.location.city}, ${park.location.country}` : ''}` : ''), + imageUrl: park?.banner_image_url, + imageId: park?.banner_image_id, + type: 'website', + enabled: !!park + }); + const fetchPhotoCount = useCallback(async (parkId: string) => { try { const { count, error } = await supabase diff --git a/src/pages/PropertyOwnerDetail.tsx b/src/pages/PropertyOwnerDetail.tsx index b12a891e..e5994716 100644 --- a/src/pages/PropertyOwnerDetail.tsx +++ b/src/pages/PropertyOwnerDetail.tsx @@ -26,6 +26,7 @@ import { VersionIndicator } from '@/components/versioning/VersionIndicator'; import { EntityHistoryTabs } from '@/components/history/EntityHistoryTabs'; import { useAuthModal } from '@/hooks/useAuthModal'; import { useDocumentTitle } from '@/hooks/useDocumentTitle'; +import { useOpenGraph } from '@/hooks/useOpenGraph'; export default function PropertyOwnerDetail() { const { slug } = useParams<{ slug: string }>(); @@ -45,6 +46,16 @@ export default function PropertyOwnerDetail() { // Update document title when owner changes useDocumentTitle(owner?.name || 'Property Owner Details'); + + // Update Open Graph meta tags + useOpenGraph({ + title: owner?.name || '', + description: owner?.description || (owner ? `${owner.name} - Property Owner${owner.headquarters_location ? ` based in ${owner.headquarters_location}` : ''}` : ''), + imageUrl: owner?.banner_image_url, + imageId: owner?.banner_image_id, + type: 'profile', + enabled: !!owner + }); useEffect(() => { if (slug) { diff --git a/src/pages/RideDetail.tsx b/src/pages/RideDetail.tsx index 35bf99be..012f4720 100644 --- a/src/pages/RideDetail.tsx +++ b/src/pages/RideDetail.tsx @@ -53,6 +53,7 @@ import { VersionIndicator } from '@/components/versioning/VersionIndicator'; import { EntityHistoryTabs } from '@/components/history/EntityHistoryTabs'; import { useAuthModal } from '@/hooks/useAuthModal'; import { useDocumentTitle } from '@/hooks/useDocumentTitle'; +import { useOpenGraph } from '@/hooks/useOpenGraph'; // Extended Ride type with additional properties for easier access interface RideWithParkId extends Ride { @@ -74,6 +75,16 @@ export default function RideDetail() { // Update document title when ride changes useDocumentTitle(ride?.name || 'Ride Details'); + + // Update Open Graph meta tags + useOpenGraph({ + title: ride?.name ? `${ride.name}${ride.park?.name ? ` at ${ride.park.name}` : ''}` : '', + description: ride?.description || (ride ? `${ride.name} - A thrilling ride${ride.park?.name ? ` at ${ride.park.name}` : ''}` : ''), + imageUrl: ride?.banner_image_url, + imageId: ride?.banner_image_id, + type: 'website', + enabled: !!ride + }); useEffect(() => { if (parkSlug && rideSlug) { diff --git a/src/pages/RideModelDetail.tsx b/src/pages/RideModelDetail.tsx index a9ea51dd..fb685389 100644 --- a/src/pages/RideModelDetail.tsx +++ b/src/pages/RideModelDetail.tsx @@ -23,6 +23,7 @@ const RideModelForm = lazy(() => import('@/components/admin/RideModelForm').then import { VersionIndicator } from '@/components/versioning/VersionIndicator'; import { EntityVersionHistory } from '@/components/versioning/EntityVersionHistory'; import { useDocumentTitle } from '@/hooks/useDocumentTitle'; +import { useOpenGraph } from '@/hooks/useOpenGraph'; export default function RideModelDetail() { const { manufacturerSlug, modelSlug } = useParams<{ manufacturerSlug: string; modelSlug: string }>(); @@ -37,6 +38,16 @@ export default function RideModelDetail() { // Update document title when model changes useDocumentTitle(model?.name || 'Ride Model Details'); + + // Update Open Graph meta tags + useOpenGraph({ + title: model?.name ? `${model.name}${manufacturer?.name ? ` by ${manufacturer.name}` : ''}` : '', + description: model?.description || (model ? `${model.name} - A ride model${manufacturer?.name ? ` by ${manufacturer.name}` : ''}` : ''), + imageUrl: model?.banner_image_url, + imageId: model?.banner_image_id, + type: 'website', + enabled: !!model + }); const [statistics, setStatistics] = useState({ rideCount: 0, photoCount: 0 }); // Fetch technical specifications from relational table