NGU-Web/src/lib/embeds.ts
Zaldimmar 6a69084de2 Replace the front page with an admin-driven home
The home page is rebuilt from scratch and configured from a new
Front page tab in the admin, backed by migration 017 and served by
GET /api/front-page.

Hero: brand, photos (crossfading slideshow with progress and pause)
or livestream (YouTube/Facebook/Vimeo embed with a LIVE badge),
switched by hand. After it, bands the admin can reorder, retitle or
hide: a countdown to the next event (series-aware), the National
Retreats carousel, a numbers band (typed in or counted from the
database), a horizontal rail of featured timeline entries, and a
"Find your way in" pathfinder replacing the old connect section.

The CRUD engine gains a `singleton` flag: the entity has one row,
made by its migration, and create and delete are refused. The list
screen opens that row and the editor drops the slug, back link and
delete. shapeSeries moves to shape.js so /front-page can share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:37:42 -05:00

62 lines
2.4 KiB
TypeScript

/* ═══════════════════════════════════════════════════════════════
EMBEDS
Turns the link an admin pastes into the URL an <iframe> can
load. People paste the page they're looking at — a YouTube watch
or live link, a Facebook video, a Vimeo page — not the embed
form, so those are recognised and rewritten. Anything else that
is https is assumed to already be an embed URL and passed
through; anything that isn't https is refused, so the hero never
frames a plain-http or javascript: URL.
Streams start muted: browsers only autoplay muted video, and a
hero that starts shouting is worse than one that asks.
═══════════════════════════════════════════════════════════════ */
function youtubeId(url: URL): string | null {
const host = url.hostname.replace(/^www\.|^m\./, '')
if (host === 'youtu.be') return url.pathname.slice(1) || null
if (host !== 'youtube.com' && host !== 'youtube-nocookie.com') return null
const v = url.searchParams.get('v')
if (v) return v
// /live/ID, /embed/ID, /shorts/ID
const match = url.pathname.match(/^\/(?:live|embed|shorts)\/([\w-]+)/)
return match?.[1] ?? null
}
/** An iframe src for a pasted stream link, or null if it can't be framed. */
export function livestreamEmbedUrl(raw?: string | null): string | null {
if (!raw) return null
let url: URL
try {
url = new URL(raw.trim())
} catch {
return null
}
if (url.protocol !== 'https:') return null
const yt = youtubeId(url)
if (yt) {
return `https://www.youtube-nocookie.com/embed/${encodeURIComponent(yt)}?autoplay=1&mute=1&playsinline=1`
}
const host = url.hostname.replace(/^www\./, '')
if (host === 'vimeo.com') {
const id = url.pathname.match(/^\/(?:event\/)?(\d+)/)?.[1]
if (id) {
return url.pathname.startsWith('/event/')
? `https://vimeo.com/event/${id}/embed?autoplay=1&muted=1`
: `https://player.vimeo.com/video/${id}?autoplay=1&muted=1`
}
}
if ((host === 'facebook.com' || host === 'fb.watch') && !url.pathname.startsWith('/plugins/')) {
return `https://www.facebook.com/plugins/video.php?href=${encodeURIComponent(url.href)}&autoplay=1&mute=1`
}
return url.href
}