mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-07-30 17:59:13 -04:00
- Introduced `OgCard` React component as the default Open Graph preview card for social sharing (1200x630). - Added `BrandFonts` module to load web fonts for accurate rendering. - Integrated `OgCard` into the media creator pipeline as a `<Still>` with a new `still:og` script. - Updated upload script to version the output and ensure compatibility with caching policies. - Included new card meta tags across web pages, defaulting to this design for links without a custom preview. - Extended theme and README to document font usage, card details, and deployment workflow.
125 lines
5.5 KiB
JavaScript
125 lines
5.5 KiB
JavaScript
/**
|
|
* Downloads the home-page hero clips into public/videos/ at build time.
|
|
*
|
|
* The videos are ~1.8 MB each and are not committed (see the root .gitignore). They live in
|
|
* Cloudflare R2, but they are pulled in during the build rather than served from R2 at runtime:
|
|
*
|
|
* - Delivery stays same-origin on Vercel's CDN, so the hero costs no extra DNS lookup or TLS
|
|
* handshake, and there is no third-party dependency in the request path for a visitor.
|
|
* - R2 is hit roughly once per deploy instead of once per visitor, so the r2.dev public URL is
|
|
* fine here — its rate limit only matters for per-visitor serving.
|
|
* - No R2 custom domain is required, which matters because that needs the domain to be a zone in
|
|
* the same Cloudflare account, and compassmeet.com is on Vercel DNS.
|
|
*
|
|
* Any failure to obtain the assets fails the build. None of them are committed, so a "successful"
|
|
* build without them ships a home page whose hero is an empty frame — louder is better.
|
|
*
|
|
* Configure `MEDIA_SOURCE_BASE_URL` in the Vercel project (and locally if you want it) to the
|
|
* bucket's public base URL — e.g. https://pub-<hash>.r2.dev. Not NEXT_PUBLIC_: this is read by the
|
|
* build, never by the browser.
|
|
*
|
|
* Upload new renders with `npm run upload:media` from media-creator/.
|
|
*/
|
|
|
|
import {createWriteStream} from 'node:fs'
|
|
import {mkdir, readFile, stat} from 'node:fs/promises'
|
|
import {dirname, join} from 'node:path'
|
|
import {Readable} from 'node:stream'
|
|
import {pipeline} from 'node:stream/promises'
|
|
import {fileURLToPath} from 'node:url'
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
const PUBLIC_DIR = join(HERE, '../public')
|
|
|
|
/**
|
|
* Bucket key -> path under public/. The posters live here too: because the build copies everything
|
|
* into public/, they are still served same-origin off Vercel's CDN, so keeping them in R2 costs the
|
|
* LCP nothing and keeps another ~270 KB of binaries out of git.
|
|
*/
|
|
const ASSETS = [
|
|
{key: 'videos/search-demo-light.mp4', dest: 'videos/search-demo-light.mp4'},
|
|
{key: 'videos/search-demo-dark.mp4', dest: 'videos/search-demo-dark.mp4'},
|
|
{key: 'images/search-demo-poster-light.jpg', dest: 'images/search-demo-poster-light.jpg'},
|
|
{key: 'images/search-demo-poster-dark.jpg', dest: 'images/search-demo-poster-dark.jpg'},
|
|
{key: 'videos/search-alert-light.mp4', dest: 'videos/search-alert-light.mp4'},
|
|
{key: 'videos/search-alert-dark.mp4', dest: 'videos/search-alert-dark.mp4'},
|
|
{key: 'images/search-alert-poster-light.jpg', dest: 'images/search-alert-poster-light.jpg'},
|
|
{key: 'images/search-alert-poster-dark.jpg', dest: 'images/search-alert-poster-dark.jpg'},
|
|
{key: 'images/vote-tally-light.png', dest: 'images/vote-tally-light.png'},
|
|
{key: 'images/vote-tally-dark.png', dest: 'images/vote-tally-dark.png'},
|
|
// The default social preview card (see OG_CARD in common/hosting/constants.ts). The filename is
|
|
// versioned because WhatsApp and X cache a preview by URL and never revalidate it — a redesign
|
|
// ships as -v2 on both sides, it never overwrites -v1.
|
|
{key: 'images/og-card-v1.jpg', dest: 'images/og-card-v1.jpg'},
|
|
]
|
|
|
|
/**
|
|
* On Vercel this comes from the project env. Locally it does not: this script runs in its own
|
|
* process before `next build`, so Next's own .env loading has not happened yet. Read the two .env
|
|
* files directly — just this one key, to avoid pulling unrelated secrets into the build process.
|
|
*/
|
|
async function resolveBase() {
|
|
if (process.env.MEDIA_SOURCE_BASE_URL) return process.env.MEDIA_SOURCE_BASE_URL
|
|
for (const envPath of [join(HERE, '../.env'), join(HERE, '../../.env')]) {
|
|
try {
|
|
const match = (await readFile(envPath, 'utf8')).match(/^MEDIA_SOURCE_BASE_URL=(.+)$/m)
|
|
if (match) return match[1].trim().replace(/^["']|["']$/g, '')
|
|
} catch {
|
|
// no .env there; try the next one
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
|
|
const exists = async (p) => {
|
|
try {
|
|
return (await stat(p)).size > 0
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
async function download({key, dest}, base) {
|
|
const url = `${base}/${key}`
|
|
const target = join(PUBLIC_DIR, dest)
|
|
await mkdir(dirname(target), {recursive: true})
|
|
|
|
const res = await fetch(url)
|
|
if (!res.ok || !res.body) {
|
|
throw new Error(`${res.status} ${res.statusText} for ${url}`)
|
|
}
|
|
await pipeline(Readable.fromWeb(res.body), createWriteStream(target))
|
|
const {size} = await stat(target)
|
|
console.log(`[media] ${dest} (${(size / 1024).toFixed(0)} KB)`)
|
|
}
|
|
|
|
async function main() {
|
|
const base = (await resolveBase()).replace(/\/$/, '')
|
|
|
|
if (!base) {
|
|
// No source configured: only acceptable if a local render already put the files in place.
|
|
const present = await Promise.all(ASSETS.map((a) => exists(join(PUBLIC_DIR, a.dest))))
|
|
if (present.every(Boolean)) {
|
|
console.log('[media] MEDIA_SOURCE_BASE_URL unset; using the local files already in public/')
|
|
return
|
|
}
|
|
const missing = ASSETS.filter((_, i) => !present[i]).map((a) => a.dest)
|
|
throw new Error(
|
|
`MEDIA_SOURCE_BASE_URL is unset and these are missing from public/:\n` +
|
|
missing.map((m) => ` - ${m}`).join('\n') +
|
|
`\nNothing about the home-page hero is committed, so this build would ship an empty frame. ` +
|
|
`Set MEDIA_SOURCE_BASE_URL to the bucket's public base URL (it is not a secret), or render ` +
|
|
`locally with media-creator.`,
|
|
)
|
|
}
|
|
|
|
// Configured but unreachable is a real misconfiguration, and shipping a hero that 404s is worse
|
|
// than failing the deploy.
|
|
for (const asset of ASSETS) await download(asset, base)
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(`[media] ${err.message}`)
|
|
process.exit(1)
|
|
})
|