# SEO for Frontend
> Rendering choices, metadata, structured data, and CWV for search ranking.
Frontend architecture guidance from fearchitect, written by Abas Turabli and last reviewed 2026-06-21. Source: https://fearchitect.com/topics/seo
Use it as reference for the task at hand. Before changing code, check this guidance against the codebase: where the code already makes a different, deliberate choice, flag the conflict instead of rewriting it. Library APIs move faster than this guide, so confirm exact signatures in the official docs linked at the end.
## Summary
Frontend decisions dominate SEO: CSR apps are crawlable but Google may delay rendering by days. SSR and SSG give crawlers HTML immediately. The Next.js Metadata API generates `
` tags at build or request time. JSON-LD structured data adds rich results. Core Web Vitals are a direct ranking signal for Google Search.
## Rendering strategy vs crawlability
| Strategy | First-fetch HTML | Google indexing speed | Use when |
| --- | --- | --- | --- |
| **SSG** | Full HTML in CDN response | Immediate — no render queue | Content is the same for all users |
| **SSR** | Full HTML per request | Immediate — no render queue | Content varies by user or URL |
| **CSR** | Empty shell; JS builds DOM | Delayed — JS render queue | Auth-gated pages with no public SEO need |
| **Partial prerendering** | Static shell + streamed dynamic slots | Shell indexed immediately | Mostly static pages with a few dynamic sections |
## Next.js Metadata API + JSON-LD structured data
Next.js App Router exports `metadata` (static) or `generateMetadata` (dynamic, async). Add JSON-LD in the same file — Googlebot reads it from the initial HTML without executing extra JS.
**Dynamic metadata and JSON-LD in an App Router page**
```tsx
import type { Metadata } from "next";
// generateMetadata runs on the server at request time.
export async function generateMetadata(
{ params }: { params: Promise<{ slug: string }> },
): Promise {
const { slug } = await params;
const post = await fetchPost(slug); // your data layer
return {
title: post.title,
description: post.excerpt,
alternates: { canonical: `https://example.com/blog/${slug}` },
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.ogImage, width: 1200, height: 630 }],
},
};
}
// JSON-LD lives in the RSC layer → present in initial HTML.
export default async function BlogPost(
{ params }: { params: Promise<{ slug: string }> },
) {
const { slug } = await params;
const post = await fetchPost(slug);
const schema = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
datePublished: post.publishedAt,
author: { "@type": "Person", name: post.author },
};
return (
<>
{/* … */}
>
);
}
```
`generateMetadata` and the page component share one `fetchPost` call — Next.js deduplicates it via `fetch` memoisation, so there is no double request.
## Structural SEO checklist
- Every public page needs a unique `` and `` — duplicates dilute ranking.
- Set `` on paginated, filtered, or UTM-tagged URLs to consolidate link equity.
- Submit an XML sitemap to Google Search Console; exclude `noindex` URLs from it.
- Keep `robots.txt` permissive for CSS, JS, and images — block only auth-gated paths.
- Use semantic HTML (``, `