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.
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
metadataBasein 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:typespecifyingimage/jpegorimage/pngavoids 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:
- 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" />
- Decorative images — empty alt, not missing alt. A missing
altattribute is a parsing error; an emptyalt=""tells screen readers to skip it.
<!-- Background decoration, icon without adjacent text label -->
<img src="bg-dots.svg" alt="" role="presentation" />
- 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>
- In Next.js
<Image />—altis required by the component. ESLint will catch it, but thealtvalue 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
| Format | Best for | Notes |
|---|---|---|
| WebP | Photographs, complex graphics | ~30% smaller than JPEG at same quality |
| AVIF | Photographs | ~50% smaller, better quality; slower to encode |
| SVG | Icons, logos, line art | Vector — scales without quality loss |
| PNG | Screenshots, transparency | Lossless; use WebP equivalent where possible |
In Next.js — next/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"
}
BreadcrumbList schema
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=emailvariants) - 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
noindexmeta 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:
- Hero image is lazy-loaded — set
loading="eager"orpriority={true}in Next.js - Image served without
srcset— full resolution downloaded on mobile - Large TTFB — server response slow; fix with caching, CDN, or SSG
- Render-blocking resources — unoptimised fonts, synchronous scripts
- 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:
- Heavy JavaScript executing on the main thread during interaction
- Large event listeners that do expensive DOM operations
- Third-party scripts (analytics, chat widgets) blocking the main thread
- Long tasks (>50ms) in synchronous JavaScript
Fixes:
- Use
React.memo,useMemo,useCallbackto prevent unnecessary re-renders - Break long tasks with
scheduler.postTask()orsetTimeout(fn, 0) - Load third-party scripts with
strategy="lazyOnload"in Next.js<Script> - Avoid synchronous
localStorageaccess 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:
- Images without
width/heightattributes - Ads or embeds inserted without reserved space
- Dynamically injected content above existing content
- Custom font loading causing text to reflow (FOUT/FOIT)
- Animations that move layout elements (use
transforminstead oftop/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
| Code | Meaning | SEO effect |
|---|---|---|
| 301 Permanent redirect | Page has moved permanently | Passes most link equity to the new URL |
| 302 Temporary redirect | Page has moved temporarily | Link equity stays on original URL |
| 307/308 | Same as 302/301 but method-preserving | Rarely needed; use 301 for SEO purposes |
| 410 Gone | Page no longer exists and will not return | Google deindexes faster than a 404 |
| 404 Not Found | Page not found | Google 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
- Use
rel="canonical"on faceted/filtered pages to consolidate signals - Block parameter-based URLs in
robots.txtif they generate no unique content - Ensure all
noindexpages are not linked prominently fromindexpages - 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
-
Pillar → cluster — A comprehensive service page links to related blog posts; blog posts link back to the service page. This creates topical authority.
-
Sidebar / related content — Every blog post links to 3 related posts + the most relevant service page.
-
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>
-
Breadcrumbs — Implemented as
<nav>withBreadcrumbListschema. Confirm every page has them. -
Footer links — Good for important pages (services, contact, about). Do not put every post in the footer.
Internal link audit
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
200for all live, indexable pages301for permanent moves404/410for deleted pages — never return200with empty/error content503for planned maintenance withRetry-Afterheader — 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-defaultis 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:
-
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). -
Paginated pages — each
/blog?page=2URL can be indexed independently with its own canonical, OR you can canonical all pages back to/blogif they do not offer unique indexable value. -
What NOT to do —
noindexon 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:
- Author pages — named authors with bios, credentials, and LinkedIn links
Personschema on author pages withsameAslinks to LinkedIndatePublishedanddateModified— always include and keep accurate- About page — company history, team, credentials
- Cited external sources — outbound links to authoritative references signal expertise
- Real testimonials with specific outcomes (not generic "great service!")
- Contact information — visible email, response time commitment
- Privacy policy, terms, cookies — trust signals
- HTTPS — basic requirement
- 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
-
metadataBaseset 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
-
noindexpages excluded from sitemap
Images
- Every
<img>has a meaningfulaltattribute (oralt=""if decorative) - Hero/LCP image has
priority={true}orloading="eager" - Images have
widthandheightspecified - WebP or AVIF format in use
- Image file names are descriptive and hyphenated
Structured data
-
Organizationschema on every page -
BreadcrumbListon all pages except homepage -
Article/BlogPostingon blog posts withdatePublishedanddateModified -
FAQPageon 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
-
datePublishedanddateModifiedaccurate
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.