# 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 ( <>