T3 Stack Open Graph Meta Tags: Add OG to Your Next.js App Router
How to add og:title, og:image, og:description and Twitter card meta tags to a T3 Stack app — using Next.js App Router metadata API.
T3 Stack and OG tags
The T3 Stack (Next.js + tRPC + Prisma + Tailwind + NextAuth) uses Next.js as its framework, which means OG tags work exactly like any Next.js App Router app. The key difference is that your data often comes through tRPC queries, and you need to call them correctly in server components and generateMetadata.
Static metadata for pages
For pages where OG data is known at build time (home, about, pricing), export a static metadata object:
// src/app/page.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "My T3 App — Build Full-Stack Apps Fast",
description: "A full-stack app built with the T3 Stack.",
openGraph: {
title: "My T3 App — Build Full-Stack Apps Fast",
description: "A full-stack app built with the T3 Stack.",
type: "website",
url: "https://yourdomain.com",
images: [
{
url: "https://yourdomain.com/og/home.png",
width: 1200,
height: 630,
alt: "My T3 App",
},
],
},
twitter: {
card: "summary_large_image",
title: "My T3 App — Build Full-Stack Apps Fast",
description: "A full-stack app built with the T3 Stack.",
images: ["https://yourdomain.com/og/home.png"],
},
};
export default function HomePage() {
return <main>...</main>;
}Dynamic metadata with tRPC + generateMetadata
For dynamic pages (blog posts, user profiles), use generateMetadata with a server-side tRPC caller. You cannot use the tRPC React hooks here because generateMetadata runs on the server:
// src/app/blog/[slug]/page.tsx
import type { Metadata } from "next";
import { api } from "@/trpc/server";
export async function generateMetadata({
params,
}: {
params: { slug: string };
}): Promise<Metadata> {
const post = await api.post.getBySlug({ slug: params.slug });
if (!post) {
return { title: "Post Not Found" };
}
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: "article",
url: `https://yourdomain.com/blog/${post.slug}`,
images: [
{
url: post.ogImage ?? `https://yourdomain.com/og/${post.slug}.png`,
width: 1200,
height: 630,
},
],
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt,
images: [post.ogImage ?? `https://yourdomain.com/og/${post.slug}.png`],
},
};
}
export default async function BlogPostPage({
params,
}: {
params: { slug: string };
}) {
const post = await api.post.getBySlug({ slug: params.slug });
if (!post) return <div>Not found</div>;
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
);
}Setting up the server-side tRPC caller
The T3 Stack scaffolds a server-side tRPC caller at src/trpc/server.ts. Make sure it's set up correctly:
// src/trpc/server.ts (auto-generated by create-t3-app)
import "server-only";
import { createCaller } from "@/server/api/root";
import { createTRPCContext } from "@/server/api/trpc";
import { headers } from "next/headers";
import { cache } from "react";
const createContext = cache(async () => {
const heads = new Headers(await headers());
heads.set("x-trpc-source", "rsc");
return createTRPCContext({ headers: heads });
});
export const api = createCaller(createContext);
// Now you can use `api.post.getBySlug()` in generateMetadata
// and server components without any React hooks.Dynamic OG images with next/og
Generate dynamic OG images per page using the Next.js ImageResponse API:
// src/app/api/og/route.tsx
import { ImageResponse } from "next/og";
import { api } from "@/trpc/server";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const slug = searchParams.get("slug");
const post = slug
? await api.post.getBySlug({ slug })
: null;
const title = post?.title ?? "My T3 App";
return new ImageResponse(
(
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
width: "1200px",
height: "630px",
background: "#0a0a0a",
color: "#fff",
padding: "60px",
}}
>
<h1 style={{ fontSize: "56px", margin: 0 }}>{title}</h1>
<p style={{ fontSize: "24px", color: "#a1a1aa", marginTop: "20px" }}>
Built with the T3 Stack
</p>
</div>
),
{ width: 1200, height: 630 }
);
}
// Then in generateMetadata, reference:
// images: [{ url: `https://yourdomain.com/api/og?slug=${post.slug}` }]Layout-level defaults
Set default OG tags in your root layout so every page has a fallback:
// src/app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
metadataBase: new URL("https://yourdomain.com"),
title: {
default: "My T3 App",
template: "%s | My T3 App",
},
description: "A full-stack app built with the T3 Stack.",
openGraph: {
type: "website",
locale: "en_US",
siteName: "My T3 App",
images: [
{
url: "/og/default.png",
width: 1200,
height: 630,
},
],
},
twitter: {
card: "summary_large_image",
creator: "@yourtwitterhandle",
},
};Verify your T3 Stack OG tags
After deploying, paste your URL into OGFixer to preview how Twitter, Slack, Discord, and LinkedIn will render your links.