
Technical SEO for Developers: Building Something Nobody Visits Is Pointless
I built a service and nobody came. How I implemented technical SEO in Next.js to drive organic traffic without a marketing budget.

I built a service and nobody came. How I implemented technical SEO in Next.js to drive organic traffic without a marketing budget.
My admin account was hijacked because of a single comment on the board. I dive deep into the 3 types of XSS (Stored, Reflected, DOM) and concrete defense strategies in React/Next.js environments, including HTML Escaping, CSP, and Cookie Security.

Do you know what the Circle (○) and Lambda (λ) symbols mean in Next.js build logs? Ensure you aren't accidentally making every page dynamic.

I tried to save money by deploying to AWS S3 instead of Vercel, but ended up with a broken site. I share the three nightmares of Static Export (Image Optimization, API Routes, Dynamic Routing) and how to fix them.

ISR works perfectly on Vercel, but fails on AWS/Docker. Let's dig into the file system cache trap and how to solve it.

I spent three months building a service. Authentication, dashboard, payment integration, dark mode. Modern stack: Next.js, TypeScript, Supabase. Clean code. Decent test coverage.
I launched. Shared it on Twitter. Posted to ProductHunt. Got some visitors on day one. Then silence.
I opened Google Search Console. Clicks: 0. Impressions: 0. Indexed pages: 0.
Google didn't know my service existed.
That's when it clicked. I had spent all my time making the service, but zero time making it findable. There are hundreds of millions of pages waiting to be indexed. You have to raise your hand and say "I'm here." That's what SEO is.
Most developers think of SEO as a marketer's job — keyword research, backlink strategies, content calendars. That's all real SEO. But underneath it is a foundation called Technical SEO that developers own entirely. You can write the best content in the world. If the technical foundation isn't there, Google can't read it.
This post is the notes I wrote after going through that painful lesson.
Think of it this way.
You opened a restaurant in an alley. The food is great. The interior is nice. But there's no sign outside. No menu posted on the window. The restaurant isn't on any maps app.
Unless someone wanders in by chance, nobody knows it exists. It doesn't matter how good the food is.
SEO is the combination of that sign, menu board, and maps registration. Specifically, it's the practice of helping search engines discover your site, understand your content, and surface it in relevant search results.
Technical SEO handles the "discover" and "understand" parts. Can search engine crawlers access your pages? Can they correctly parse what each page is about? Does the site structure make logical sense? Those are technical questions with technical answers — code answers.
Everything inside <head> that describes your page: title, description, images. These are what appear in search results. They're also what show up in the link preview cards on Slack and Twitter.
If metadata is a business card, <title> is your name. <meta name="description"> is your one-line pitch. The image that renders when you paste a link somewhere is driven by Open Graph tags.
In Next.js App Router, generateMetadata() handles all of this:
// app/[locale]/blog/[slug]/page.tsx
import type { Metadata } from 'next';
export async function generateMetadata({
params,
}: {
params: { slug: string; locale: string };
}): Promise<Metadata> {
const post = await getPostBySlug(params.slug);
if (!post) return {};
const title = params.locale === 'ko' ? post.title_ko : post.title_en;
const description =
params.locale === 'ko' ? post.description_ko : post.description_en;
const ogImage = post.cover_image ?? '/images/og-default.png';
return {
title,
description,
openGraph: {
title,
description,
images: [{ url: ogImage, width: 1200, height: 630 }],
type: 'article',
publishedTime: post.date,
tags: post.tags ?? [],
},
twitter: {
card: 'summary_large_image',
title,
description,
images: [ogImage],
},
alternates: {
canonical: `https://example.com/${params.locale}/blog/${params.slug}`,
},
};
}
The alternates.canonical field prevents duplicate content issues. When you have Korean and English versions of the same post at different URLs, this tells Google which URL is the authoritative one. Essential for multilingual sites.
A sitemap tells Google which pages exist on your site. It's the equivalent of registering your restaurant on Google Maps. You could wait for crawlers to discover every page by following links, but submitting a sitemap is faster, more reliable, and ensures nothing gets missed.
Next.js 13+ generates /sitemap.xml automatically from an app/sitemap.ts file:
// app/sitemap.ts
import type { MetadataRoute } from 'next';
import { getAllPosts } from '@/lib/queries';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts();
const locales = ['ko', 'en'] as const;
const baseUrl = 'https://example.com';
const staticRoutes = ['', '/blog', '/projects', '/about'].flatMap((route) =>
locales.map((locale) => ({
url: `${baseUrl}/${locale}${route}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: route === '' ? 1 : 0.8,
}))
);
const postRoutes = posts.flatMap((post) =>
locales.map((locale) => ({
url: `${baseUrl}/${locale}/blog/${post.slug}`,
lastModified: new Date(post.date),
changeFrequency: 'weekly' as const,
priority: 0.6,
}))
);
return [...staticRoutes, ...postRoutes];
}
Once the sitemap exists, submit it in Google Search Console. Without submission, Google may eventually find it on its own — or may not. Don't leave it to chance.
JSON-LD (structured data) is what makes Rich Results possible — those search result cards that include star ratings, images, dates, and other visual extras. When a restaurant listing in Google Maps shows hours, ratings, and photos, that's structured data at work.
For a blog, the BlogPosting schema type is the right fit:
// components/blog/JsonLd.tsx
export function BlogPostJsonLd({
title,
description,
date,
author,
url,
image,
}: BlogPostJsonLdProps) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: title,
description,
datePublished: date,
dateModified: date,
author: {
'@type': 'Person',
name: author,
},
image,
url,
publisher: {
'@type': 'Organization',
name: 'Codemapo',
logo: {
'@type': 'ImageObject',
url: 'https://example.com/images/logo.png',
},
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
);
}
Render this component in your page, and the JSON-LD ends up in <head>. Use Google's Rich Results Test to validate it.
Since 2021, Google has factored Core Web Vitals into search rankings. Great content in a slow site loses to decent content in a fast one. Three metrics to understand:
| Metric | Measures | Target |
|---|---|---|
| LCP (Largest Contentful Paint) | How fast the main content renders | Under 2.5s |
| CLS (Cumulative Layout Shift) | How much layout shifts during load | Under 0.1 |
| INP (Interaction to Next Paint) | How fast the page responds to input | Under 200ms |
The most common developer mistakes:
LCP: Hero images that use lazy loading. Next.js <Image> defaults to loading="lazy", which is correct for below-the-fold images. But images at the top of the page — your hero banner, cover photo — need priority to load immediately:
<Image
src="/images/hero.webp"
alt="Hero image"
width={1200}
height={600}
priority
/>
CLS: Images without explicit dimensions. When a browser doesn't know an image's size in advance, it reserves no space. The image loads, pushes everything else down, and you get a CLS penalty. Always specify width and height. Font loading causes CLS too — use font-display: swap and preload critical fonts.
INP: Heavy synchronous event handlers. If clicking something triggers a big synchronous computation, the response time tanks. React's useTransition can defer low-priority state updates to keep the main thread responsive.
Measure with Lighthouse in Chrome DevTools. Real user data is in Google Search Console under "Core Web Vitals."
This was the trap I didn't see coming.
In a React SPA (Create React App, Vite), the browser receives a nearly empty HTML file and JavaScript builds the page content on the client. Google's crawler can execute JavaScript — but not right away. Crawling and rendering happen in separate pipelines with potentially days of delay between them. And if JavaScript rendering fails for any reason, an empty page gets indexed.
Server-side rendering or static generation sends Google a fully rendered HTML response. No JavaScript execution needed. The crawler reads the content directly.
This is one of the most compelling reasons to migrate from CRA to Next.js. Next.js App Router uses Server Components by default — SSR out of the box. The SEO foundation comes for free.
A site built with pure client-side rendering is like a restaurant that only shows its menu after you've already sat down and ordered. Google's crawler isn't patient enough to wait.
When you launch, setting up Google Search Console should be your very first action. It's registering your restaurant on Google Maps after you open the doors.
The process:
/sitemap.xml)Within a few days, search performance data starts flowing in. Which queries is your site showing up for? What's the click-through rate? This data is the foundation for any content strategy. You can't optimize what you can't measure.
Missing meta descriptions: Without <meta name="description">, Google picks arbitrary text from your page body and shows it in search results. Whatever text it chooses might make no sense out of context.
Same title on every page: If every page has <title>Codemapo</title>, Google can't distinguish them from each other. Each page needs a unique, descriptive title.
Images without alt text: Missing out on image search traffic. Also breaks accessibility for screen reader users. One small fix, two wins.
No canonical URLs on multilingual sites: If /ko/blog/post-1 and /en/blog/post-1 contain similar content, Google may flag them as duplicate content. Set canonical and hreflang tags to declare the relationship explicitly.
Sitemap created, never submitted: A sitemap file sitting at /sitemap.xml doesn't automatically get picked up. Submitting it in Search Console is a separate, required step.
Technical SEO isn't glamorous. There's no magic formula. But without this foundation, the best content and the best product both disappear into the depths of search results.
Here's the short version:
SEO isn't about tricking Google. It's about making sure Google can find what you built, understand what it's for, and show it to the people who are looking for exactly that thing.
Put up the sign. Write the menu. Add yourself to the map. Then let the food speak for itself.