Render in the loader, print the URL in the meta export.
A Remix `loader` runs only on the server, so the API key stays there; its result reaches the `meta` export, which Remix renders into the document head on the server — exactly where link-preview crawlers read og:image. The same pattern works in React Router v7 and later, Remix's successor: there `meta` receives `loaderData` instead of `data`, and the loader returns a plain object instead of `json()`. (Remix 3 is a separate, newer framework this snippet does not cover.)
app/routes/blog.$slug.tsx
import type { LoaderFunctionArgs, MetaFunction } from '@remix-run/node';
import { json } from '@remix-run/node';
import { getPost } from '~/models/post.server';
export async function loader({ params }: LoaderFunctionArgs) {
const post = await getPost(params.slug!);
const res = await fetch('https://shareglint.com/v1/images?wait=true', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SHAREGLINT_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
template: 'blog-og',
modifications: { title: post.title, author: post.author, site: 'yourblog.com' },
}),
});
const { url: ogImage } = (await res.json()) as { url: string };
return json({ post, ogImage });
}
export const meta: MetaFunction<typeof loader> = ({ data }) => [
{ title: data?.post.title },
{ property: 'og:title', content: data?.post.title },
{ property: 'og:image', content: data?.ogImage },
{ property: 'og:image:width', content: '1200' },
{ property: 'og:image:height', content: '630' },
{ name: 'twitter:card', content: 'summary_large_image' },
];
The meta of the deepest matching route wins, and React Router's docs are explicit that "the entire meta descriptor array is replaced, not merged". A post route that returns only a title drops the og:image tags of its parent layout — return the full set, image included, from every route that should have one.
With ?wait=true the finished image arrives on the same connection — usually in about a second.
{
"id": "img_01m08s3ez1zcv28h335g9923bw",
"status": "completed",
"url": "https://cdn.shareglint.com/renders/img_01m08s3ez1zcv28h335g9923bw.png",
"duration_ms": 912
}
100 images a month free, then $9 for 2,000. The same post rendered twice returns the existing URL and uses no quota — so rebuilding your site costs nothing.