webdev.complete
📡 SEO & Deployment
🚦Going to Production
Lesson 108 of 117
20 min

SEO Essentials

Meta, OG, JSON-LD, sitemap.xml, robots.txt, canonicals.

SEO sounds like marketing magic. It is mostly not. About 80% of search engine optimization is just telling the various crawlers (Google, Bing, Twitter, LinkedIn, OpenGraph-aware messaging apps) what your page is, in the exact tags they expect. Once you know the tags, you can ship them with a single Next.js metadata object. Let's go through what each tag is for and what shows up where.

The triad every page needs

  1. Title. Shows in search results and the browser tab. Keep it 50-60 characters; longer gets truncated.
  2. Meta description. The snippet beneath the title in search results. 150-160 characters, written as a real sentence aimed at humans.
  3. Canonical URL.Says "if you find this page via a different URL (with a tracking parameter, say), the authoritative version lives here."
html
<title>How to build a blog with Next.js | apricot.dev</title>
<meta name="description" content="A practical 30-minute walkthrough of building a static blog with Next.js App Router, MDX, and incremental static regeneration.">
<link rel="canonical" href="https://apricot.dev/blog/nextjs-blog">
Don't keyword-stuff the title
Real example of what not to do: "Best Cheap Affordable Blog Tutorial 2025 - Next.js React SEO Guide Free." Modern Google penalizes this. Write the title as if you were the user reading the results page.

Open Graph: rich previews in social shares

When someone pastes your URL into Slack, Discord, iMessage, LinkedIn, or Twitter/X, the platform fetches the page and looks for Open Graph tags to render a preview card. No OG tags, no card.

html
<meta property="og:title" content="How to build a blog with Next.js">
<meta property="og:description" content="A practical 30-minute walkthrough.">
<meta property="og:image" content="https://apricot.dev/og/nextjs-blog.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:url" content="https://apricot.dev/blog/nextjs-blog">
<meta property="og:type" content="article">
<meta property="og:site_name" content="apricot.dev">
The magic image size is 1200×630
Every social platform crops to roughly a 1.91:1 ratio. 1200 × 630 is the sweet spot that survives every crop without losing the title. Keep important text in the middle 60%.

Twitter cards

Twitter (and X) historically used its own tag set. You can usually skip these if your OG tags are right - X falls back to OG. But for full control:

html
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@apricotdev">
<meta name="twitter:title" content="How to build a blog with Next.js">
<meta name="twitter:description" content="A practical 30-minute walkthrough.">
<meta name="twitter:image" content="https://apricot.dev/og/nextjs-blog.png">

JSON-LD: structured data Google can parse

Rich results (those special star ratings, recipe cards, breadcrumb trails on the search results page) come from structured data: machine-readable metadata you embed in a <script type="application/ld+json"> tag, using schemas defined at schema.org.

The schemas you'll use most:

  • Article - for blog posts and editorial content.
  • Organization - for your site as a whole.
  • BreadcrumbList - to render breadcrumb trails in results.
  • Product - e-commerce.
  • FAQPage, HowTo - generally penalized now, use sparingly.
Article schema
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "How to build a blog with Next.js",
  "image": ["https://apricot.dev/og/nextjs-blog.png"],
  "datePublished": "2026-05-15",
  "dateModified": "2026-05-24",
  "author": [{
    "@type": "Person",
    "name": "Abhishek",
    "url": "https://apricot.dev/about"
  }],
  "publisher": {
    "@type": "Organization",
    "name": "apricot.dev",
    "logo": { "@type": "ImageObject", "url": "https://apricot.dev/logo.png" }
  }
}
</script>
BreadcrumbList
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://apricot.dev/" },
    { "@type": "ListItem", "position": 2, "name": "Blog", "item": "https://apricot.dev/blog" },
    { "@type": "ListItem", "position": 3, "name": "How to build a blog with Next.js" }
  ]
}
</script>

Test with Google's Rich Results Test. It tells you exactly which properties Google parsed and which it ignored.

Doing it all in Next.js metadata API

App Router has a first-class metadata system. Export a metadata object (or async generateMetadata function) and Next handles the head tags:

app/blog/[slug]/page.tsx
import type { Metadata } from "next";

export async function generateMetadata(
  { params }: { params: { slug: string } },
): Promise<Metadata> {
  const post = await getPost(params.slug);
  const url = `https://apricot.dev/blog/${post.slug}`;

  return {
    title: `${post.title} | apricot.dev`,
    description: post.excerpt,
    alternates: { canonical: url },
    openGraph: {
      title: post.title,
      description: post.excerpt,
      url,
      type: "article",
      images: [
        { url: post.ogImage, width: 1200, height: 630 },
      ],
    },
    twitter: {
      card: "summary_large_image",
      title: post.title,
      description: post.excerpt,
      images: [post.ogImage],
    },
  };
}

export default async function Page({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return (
    <>
      <article>{/* ... */}</article>
      {/* JSON-LD as a regular <script> tag */}
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(buildArticleSchema(post)) }}
      />
    </>
  );
}

sitemap.xml and robots.txt

Two tiny files that tell crawlers where to look and where not to:

public/sitemap.xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://apricot.dev/</loc>
    <lastmod>2026-05-24</lastmod>
  </url>
  <url>
    <loc>https://apricot.dev/blog/nextjs-blog</loc>
    <lastmod>2026-05-15</lastmod>
  </url>
</urlset>
public/robots.txt
User-agent: *
Allow: /
Disallow: /admin/
Disallow: /api/

Sitemap: https://apricot.dev/sitemap.xml

Next.js can generate both: drop a sitemap.ts and robots.ts in app/ and it routes them automatically:

app/sitemap.ts
import { getAllPosts } from "@/lib/posts";

export default async function sitemap() {
  const posts = await getAllPosts();
  return [
    { url: "https://apricot.dev/", lastModified: new Date() },
    ...posts.map((p) => ({
      url: `https://apricot.dev/blog/${p.slug}`,
      lastModified: p.updatedAt,
    })),
  ];
}

Core Web Vitals: yes, they are a ranking signal

Since the "Page Experience" update, Google's ranking includes Core Web Vitals as a real (if modest) signal. A fast site won't outrank a relevant slow one, but among equally relevant pages, the faster one wins. The full perf lesson covers the thresholds; the SEO takeaway is: publish for Search Console's Core Web Vitals report and treat reds as bugs.

A myth and a truth
Myth:"Meta keywords help ranking." They haven't since 2009. Don't bother. Truth: Real-user Core Web Vitals are a ranking signal, especially for mobile.

Quiz

Quiz1 / 4

What's the ideal dimensions for an Open Graph share image?

Recap

  • Every page needs a title (50-60 char), meta description (150-160), and canonical URL.
  • Open Graph tags drive rich social previews. The image should be 1200×630.
  • JSON-LD (schema.org) unlocks rich results. Article, Organization, BreadcrumbList are the staples.
  • In Next.js App Router, use the metadata export or generateMetadata function for all head tags.
  • Drop app/sitemap.ts and app/robots.ts; Next routes them automatically.
  • Core Web Vitalsare a ranking signal. Watch Search Console's field report.
Built with Next.js, Tailwind & Sandpack.
Learn. Build. Ship.