SEO

  • SEO
  • Technical SEO
  • Next.js
  • Core Web Vitals
  • Structured Data

Technical SEO: The Implementation Layer (A Complete Field Guide)

Technical SEO from rendering to Search Console: metadata, canonicals, JSON-LD, Core Web Vitals, crawl budget, internal links, and JavaScript pitfalls explained.

· Updated Jun 7, 2026· 26 min read· Varnox Team

Most SEO writing stops at titles, descriptions, and keyword maps. That work matters, but it does not decide whether a page earns a stable place in the index. What decides that is the layer beneath the copy — how HTML is produced, how assets load, how crawlers reconcile URLs, and how your stack signals trust.

This guide covers that layer: rendering choices, the full <head>, images, structured data, sitemaps and robots, Core Web Vitals, redirects, crawl hygiene, JavaScript pitfalls, internationalization, pagination, E-E-A-T signals, and Search Console habits worth keeping weekly. If you have published strong content that still refused to rank, the answer is rarely “more content.” It is usually here.

Who it is for: engineers, technical leads, and founders who own the codebase — or who need to audit an agency’s work with confidence.


1. Rendering mode and its SEO consequences

The single biggest technical SEO decision for modern web apps is how the HTML is produced.

Server-Side Rendering (SSR)

The server generates a complete HTML response for every request. Googlebot receives fully populated <head> tags, visible body text, and structured data in the first byte. SSR is the safest choice for pages that must rank and change frequently (e-commerce, news, lead-gen landing pages).

Next.js App Router default: All Server Components render on the server. The HTML is complete before it reaches the client.

Static Site Generation (SSG)

HTML is pre-built at deploy time and served from a CDN edge node. Fastest possible TTFB (time to first byte). Ideal for content that changes infrequently — blog posts, documentation, service pages.

In Next.js: Any page.tsx that does not use runtime data is automatically static. Use generateStaticParams for dynamic routes.

Incremental Static Regeneration (ISR)

Pages are statically generated but can revalidate on a schedule or on-demand. Useful for content that changes occasionally (product pages, news). Google treats these as normal pages once they are indexed.

Client-Side Rendering (CSR) — avoid for indexable content

Content renders entirely in the browser via JavaScript. Googlebot can execute JavaScript, but the crawl/render pipeline is asynchronous and delayed by potentially hours or days. Content in CSR components is indexed less reliably, especially for new or low-authority pages.

Rule: Any text, heading, or link that matters for SEO must be in the initial HTML response — not populated by a client-side useEffect or API call after hydration.

Practical test

In Chrome: View Source (Ctrl+U). If your target text appears in the raw HTML, search engines see it immediately. If it does not — you have a rendering SEO problem regardless of how the page looks in the browser.


2. The complete <head> — every tag explained

<title> tag

<title>WireGuard VPN Setup for Remote Teams | Varnox</title>
  • Between 50–60 characters (Google truncates at roughly 600px of pixel width, not character count)
  • Unique per page — no templates that produce identical titles
  • Front-load the keyword — "WireGuard VPN Setup" not "Varnox | WireGuard VPN Setup"
  • Do not keyword stuff; it reads like spam

In Next.js App Router:

// app/services/wireguard-vpn/page.tsx
export const metadata: Metadata = {
  title: "WireGuard VPN Setup for Remote Teams | Varnox",
}

Or use the template pattern in layout.tsx:

// app/layout.tsx
export const metadata: Metadata = {
  title: {
    default: "Varnox",
    template: "%s | Varnox",
  },
}

Meta description

<meta name="description" content="Self-hosted WireGuard VPN for remote teams — server setup, client profiles for all platforms, key lifecycle, and written handoff documentation. Request a free quote." />
  • 140–160 characters
  • Not a ranking factor directly, but influences click-through rate from SERPs
  • Summarise the page value, not the company
  • Google may rewrite descriptions frequently — write a good one anyway for CTR when it is shown

Meta robots

<meta name="robots" content="index, follow" />

Defaults to index, follow if absent. Use it explicitly when you need to restrict:

<meta name="robots" content="noindex, nofollow" />   <!-- staging, thank-you pages -->
<meta name="robots" content="noindex, follow" />      <!-- paginated duplicates -->
<meta name="robots" content="index, nofollow" />      <!-- rare; content you trust but links you do not -->

X-Robots-Tag header — Same instructions in an HTTP response header. Applies to non-HTML files (PDFs, images). Set in NGINX or Next.js middleware:

add_header X-Robots-Tag "noindex, nofollow";

Canonical URL

<link rel="canonical" href="https://varnox.io/services/wireguard-vpn" />

Tells Google which URL to index when the same (or similar) content is accessible at multiple URLs — pagination, UTM parameters, session IDs, http vs https, trailing slashes.

Critical implementation details:

  • Self-referencing canonicals on every indexable page — not just duplicates
  • Canonical must be absolute, not relative
  • metadataBase in Next.js must be set or canonical URLs resolve incorrectly
// app/layout.tsx
export const metadata: Metadata = {
  metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL ?? "https://varnox.io"),
}

Open Graph tags

<meta property="og:type" content="website" />
<meta property="og:title" content="WireGuard VPN Setup for Remote Teams" />
<meta property="og:description" content="Self-hosted WireGuard VPN — server, keys, client profiles, and ops documentation." />
<meta property="og:image" content="https://varnox.io/opengraph-image" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://varnox.io/services/wireguard-vpn" />
<meta property="og:site_name" content="Varnox" />

OG tags control how your pages appear when shared on LinkedIn, Slack, WhatsApp, Facebook, and other platforms. They do not affect Google rankings directly but matter for CTR on social traffic.

OG image requirements:

  • 1200 × 630px minimum (2400 × 1260px for Retina)
  • Under 8MB
  • og:image:type specifying image/jpeg or image/png avoids parsing errors
  • Use absolute URLs including https://

In Next.js — opengraph-image.tsx:

// app/opengraph-image.tsx
import { ImageResponse } from "next/og"

export const size = { width: 1200, height: 630 }
export const contentType = "image/png"

export default function OGImage() {
  return new ImageResponse(
    <div style={{ background: "#09090b", width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center" }}>
      <h1 style={{ color: "white", fontSize: 64 }}>Varnox</h1>
    </div>
  )
}

Twitter Card tags

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@varnoxio" />
<meta name="twitter:title" content="WireGuard VPN Setup for Remote Teams" />
<meta name="twitter:description" content="Self-hosted WireGuard — server, keys, client profiles, and ops docs." />
<meta name="twitter:image" content="https://varnox.io/opengraph-image" />

Twitter (X) falls back to og: tags if twitter: tags are absent, but explicit tags prevent rendering bugs on edge cases.


3. Image SEO — the complete picture

Images are commonly the worst-optimised SEO element on a site. Here is every lever:

Alt text — why it matters and how to write it

Alt text serves two purposes: accessibility (screen readers announce it) and SEO (text signal for image search and surrounding context).

Rules:

  1. Informative images — describe what the image shows and its purpose on the page. Do not start with "image of" or "photo of".
<!-- Wrong -->
<img src="wireguard-setup.png" alt="image of WireGuard setup" />

<!-- Right -->
<img src="wireguard-setup.png" alt="WireGuard peer configuration file showing AllowedIPs and DNS settings" />
  1. Decorative images — empty alt, not missing alt. A missing alt attribute is a parsing error; an empty alt="" tells screen readers to skip it.
<!-- Background decoration, icon without adjacent text label -->
<img src="bg-dots.svg" alt="" role="presentation" />
  1. Linked images — alt text describes the link destination, not the image contents.
<a href="/services/wireguard-vpn">
  <img src="vpn-icon.svg" alt="WireGuard VPN setup service" />
</a>
  1. In Next.js <Image />alt is required by the component. ESLint will catch it, but the alt value still needs to be meaningful.
import Image from "next/image"

<Image
  src="/screenshots/wireguard-config.png"
  alt="WireGuard server configuration file with peers and allowed IP ranges"
  width={1200}
  height={800}
/>

Image formats

FormatBest forNotes
WebPPhotographs, complex graphics~30% smaller than JPEG at same quality
AVIFPhotographs~50% smaller, better quality; slower to encode
SVGIcons, logos, line artVector — scales without quality loss
PNGScreenshots, transparencyLossless; use WebP equivalent where possible

In Next.jsnext/image automatically serves WebP/AVIF via the Accept header when using the default loader. Set formats in next.config.ts if you want explicit control:

images: {
  formats: ["image/avif", "image/webp"],
}

Image dimensions and CLS

Always specify width and height on images. Without them, the browser does not know how much space to reserve, causing layout shift (bad for CLS).

<Image src="/hero.png" width={1200} height={630} alt="..." />

For responsive images with unknown aspect ratios, use fill with a positioned parent:

<div style={{ position: "relative", aspectRatio: "16 / 9" }}>
  <Image src="/hero.png" fill alt="..." style={{ objectFit: "cover" }} />
</div>

Lazy loading and priority

All images below the fold should be lazy-loaded (Next.js Image default). The hero image and first visible content image must NOT be lazy-loaded — set priority={true} to preload it.

<Image src="/hero.jpg" priority alt="..." width={1200} height={600} />

A lazy-loaded LCP image is one of the most common Core Web Vitals failures.

Image file naming and URL structure

/images/wireguard-server-setup-linux.webp    ✓ — descriptive, hyphenated
/images/img_20260101_142355.jpg              ✗ — meaningless
/images/wireguard setup server linux!!.jpg   ✗ — spaces and special characters

4. Structured data (JSON-LD) — complete schema reference

Structured data helps Google understand what a page is about and qualify it for rich results where still eligible — review stars, breadcrumb trails, sitelinks search boxes, and article features. FAQ dropdowns in Google Search are not one of them for typical business sites (see FAQPage below).

Always use JSON-LD (not Microdata or RDFa). Place it in a <script type="application/ld+json"> tag, server-rendered.

Organization schema

Goes on every page. Establishes your brand identity, logo, and social profiles in Knowledge Graph.

{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": "Varnox",
  "url": "https://varnox.io",
  "logo": "https://varnox.io/varnox_dark_default.svg",
  "description": "IT services: web apps, Linux servers, WireGuard VPN, and business VoIP.",
  "email": "[email protected]",
  "sameAs": [
    "https://www.linkedin.com/company/varnoxio/",
    "https://github.com/varnoxio"
  ]
}

WebSite schema

Use on the homepage. Add SearchAction only if you have working on-site search — do not point at a URL that 404s.

{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "Varnox",
  "url": "https://varnox.io"
}

Enables breadcrumb rich results in SERPs. Implement on every page except the homepage.

{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://varnox.io/"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Services",
      "item": "https://varnox.io/services"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "WireGuard VPN"
    }
  ]
}

FAQPage schema

FAQPage schema marks up question-and-answer content so search engines and assistants can parse individual Q&A pairs.

Google removed FAQ rich results for most sites (August 2023, still true in 2026): Expandable FAQ accordions in Google Search now appear only for a narrow set of well-known government and health/medical sources. For service, local, and commercial sites — including NYC businesses — FAQPage JSON-LD will not produce FAQ snippets in Google. Do not sell or buy SEO on the promise of FAQ dropdowns.

Still worth doing when FAQs are visible on the page:

  • Helps readers scan answers on the page (primary win)
  • Supports Bing and other engines that may still surface FAQ-style results
  • Improves machine-readability for AI Overviews and assistants (without guaranteeing placement)

Do not: Add FAQ markup for questions that are not shown on the page, or treat FAQ schema as a ranking tactic.

{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "What is WireGuard?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "WireGuard is an open-source VPN protocol built into the Linux kernel. It uses modern cryptography and is significantly simpler to configure than OpenVPN or IPsec."
      }
    }
  ]
}

Service / ProfessionalService schema

For service-based businesses — helps Google understand what you offer.

{
  "@context": "https://schema.org",
  "@type": "ProfessionalService",
  "name": "Varnox — WireGuard VPN Setup",
  "serviceType": "VPN Configuration",
  "provider": {
    "@type": "Organization",
    "name": "Varnox"
  },
  "areaServed": "Worldwide",
  "description": "Self-hosted remote-access VPN using WireGuard..."
}

Article / BlogPosting schema

For blog content. Helps search engines understand authorship and dates; does not guarantee any specific SERP feature.

{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "Technical SEO: The Complete Implementation Guide",
  "author": {
    "@type": "Organization",
    "name": "Varnox",
    "url": "https://varnox.io/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "Varnox",
    "logo": {
      "@type": "ImageObject",
      "url": "https://varnox.io/varnox_dark_default.svg"
    }
  },
  "datePublished": "2026-05-01",
  "dateModified": "2026-05-01",
  "image": "https://varnox.io/blog/technical-seo/og.png"
}

HowTo schema

For step-by-step guides — structured as a series of named steps.

Google removed HowTo rich results in September 2023 (same announcement as FAQPage). HowTo schema can still describe steps for readers and other systems, but do not count on Google showing HowTo carousels for general technical content.

{
  "@context": "https://schema.org",
  "@type": "HowTo",
  "name": "How to Set Up WireGuard VPN on Ubuntu",
  "step": [
    {
      "@type": "HowToStep",
      "name": "Install WireGuard",
      "text": "Run: sudo apt install wireguard"
    }
  ]
}

Review / AggregateRating schema

If you have genuine client reviews, this adds star ratings to SERPs.

{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": "Varnox",
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "5",
    "reviewCount": "12"
  }
}

Only add this if you have real reviews. Fake structured data ratings violate Google's guidelines and can result in manual penalties.


5. Sitemap.xml — complete implementation

A sitemap tells Google which URLs exist and should be crawled. It does not guarantee indexing but speeds up discovery.

What to include

  • All indexable pages: homepage, service pages, blog posts, category pages, about, contact
  • Canonical URL only (not ?utm_source=email variants)
  • Updated <lastmod> date reflecting real content changes (not today's date on every page — Google will stop trusting it)
  • <priority> and <changefreq> are largely ignored by Google but harmless

What to exclude

  • /admin/*, /api/*, /staging/* — not indexable
  • Paginated pages beyond page 1 (use canonical instead)
  • Thank-you and confirmation pages
  • URLs with noindex meta tag — never include them in sitemap

Next.js App Router — app/sitemap.ts

import type { MetadataRoute } from "next"
import { getAllPosts } from "@/lib/content"
import { SERVICE_SLUGS } from "@/lib/service-pages"

const BASE = process.env.NEXT_PUBLIC_SITE_URL ?? "https://varnox.io"

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

  const staticPages: MetadataRoute.Sitemap = [
    { url: `${BASE}/`, lastModified: new Date(), changeFrequency: "weekly", priority: 1.0 },
    { url: `${BASE}/services`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.9 },
    { url: `${BASE}/about`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.7 },
    { url: `${BASE}/contact`, lastModified: new Date(), changeFrequency: "monthly", priority: 0.8 },
    { url: `${BASE}/blog`, lastModified: new Date(), changeFrequency: "weekly", priority: 0.7 },
  ]

  const servicePages: MetadataRoute.Sitemap = SERVICE_SLUGS.map((slug) => ({
    url: `${BASE}/services/${slug}`,
    lastModified: new Date(),
    changeFrequency: "monthly" as const,
    priority: 0.85,
  }))

  const postPages: MetadataRoute.Sitemap = posts.map((p) => ({
    url: `${BASE}/blog/${p.slug}`,
    lastModified: new Date(p.updatedAt ?? p.publishedAt),
    changeFrequency: "monthly" as const,
    priority: 0.6,
  }))

  return [...staticPages, ...servicePages, ...postPages]
}

Submit sitemap to Google Search Console

After deployment, go to Search Console → Sitemaps and submit your canonical sitemap URL (for example https://varnox.io/sitemap.xml on this site). Monitor for errors weekly.


6. Robots.txt — the right configuration

User-agent: *
Allow: /

Disallow: /api/
Disallow: /_next/
Disallow: /admin/

Sitemap: https://varnox.io/sitemap.xml

Common mistakes:

  • Disallow: / — blocks your entire site. Seen more often than you would believe after a staging environment robots.txt gets promoted to production.
  • Disallowing CSS and JavaScript files — prevents Google from rendering your pages correctly
  • Crawl-delay — Google ignores this directive; use Search Console's crawl rate settings instead
  • Missing Sitemap: directive — a missed opportunity for faster crawling

In Next.js App Router — app/robots.ts:

import type { MetadataRoute } from "next"

export default function robots(): MetadataRoute.Robots {
  const BASE = process.env.NEXT_PUBLIC_SITE_URL ?? "https://varnox.io"
  return {
    rules: [
      { userAgent: "*", allow: "/", disallow: ["/api/", "/admin/"] },
    ],
    sitemap: `${BASE}/sitemap.xml`,
  }
}

7. Core Web Vitals — every metric explained with fixes

Core Web Vitals are Google's primary page experience signals. They affect ranking, especially in competitive niches.

LCP — Largest Contentful Paint

Measures when the largest visible element (usually a hero image or H1) finishes loading. Target: under 2.5 seconds.

What counts as LCP element:

  • <img> and <image> elements
  • <video> poster images
  • Elements with CSS background-image
  • Block-level elements containing text

LCP killers:

  1. Hero image is lazy-loaded — set loading="eager" or priority={true} in Next.js
  2. Image served without srcset — full resolution downloaded on mobile
  3. Large TTFB — server response slow; fix with caching, CDN, or SSG
  4. Render-blocking resources — unoptimised fonts, synchronous scripts
  5. No preload for LCP image — add <link rel="preload" as="image" href="..."> in <head>

Fix in Next.js:

// Hero image — must have priority
<Image
  src="/hero.jpg"
  alt="Server room"
  width={1400}
  height={700}
  priority          // ← generates <link rel="preload">
  sizes="(max-width: 768px) 100vw, 50vw"
/>
// next.config.ts — ensure images are optimised
const nextConfig = {
  images: {
    formats: ["image/avif", "image/webp"],
    minimumCacheTTL: 2592000, // 30 days
  },
}

INP — Interaction to Next Paint (replaced FID in 2024)

Measures the delay between user interaction and visual response. Target: under 200ms.

INP killers:

  1. Heavy JavaScript executing on the main thread during interaction
  2. Large event listeners that do expensive DOM operations
  3. Third-party scripts (analytics, chat widgets) blocking the main thread
  4. Long tasks (>50ms) in synchronous JavaScript

Fixes:

  • Use React.memo, useMemo, useCallback to prevent unnecessary re-renders
  • Break long tasks with scheduler.postTask() or setTimeout(fn, 0)
  • Load third-party scripts with strategy="lazyOnload" in Next.js <Script>
  • Avoid synchronous localStorage access in components
import Script from "next/script"

// Google Analytics — load after page is interactive, not during
<Script
  src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXX"
  strategy="lazyOnload"
/>

CLS — Cumulative Layout Shift

Measures unexpected layout movement during page load. Target: under 0.1.

CLS killers:

  1. Images without width/height attributes
  2. Ads or embeds inserted without reserved space
  3. Dynamically injected content above existing content
  4. Custom font loading causing text to reflow (FOUT/FOIT)
  5. Animations that move layout elements (use transform instead of top/left/margin)

Image dimensions — the most common fix:

// Always specify dimensions or use fill with positioned container
<Image src="/logo.png" width={132} height={36} alt="Varnox" />

// For responsive unknown heights:
<div style={{ position: "relative", aspectRatio: "16/9" }}>
  <Image src="/hero.jpg" fill style={{ objectFit: "cover" }} alt="..." />
</div>

Font loading — eliminate FOUT:

/* In globals.css — use font-display: optional for critical fonts */
@font-face {
  font-family: "Syne";
  font-display: optional; /* renders fallback if font is not cached; no swap flash */
}

In Next.js, use next/font — it automatically adds font-display: optional and generates the size-adjust fallback to eliminate layout shift:

import { Syne, Plus_Jakarta_Sans } from "next/font/google"

const syne = Syne({
  subsets: ["latin"],
  weight: ["700", "800"],
  display: "optional",
  variable: "--font-heading",
})

8. URL structure and redirects

URL best practices

Good:  /services/wireguard-vpn-setup
Bad:   /services?id=4&type=vpn
Bad:   /Services/WireGuard_VPN_Setup
Bad:   /s/wg
  • All lowercase, hyphens not underscores, no query parameters for primary content
  • Keep URLs stable — changing URLs requires 301 redirects and costs some link equity
  • Shorter is generally better (under 80 characters), but do not keyword-stuff

301 vs 302 vs 410

CodeMeaningSEO effect
301 Permanent redirectPage has moved permanentlyPasses most link equity to the new URL
302 Temporary redirectPage has moved temporarilyLink equity stays on original URL
307/308Same as 302/301 but method-preservingRarely needed; use 301 for SEO purposes
410 GonePage no longer exists and will not returnGoogle deindexes faster than a 404
404 Not FoundPage not foundGoogle eventually deindexes

Use 410 (not 404) for pages you intentionally removed — it signals Google to deindex faster.

Trailing slash consistency

Pick one (/services/ or /services) and enforce it via redirect. In Next.js:

// next.config.ts
const nextConfig = {
  trailingSlash: false, // or true — pick one and stick to it
}

Redirect chains

Each redirect in a chain loses a small amount of link equity and adds latency. Keep it to one hop:

Bad:  /old-page → /intermediate-page → /new-page  (2 hops)
Good: /old-page → /new-page  (1 hop)

Audit existing redirects quarterly and collapse chains.


9. Crawl budget — when it matters and how to optimise it

Crawl budget = the number of URLs Googlebot will crawl on your site within a given period. For sites under 1,000 pages it is rarely a constraint. It becomes important at scale (10,000+ pages) or for large e-commerce sites.

What wastes crawl budget

  • Infinite scroll or faceted navigation generating thousands of unique URLs
  • UTM parameters and session IDs creating URL variants of the same content
  • Soft 404 pages (pages returning 200 with "not found" content)
  • Blocked but linked resources
  • Redirect loops
  • URLs with tracking parameters in internal links

How to preserve it

  1. Use rel="canonical" on faceted/filtered pages to consolidate signals
  2. Block parameter-based URLs in robots.txt if they generate no unique content
  3. Ensure all noindex pages are not linked prominently from index pages
  4. Fix soft 404s — pages that say "no results" but return HTTP 200

10. Internal linking strategy

Internal links pass authority between pages and help Google discover new content.

High-impact internal linking patterns

  1. Pillar → cluster — A comprehensive service page links to related blog posts; blog posts link back to the service page. This creates topical authority.

  2. Sidebar / related content — Every blog post links to 3 related posts + the most relevant service page.

  3. In-body contextual links — Links embedded in running paragraph text carry more weight than navigation links. Anchor text should be descriptive:

<!-- Good: descriptive anchor text -->
<a href="/services/wireguard-vpn">WireGuard VPN setup</a>

<!-- Bad: generic anchor text -->
<a href="/services/wireguard-vpn">click here</a>
  1. Breadcrumbs — Implemented as <nav> with BreadcrumbList schema. Confirm every page has them.

  2. Footer links — Good for important pages (services, contact, about). Do not put every post in the footer.

Crawl your site with Screaming Frog or Sitebulb (free tier available). Look for:

  • Pages with zero internal inbound links (orphaned pages)
  • Pages with only one internal link (underpowered)
  • Broken internal links (4xx)
  • Redirect chains in internal links (link directly to the final destination)

11. HTTP headers and server configuration for SEO

These are set in NGINX or via Next.js middleware — most site owners never touch them.

HTTPS and HSTS

All sites must serve over HTTPS. HTTP pages rank lower and trigger browser warnings. HSTS (HTTP Strict Transport Security) tells browsers to always use HTTPS:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

Add to the HSTS preload list at hstspreload.org for maximum protection.

Correct HTTP status codes

  • 200 for all live, indexable pages
  • 301 for permanent moves
  • 404/410 for deleted pages — never return 200 with empty/error content
  • 503 for planned maintenance with Retry-After header — Google will not deindex during temporary downtime

Compression

Uncompressed HTML increases TTFB. Enable gzip in NGINX (built-in) or Brotli if you have the module:

# gzip — included in all NGINX builds
gzip on;
gzip_types text/html text/css application/javascript application/json;
gzip_comp_level 6;

# Brotli — requires ngx_brotli module (not in stock NGINX packages)
# On Ubuntu: apt install libnginx-mod-brotli
# Brotli compresses ~15-20% better than gzip at equivalent CPU cost
# brotli on;
# brotli_comp_level 6;
# brotli_types text/html text/css application/javascript application/json;

Cache-Control for static assets

Long cache TTLs for fingerprinted assets (CSS, JS, images) drastically improve repeat visit performance:

location ~* \.(css|js|woff2|svg|png|webp|avif)$ {
  add_header Cache-Control "public, max-age=31536000, immutable";
}

12. JavaScript SEO pitfalls

Client-rendered content

Content in useEffect, useState populated after hydration, or fetched client-side via fetch is not in the initial HTML. Google's render queue delays indexing of this content by hours to days on new pages.

Audit: Disable JavaScript in your browser (DevTools → Settings → Disable JavaScript) and check that all critical content is still visible.

Event listeners not recognised by crawlers

Googlebot follows href in <a> tags. Buttons with onClick handlers that navigate or open content are not followed.

<!-- Bad: Googlebot cannot follow this -->
<button onClick={() => router.push("/services")}>Our services</button>

<!-- Good -->
<a href="/services">Our services</a>

Hydration errors

Mismatches between server-rendered HTML and client-rendered HTML cause React to re-render from scratch, destroying the SSR content temporarily. Causes inconsistent indexing.

Fix hydration issues — they appear as warnings in the browser console: Warning: Text content did not match.

Dynamic import() and code splitting

Components loaded with dynamic(() => import(...)) are client-side by default in Next.js. Content inside them will not appear in the initial HTML unless you specify ssr: true (the default for App Router is SSR unless you add 'use client').


13. hreflang for multilingual sites

If you serve the same content in multiple languages or regions, hreflang tells Google which version to show which user:

<link rel="alternate" hreflang="en" href="https://varnox.io/services/wireguard-vpn" />
<link rel="alternate" hreflang="en-gb" href="https://varnox.io/gb/services/wireguard-vpn" />
<link rel="alternate" hreflang="x-default" href="https://varnox.io/services/wireguard-vpn" />

Rules:

  • Must be bidirectional — every alternate page must reference all others including itself
  • x-default is the fallback for users not matching any language
  • Can also be implemented in sitemap.xml instead of <head>
  • Incorrect hreflang is one of the most common international SEO issues

14. Pagination

rel="prev" and rel="next" were deprecated by Google in 2019. They are now ignored.

Current best practice:

  1. Infinite scroll — load additional content into the page and update the URL with history.replaceState(). Each incremental URL should have a canonical pointing to the main paginated URL (or stand alone if content warrants it).

  2. Paginated pages — each /blog?page=2 URL can be indexed independently with its own canonical, OR you can canonical all pages back to /blog if they do not offer unique indexable value.

  3. What NOT to donoindex on pages 2+ combined with no next/prev signals. This prevents indexing of posts that only appear on page 2 and beyond.


15. E-E-A-T — Experience, Expertise, Authoritativeness, Trustworthiness

E-E-A-T is not a direct ranking factor (there is no E-E-A-T score) but describes what Quality Raters assess. High E-E-A-T correlates with ranking durability.

Practical implementation signals:

  1. Author pages — named authors with bios, credentials, and LinkedIn links
  2. Person schema on author pages with sameAs links to LinkedIn
  3. datePublished and dateModified — always include and keep accurate
  4. About page — company history, team, credentials
  5. Cited external sources — outbound links to authoritative references signal expertise
  6. Real testimonials with specific outcomes (not generic "great service!")
  7. Contact information — visible email, response time commitment
  8. Privacy policy, terms, cookies — trust signals
  9. HTTPS — basic requirement
  10. No misleading content — exaggerated claims harm E-E-A-T

16. Search Console — the SEO tool you must use weekly

Google Search Console is free and provides data no third-party tool has:

Coverage report

Shows which pages are indexed, excluded, and why. Common exclusion reasons:

  • Crawled but not indexed — Google chose not to index; thin content, duplicate, or low quality
  • Discovered but not crawled — URL is known but not yet fetched; increase content quality/internal links
  • Excluded by noindex — intentional; verify these are correct
  • Redirect — URLs permanently redirected; should be none

Performance report

Click-through rate (CTR), impressions, average position, and queries per page. Use this to find:

  • Pages with high impressions but low CTR (fix title/description)
  • Pages ranking position 4–10 (low-hanging improvement targets)
  • Queries that trigger your pages you did not intend to target

Core Web Vitals report

Shows real-user LCP, INP, and CLS data. More reliable than lab data for diagnosing real issues.

URL Inspection Tool

Test any URL: rendering, indexing status, canonical, mobile usability, structured data. Use after major changes.


The checklist

Use this before launching any new page or after a site migration:

Foundation

  • HTTPS enabled; HTTP redirects to HTTPS
  • metadataBase set in Next.js layout
  • Canonical URL on every indexable page (self-referencing)
  • Unique <title> per page (50–60 chars, keyword-first)
  • Unique meta description per page (140–160 chars)
  • Robots.txt not blocking CSS/JS; includes sitemap URL
  • Sitemap.xml submitted to Google Search Console
  • noindex pages excluded from sitemap

Images

  • Every <img> has a meaningful alt attribute (or alt="" if decorative)
  • Hero/LCP image has priority={true} or loading="eager"
  • Images have width and height specified
  • WebP or AVIF format in use
  • Image file names are descriptive and hyphenated

Structured data

  • Organization schema on every page
  • BreadcrumbList on all pages except homepage
  • Article/BlogPosting on blog posts with datePublished and dateModified
  • FAQPage on FAQ sections only when Q&A is visible (expect no Google FAQ rich result for commercial sites)
  • Test all structured data with Google's Rich Results Test

Performance (Core Web Vitals)

  • LCP under 2.5s on mobile (measure with PageSpeed Insights)
  • CLS under 0.1 — all images have dimensions; fonts use font-display: optional
  • INP under 200ms — no heavy synchronous JavaScript in interaction handlers
  • Third-party scripts loaded with strategy="lazyOnload"

Technical

  • All internal links use <a href="">, not click handlers
  • No JavaScript-only navigation for important content
  • Trailing slash consistent site-wide
  • No redirect chains in internal links
  • 301 used for all permanent moves
  • 410 used for intentionally removed pages

Content

  • Every page has a unique H1
  • H1 matches the intent signalled by <title>
  • No heading levels skipped (H1 → H2 → H3)
  • Author attributed on blog posts
  • datePublished and dateModified accurate

Technical SEO is not a checklist you tick once at launch. It is the contract between your stack and every crawler that visits: here is what this URL is, here is why it deserves attention, and here is nothing that wastes your time. When that contract is clear, well-written content has room to work.

For share previews and GBP alignment, consistent Open Graph (1200×630) and profile assets matter as much as on-page tags. We maintain a free logo size generator and a platform-by-platform guide if you are standardizing brand files after a technical pass.

If you want this implemented in your codebase — not as a slide deck, but as shipped changes and documented runbooks — talk to Varnox.

Related articles

← Back to blog

Book your free consultation