AboutBlogContact
Web DevelopmentMay 1, 2026 7 min read 7

Next.js SEO Optimization in 2026: The Complete Technical Guide

AunimedaAunimeda
📋 Table of Contents

Next.js SEO Optimization in 2026: The Complete Technical Guide

Next.js App Router gives you the right primitives for SEO — server rendering by default, a typed Metadata API, and built-in image optimization. But none of that is automatic. This guide covers every technical SEO lever in Next.js 15, with working code.


Metadata API

The App Router's Metadata API replaces the old <Head> component. All metadata is server-rendered — no JavaScript needed.

Static Metadata

// app/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  // Basic
  title: {
    default: 'Aunimeda — Custom Software Development',
    template: '%s | Aunimeda', // "About | Aunimeda", "Blog | Aunimeda"
  },
  description: 'We build web apps, mobile apps, and AI solutions for businesses in Central Asia and beyond.',

  // Open Graph (Facebook, LinkedIn, Telegram, WhatsApp previews)
  openGraph: {
    type: 'website',
    locale: 'en_US',
    url: 'https://aunimeda.com',
    siteName: 'Aunimeda',
    title: 'Aunimeda — Custom Software Development',
    description: 'We build web apps, mobile apps, and AI solutions.',
    images: [
      {
        url: 'https://aunimeda.com/images/og-image.jpg',
        width: 1200,
        height: 630,
        alt: 'Aunimeda — Custom Software Development',
      },
    ],
  },

  // Twitter/X card
  twitter: {
    card: 'summary_large_image',
    title: 'Aunimeda — Custom Software Development',
    description: 'We build web apps, mobile apps, and AI solutions.',
    images: ['https://aunimeda.com/images/og-image.jpg'],
  },

  // Canonical URL
  alternates: {
    canonical: 'https://aunimeda.com',
  },

  // Robots
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      'max-video-preview': -1,
      'max-image-preview': 'large',
      'max-snippet': -1,
    },
  },

  // Verification tags
  verification: {
    google: 'your-verification-code',
    yandex: 'your-yandex-verification',
  },
};

Dynamic Metadata (per page)

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
import { getPost } from '@/lib/blog';
import { notFound } from 'next/navigation';

interface Props {
  params: { slug: string };
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const post = await getPost(params.slug);
  if (!post) return {};

  const ogImageUrl = `https://aunimeda.com/api/og?title=${encodeURIComponent(post.title)}`;

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      type: 'article',
      title: post.title,
      description: post.excerpt,
      publishedTime: post.date,
      authors: ['Aunimeda'],
      tags: post.tags,
      images: [{ url: ogImageUrl, width: 1200, height: 630 }],
    },
    alternates: {
      canonical: `https://aunimeda.com/blog/${post.slug}`,
    },
  };
}

export default async function BlogPost({ params }: Props) {
  const post = await getPost(params.slug);
  if (!post) notFound();
  // ...
}

Dynamic OG Image Generation

Generate social preview images server-side with @vercel/og:

// app/api/og/route.tsx
import { ImageResponse } from 'next/og';

export const runtime = 'edge';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const title = searchParams.get('title') ?? 'Aunimeda';

  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          flexDirection: 'column',
          width: '100%',
          height: '100%',
          background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 100%)',
          padding: 60,
          justifyContent: 'space-between',
        }}
      >
        <div style={{ display: 'flex', alignItems: 'center' }}>
          <div style={{ fontSize: 24, color: '#60a5fa', fontWeight: 700 }}>
            AUNIMEDA
          </div>
        </div>

        <div
          style={{
            fontSize: title.length > 60 ? 44 : 56,
            fontWeight: 800,
            color: '#f1f5f9',
            lineHeight: 1.2,
            maxWidth: 900,
          }}
        >
          {title}
        </div>

        <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
          <div style={{ fontSize: 20, color: '#94a3b8' }}>aunimeda.com</div>
        </div>
      </div>
    ),
    { width: 1200, height: 630 }
  );
}

Structured Data (JSON-LD)

Structured data enables rich results in Google Search — star ratings, FAQ dropdowns, breadcrumbs.

// components/JsonLd.tsx
export function JsonLd({ data }: { data: Record<string, unknown> }) {
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
    />
  );
}

// app/layout.tsx — Organization schema (sitewide)
import { JsonLd } from '@/components/JsonLd';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  const organizationSchema = {
    '@context': 'https://schema.org',
    '@type': 'Organization',
    name: 'Aunimeda',
    url: 'https://aunimeda.com',
    logo: 'https://aunimeda.com/logo.png',
    contactPoint: {
      '@type': 'ContactPoint',
      contactType: 'sales',
      email: 'hello@aunimeda.com',
      availableLanguage: ['English', 'Russian'],
    },
    sameAs: [
      'https://t.me/aunimeda',
      'https://instagram.com/aunimeda',
    ],
  };

  return (
    <html>
      <body>
        <JsonLd data={organizationSchema} />
        {children}
      </body>
    </html>
  );
}
// app/services/[slug]/page.tsx — Service schema
const serviceSchema = {
  '@context': 'https://schema.org',
  '@type': 'Service',
  name: service.title,
  description: service.description,
  provider: {
    '@type': 'Organization',
    name: 'Aunimeda',
    url: 'https://aunimeda.com',
  },
  areaServed: ['KG', 'KZ', 'RU'],
  serviceType: service.category,
};

// app/blog/[slug]/page.tsx — Article schema
const articleSchema = {
  '@context': 'https://schema.org',
  '@type': 'TechArticle',
  headline: post.title,
  description: post.excerpt,
  datePublished: post.date,
  dateModified: post.updatedAt ?? post.date,
  author: {
    '@type': 'Organization',
    name: 'Aunimeda',
    url: 'https://aunimeda.com',
  },
  publisher: {
    '@type': 'Organization',
    name: 'Aunimeda',
    logo: {
      '@type': 'ImageObject',
      url: 'https://aunimeda.com/logo.png',
    },
  },
  mainEntityOfPage: `https://aunimeda.com/blog/${post.slug}`,
};

// FAQ schema — shows dropdown answers in search results
const faqSchema = {
  '@context': 'https://schema.org',
  '@type': 'FAQPage',
  mainEntity: faqs.map(faq => ({
    '@type': 'Question',
    name: faq.question,
    acceptedAnswer: {
      '@type': 'Answer',
      text: faq.answer,
    },
  })),
};

Sitemap Generation

// app/sitemap.ts
import type { MetadataRoute } from 'next';
import { getAllPosts } from '@/lib/blog';
import { getAllServices } from '@/lib/services';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const baseUrl = 'https://aunimeda.com';
  const now = new Date();

  // Static pages
  const staticPages: MetadataRoute.Sitemap = [
    { url: baseUrl, lastModified: now, changeFrequency: 'weekly', priority: 1 },
    { url: `${baseUrl}/about`, lastModified: now, changeFrequency: 'monthly', priority: 0.8 },
    { url: `${baseUrl}/contact`, lastModified: now, changeFrequency: 'monthly', priority: 0.8 },
    { url: `${baseUrl}/blog`, lastModified: now, changeFrequency: 'daily', priority: 0.9 },
  ];

  // Service pages
  const services = await getAllServices();
  const servicePages: MetadataRoute.Sitemap = services.map(service => ({
    url: `${baseUrl}/services/${service.slug}`,
    lastModified: now,
    changeFrequency: 'monthly',
    priority: 0.9,
  }));

  // Blog posts
  const posts = await getAllPosts();
  const blogPages: MetadataRoute.Sitemap = posts.map(post => ({
    url: `${baseUrl}/blog/${post.slug}`,
    lastModified: new Date(post.date),
    changeFrequency: 'monthly',
    priority: 0.7,
  }));

  return [...staticPages, ...servicePages, ...blogPages];
}
// app/robots.ts
import type { MetadataRoute } from 'next';

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      {
        userAgent: '*',
        allow: '/',
        disallow: ['/api/', '/admin/'],
      },
    ],
    sitemap: 'https://aunimeda.com/sitemap.xml',
  };
}

Internationalization (i18n) SEO

For multi-language sites, hreflang tags tell Google which version to show to which audience:

// app/[locale]/layout.tsx
import type { Metadata } from 'next';

export async function generateMetadata({
  params,
}: {
  params: { locale: string };
}): Promise<Metadata> {
  const locales = ['en', 'ru', 'kg', 'kz'];
  const baseUrl = 'https://aunimeda.com';

  return {
    alternates: {
      canonical: `${baseUrl}/${params.locale}`,
      languages: Object.fromEntries(
        locales.map(locale => [locale, `${baseUrl}/${locale}`])
      ),
    },
  };
}

Add hreflang link tags in the HTML head for each alternate URL — critical for multilingual sites.


Core Web Vitals

Google uses Core Web Vitals as a ranking signal. Target: all green.

LCP (Largest Contentful Paint) — target < 2.5s

The hero image is almost always the LCP element. Prioritize it:

// app/page.tsx
import Image from 'next/image';

export default function HomePage() {
  return (
    <section>
      {/* LCP image: preload with priority */}
      <Image
        src="/hero.webp"
        alt="Custom software development"
        width={1200}
        height={600}
        priority           // Adds <link rel="preload"> in <head>
        fetchPriority="high"
        sizes="100vw"
        quality={85}
      />
    </section>
  );
}

CLS (Cumulative Layout Shift) — target < 0.1

Always define dimensions for images and dynamic content to prevent layout shifts:

// Always provide width/height or use fill with a sized container
<div style={{ position: 'relative', height: '400px' }}>
  <Image
    src={post.coverImage}
    alt={post.title}
    fill
    sizes="(max-width: 768px) 100vw, 50vw"
    style={{ objectFit: 'cover' }}
  />
</div>

// Reserve space for ads/embeds before they load
<div style={{ minHeight: '250px' }}>
  <AdComponent />
</div>

INP (Interaction to Next Paint) — target < 200ms

// Use transitions for non-critical state updates
import { startTransition } from 'react';

function SearchBar() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  function handleChange(value: string) {
    setQuery(value);
    // Mark the results update as non-urgent — keeps the input responsive
    startTransition(() => {
      setResults(searchProducts(value));
    });
  }

  return <input value={query} onChange={e => handleChange(e.target.value)} />;
}

Measure and Monitor

// app/layout.tsx — report Web Vitals
export function reportWebVitals(metric: {
  name: string;
  value: number;
  rating: 'good' | 'needs-improvement' | 'poor';
}) {
  if (metric.rating === 'poor') {
    console.warn(`Poor ${metric.name}: ${metric.value}`);
    // Send to analytics
    fetch('/api/vitals', {
      method: 'POST',
      body: JSON.stringify(metric),
      keepalive: true, // Survives page navigation
    });
  }
}

Tools to validate your work:

  • Google Search Console — real impressions, clicks, and Core Web Vitals from actual users
  • PageSpeed Insights — lab data for specific pages
  • Screaming Frog — crawl audit (find missing metas, broken links, redirects)
  • Rich Results Test — verify structured data

Aunimeda builds SEO-optimized websites and web applications with Next.js — technically correct from the ground up, not bolted on.

Contact us to discuss your project. See also: Web Development, Corporate Website Development, E-commerce Development

Read Also

How to Build an E-commerce Website That Actually Sells in 2026aunimeda
Web Development

How to Build an E-commerce Website That Actually Sells in 2026

The difference between an e-commerce site that converts at 1% and one that converts at 4% isn't design — it's architecture decisions made before a line of code is written. Here's the playbook.

Web Vitals & Lighthouse 100: Practical Optimization Guide 2026aunimeda
Web Development

Web Vitals & Lighthouse 100: Practical Optimization Guide 2026

Achieving Lighthouse 100 on a real-world production Next.js app - not a blank page. Covers LCP, INP (replaced FID in 2024), CLS, TTFB, font optimization, image optimization, JS bundle analysis, and CSS critical path - with specific code changes.

Node.js vs Bun vs Deno in 2026: Runtime Comparison with Real Benchmarksaunimeda
Web Development

Node.js vs Bun vs Deno in 2026: Runtime Comparison with Real Benchmarks

Bun 1.x is production-stable. Deno 2.0 supports npm packages. Node.js 22 has native TypeScript. The runtime landscape changed. Here's what the numbers actually show and when each runtime makes sense for real projects.

Need IT development for your business?

We build websites, mobile apps and AI solutions. Free consultation.

Get Consultation All articles