mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 09:51:13 -05:00
feat: Implement dynamic OG images
This commit is contained in:
78
src/hooks/useOpenGraph.ts
Normal file
78
src/hooks/useOpenGraph.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import { Skeleton } from '@/components/ui/skeleton';
|
|||||||
import { Header } from '@/components/layout/Header';
|
import { Header } from '@/components/layout/Header';
|
||||||
import { Footer } from '@/components/layout/Footer';
|
import { Footer } from '@/components/layout/Footer';
|
||||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||||
|
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
||||||
|
|
||||||
export default function BlogPost() {
|
export default function BlogPost() {
|
||||||
const { slug } = useParams<{ slug: string }>();
|
const { slug } = useParams<{ slug: string }>();
|
||||||
@@ -36,6 +37,16 @@ export default function BlogPost() {
|
|||||||
// Update document title when post changes
|
// Update document title when post changes
|
||||||
useDocumentTitle(post?.title || 'Blog Post');
|
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(() => {
|
useEffect(() => {
|
||||||
if (slug) {
|
if (slug) {
|
||||||
supabase.rpc('increment_blog_view_count', { post_slug: slug });
|
supabase.rpc('increment_blog_view_count', { post_slug: slug });
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { EntityHistoryTabs } from '@/components/history/EntityHistoryTabs';
|
|||||||
import { trackPageView } from '@/lib/viewTracking';
|
import { trackPageView } from '@/lib/viewTracking';
|
||||||
import { useAuthModal } from '@/hooks/useAuthModal';
|
import { useAuthModal } from '@/hooks/useAuthModal';
|
||||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||||
|
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
||||||
|
|
||||||
export default function DesignerDetail() {
|
export default function DesignerDetail() {
|
||||||
const { slug } = useParams<{ slug: string }>();
|
const { slug } = useParams<{ slug: string }>();
|
||||||
@@ -42,6 +43,16 @@ export default function DesignerDetail() {
|
|||||||
// Update document title when designer changes
|
// Update document title when designer changes
|
||||||
useDocumentTitle(designer?.name || 'Designer Details');
|
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(() => {
|
useEffect(() => {
|
||||||
if (slug) {
|
if (slug) {
|
||||||
fetchDesignerData();
|
fetchDesignerData();
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { VersionIndicator } from '@/components/versioning/VersionIndicator';
|
|||||||
import { EntityHistoryTabs } from '@/components/history/EntityHistoryTabs';
|
import { EntityHistoryTabs } from '@/components/history/EntityHistoryTabs';
|
||||||
import { useAuthModal } from '@/hooks/useAuthModal';
|
import { useAuthModal } from '@/hooks/useAuthModal';
|
||||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||||
|
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
||||||
|
|
||||||
export default function ManufacturerDetail() {
|
export default function ManufacturerDetail() {
|
||||||
const { slug } = useParams<{ slug: string }>();
|
const { slug } = useParams<{ slug: string }>();
|
||||||
@@ -43,6 +44,16 @@ export default function ManufacturerDetail() {
|
|||||||
// Update document title when manufacturer changes
|
// Update document title when manufacturer changes
|
||||||
useDocumentTitle(manufacturer?.name || 'Manufacturer Details');
|
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(() => {
|
useEffect(() => {
|
||||||
if (slug) {
|
if (slug) {
|
||||||
fetchManufacturerData();
|
fetchManufacturerData();
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { VersionIndicator } from '@/components/versioning/VersionIndicator';
|
|||||||
import { EntityHistoryTabs } from '@/components/history/EntityHistoryTabs';
|
import { EntityHistoryTabs } from '@/components/history/EntityHistoryTabs';
|
||||||
import { useAuthModal } from '@/hooks/useAuthModal';
|
import { useAuthModal } from '@/hooks/useAuthModal';
|
||||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||||
|
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
||||||
|
|
||||||
export default function OperatorDetail() {
|
export default function OperatorDetail() {
|
||||||
const { slug } = useParams<{ slug: string }>();
|
const { slug } = useParams<{ slug: string }>();
|
||||||
@@ -46,6 +47,16 @@ export default function OperatorDetail() {
|
|||||||
// Update document title when operator changes
|
// Update document title when operator changes
|
||||||
useDocumentTitle(operator?.name || 'Operator Details');
|
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(() => {
|
useEffect(() => {
|
||||||
if (slug) {
|
if (slug) {
|
||||||
fetchOperatorData();
|
fetchOperatorData();
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { VersionIndicator } from '@/components/versioning/VersionIndicator';
|
|||||||
import { EntityHistoryTabs } from '@/components/history/EntityHistoryTabs';
|
import { EntityHistoryTabs } from '@/components/history/EntityHistoryTabs';
|
||||||
import { useAuthModal } from '@/hooks/useAuthModal';
|
import { useAuthModal } from '@/hooks/useAuthModal';
|
||||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||||
|
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
||||||
|
|
||||||
export default function ParkDetail() {
|
export default function ParkDetail() {
|
||||||
const {
|
const {
|
||||||
@@ -52,6 +53,16 @@ export default function ParkDetail() {
|
|||||||
// Update document title when park changes
|
// Update document title when park changes
|
||||||
useDocumentTitle(park?.name || 'Park Details');
|
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) => {
|
const fetchPhotoCount = useCallback(async (parkId: string) => {
|
||||||
try {
|
try {
|
||||||
const { count, error } = await supabase
|
const { count, error } = await supabase
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { VersionIndicator } from '@/components/versioning/VersionIndicator';
|
|||||||
import { EntityHistoryTabs } from '@/components/history/EntityHistoryTabs';
|
import { EntityHistoryTabs } from '@/components/history/EntityHistoryTabs';
|
||||||
import { useAuthModal } from '@/hooks/useAuthModal';
|
import { useAuthModal } from '@/hooks/useAuthModal';
|
||||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||||
|
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
||||||
|
|
||||||
export default function PropertyOwnerDetail() {
|
export default function PropertyOwnerDetail() {
|
||||||
const { slug } = useParams<{ slug: string }>();
|
const { slug } = useParams<{ slug: string }>();
|
||||||
@@ -46,6 +47,16 @@ export default function PropertyOwnerDetail() {
|
|||||||
// Update document title when owner changes
|
// Update document title when owner changes
|
||||||
useDocumentTitle(owner?.name || 'Property Owner Details');
|
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(() => {
|
useEffect(() => {
|
||||||
if (slug) {
|
if (slug) {
|
||||||
fetchOwnerData();
|
fetchOwnerData();
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import { VersionIndicator } from '@/components/versioning/VersionIndicator';
|
|||||||
import { EntityHistoryTabs } from '@/components/history/EntityHistoryTabs';
|
import { EntityHistoryTabs } from '@/components/history/EntityHistoryTabs';
|
||||||
import { useAuthModal } from '@/hooks/useAuthModal';
|
import { useAuthModal } from '@/hooks/useAuthModal';
|
||||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||||
|
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
||||||
|
|
||||||
// Extended Ride type with additional properties for easier access
|
// Extended Ride type with additional properties for easier access
|
||||||
interface RideWithParkId extends Ride {
|
interface RideWithParkId extends Ride {
|
||||||
@@ -75,6 +76,16 @@ export default function RideDetail() {
|
|||||||
// Update document title when ride changes
|
// Update document title when ride changes
|
||||||
useDocumentTitle(ride?.name || 'Ride Details');
|
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(() => {
|
useEffect(() => {
|
||||||
if (parkSlug && rideSlug) {
|
if (parkSlug && rideSlug) {
|
||||||
fetchRideData();
|
fetchRideData();
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const RideModelForm = lazy(() => import('@/components/admin/RideModelForm').then
|
|||||||
import { VersionIndicator } from '@/components/versioning/VersionIndicator';
|
import { VersionIndicator } from '@/components/versioning/VersionIndicator';
|
||||||
import { EntityVersionHistory } from '@/components/versioning/EntityVersionHistory';
|
import { EntityVersionHistory } from '@/components/versioning/EntityVersionHistory';
|
||||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||||
|
import { useOpenGraph } from '@/hooks/useOpenGraph';
|
||||||
|
|
||||||
export default function RideModelDetail() {
|
export default function RideModelDetail() {
|
||||||
const { manufacturerSlug, modelSlug } = useParams<{ manufacturerSlug: string; modelSlug: string }>();
|
const { manufacturerSlug, modelSlug } = useParams<{ manufacturerSlug: string; modelSlug: string }>();
|
||||||
@@ -37,6 +38,16 @@ export default function RideModelDetail() {
|
|||||||
|
|
||||||
// Update document title when model changes
|
// Update document title when model changes
|
||||||
useDocumentTitle(model?.name || 'Ride Model Details');
|
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 });
|
const [statistics, setStatistics] = useState({ rideCount: 0, photoCount: 0 });
|
||||||
|
|
||||||
// Fetch technical specifications from relational table
|
// Fetch technical specifications from relational table
|
||||||
|
|||||||
Reference in New Issue
Block a user