Social & Discovery
The Verbiage
Metadata Strategy: Global vs. Dynamic
Next.js 15 provides two complementary approaches to metadata management: global defaults and dynamic overrides. This dual-layer strategy ensures every page has appropriate social tags while allowing individual pages to customize their metadata.
Global Metadata (app/layout.tsx): Defines the site-wide defaults that apply to every page unless overridden. This includes:
- Default title template: "%s | cjs.codes" (where %s is replaced by page-specific titles)
- Default description: "Master the architectural verbiage to direct AI with absolute precision."
- Default OG image: Points to /api/og?title=The Programmer's Bible for the homepage
- Base URL: metadataBase set to https://cjs.codes for absolute URL resolution
Dynamic Metadata (app/library/[volume]/[chapter]/page.tsx): Uses the generateMetadata function to override global defaults with chapter-specific data. This function runs at build time (for static pages) or request time (for dynamic pages) to generate unique metadata for each chapter.
The Strategy: Global metadata provides a safety net—every page has valid social tags even if generateMetadata fails. Dynamic metadata provides precision—each chapter gets accurate, content-specific tags that maximize shareability and SEO value.
The generateMetadata Function: Chapter Slugs to Unique Metadata
The generateMetadata function transforms URL parameters into rich metadata by looking up chapter data from our single source of truth: library-mapping.ts.
The Process:
1. Extract Params: Receives { volume: "our-foundations", chapter: "grid-architecture" } from the URL
2. Lookup Data: Searches libraryVolumes array to find the matching volume and chapter
3. Construct Title: Combines chapter and volume: "Grid Architecture - Our Foundations"
4. Pull Description: Uses chapter.description from the mapping (150 characters, optimized for social previews)
5. Build URLs: Creates canonical URL and dynamic OG image URL
6. Return Metadata: Returns a Metadata object that Next.js injects into <head>
The Result: Every chapter page gets:
- Unique title: "Chapter Title - Volume Name | cjs.codes"
- Accurate description from the mapping file
- Custom OG image URL: /api/og?title=Chapter%20Title%20-%20Volume%20Name
- Proper OpenGraph and Twitter Card tags
Why This Works: By using chapter slugs to look up data, we ensure metadata always matches the actual content. When a chapter is added to library-mapping.ts, its metadata is automatically available—no manual updates needed.
OG Image Automation: The ImageResponse Implementation
Our /api/og route uses Next.js's ImageResponse API to generate social preview images on-demand. This eliminates the need to maintain hundreds of static image files while ensuring every chapter gets a branded, professional social card.
The Route (app/api/og/route.tsx):
- Runtime: Uses edge runtime for fast, global image generation
- Method: Exports async GET(request: NextRequest) function
- Input: Extracts title query parameter (e.g., /api/og?title=Grid%20Architecture)
- Output: Returns a 1200x630px PNG image
The ImageResponse API: ImageResponse from next/og allows us to render React-like JSX into an image. This means we can:
- Use familiar React syntax for layout
- Apply CSS-in-JS styles directly
- Load custom fonts from Google Fonts CDN
- Generate images dynamically based on query parameters
The Implementation Flow:
1. Extract Title: searchParams.get("title") gets the chapter title from the URL
2. Load Font: Fetches Inter Bold from Google Fonts CDN (with system font fallback)
3. Render JSX: Uses ImageResponse to render a React-like component tree
4. Apply Styles: Sets gradient background (linear-gradient(to bottom right, #18181b, #3f3f46)), white text, and responsive typography
5. Return Image: Next.js converts the JSX to a PNG and returns it as an image response
The Design: - Background: Dark gradient from zinc-900 to zinc-700 for a professional, modern look - Typography: Inter Bold (700 weight) in large, white text (72px default, 56px for long titles) - Layout: Centered title with "cjs.codes" branding at the bottom - Dimensions: 1200x630px (standard OG image size for all social platforms)
The Benefits: - Zero Asset Management: No need to create, upload, or maintain image files - Brand Consistency: Every image uses the same design system automatically - Performance: Edge runtime ensures fast generation globally - Scalability: Adding 1000 chapters requires zero image maintenance
Verification Tools: Testing Social Metadata
Before deploying, it's essential to verify that social metadata is working correctly. Two tools provide comprehensive testing:
OpenGraph.xyz (https://www.opengraph.xyz/): - Enter any URL to see how it will appear when shared on Facebook, LinkedIn, and other OpenGraph-compatible platforms - Shows the rendered preview card with title, description, and image - Validates all OpenGraph tags and highlights any missing or incorrect metadata - Provides debugging information for troubleshooting
Twitter Card Validator (https://cards-dev.twitter.com/validator):
- Twitter's official tool for testing Twitter Card metadata
- Shows exactly how the link will appear in Twitter previews
- Validates twitter:card, twitter:title, twitter:description, and twitter:image tags
- Allows you to request a fresh crawl if metadata was recently updated
Testing Workflow:
1. Deploy the site or use a preview URL
2. Visit a chapter page (e.g., https://cjs.codes/library/our-foundations/grid-architecture)
3. Copy the URL and paste it into both validators
4. Verify that:
- Title matches the chapter title
- Description is the 150-character description from the mapping
- Image loads correctly and displays the chapter title
- All tags are present and correctly formatted
Common Issues:
- Image not loading: Check that /api/og route is accessible and returns a valid image
- Wrong title/description: Verify library-mapping.ts has correct data for the chapter
- Cached preview: Social platforms cache metadata—use the validator's "refresh" feature to force a new crawl
Troubleshooting Dynamic Images
When OG images fail to render or social platforms can't find them, these are the most common causes:
The Content-Type Trap
The Problem: If ImageResponse doesn't explicitly set Content-Type: image/png, browsers and social crawlers may not recognize the response as an image. This causes the image to appear as broken or not load at all.
The Solution: Always explicitly set the Content-Type header on the ImageResponse:
`typescript
const imageResponse = new ImageResponse(/* ... */);
imageResponse.headers.set("Content-Type", "image/png");
return imageResponse;
Why It Matters: Social platform crawlers are strict about MIME types. Without the correct Content-Type header, they'll reject the image even if the binary data is valid. This is especially critical for Twitter and LinkedIn, which validate image responses before displaying them.
The Fix: In app/api/og/route.tsx, ensure you set the header after creating the ImageResponse but before returning it. The ImageResponse constructor doesn't automatically set this header in all edge runtime environments.
The Satori Limitation
The Problem: The underlying library (Satori) that powers ImageResponse only supports Flexbox layouts, not CSS Grid. Using display: grid or any Grid properties will cause the renderer to fail silently or produce broken layouts.
The Solution: Always use Flexbox for OG image layouts:
`typescript
// ✅ Correct: Flexbox
<div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
<h1>{title}</h1>
</div>
// ❌ Wrong: CSS Grid (will break)
<div style={{ display: "grid", gridTemplateColumns: "1fr" }}>
<h1>{title}</h1>
</div>
Supported Properties: Satori supports:
- display: flex (and flex-direction, align-items, justify-content)
- position: absolute and position: relative
- Standard text and color properties
- Borders, padding, margins
Unsupported Properties: Satori does NOT support:
- display: grid or any Grid properties
- display: table or table layouts
- Complex transforms or animations
- Some advanced CSS features
The Fix: If your OG image layout isn't rendering, check for any Grid usage. Convert all layouts to Flexbox equivalents. For example, a two-column grid becomes a flex container with flexDirection: "row".
MetadataBase: The Absolute URL Requirement
The Problem: Without a defined metadataBase in app/layout.tsx, social crawlers cannot resolve relative OG image URLs. They'll treat /api/og?title=... as a relative path and fail to construct the full URL.
The Solution: Always define metadataBase in your root layout:
`typescript
// app/layout.tsx
export const metadata: Metadata = {
metadataBase: new URL("https://cjs.codes"),
// ... rest of metadata
};
Why It Matters: When generateMetadata returns a relative URL like /api/og?title=..., Next.js needs metadataBase to convert it to an absolute URL for social platforms. Without it:
- Facebook/LinkedIn crawlers see /api/og?title=... and can't resolve it
- Twitter Card Validator shows "Image URL is invalid"
- OpenGraph.xyz reports "Image not found"
The Fix: Ensure metadataBase is set to your production domain. For development, you can use process.env.NEXT_PUBLIC_SITE_URL or hardcode the production URL. The metadataBase is used to resolve all relative URLs in metadata, including OG images.
Verification: After setting metadataBase, check the rendered HTML. The og:image meta tag should show an absolute URL: https://cjs.codes/api/og?title=..., not a relative one.
The Complete Social Sharing Flow
When a user shares a chapter link, here's what happens:
1. User Shares URL: Copies https://cjs.codes/library/our-foundations/grid-architecture and pastes it into Twitter/LinkedIn/Facebook
2. Platform Crawls URL: Social platform's crawler requests the page HTML
3. Next.js Calls generateMetadata: Function receives { volume: "our-foundations", chapter: "grid-architecture" }
4. Metadata Lookup: Searches libraryVolumes to find matching chapter data
5. Metadata Generated: Returns Metadata object with title, description, and OG image URL
6. HTML Injected: Next.js converts metadata to <meta> tags and injects into <head>
7. OG Image Requested: Platform crawler requests /api/og?title=Grid%20Architecture%20-%20Our%20Foundations
8. Image Generated: Edge runtime renders 1200x630px image with title on dark gradient background
9. Preview Rendered: Platform displays custom card with title, description, and branded image
The Result: Every shared chapter link gets a beautiful, branded social card that accurately represents the content, all without maintaining static assets or hardcoded metadata. The entire process is automated from a single source of truth: library-mapping.ts.
The Blueprint
// ============================================
// Metadata Generation (app/library/[volume]/[chapter]/page.tsx)
// ============================================
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { volume: volumeId, chapter: chapterSlug } = await params;
// Pull from single source of truth: library-mapping.ts
const volume = libraryVolumes.find((v) => v.id === volumeId);
const chapter = volume?.chapters.find((c) => c.slug === chapterSlug);
if (!volume || !chapter) {
return {
title: "Chapter Not Found",
};
}
// Construct metadata from mapping file
const title = `${chapter.title} - ${volume.title}`;
const description = chapter.description; // 150-character description from mapping
const url = `https://cjs.codes/library/${volumeId}/${chapterSlug}`;
// Dynamic OG image URL
const ogImageUrl = `/api/og?title=${encodeURIComponent(title)}`;
return {
title,
description,
openGraph: {
title,
description,
url,
type: "article",
siteName: "cjs.codes",
images: [
{
url: ogImageUrl,
width: 1200,
height: 630,
alt: title,
},
],
},
twitter: {
card: "summary_large_image",
title,
description,
images: [ogImageUrl],
},
};
}
// ============================================
// Dynamic OG Image API (app/api/og/route.tsx)
// ============================================
import { ImageResponse } from "next/og";
import { NextRequest } from "next/server";
export const runtime = "edge";
// Load Inter Bold font from Google Fonts CDN
async function loadFont() {
try {
const fontUrl = "https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiJ-Ek-_EeA.woff2";
const response = await fetch(fontUrl, {
headers: { "User-Agent": "Mozilla/5.0" },
});
if (!response.ok) return null;
return await response.arrayBuffer();
} catch (error) {
return null;
}
}
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const title = searchParams.get("title") || "The Programmer's Bible";
// Load Inter font
const fontData = await loadFont();
const hasCustomFont = fontData !== null;
const fontFamily = hasCustomFont ? "Inter" : "system-ui, -apple-system, sans-serif";
return new ImageResponse(
(
<div
style={{
height: "100%",
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
// Dark gradient background: from-zinc-900 to-zinc-700
background: "linear-gradient(to bottom right, #18181b, #3f3f46)",
padding: "80px 100px",
}}
>
<h1
style={{
fontSize: title.length > 60 ? "56px" : "72px",
fontWeight: 700,
lineHeight: 1.1,
color: "#ffffff", // White text
fontFamily: fontFamily,
letterSpacing: "-0.02em",
textAlign: "center",
marginBottom: "0",
}}
>
{title}
</h1>
<div
style={{
marginTop: "80px",
fontSize: "28px",
color: "#a1a1aa", // zinc-400
fontFamily: fontFamily,
fontWeight: 500,
letterSpacing: "0.05em",
}}
>
cjs.codes
</div>
</div>
),
{
width: 1200,
height: 630,
fonts: hasCustomFont && fontData
? [{ name: "Inter", data: fontData, style: "normal", weight: 700 }]
: [],
}
);
} catch (error) {
// Fallback image on error
return new ImageResponse(
(
<div
style={{
height: "100%",
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
background: "linear-gradient(to bottom right, #18181b, #3f3f46)",
fontSize: "48px",
color: "#ffffff",
fontFamily: "system-ui, sans-serif",
}}
>
<div style={{ marginBottom: "20px" }}>cjs.codes — The Programmer's Bible</div>
<div style={{ fontSize: "24px", color: "#a1a1aa" }}>cjs.codes</div>
</div>
),
{ width: 1200, height: 630 }
);
}
}
// ============================================
// Library Mapping Structure (config/library-mapping.ts)
// ============================================
export type Chapter = {
title: string;
slug: string;
description: string; // 150 characters recommended
keywords?: string[];
}
// Example chapter entry:
{
title: "Grid Architecture",
slug: "grid-architecture",
description: "The CSS Grid shell for sidebar and content layout. Documents the mobile-first responsive pattern and the 1fr constraint fix.", // 150 chars
keywords: ["layout", "grid", "sidebar", "css", "responsive"]
}
// ============================================
// How It Works
// ============================================
// 1. Chapter added to library-mapping.ts with 150-character description
// 2. generateMetadata pulls title and description from mapping
// 3. OG image URL constructed: /api/og?title=[ChapterTitle]
// 4. Social platform requests OG image
// 5. API route generates 1200x630px image with title
// 6. Social platform displays custom card
// ============================================
// The Complete Metadata Flow
// ============================================
// User shares: https://cjs.codes/library/our-foundations/grid-architecture
// ↓
// Social platform crawls URL
// ↓
// Next.js calls generateMetadata({ volume: "our-foundations", chapter: "grid-architecture" })
// ↓
// Function looks up chapter in library-mapping.ts
// ↓
// Returns Metadata object with:
// - title: "Grid Architecture - Our Foundations"
// - description: "The CSS Grid shell for sidebar and content layout..."
// - og:image: "/api/og?title=Grid%20Architecture%20-%20Our%20Foundations"
// ↓
// Next.js injects <meta> tags into <head>
// ↓
// Social platform requests /api/og?title=...
// ↓
// API route generates image and returns PNG
// ↓
// Social platform displays custom card with title, description, and imageThe AI Context
cjs.codes uses dynamic metadata generation that links library-mapping.ts to HTML head tags. The generateMetadata function in app/library/[volume]/[chapter]/page.tsx pulls chapter titles and descriptions from the mapping file and automatically injects them into OpenGraph and Twitter Card meta tags. The site also uses a dynamic OG image API route (/api/og) that generates custom 1200x630px social preview images on-demand using ImageResponse from next/og. Chapter descriptions in library-mapping.ts should be 150 characters to ensure they display properly in all social previews without truncation.Directive for AI Agents
Copy and paste this directive to instruct an AI assistant on how to maintain this specific pattern:
## Directive for AI Agents
When proposing a new content strategy or creating new volumes/chapters for cjs.codes:
**Automated OG Image Generation Requirement:**
- **ALWAYS ensure the content strategy includes a plan for automated OpenGraph image generation**
- This maximizes shareability and brand consistency across all social platforms
- Never propose static image assets for social sharing—always use dynamic generation
- The `/api/og` route automatically generates branded images for every chapter
**Metadata Requirements:**
- **ALWAYS include a 150-character description** in the `library-mapping.ts` file for each chapter
- The description should be informative and work well in social previews
- Make the first 120 characters the most important (for platforms that truncate)
- Include key search terms that users might search for
**The Description Standard:**
- Target: Exactly 150 characters (including spaces)
- Purpose: Used in meta descriptions, OpenGraph tags, and Twitter Cards
- Format: Clear, concise, and descriptive of the chapter content
- Example: `"The CSS Grid shell for sidebar and content layout. Documents the mobile-first responsive pattern and the 1fr constraint fix."`
**Metadata Generation:**
- The `generateMetadata` function automatically pulls from `library-mapping.ts`
- No need to manually add metadata to page files
- The description field in the mapping is used for all social platforms
- OG images are automatically generated from the chapter title via `/api/og` route
**OG Image Implementation:**
- Uses `ImageResponse` from `next/og` for dynamic image generation
- Dark gradient background (zinc-900 to zinc-700) with white Inter Bold text
- 1200x630px standard OG image size
- Edge runtime for fast, global image generation
- Automatically handles font loading with system font fallback
**Adding New Chapters:**
1. Add chapter entry to `library-mapping.ts` with:
- `title`: Chapter title
- `slug`: URL-friendly slug
- `description`: 150-character description (required)
- `keywords`: Optional array of search keywords
2. Create the page file at `app/library/[volume]/[chapter]/page.tsx`
3. The metadata will be automatically generated from the mapping file
4. The OG image will be automatically generated from the title via `/api/og`
**Verification:**
- Test metadata using OpenGraph.xyz and Twitter Card Validator
- Verify OG images load correctly and display chapter titles
- Ensure all social platforms show correct preview cards
**The Rule**: When proposing a new content strategy, always ensure it includes a plan for automated OpenGraph image generation to maximize shareability and brand consistency. Never skip the description field—it's required for proper social sharing and SEO.