Skip to content
TECHNICAL SEO / FIELD NOTE 103

Next.js SEO: A Complete Senior's Guide (App Router, ISR, Metadata API)

Reading map: Rendering Modes and Their SEO Impact; App Router vs Pages Router: SEO Differences; The Metadata API: metadata.ts and generateMetadata; Sitemap and Robots at the Edge
A reading map of this field note. Download SVG ↓

If you've been building Next.js applications for more than a year, you already know that the shift from Pages Router to App Router wasn't just a routing change — it fundamentally rewired how metadata, rendering, and crawlability interact. In 2026, with Next.js 15 stable and Partial Prerendering (PPR) landing in production builds, the SEO surface area of a Next.js application has never been broader or more nuanced.

This guide is opinionated, deeply technical, and assumes you're comfortable with React Server Components, TypeScript, and the concept of rendering boundaries. We skip the basics. If you need a primer on what meta tags are, this isn't for you. If you're a senior engineer trying to ship a production-grade Next.js application that ranks, read on.

We'll cover the Metadata API end-to-end, ISR strategies that actually help crawlers, sitemap and robots generation at the edge, dynamic OG image generation, structured data patterns, and the common mistakes that cost real organic traffic.

Rendering Modes and Their SEO Impact

Before touching a single line of metadata code, you need to be clear-eyed about your rendering strategy. Each rendering mode produces fundamentally different HTML at crawl time, and Googlebot's behavior varies accordingly.

Comparison: SSG vs SSR vs ISR vs CSR for SEO

Mode HTML at Crawl Time TTFB Freshness SEO Suitability Next.js Mechanism
SSG (Static Site Generation) Full HTML, pre-built Fastest (CDN edge) Stale until redeploy Excellent for stable content generateStaticParams, no dynamic
ISR (Incremental Static Regeneration) Full HTML, periodically refreshed Fast (CDN with revalidation) Configurable staleness window Excellent for semi-dynamic content revalidate export, revalidatePath
SSR (Server-Side Rendering) Full HTML, per-request Slower (compute per request) Always fresh Good for highly dynamic, personalized dynamic = 'force-dynamic'
PPR (Partial Prerendering) Static shell + streamed dynamic slots Fast shell, streamed content Mixed per component Good — static shell is indexable experimental.ppr, Suspense boundaries
CSR (Client-Side Rendering) Empty shell, JS-rendered Fast shell, slow content Always fresh Poor — avoid for indexable content 'use client' with no server fallback

The practical rule: any URL you want indexed must return meaningful HTML without JavaScript execution. Googlebot does render JavaScript, but its crawl budget is finite and its rendering queue introduces multi-day delays. Don't rely on it.

App Router vs Pages Router: SEO Differences

Feature Pages Router App Router (Next.js 13+)
Metadata API next/head, manual <Head> File-based metadata export, generateMetadata()
Head deduplication Manual, error-prone Automatic via segment merging
Async metadata Requires workarounds Native async generateMetadata
Sitemap/Robots Custom API routes or files in /public File conventions: sitemap.ts, robots.ts
OG Image generation Custom API route with @vercel/og opengraph-image.tsx convention or route handler
JSON-LD injection Inside <Head> or custom _document.tsx Via metadata or inline <script> in RSC
RSC support No Yes — server-only data fetching for SEO data
Streaming & SEO No streaming Streaming via Suspense; head is always pre-sent

The App Router is not merely a new API — it's a different mental model. Metadata is no longer something you imperatively inject; it's something you export as data that Next.js compiles into the document head. This matters for SEO because metadata resolution happens at build time or request time on the server, never in the browser.

If you're maintaining a Pages Router application in 2026, migrating to App Router purely for SEO wins is a strong business case. The metadata deduplication alone eliminates entire categories of duplicate-tag bugs that silently damage rankings.

The Metadata API: metadata.ts and generateMetadata

Static Metadata Export

The simplest form is a static metadata export from any page.tsx or layout.tsx.

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

export const metadata: Metadata = {
  title: {
    default: 'Acme Corp — Enterprise Tooling',
    template: '%s | Acme Corp',
  },
  description: 'Acme Corp builds developer tooling for high-scale teams.',
  keywords: ['developer tools', 'CI/CD', 'observability'],
  authors: [{ name: 'Acme Engineering', url: 'https://acme.com/team' }],
  openGraph: {
    title: 'Acme Corp — Enterprise Tooling',
    description: 'Acme Corp builds developer tooling for high-scale teams.',
    url: 'https://acme.com',
    siteName: 'Acme Corp',
    images: [
      {
        url: 'https://acme.com/og/default.png',
        width: 1200,
        height: 630,
        alt: 'Acme Corp platform overview',
      },
    ],
    locale: 'en_US',
    type: 'website',
  },
  twitter: {
    card: 'summary_large_image',
    title: 'Acme Corp',
    description: 'Enterprise tooling for high-scale teams.',
    creator: '@acmecorp',
    images: ['https://acme.com/og/default.png'],
  },
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      'max-video-preview': -1,
      'max-image-preview': 'large',
      'max-snippet': -1,
    },
  },
  alternates: {
    canonical: 'https://acme.com',
  },
  metadataBase: new URL('https://acme.com'),
}

Note metadataBase — this is critical. Without it, relative URLs in openGraph.images or alternates will be resolved incorrectly, and you'll ship malformed OG tags to production silently.

Dynamic Metadata with generateMetadata

For routes where metadata depends on route params or fetched data, use generateMetadata. It colocates data-fetching with metadata generation and runs exclusively on the server.

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

type Props = {
  params: { slug: string }
  searchParams: { [key: string]: string | string[] | undefined }
}

export async function generateMetadata(
  { params }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {
  const post = await getPost(params.slug)

  if (!post) {
    return {
      title: 'Post Not Found',
      robots: { index: false },
    }
  }

  // Optionally inherit from parent layout metadata
  const previousImages = (await parent).openGraph?.images || []

  return {
    title: post.title,
    description: post.excerpt,
    alternates: {
      canonical: https://acme.com/blog/${post.slug},
    },
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      publishedTime: post.publishedAt,
      modifiedTime: post.updatedAt,
      authors: [post.author.url],
      images: [
        {
          url: /api/og?title=${encodeURIComponent(post.title)}&author=${encodeURIComponent(post.author.name)},
          width: 1200,
          height: 630,
        },
        ...previousImages,
      ],
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
    },
  }
}

export async function generateStaticParams() {
  const posts = await getAllPostSlugs()
  return posts.map((slug) => ({ slug }))
}

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

  return <article>{/* render post */}</article>
}

A critical architectural note: Next.js automatically deduplicates fetch calls within the same request using its built-in request memoization. The getPost call in generateMetadata and in the page component is only executed once. You don't need a shared cache layer at this level.

Sitemap and Robots at the Edge

Generating sitemap.ts

Drop a sitemap.ts file in the app directory. Next.js handles routing and content-type headers automatically.

// app/sitemap.ts
import type { MetadataRoute } from 'next'
import { getAllPosts } from '@/lib/api'

export const revalidate = 3600 // regenerate every hour

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts()

  const postEntries: MetadataRoute.Sitemap = posts.map((post) => ({
    url: https://acme.com/blog/${post.slug},
    lastModified: new Date(post.updatedAt),
    changeFrequency: 'weekly',
    priority: 0.8,
  }))

  return [
    {
      url: 'https://acme.com',
      lastModified: new Date(),
      changeFrequency: 'daily',
      priority: 1.0,
    },
    {
      url: 'https://acme.com/blog',
      lastModified: new Date(),
      changeFrequency: 'daily',
      priority: 0.9,
    },
    ...postEntries,
  ]
}

For large sites with tens of thousands of URLs, use sitemap indexes. Next.js supports multiple sitemap files via the generateSitemaps function:

// app/sitemap.ts (index approach for large sites)
import type { MetadataRoute } from 'next'

export async function generateSitemaps() {
  // Returns IDs used to generate individual sitemaps
  return [{ id: 0 }, { id: 1 }, { id: 2 }]
}

export default async function sitemap({
  id,
}: {
  id: number
}): Promise<MetadataRoute.Sitemap> {
  const PAGE_SIZE = 50_000
  const posts = await getPostsBatch({ offset: id * PAGE_SIZE, limit: PAGE_SIZE })

  return posts.map((post) => ({
    url: https://acme.com/blog/${post.slug},
    lastModified: new Date(post.updatedAt),
  }))
}

robots.ts Convention

// app/robots.ts
import type { MetadataRoute } from 'next'

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

Blocking AI crawlers like GPTBot, CCBot, and Claude-Web has become a standard production practice in 2026 for content-sensitive applications. Add them to your robots disallow rules deliberately.

ISR for SEO: Staleness, Revalidation, and On-Demand

ISR is the single most impactful rendering decision you can make for content-heavy Next.js applications. The pattern is: build once at deploy, serve from CDN edge, regenerate in the background when stale.

// app/blog/[slug]/page.tsx
// Time-based ISR — revalidate every 60 seconds
export const revalidate = 60

// Or fetch-level revalidation (more granular)
async function getPost(slug: string) {
  const res = await fetch(https://api.acme.com/posts/${slug}, {
    next: { revalidate: 300 }, // 5 minutes
  })
  if (!res.ok) throw new Error('Failed to fetch post')
  return res.json()
}

For content that should update immediately after a CMS publish event, use on-demand revalidation via a webhook handler:

// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const authHeader = request.headers.get('authorization')
  if (authHeader !== Bearer ${process.env.REVALIDATION_SECRET}) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const body = await request.json()
  const { slug, type } = body

  if (type === 'post') {
    // Revalidate specific path
    revalidatePath(/blog/${slug})
    // Also revalidate any tag-based cache entries
    revalidateTag(post-${slug})
    revalidateTag('posts-list')
  }

  return NextResponse.json({ revalidated: true, at: new Date().toISOString() })
}

The SEO implication: with on-demand ISR, Googlebot sees fresh content within seconds of a CMS publish, not hours. This matters for news sites, e-commerce with changing inventory, and documentation sites where stale content erodes trust signals.

See also: [Internal: Next.js Caching Deep Dive] for a full breakdown of the four caching layers in Next.js 15.

Dynamic OG Images with ImageResponse

Static OG images are table stakes. Dynamic OG images — generated per page with the actual title, author, and branding — measurably improve click-through rates from social shares.

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

export const runtime = 'edge'

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url)
  const title = searchParams.get('title') || 'Acme Corp'
  const author = searchParams.get('author') || 'Acme Team'

  // Load font from public directory or fetch from CDN
  const fontData = await fetch(
    new URL('/fonts/Inter-Bold.ttf', request.url)
  ).then((res) => res.arrayBuffer())

  return new ImageResponse(
    (
      <div
        style={{
          background: 'linear-gradient(135deg, #0f172a 0%, #1e3a5f 100%)',
          width: '100%',
          height: '100%',
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'space-between',
          padding: '60px 80px',
          fontFamily: 'Inter',
        }}
      >
        <div style={{ display: 'flex', alignItems: 'center' }}>
          <span style={{ color: '#60a5fa', fontSize: 28, fontWeight: 700 }}>
            ACME CORP
          </span>
        </div>
        <div
          style={{
            color: '#f8fafc',
            fontSize: title.length > 60 ? 48 : 64,
            fontWeight: 700,
            lineHeight: 1.2,
            maxWidth: 900,
          }}
        >
          {title}
        </div>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
          <span style={{ color: '#94a3b8', fontSize: 24 }}>by {author}</span>
          <span style={{ color: '#94a3b8', fontSize: 20 }}>acme.com</span>
        </div>
      </div>
    ),
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: 'Inter',
          data: fontData,
          style: 'normal',
          weight: 700,
        },
      ],
    }
  )
}

Running this on the Edge runtime means sub-50ms cold starts globally. Cache the output aggressively: set Cache-Control: public, max-age=86400, stale-while-revalidate=604800 and consider an upstream CDN in front of the route for high-traffic sites.

The file-convention alternative — app/blog/[slug]/opengraph-image.tsx — works for simpler cases and integrates directly with generateMetadata, but the API route approach gives you more control over caching headers and parameter validation.

React Server Components and Crawlability

RSC output is rendered HTML. Period. From a crawler's perspective, an RSC page is indistinguishable from traditional server-rendered HTML — which is exactly what you want.

The SEO-critical implication is that data fetching in Server Components does not require client-side hydration for the fetched data to appear in the HTML. Googlebot sees the fully-rendered content immediately.

// app/products/[id]/page.tsx
// This is a Server Component by default — no 'use client' directive

async function ProductPage({ params }: { params: { id: string } }) {
  // This fetch runs on the server. The result is in the initial HTML.
  const product = await fetch(https://api.acme.com/products/${params.id}, {
    next: { tags: [product-${params.id}] },
  }).then((r) => r.json())

  return (
    <main>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <div itemScope itemType="https://schema.org/Product">
        <meta itemProp="name" content={product.name} />
        <meta itemProp="description" content={product.description} />
        {/* Structured data via microdata — alternative to JSON-LD */}
      </div>
    </main>
  )
}

The boundary mistake that hurts SEO: marking a component as 'use client' when it doesn't need client interactivity, and then fetching SEO-critical data inside a useEffect. That data won't be in the initial HTML. Move data-fetching up to the nearest Server Component.

For more on RSC architecture patterns: [Internal: RSC Architecture Patterns for Production].

Partial Prerendering and SEO Implications

Partial Prerendering (PPR), stabilized in Next.js 15, is a hybrid rendering model: the static shell of a page is prerendered at build time, and dynamic slots are filled via streaming at request time. From an SEO perspective, the question is: what do crawlers see in the initial response?

// next.config.ts — enable PPR
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  experimental: {
    ppr: true, // or 'incremental' for opt-in per route
  },
}

export default nextConfig
// app/dashboard/page.tsx — PPR example
import { Suspense } from 'react'
import { StaticHero } from '@/components/StaticHero'
import { DynamicFeed } from '@/components/DynamicFeed'

// Mark page as PPR-enabled
export const experimental_ppr = true

export default function Page() {
  return (
    <main>
      {/* This renders at build time — immediately in HTML */}
      <StaticHero />

      {/* This streams in at request time — fallback is in initial HTML */}
      <Suspense fallback={<div>Loading feed...</div>}>
        <DynamicFeed />
      </Suspense>
    </main>
  )
}

The SEO answer on PPR: the static shell is fully indexable. The Suspense fallback appears in the initial HTML stream. If your dynamic content is SEO-critical (article body, product description), it must not live inside a Suspense boundary without a meaningful fallback. Move it to the static shell, or ensure the dynamic component resolves quickly enough to stream before Googlebot's rendering timeout.

PPR shines for authenticated dashboards where the shell (navigation, brand, static copy) is public and the content is user-specific. Don't use PPR as an excuse to put indexable content behind Suspense — that's the pattern that will cost you rankings.

JSON-LD and Structured Data Patterns

In the App Router, inject JSON-LD directly in Server Components using an inline <script> tag. This is cleaner than the next/head workaround required in Pages Router.

// app/blog/[slug]/page.tsx
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug)

  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: post.title,
    description: post.excerpt,
    image: https://acme.com/api/og?title=${encodeURIComponent(post.title)},
    datePublished: post.publishedAt,
    dateModified: post.updatedAt,
    author: {
      '@type': 'Person',
      name: post.author.name,
      url: post.author.url,
    },
    publisher: {
      '@type': 'Organization',
      name: 'Acme Corp',
      logo: {
        '@type': 'ImageObject',
        url: 'https://acme.com/logo.png',
      },
    },
    mainEntityOfPage: {
      '@type': 'WebPage',
      '@id': https://acme.com/blog/${post.slug},
    },
  }

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <article>
        <h1>{post.title}</h1>
        {/* post content */}
      </article>
    </>
  )
}

For product pages, use Product schema with Offer and AggregateRating. For FAQ pages, use FAQPage schema. Google's rich result types are well-documented at Google Search Central — Structured Data.

Related reading: [Internal: Schema Markup Patterns for E-commerce in 2026].

FAQ

Does Next.js App Router automatically handle canonical URLs?

No. You must set alternates.canonical in your metadata explicitly. Next.js will not infer canonical URLs from the current route. Failing to set canonicals is one of the top reasons Next.js sites develop duplicate content issues — particularly when you have pagination, filtered URLs (?sort=price), or locale prefixes (/en/, /de/). Always set metadataBase at the root layout level and override alternates.canonical per page.

How does Next.js handle hreflang for multilingual SEO?

Use the alternates.languages field in the metadata object. Next.js will render the correct <link rel="alternate" hreflang="..."> tags in the document head. Combine this with Next.js middleware for locale detection and the [locale] segment in your App Router structure. The x-default hreflang is supported — include it explicitly for your default language URL.

export const metadata: Metadata = {
  alternates: {
    canonical: 'https://acme.com/en/blog/my-post',
    languages: {
      'en-US': 'https://acme.com/en/blog/my-post',
      'de-DE': 'https://acme.com/de/blog/my-post',
      'x-default': 'https://acme.com/en/blog/my-post',
    },
  },
}

What's the best way to handle noindex for dynamic routes?

Return robots: { index: false } from generateMetadata based on the data state. For example, draft posts, out-of-stock products, or empty search result pages should be noindexed. Do not rely on X-Robots-Tag headers alone — set both the meta robots tag and the HTTP header for belt-and-suspenders coverage. In Next.js, you can set response headers via middleware or route handlers.

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

  if (!post || post.status === 'draft') {
    return {
      robots: { index: false, follow: false },
    }
  }
  // ... normal metadata
}

How should I handle paginated content for SEO in App Router?

Google deprecated rel=prev/next pagination hints in 2019. The current best practice is: set a canonical on each paginated page pointing to itself (not page 1), ensure each page has unique, meaningful content in the title and description, and avoid noindexing paginated pages unless they're truly thin. For infinite scroll patterns, provide a paginated HTML fallback — do not rely on JavaScript-loaded content for indexable items. Use searchParams in your App Router page to drive server-side pagination and ensure each page URL returns the correct content.

Does using the Edge runtime affect SEO?

The Edge runtime affects what Node.js APIs are available (no filesystem, no native modules), but it does not affect the HTML output that crawlers see. Edge-rendered pages are served from globally distributed compute, which improves TTFB — a Core Web Vitals signal that indirectly influences SEO. Use Edge runtime for middleware (locale detection, A/B testing, auth redirects) and for OG image generation. For full page rendering, the Node.js runtime gives you more flexibility; the Edge runtime is best for latency-sensitive endpoints. See the Next.js Edge runtime documentation for the full API surface comparison.

How do I prevent Next.js from exposing internal routes in my sitemap?

Be explicit in your sitemap.ts — only include URLs you want indexed. Don't dynamically enumerate your file system or route tree; enumerate from your data source (CMS, database). Additionally, use robots.ts to disallow /api/, /_next/, /admin/, and any other internal paths. Verify your sitemap with Google Search Console after every major routing change.

Is metadata in layout.tsx inherited by nested pages?

Yes, with merging semantics. Next.js performs a deep merge of metadata from the root layout down through nested layouts to the page. For simple string fields like description, the most specific (deepest) value wins. For the title field, use the template pattern in parent layouts and default fallbacks. For openGraph, you'll need to explicitly re-declare nested layout values in page-level metadata or use the parent parameter in generateMetadata to inherit and extend them. Don't assume complex objects merge automatically — test with the Next.js metadata debugger or inspect the rendered head in staging.

Key Takeaways

  • Always set metadataBase at the root layout. Every relative URL in your metadata resolves against it. Forgetting this is a silent production bug.
  • Use generateMetadata for dynamic routes — it's async, server-only, and deduplicates fetches with the page component via Next.js request memoization.
  • ISR + on-demand revalidation is the correct default for content sites. Time-based ISR alone leaves a revalidation window where crawlers see stale content.
  • Don't put SEO-critical content inside Suspense boundaries in PPR pages unless you understand exactly what the crawler receives in the initial HTML stream.
  • JSON-LD in Server Components via dangerouslySetInnerHTML is the canonical App Router pattern — it's rendered server-side and never requires client JavaScript.
  • Block AI crawlers deliberately in robots.ts. Make this an explicit decision, not an oversight.
  • Canonical URLs are not automatic. Set alternates.canonical on every indexable page. For filtered, sorted, or paginated URLs, this is non-negotiable.
  • Dynamic OG images on the Edge runtime are fast enough to serve on every request without a separate pre-generation pipeline. Cache them at the CDN layer.
  • The App Router's sitemap and robots file conventions are production-ready. Stop writing custom API routes for these unless you have highly specific requirements.
  • RSC is your SEO baseline. If a component fetches data and that data must be indexed, it must be a Server Component. 'use client' + useEffect data fetching is invisible to crawlers.

Conclusion

Next.js 14/15 with the App Router gives you one of the most capable SEO platforms available in 2026. The Metadata API eliminates entire classes of duplicate-tag bugs. ISR with on-demand revalidation solves the freshness problem without sacrificing edge performance. RSC makes the correct pattern — server-rendered data — the path of least resistance. PPR opens the door to genuinely hybrid pages where performance and indexability aren't a trade-off.

The failure modes have shifted too. The old mistakes — forgetting to add meta tags, shipping CSR-only pages — are harder to make accidentally. The new mistakes are subtler: forgetting metadataBase, putting canonical-worthy content inside Suspense, over-relying on time-based ISR when on-demand revalidation is available, or marking components as client components unnecessarily and losing server-rendered data in the process.

Treat SEO as a first-class architectural concern, not a checklist to run before launch. Your rendering decisions, caching strategy, and data-fetching patterns are your SEO strategy. In Next.js App Router, they're one and the same.

For a practical audit of your existing Next.js application: [Internal: Next.js SEO Audit Checklist — 2026 Edition].

YOUR READING CHECKLIST

Make the ideas stick.

Mark the sections you’ve worked through. Saved in this browser.

0 of 4 reviewed
Andrii Stanetskyi
ABOUT THE AUTHOR

Andrii Stanetskyi

Head of SEO / Technical SEO Lead based in Tallinn, Estonia. Technical architecture, enterprise eCommerce, Python automation, and AI-assisted workflows.

More about Andrii ↗
LET’S FIND THE REAL BOTTLENECK

A clearer picture.
A practical next step.

Get a focused SEO audit or a consultation on your next technical decision. We’ll agree on the scope and fee before any work begins.

01 / Diagnose02 / Prioritize03 / Plan
How can I help?

Scope and fee agreed before any work begins.

Choose your language

Explore SEO services in 26 languages. Journal articles retain their original language.

ENEnglish↗DEDeutsch↗FRFrançais↗ESEspañol↗ITItaliano↗PTPortuguês↗NLNederlands↗PLPolski↗SVSvenska↗DADansk↗FISuomi↗NONorsk↗ETEesti↗LVLatviešu↗LTLietuvių↗CSČeština↗RORomână↗HUMagyar↗ELΕλληνικά↗BGБългарски↗HRHrvatski↗SKSlovenčina↗SLSlovenščina↗RUРусский↗UKУкраїнська↗TRTürkçe↗
LET’S WORK ON YOUR WEBSITE
A CLEAR NEXT STEP

Let’s talk
about your site.

A focused SEO audit or a conversation about a specific challenge. Tell me where you are and what you want to change.

Andrii Stanetskyi
Andrii StanetskyiHead of SEO / Technical SEO Lead
[email protected] ↗
How can I help?

Scope and fee agreed before any work begins.