Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions netlify.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ 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, so
# the Linux binary is missing from the zipped function. Keep the package
# external so node_modules/@takumi-rs/core (and the matching @takumi-rs/core-
# linux-x64-gnu installed by pnpm on the build machine) is shipped as-is.
external_node_modules = ["@takumi-rs/core"]

[[headers]]
for = "/*"
Expand Down
13 changes: 13 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,19 @@ 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)
return new Response('Failed to generate OG image', { status: 500 })
}

return result
},
},
Expand Down
15 changes: 13 additions & 2 deletions src/utils/og.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getRequest } from '@tanstack/react-start/server'
import type { LibraryId } from '~/libraries'
import { canonicalUrl } from './seo'
import {
Expand All @@ -20,11 +21,21 @@ type OgImageOptions = {
* og:image URLs MUST be reachable on the same deploy that emitted them
* — social-card validators fetch the URL from the meta tag verbatim.
*
* 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: on a Netlify deploy
* preview the request hits `deploy-preview-N--tanstack.netlify.app`, so
* the og:image must point there too. `process.env.DEPLOY_PRIME_URL` and
* friends turn out to be unreliable inside the bundled SSR function, so
* we read the origin from the live request instead.
*/
function getOgOrigin(): string {
if (!import.meta.env.SSR) return DEFAULT_SITE_URL
try {
const request = getRequest()
if (request?.url) return new URL(request.url).origin
} catch {
// getRequest() throws if called outside an SSR request context
// (e.g. build-time prerender). Fall through to the env-var fallback.
}
const env = process.env
const origin =
env.DEPLOY_PRIME_URL || env.DEPLOY_URL || env.URL || env.SITE_URL
Expand Down
7 changes: 6 additions & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,12 @@ export default defineConfig({
importProtection: {
behavior: 'error',
client: {
files: ['**/*.server.*', '**/server/**'],
// src/utils/og.ts imports getRequest from @tanstack/react-start/server
// to derive the og:image origin from the live request — uses are
// gated by `import.meta.env.SSR`, so Vite tree-shakes the import out
// of the client bundle. Allowlist the file so the static import
// doesn't trip the protection check during bundling.
files: ['**/*.server.*', '**/server/**', '**/utils/og.ts'],
specifiers: [
'@tanstack/react-start/server',
'uploadthing/server',
Expand Down
Loading