techGalen Guan

Next.js SEO Audit: From Empty Shells to Search-Ready in 7 Steps

I recently shipped a full SEO overhaul on my personal site (built with Next.js 14, standalone Docker deployment). The starting point was grim — four core pages rendered as empty 'use client' shells invisible to crawlers. The end result: proper SSR, bilingual blog routes, structured data, favicons, sitemap, RSS, and search engine verification.

This post documents every step, so you can replicate it on your own Next.js site.

The Starting Point: What Was Wrong

A quick audit revealed problems across four priority levels:

Priority Issue Impact
P0 Homepage, About, Projects, Blog were 'use client' Crawlers see empty HTML — zero indexing
P1 No custom 404 page Soft 404s dilute crawl budget
P1 Image optimization disabled (unoptimized: true) No WebP/AVIF, slower loads
P2 Chinese blog content not in sitemap/RSS Half the content invisible to engines
P2 Person JSON-LD incomplete Missing E-E-A-T signals
P3 No web manifest, incomplete favicons Poor PWA/mobile experience
P3 No keywords meta, no Chinese description Missing Baidu/Sogou signals

Step 1: Migrate Client Components to Server Components (P0)

This was the single most impactful change. Four pages — /, /about, /projects, /blog — were 'use client' at the top level. Googlebot received empty <div id="__next"> shells with zero content.

Pattern:

  1. Remove 'use client' from page.tsx — make it a Server Component
  2. Extract all client logic into a sibling ClientComponent.tsx (e.g. HomeClient.tsx)
  3. Server Component passes data as props
  4. For i18n: server renders default language, useLanguage handles runtime switching

Example — Homepage:

// src/app/page.tsx (Server Component — no 'use client')
import HomeClient from './HomeClient';

export const metadata: Metadata = {
  title: '...',
  description: '...',
};

export default function HomePage() {
  return <HomeClient />;
}
// src/app/HomeClient.tsx ('use client')
'use client';
// ... all interactive logic here

After migration, npm run build confirmed all four pages now show ○ (Static) — fully pre-rendered at build time.

Step 2: Custom 404 + Image Optimization (P1)

404 Page: Created src/app/not-found.tsx with branded UI and a "Back to Home" link. Proper 404 responses prevent soft-404 confusion.

Image Optimization: Removed images: { unoptimized: true } from next.config.js. Next.js now auto-generates WebP/AVIF for all <Image> components.

Step 3: Bilingual Blog Routes with Hreflang (P2)

The blog had English content in /blog/slug and Chinese in /content/blog/zh/, but the route structure only served /blog/*. Chinese posts were invisible to crawlers.

Implemented Option B — locale-prefixed routes:

/en/blog/my-post      ← English
/zh/blog/my-post      ← Chinese
/blog/my-post         → 301 redirect (Moved Permanently) → /en/blog/my-post

Key files:

File Purpose
src/app/[locale]/blog/[slug]/page.tsx Locale-aware blog post with generateMetadata (hreflang)
src/app/[locale]/blog/page.tsx Locale-aware blog list
src/lib/i18n.ts LOCALES, localePath(), blogAlternates() utilities
src/middleware.ts /blog/en/blog redirect

The redirect middleware ensures backward compatibility:

// src/middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  if (pathname === '/blog' || pathname === '/blog/') {
    return NextResponse.redirect(new URL('/en/blog', request.url), 301);
  }
  if (pathname.startsWith('/blog/')) {
    const slug = pathname.replace('/blog/', '');
    return NextResponse.redirect(new URL(`/en/blog/${slug}`, request.url), 301);
  }
}

Sitemap updated to emit both locale variants (hreflang alternates are set per-page via generateMetadata):

// src/app/sitemap.ts
import { MetadataRoute } from 'next'

export default function sitemap(): MetadataRoute.Sitemap {
  const locales = ['en', 'zh']
  const slugs = getAllSlugs()
  return slugs.flatMap(slug =>
    locales.map(locale => ({
      url: `https://guancyxx.cn/${locale}/blog/${slug}`,
      lastModified: new Date(),
    }))
  )
}

RSS feeds serve per-locale: /feed.xml (EN) and /zh/feed.xml (ZH).

Step 4: Complete Person JSON-LD (P2)

Enhanced the Person structured data with fields that strengthen E-E-A-T signals:

{
  "@type": "Person",
  "name": "Galen Guan",
  "image": "https://guancyxx.cn/images/profile.jpg",
  "alumniOf": { "@type": "Organization", "name": "..." },
  "knowsLanguage": ["en", "zh"],
  "sameAs": [
    "https://github.com/guancyxx",
    "https://www.linkedin.com/in/guancyxx"
  ]
}

Step 5: Web Manifest + Multi-Size Favicons (P3)

Generated favicons from source logo using Pillow:

from PIL import Image

img = Image.open('public/logo.png')
sizes = [(16,16), (32,32), (48,48)]
# favicon.ico, favicon-32x32.png, favicon-16x16.png,
# apple-touch-icon.png (180x180), android-chrome-192x192.png

Created public/site.webmanifest for PWA installability and updated metadata.icons in root layout.

Step 6: Keywords Meta + Bilingual Descriptions

Added <meta name="keywords"> to all pages with relevant terms for both Baidu and Google:

Galen Guan, 官小西, full-stack developer, AI product engineer,
React, Python, TypeScript, Next.js, open source,
AI开发, 全栈开发, 人工智能, 深度学习

Descriptions now end with a Chinese summary — critical for Baidu/Sogou which index the full Chinese text:

Building AI-powered products with 10+ years of full-stack expertise...
全栈开发与AI产品工程,10年+经验,开源贡献者。

Step 7: Search Engine Verification

  • Google Search Console: Verified via DNS TXT record
  • Bing Webmaster Tools: Added msvalidate.01 meta tag to root layout
  • Sitemap submission: Done via GSC interface

Results

Metric Before After
Crawlable pages 1 (404) 10+ (all SSR)
Blog routes 3 (EN only) 6 (EN + ZH with hreflang)
Structured data Person only Person + BlogPosting
Favicons 1 (logo.png) 5 (ico + 4 PNG sizes)
RSS feeds 1 (EN) 2 (EN + ZH)
Keywords meta None Bilingual EN+ZH
Search verification None Google + Bing

Bonus: src/app/robots.ts — A properly configured robots.txt that disallows /api/ and /_next/ while linking to the sitemap. Small detail, but signals professionalism to crawlers.

Key Takeaways

  1. SSR first — the single biggest SEO win for any SPA/CSR site
  2. Hreflang for multilingual — don't skip alternates.languages in metadata
  3. Bilingual descriptions — essential for CJK search engines (Baidu, Sogou, Naver)
  4. JSON-LD completenessimage, alumniOf, sameAs all matter for E-E-A-T
  5. Build verification — always run npm run build and confirm pages are ○ (Static)

The full audit checklist lives in SEO.md at the project root. If you're running a Next.js personal site, I'd recommend going through the same audit process before launching — it takes a day but saves months of wondering why Google can't find you.