Add rich Open Graph / Twitter share metadata + canonical share link to event page
- Per-event og:title/description/image/url + twitter:summary_large_image via useSeoMeta - Share description composes date + location + blurb; falls back to site card image - handleShare now uses the canonical /#/event/... URL, richer text, and toast feedback on clipboard copy Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
87c81a63c9
commit
29d09eb6ec
1 changed files with 66 additions and 7 deletions
|
|
@ -39,6 +39,7 @@ import {
|
||||||
Megaphone,
|
Megaphone,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||||
|
import { useToast } from '@/hooks/useToast';
|
||||||
|
|
||||||
function formatTime(timestamp: number): string {
|
function formatTime(timestamp: number): string {
|
||||||
const date = new Date(timestamp * 1000);
|
const date = new Date(timestamp * 1000);
|
||||||
|
|
@ -80,12 +81,53 @@ export default function EventDetailPage() {
|
||||||
const { data: event, isLoading, error } = useSkateEvent(identifier || '', pubkey);
|
const { data: event, isLoading, error } = useSkateEvent(identifier || '', pubkey);
|
||||||
const author = useAuthor(event?.pubkey);
|
const author = useAuthor(event?.pubkey);
|
||||||
const { user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
const isOwner = user && event && user.pubkey === event.pubkey;
|
const isOwner = user && event && user.pubkey === event.pubkey;
|
||||||
|
|
||||||
|
// Canonical, shareable URL for this event (HashRouter → keep the /#/ prefix).
|
||||||
|
const shareUrl = event
|
||||||
|
? `https://skate-spots.org/#/event/${event.pubkey}/${encodeURIComponent(event.identifier)}`
|
||||||
|
: 'https://skate-spots.org';
|
||||||
|
|
||||||
|
// og:image must be an absolute URL. Fall back to the site social card when the
|
||||||
|
// event has no cover so shared links never render blank.
|
||||||
|
const ogImage = event?.image || 'https://skate-spots.org/images/icon-1024.png';
|
||||||
|
|
||||||
|
// Compose a punchy share description: date + location, then the blurb.
|
||||||
|
const shareDescription = (() => {
|
||||||
|
if (!event) return 'View this skateboarding event on SkateSpots.';
|
||||||
|
const bits: string[] = [];
|
||||||
|
if (event.start) {
|
||||||
|
bits.push(
|
||||||
|
new Date(event.start * 1000).toLocaleDateString(undefined, {
|
||||||
|
weekday: 'short',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (event.location) bits.push(`📍 ${event.location}`);
|
||||||
|
const prefix = bits.length ? `${bits.join(' · ')} — ` : '';
|
||||||
|
const body = event.description?.trim() || 'A skateboarding event on SkateSpots.';
|
||||||
|
return `${prefix}${body}`.slice(0, 200);
|
||||||
|
})();
|
||||||
|
|
||||||
|
const shareTitle = event ? `${event.title} · SkateSpots` : 'Skate Event · SkateSpots';
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: event ? `${event.title} - SkateSpots` : 'Skate Event - SkateSpots',
|
title: shareTitle,
|
||||||
description: event?.description || 'View this skateboarding event on SkateSpots.',
|
description: shareDescription,
|
||||||
|
ogTitle: shareTitle,
|
||||||
|
ogDescription: shareDescription,
|
||||||
|
ogType: 'article',
|
||||||
|
ogUrl: shareUrl,
|
||||||
|
ogImage,
|
||||||
|
ogSiteName: 'SkateSpots',
|
||||||
|
twitterCard: 'summary_large_image',
|
||||||
|
twitterTitle: shareTitle,
|
||||||
|
twitterDescription: shareDescription,
|
||||||
|
twitterImage: ogImage,
|
||||||
});
|
});
|
||||||
|
|
||||||
const metadata = author.data?.metadata;
|
const metadata = author.data?.metadata;
|
||||||
|
|
@ -132,18 +174,35 @@ export default function EventDetailPage() {
|
||||||
}, [event]);
|
}, [event]);
|
||||||
|
|
||||||
const handleShare = async () => {
|
const handleShare = async () => {
|
||||||
|
const shareText = event
|
||||||
|
? `${event.title} — a skate event on SkateSpots`
|
||||||
|
: 'Check out this skate event on SkateSpots';
|
||||||
|
|
||||||
if (navigator.share) {
|
if (navigator.share) {
|
||||||
try {
|
try {
|
||||||
await navigator.share({
|
await navigator.share({
|
||||||
title: event?.title,
|
title: event?.title,
|
||||||
text: event?.description || `Check out this skate event: ${event?.title}`,
|
text: shareText,
|
||||||
url: window.location.href,
|
url: shareUrl,
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
} catch {
|
} catch {
|
||||||
// User cancelled or share failed
|
// User cancelled or share failed — fall through to clipboard.
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
navigator.clipboard.writeText(window.location.href);
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(shareUrl);
|
||||||
|
toast({
|
||||||
|
title: 'Link copied',
|
||||||
|
description: 'Event link copied to your clipboard — paste it anywhere.',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
toast({
|
||||||
|
title: 'Copy failed',
|
||||||
|
description: shareUrl,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue