OgCard and useOgData read link previews through two routes in your app: one that fetches a page's Open Graph data, and one that serves its image from your origin. @once-ui-system/core/next ships both, ready-made. Each factory returns { GET }, so a route file is two lines.
Both routes fetch URLs that your visitors choose, from your server. That makes them a server-side request forgery target, so the handlers only ever request public addresses: every URL and every redirect is checked, and the connection goes to the exact address that was checked.
These are the paths OgCard and useOgData call by default, so nothing else changes. To serve them somewhere else, see Custom paths below.
The handlers need the Node.js runtime, which is the default for route handlers. If a route file you are replacing sets export const runtime = "edge", remove that line.
The fetch route answers GET /api/og/fetch?url=… with the page's metadata as JSON. image and faviconUrl are absolute URLs, resolved against the page.
The proxy route answers GET /api/og/proxy?url=… with the image itself, sent with X-Content-Type-Options: nosniff and a sandboxing Content-Security-Policy.
Failures are JSON, { "error": "…" }, and never cached:
- 400: missing or malformed url, or a URL that is not allowed (a private address, a blocked domain, not HTTPS)
- 403: a cross-origin caller, when sameOrigin is on
- 413: the image is larger than maxBytes (proxy)
- 415: not an HTML page (fetch), or not an allowed image type (proxy)
- 502: the upstream failed (an error status, too many redirects, a dropped connection); the proxy forwards an upstream 4xx or 5xx as is
- 504: the upstream did not finish within timeout
- 500: not running on the Node.js runtime
Both factories take the same options as handleOGFetch and handleOGProxy, which they wrap. Options are read once, when the route is created.
DEFAULT_OG_IMAGE_TYPES is the raster formats: AVIF, WebP, PNG, APNG, JPEG, GIF, BMP and ICO. SVG is left out because it can carry script; add "image/svg+xml" only if you need it.
Mount the routes anywhere, then tell the client where they are. OgRoutesProvider sets the paths for every OgCard and useOgData below it, and Meta.generate takes the same object for its default social image.
Without a provider, the paths are /api/og/fetch, /api/og/proxy and /api/og/generate. OgCard's serviceConfig and useOgData's explicit arguments still take precedence.
Before anything is requested, and again for every redirect:
- Schemes other than https: (and http: with allowHttp), and URLs with credentials.
- Internal hostnames: localhost, *.local, *.internal, *.home.arpa and single-label names such as http://intranet/.
- IP addresses that are not public, written any way the URL parser accepts (127.0.0.1, 2130706433, 0x7f.1, [::ffff:127.0.0.1]): private, loopback, link-local, unique-local, carrier-grade NAT, multicast, unspecified, reserved and documentation ranges, and the cloud metadata endpoints (169.254.169.254, fd00:ec2::254).
- Hostnames that resolve to any such address. Every record is checked, not only the first.
The hostname is resolved and checked inside the connection itself, and the socket connects to the address that was checked. A DNS answer that changes between a check and the request (DNS rebinding) has nothing to slip through.
Rate limiting, authentication or logging wrap the handler:
useOgData rewrites a cross-origin image to your proxy route, so OgCard renders /api/og/proxy?url=… through next/image. Since Next.js 16, a local image src with a query string is only optimized when images.localPatterns allows it:
- The handlers connect directly; they do not read HTTP_PROXY or HTTPS_PROXY.
- Outside Next.js, use handleOGFetch and handleOGProxy from @once-ui-system/core/server: they take a standard Request and return a Response. Import them through a bundler (Vite, webpack, esbuild or a framework's server build); the package is published for bundlers, and plain Node cannot load it on its own.
- The social image route that Meta.generate points at (/api/og/generate) is yours to design with next/og; it renders text you pass it and fetches no URLs from the request.