Back to Blog

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.

Viprasol Tech Team
June 6, 2027
12 min read

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

ScopeTeamTimelineCost Range
Static metadata + OG defaults1 devHalf a day$150โ€“300
Dynamic generateMetadata per page type1 dev1 day$300โ€“600
JSON-LD for all page types1 dev1 day$300โ€“600
OG image with next/og + sitemap + robots1 dev1โ€“2 days$400โ€“800

See Also


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 metadata with title template, metadataBase, OG defaults, Twitter card, robots, verification
  • generateMetadata for blog posts: article OG type, publishedTime, alternates.canonical
  • JsonLdArticle and JsonLdOrganization components with dangerouslySetInnerHTML
  • opengraph-image.tsx with ImageResponse: gradient background, truncated title, excerpt footer
  • sitemap.ts: static pages + dynamic blog posts from Prisma, changeFrequency, priority
  • robots.ts: disallow /api/ /dashboard/ /settings/ /admin/ /_next/

Talk to our team about your SEO implementation โ†’

Or explore our web development services.

Share this article:

About the Author

V

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.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

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

Viprasol ยท Web Development

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.