Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ included_files = [
"public/fonts/Inter-ExtraBold.ttf",
"public/images/logos/splash-dark.png",
]
# @takumi-rs/core ships platform-specific .node binaries via napi-rs's runtime
# require() dispatcher — esbuild can't statically trace the optional deps. Keep
# the napi loader external so node_modules/@takumi-rs/core ships as-is, and
# also list the Linux x64 binary package explicitly: Netlify's bundler only
# follows declared deps for `external_node_modules`, so the optional platform
# package would otherwise be dropped (verified — function failed at runtime
# with "Cannot find native binding"). Netlify functions run on AWS Lambda
# Amazon Linux 2 (glibc, x64), hence linux-x64-gnu.
external_node_modules = ["@takumi-rs/core", "@takumi-rs/core-linux-x64-gnu"]

[[headers]]
for = "/*"
Expand Down
30 changes: 30 additions & 0 deletions src/routes/api/og/$library[.png].ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,36 @@ export const Route = createFileRoute('/api/og/$library.png')({
return new Response(`Unknown library: ${libraryId}`, { status: 404 })
}

// ImageResponse builds the Response synchronously (status 200, image
// content-type) and renders inside a ReadableStream. If the render
// throws — e.g. takumi's native binding fails to load — the stream
// is errored but the response headers are already sent, producing
// an empty 200 OK that gets cached at the edge. Await the ready
// promise so render errors surface as 500s.
try {
await result.ready
} catch (error) {
console.error('Failed to generate OG image', error)
// Surface the underlying message+stack in the response body so we
// can diagnose Netlify-only render failures without log access.
// TODO: trim back to "Failed to generate OG image" once the takumi
// binding load on Netlify is verified working.
const detail =
error instanceof Error
? `${error.name}: ${error.message}\n${error.stack ?? ''}\n${
error.cause instanceof Error
? `caused by ${error.cause.name}: ${error.cause.message}\n${error.cause.stack ?? ''}`
: error.cause
? `caused by ${String(error.cause)}`
: ''
}`
: String(error)
return new Response(`Failed to generate OG image\n\n${detail}`, {
status: 500,
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

return result
},
},
Expand Down
43 changes: 26 additions & 17 deletions src/utils/og.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createIsomorphicFn } from '@tanstack/react-start'
import { getRequest } from '@tanstack/react-start/server'
import type { LibraryId } from '~/libraries'
import { canonicalUrl } from './seo'
import {
MAX_OG_DESCRIPTION_LENGTH,
MAX_OG_TITLE_LENGTH,
Expand All @@ -16,20 +17,32 @@ type OgImageOptions = {
/**
* Absolute origin to use for og:image URLs.
*
* Unlike canonical links (which must always point to production),
* og:image URLs MUST be reachable on the same deploy that emitted them
* — social-card validators fetch the URL from the meta tag verbatim.
* Unlike canonical links (which always point to production), og:image
* URLs MUST be reachable on the same deploy that emitted them — social-
* card validators fetch the URL from the meta tag verbatim, so on a
* Netlify deploy preview the og:image must point at the preview origin,
* not at production.
*
* On Netlify preview/branch deploys, `URL` is still the production URL,
* but `DEPLOY_PRIME_URL` is the deploy's own origin. Prefer that.
* The incoming request URL is the source of truth. `process.env.URL` /
* `DEPLOY_PRIME_URL` etc. turned out to be unreliable inside our bundled
* SSR function, so read the origin from the live request via TanStack
* Start's `getRequest()`. The server import is referenced only inside
* `.server()`, which the start compiler treats as a client-safe boundary
* — the import is tree-shaken from the client bundle.
*/
function getOgOrigin(): string {
if (!import.meta.env.SSR) return DEFAULT_SITE_URL
const env = process.env
const origin =
env.DEPLOY_PRIME_URL || env.DEPLOY_URL || env.URL || env.SITE_URL
return (origin ?? DEFAULT_SITE_URL).replace(/\/$/, '')
}
const getOgOrigin = createIsomorphicFn()
.server((): string => {
try {
const request = getRequest()
if (request?.url) return new URL(request.url).origin
} catch {
// getRequest() throws if called outside an SSR request context.
}
return DEFAULT_SITE_URL
})
.client((): string =>
typeof window !== 'undefined' ? window.location.origin : DEFAULT_SITE_URL,
)

/**
* Absolute URL for a package-themed OG image.
Expand All @@ -56,9 +69,5 @@ export function ogImageUrl(
const qs = params.toString()
const path = `/api/og/${libraryId}.png${qs ? `?${qs}` : ''}`

// On client (which can't happen in head() but guards against misuse),
// fall through to canonicalUrl which uses the production hostname.
if (!import.meta.env.SSR) return canonicalUrl(path)

return `${getOgOrigin()}${path}`
}
Loading