SiteAssets SiteAssets
Blog · Engineering 7 min read

Open Graph in Next.js, Astro and Nuxt: three framework traps

Every modern framework can emit Open Graph tags correctly. Almost every broken implementation fails for one of three reasons, and each framework fails in its own way.

7 min read SiteAssets team

The tags are simple. The hard part is the delivery: whether they exist in the first HTML byte, whether the URL is absolute, and whether the build or the client eventually overwrites them.

Trap one: metadata that exists only after hydration

Crawlers do not run your JavaScript. Any tag set in useEffect, onMounted, or a client-side document.head mutation is invisible. This is the single most common failure across all three frameworks, and it is invisible in the browser because you are looking at the hydrated DOM.

The test is view source, not inspect element. If the meta tags are absent from the raw response, the implementation is client-side even when it appears correct on screen.

Next.js: metadataBase and relative images

The Metadata API resolves relative image paths against metadataBase. If metadataBase is unset, the framework falls back to localhost or the deployment host, which produces an og:image that only works from your machine.

typescript
export const metadata: Metadata = {
  metadataBase: new URL('https://example.com'),
  openGraph: {
    images: [{ url: '/og/article.png', width: 1200, height: 630 }],
  },
};

A second Next-specific trap: a root layout that sets openGraph globally and a page that sets only a title. Metadata merges shallowly in places and replaces in others, so the page can lose the inherited image. Declare the image on the page or verify the merged output.

Astro: static routes and build-time data

In a prerendered route, tags are captured at build time. Content fetched inside the page component after build — or a CMS value read from a client-side store — never reaches the static HTML. For dynamic metadata you need SSR or a route that renders on request.

astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
const { title, image } = Astro.props;
---
<BaseLayout title={title} image={image}>
  <!-- BaseLayout emits og:title / og:image from the props -->
</BaseLayout>

A subtler Astro trap: a layout that emits og:image with a relative path because the site config is unset during a preview build. Set the site URL in the config so generated tags are absolute in every environment.

Nuxt: useSeoMeta and the SSR payload

useSeoMeta is reactive and renders server-side by default, which is correct. The failure appears when a value is derived from a browser-only store or an async call that resolves after the initial render. The tag then exists on the client and not in the SSR HTML.

typescript
useSeoMeta({
  ogTitle: article.value.title,
  ogDescription: article.value.summary,
  ogImage: 'https://example.com/og/article.png',
  twitterCard: 'summary_large_image',
});

Another Nuxt-specific issue: a runtime config value that is undefined at build time produces og:image with a trailing slash and no filename. Prefix the asset path explicitly rather than concatenating a possibly empty base.

The one test that covers all three

  1. 01 Deploy to a public URL, never test on localhost.
  2. 02 Run curl -s against the URL and grep for og: and twitter: tags.
  3. 03 Confirm the image value starts with https:// and returns 200.
  4. 04 Confirm no tag contains localhost, 127.0.0.1 or a preview hostname.
  5. 05 Paste the URL into each platform composer and compare the rendered card.

None of these traps require framework-specific knowledge to diagnose. They all reduce to the same question: is the final, absolute tag present in the first HTML response? Answer that with curl and the framework stops mattering.

Try it on your own site

Fifteen checks, about five seconds.

Paste a deployed URL and read the check table: weight, status and the rule behind every row.