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