Next.js Metadata and SEO: generateMetadata, OpenGraph, JSON-LD, and Dynamic Sitemaps
Implement complete SEO in Next.js App Router. Covers static and dynamic generateMetadata, OpenGraph images with next/og, JSON-LD structured data for articles and organizations, robots.txt configuration, and dynamic sitemap.xml generation.
Next.js App Router's metadata system handles SEO at the file system level โ no more <Head> components scattered through pages, no more duplicate tags from nested components. Every route segment can export a metadata object or a generateMetadata function, and Next.js merges them intelligently from root layout down to the leaf page.
Static Metadata
// app/layout.tsx โ root metadata (applies to all pages unless overridden)
import type { Metadata } from "next";
export const metadata: Metadata = {
// Title template: leaf pages set title, layout provides template
title: {
default: "Viprasol โ Software That Works",
template: "%s | Viprasol", // "%s" replaced by page title
},
description: "We build web applications, trading software, and AI tools for growing businesses.",
// Canonical URL base
metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL ?? "https://viprasol.com"),
// OpenGraph defaults
openGraph: {
type: "website",
siteName: "Viprasol",
locale: "en_US",
images: [{ url: "/images/og-default.jpg", width: 1200, height: 630 }],
},
// Twitter Card
twitter: {
card: "summary_large_image",
site: "@viprasol",
creator: "@viprasol",
},
// Robots
robots: {
index: true,
follow: true,
googleBot: { index: true, follow: true, "max-image-preview": "large" },
},
// Verification tags
verification: {
google: process.env.GOOGLE_SITE_VERIFICATION,
},
// App icons
icons: {
icon: "/favicon.ico",
apple: "/apple-touch-icon.png",
shortcut: "/favicon-32x32.png",
},
};
Dynamic Metadata: Blog Posts
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
import { prisma } from "@/lib/prisma";
import { notFound } from "next/navigation";
interface PageProps {
params: { slug: string };
}
// generateMetadata: called at request time (or build time for static pages)
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const post = await prisma.blogPost.findUnique({
where: { slug: params.slug },
select: {
title: true, excerpt: true, slug: true,
author: true, publishedAt: true, image: true, tags: true,
},
});
if (!post) return { title: "Post Not Found" };
const url = `/blog/${post.slug}`;
return {
title: post.title, // Becomes "Post Title | Viprasol" via template
description: post.excerpt ?? undefined,
openGraph: {
type: "article",
title: post.title,
description: post.excerpt ?? undefined,
url,
images: post.image
? [{ url: post.image, width: 1200, height: 630, alt: post.title }]
: undefined,
publishedTime: post.publishedAt?.toISOString(),
authors: [post.author],
tags: post.tags,
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.excerpt ?? undefined,
images: post.image ? [post.image] : undefined,
},
// Canonical URL
alternates: { canonical: url },
};
}
export default async function BlogPostPage({ params }: PageProps) {
const post = await prisma.blogPost.findUnique({ where: { slug: params.slug } });
if (!post) notFound();
return (
<>
{/* JSON-LD structured data โ injected alongside metadata */}
<JsonLdArticle post={post} />
<PostContent post={post} />
</>
);
}
๐ Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different โ senior engineers only, direct Slack access, and a 5.0โ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript โ production-grade stack
- Fixed-price contracts โ no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
JSON-LD Structured Data
// components/seo/json-ld.tsx โ inject structured data as <script> tags
interface ArticleJsonLdProps {
post: {
title: string;
excerpt: string | null;
slug: string;
author: string;
publishedAt: Date | null;
image: string | null;
};
}
export function JsonLdArticle({ post }: ArticleJsonLdProps) {
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "https://viprasol.com";
const schema = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
description: post.excerpt,
image: post.image ? `${appUrl}${post.image}` : undefined,
url: `${appUrl}/blog/${post.slug}`,
datePublished: post.publishedAt?.toISOString(),
dateModified: post.publishedAt?.toISOString(),
author: {
"@type": "Organization",
name: post.author,
url: appUrl,
},
publisher: {
"@type": "Organization",
name: "Viprasol Tech",
url: appUrl,
logo: {
"@type": "ImageObject",
url: `${appUrl}/images/logo.png`,
},
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
// Organization schema for homepage
export function JsonLdOrganization() {
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "https://viprasol.com";
const schema = {
"@context": "https://schema.org",
"@type": "Organization",
name: "Viprasol Tech",
url: appUrl,
logo: `${appUrl}/images/logo.png`,
description: "Custom software development for web, trading, and AI applications.",
address: {
"@type": "PostalAddress",
addressLocality: "Jind",
addressRegion: "Haryana",
addressCountry: "IN",
},
contactPoint: {
"@type": "ContactPoint",
contactType: "customer service",
url: `${appUrl}/contact`,
},
sameAs: [
"https://twitter.com/viprasol",
"https://linkedin.com/company/viprasol",
],
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
OpenGraph Image with next/og
// app/blog/[slug]/opengraph-image.tsx โ auto-generates OG image at /blog/[slug]/opengraph-image
import { ImageResponse } from "next/og";
import { prisma } from "@/lib/prisma";
export const runtime = "edge";
export const contentType = "image/png";
export const size = { width: 1200, height: 630 };
export default async function OgImage({ params }: { params: { slug: string } }) {
const post = await prisma.blogPost.findUnique({
where: { slug: params.slug },
select: { title: true, excerpt: true, author: true },
});
const title = post?.title ?? "Viprasol Blog";
const excerpt = post?.excerpt ?? "Software development insights and tutorials.";
return new ImageResponse(
(
<div
style={{
background: "linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%)",
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
padding: "60px",
fontFamily: "system-ui, sans-serif",
color: "white",
}}
>
{/* Logo area */}
<div style={{ display: "flex", alignItems: "center", marginBottom: "auto" }}>
<div
style={{
background: "rgba(255,255,255,0.15)",
borderRadius: "12px",
padding: "8px 16px",
fontSize: "18px",
fontWeight: 700,
}}
>
Viprasol
</div>
</div>
{/* Title */}
<div
style={{
fontSize: "52px",
fontWeight: 800,
lineHeight: 1.1,
marginBottom: "20px",
maxWidth: "900px",
}}
>
{title.length > 60 ? title.slice(0, 57) + "โฆ" : title}
</div>
{/* Excerpt */}
<div
style={{
fontSize: "22px",
opacity: 0.8,
maxWidth: "800px",
lineHeight: 1.4,
}}
>
{excerpt?.slice(0, 120)}
</div>
{/* Footer */}
<div
style={{
display: "flex",
alignItems: "center",
marginTop: "40px",
fontSize: "18px",
opacity: 0.7,
}}
>
viprasol.com/blog
</div>
</div>
),
{ ...size }
);
}
๐ Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4โ8 weeks, full platforms in 3โ5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
Dynamic Sitemap
// app/sitemap.ts โ generates /sitemap.xml automatically
import type { MetadataRoute } from "next";
import { prisma } from "@/lib/prisma";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "https://viprasol.com";
// Static pages
const staticPages: MetadataRoute.Sitemap = [
{ url: appUrl, lastModified: new Date(), changeFrequency: "weekly", priority: 1.0 },
{ url: `${appUrl}/blog`, lastModified: new Date(), changeFrequency: "daily", priority: 0.9 },
{ url: `${appUrl}/services`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
{ url: `${appUrl}/services/web-development`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
{ url: `${appUrl}/services/ai-machine-learning`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
{ url: `${appUrl}/services/cloud-solutions`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
{ url: `${appUrl}/contact`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.7 },
];
// Dynamic blog posts
const posts = await prisma.blogPost.findMany({
where: { publishedAt: { not: null } },
select: { slug: true, publishedAt: true },
orderBy: { publishedAt: "desc" },
});
const postPages: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${appUrl}/blog/${post.slug}`,
lastModified: post.publishedAt ?? new Date(),
changeFrequency: "monthly" as const,
priority: 0.7,
}));
return [...staticPages, ...postPages];
}
robots.txt
// app/robots.ts โ generates /robots.txt
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? "https://viprasol.com";
return {
rules: [
{
userAgent: "*",
allow: "/",
disallow: [
"/api/",
"/dashboard/",
"/settings/",
"/admin/",
"/_next/",
],
},
],
sitemap: `${appUrl}/sitemap.xml`,
};
}
Cost and Timeline Estimates
| Scope | Team | Timeline | Cost Range |
|---|---|---|---|
| Static metadata + OG defaults | 1 dev | Half a day | $150โ300 |
| Dynamic generateMetadata per page type | 1 dev | 1 day | $300โ600 |
| JSON-LD for all page types | 1 dev | 1 day | $300โ600 |
| OG image with next/og + sitemap + robots | 1 dev | 1โ2 days | $400โ800 |
See Also
- Next.js OpenGraph Images
- Next.js Static Generation
- Next.js Cache Revalidation
- Next.js Performance Optimization
- Next.js Internationalization
Working With Viprasol
SEO in Next.js App Router is cleaner than Pages Router but has gotchas: metadataBase must be set for absolute URLs in OG images, generateMetadata runs at build time for static pages (don't assume request context), and JSON-LD must avoid XSS by not interpolating user input directly into the script tag. Our team implements the full metadata stack: title templates, dynamic OG metadata, ImageResponse OG images, JSON-LD article and organization schemas, dynamic sitemap, and robots.txt.
What we deliver:
- Root
metadatawith title template,metadataBase, OG defaults, Twitter card, robots, verification generateMetadatafor blog posts: article OG type, publishedTime, alternates.canonicalJsonLdArticleandJsonLdOrganizationcomponents withdangerouslySetInnerHTMLopengraph-image.tsxwithImageResponse: gradient background, truncated title, excerpt footersitemap.ts: static pages + dynamic blog posts from Prisma,changeFrequency,priorityrobots.ts: disallow /api/ /dashboard/ /settings/ /admin/ /_next/
Talk to our team about your SEO implementation โ
Or explore our web development services.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms โ we build it all with Next.js and React.
Free consultation โข No commitment โข Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse โฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ from architecture through deployment.