teqo-backup/src/utilities/ogImageReads.ts
Francisco Solla 75e5825b05 refactor(S27-FOLLOWUP-DRY): dono único do OG image de fallback do frontend público (Closes #1276)
O fallback de imagem OG (própria → global `metadata` → null, sempre absoluto
no deployment origin) deixa de estar copiado em 6–7 páginas públicas e ganha
o dono server-only `src/utilities/ogImageReads.ts` (`resolveOgImage(configured)
→ { url, media }`), com `resolveShareLinkOgImageUrl` delegando (sem twin) e os
8 consumidores migrados — 2 do S27, jingles, corte, abaixo-assinado ×2, artigo
e layout. `seo.ts` segue puro; sem migration, sem Consent/access, Impeccable A.

- unit novo do resolver (8 casos) + pin do módulo em codebaseConventions; int
  do shareLink verde sem tocar asserção; e2e frontendConteudos/frontendJingles/
  campaignSpeechCut/frontendShareLink/frontend verdes, com asserts novos de
  og:image (helper `metaContent` extraído do spec do share-link)
- dono novo entra no E2E_AFFECTED_MANIFEST nas 4 entradas de OG públicas
- deltas intencionais registrados: deployment origin no lugar do canônico em
  corte/abaixo-assinado/artigo e OG absoluto no layout
2026-09-23 14:30:40 -03:00

76 lines
3.3 KiB
TypeScript

import 'server-only'
import type { Media } from '@/payload-types'
import { getCachedDocumentById } from '@/utilities/documentReads'
import { getCachedGlobal } from '@/utilities/globalReads'
import { resolveDeploymentOrigin, resolveSiteMetadata, toAbsoluteUrl } from '@/utilities/seo'
/**
* A page's own OG image: an upload (`Media`), a bare upload id (depth-0 reads
* and poisoned cache entries) or an already-resolved URL string.
*/
export type OgImageSource = Media | number | string | null | undefined
export type ResolvedOgImage = {
/**
* Absolute URL — app paths resolved on the deployment origin; an
* already-absolute URL (e.g. a YouTube thumbnail) passes through. `null`
* when nothing resolved.
*/
url: string | null
/** The chosen `Media` when the source is an upload; `null` for URL sources. */
media: Media | null
}
const mediaOf = (value: OgImageSource): Media | null =>
typeof value === 'object' && value !== null ? value : null
const resolveMediaSource = async (value: OgImageSource): Promise<Media | null> =>
typeof value === 'number' ? getCachedDocumentById('media', String(value))() : mediaOf(value)
/**
* Absolute URL on the deployment origin. An already-absolute URL (e.g. a
* YouTube thumbnail) passes through; a relative path needs an origin — never a
* relative URL the WhatsApp crawler cannot resolve.
*/
const absolutize = (url: string | null | undefined, origin: string | null): string | null => {
const trimmed = url?.trim()
if (!trimmed) return null
if (origin) return toAbsoluteUrl(trimmed, origin)
return /^https?:\/\//i.test(trimmed) ? trimmed : null
}
/**
* Single owner of the public frontend's OG image fallback: the page's own
* image when it has a usable URL, otherwise the site default image of the
* `metadata` global (resolving the id when the read came back at depth 0).
*
* Media lives behind this app's `/api/media/file/…` proxy, so asset URLs are
* built on the deployment origin (`NEXT_PUBLIC_SITE_URL`), never on the
* `metadata` global `URL`: the global can be a canonical domain served by
* another platform (today the legacy WordPress), which 404s the proxy path and
* makes WhatsApp drop the thumbnail. The global `siteUrl` is the fallback when
* the env is missing.
*
* `media` is the resolved upload when the chosen source is one, so call sites
* that emit width/height/alt/mimeType keep the full document; URL-only sources
* return `media: null`. Canonical and JSON-LD `url` are NOT this module's
* business — they keep the global-first chain of `resolveSiteMetadata`.
*/
export const resolveOgImage = async (configured: OgImageSource): Promise<ResolvedOgImage> => {
const globalMetadata = await getCachedGlobal('metadata')()
const { siteUrl } = resolveSiteMetadata(globalMetadata)
const deploymentOrigin = resolveDeploymentOrigin(siteUrl)
const configuredMedia = await resolveMediaSource(configured)
const configuredUrl = absolutize(
typeof configured === 'string' ? configured : configuredMedia?.url,
deploymentOrigin,
)
if (configuredUrl) return { url: configuredUrl, media: configuredMedia }
const fallbackMedia = await resolveMediaSource(globalMetadata.image)
const fallbackUrl = absolutize(fallbackMedia?.url, deploymentOrigin)
return { url: fallbackUrl, media: fallbackUrl ? fallbackMedia : null }
}