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
- Title. Shows in search results and the browser tab. Keep it 50-60 characters; longer gets truncated.
- Meta description. The snippet beneath the title in search results. 150-160 characters, written as a real sentence aimed at humans.
- Canonical URL.Says "if you find this page via a different URL (with a tracking parameter, say), the authoritative version lives here."
<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">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.
<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">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:
<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.
<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><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:
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:
<?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>User-agent: *
Allow: /
Disallow: /admin/
Disallow: /api/
Sitemap: https://apricot.dev/sitemap.xmlNext.js can generate both: drop a sitemap.ts and robots.ts in app/ and it routes them automatically:
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.
Quiz
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
metadataexport orgenerateMetadatafunction for all head tags. - Drop
app/sitemap.tsandapp/robots.ts; Next routes them automatically. - Core Web Vitalsare a ranking signal. Watch Search Console's field report.