Skip to main content
Advertisement

11.5 Metadata, Environment Variables, and Deployment — Metadata Types and process.env Safety

Metadata API Types

The Metadata API in Next.js 13+ is fully type-safe.

Static Metadata

// app/blog/page.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
title: 'Blog',
description: 'Check out the latest posts.',
keywords: ['TypeScript', 'Next.js', 'Development'],
authors: [{ name: 'John Doe', url: 'https://example.com' }],

openGraph: {
title: 'Blog',
description: 'Check out the latest posts.',
url: 'https://example.com/blog',
siteName: 'My Blog',
images: [
{
url: 'https://example.com/og.png',
width: 1200,
height: 630,
alt: 'OG Image',
},
],
locale: 'en_US',
type: 'website',
},

twitter: {
card: 'summary_large_image',
title: 'Blog',
description: 'Check out the latest posts.',
images: ['https://example.com/og.png'],
creator: '@username',
},

robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-image-preview': 'large',
},
},

alternates: {
canonical: 'https://example.com/blog',
languages: {
'ko-KR': 'https://example.com/ko/blog',
'en-US': 'https://example.com/en/blog',
},
},
};

Dynamic Metadata

// app/blog/[slug]/page.tsx
import { Metadata, ResolvingMetadata } from 'next';

interface Props {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | undefined }>;
}

export async function generateMetadata(
{ params }: Props,
parent: ResolvingMetadata // Access parent metadata
): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);

// Use parent metadata
const parentImages = (await parent).openGraph?.images ?? [];

if (!post) {
return {
title: 'Post not found',
};
}

return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [
post.coverImage
? { url: post.coverImage, width: 1200, height: 630 }
: ...parentImages,
],
type: 'article',
publishedTime: post.publishedAt.toISOString(),
authors: [post.author.name],
},
};
}

metadataBase Configuration

// app/layout.tsx
export const metadata: Metadata = {
metadataBase: new URL('https://example.com'), // Base for relative URLs
title: {
default: 'My App',
template: '%s | My App', // Child pages: "Page Title | My App"
},
description: 'Description of my app.',
};

process.env Type Safety

Basic Method: declare namespace

// src/types/env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
// Required env vars
NODE_ENV: 'development' | 'production' | 'test';
DATABASE_URL: string;
NEXTAUTH_SECRET: string;
NEXTAUTH_URL: string;

// Optional env vars
PORT?: string;
LOG_LEVEL?: 'debug' | 'info' | 'warn' | 'error';
}
}
// Type-safe usage
const db = new Client({ connectionString: process.env.DATABASE_URL }); // string
const port = parseInt(process.env.PORT ?? '3000'); // string | undefined → number

Type declarations alone don't guarantee values exist at runtime. Zod validates at app startup.

// lib/env.ts
import { z } from 'zod';

const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
DATABASE_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(32),
NEXTAUTH_URL: z.string().url(),
PORT: z.coerce.number().int().positive().default(3000),

// Optional
SMTP_HOST: z.string().optional(),
SMTP_PORT: z.coerce.number().optional(),

// Client-side (NEXT_PUBLIC_ prefix)
NEXT_PUBLIC_API_URL: z.string().url(),
NEXT_PUBLIC_GA_ID: z.string().optional(),
});

// Validate and parse env vars
function validateEnv() {
const result = envSchema.safeParse(process.env);

if (!result.success) {
console.error('❌ Environment variable errors:');
result.error.issues.forEach(issue => {
console.error(` ${issue.path.join('.')}: ${issue.message}`);
});
process.exit(1); // Stop server startup
}

return result.data;
}

export const env = validateEnv();
// env.DATABASE_URL: string (no undefined, validated)
// Usage
import { env } from '@/lib/env';

const db = new Client({ connectionString: env.DATABASE_URL });
const port = env.PORT; // number (already parsed)

@t3-oss/env-nextjs (T3 Stack Approach)

npm install @t3-oss/env-nextjs zod
// env.mjs
import { createEnv } from '@t3-oss/env-nextjs';
import { z } from 'zod';

export const env = createEnv({
server: {
DATABASE_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(32),
NEXTAUTH_URL: z.string().url(),
},
client: {
NEXT_PUBLIC_API_URL: z.string().url(),
NEXT_PUBLIC_GA_ID: z.string().optional(),
},
// Explicit process.env pass (Edge Runtime compatibility)
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
NEXT_PUBLIC_GA_ID: process.env.NEXT_PUBLIC_GA_ID,
},
});

Vercel Deployment Integration

vercel.json Types

// vercel.json
{
"framework": "nextjs",
"buildCommand": "npm run build",
"devCommand": "npm run dev",
"installCommand": "npm install",
"regions": ["iad1"],
"env": {
"DATABASE_URL": "@database-url"
},
"headers": [
{
"source": "/api/(.*)",
"headers": [
{ "key": "Cache-Control", "value": "no-store" }
]
}
]
}

Edge Runtime Configuration

// app/api/geo/route.ts
import { NextRequest } from 'next/server';

export const runtime = 'edge'; // Specify Edge Runtime

export async function GET(request: NextRequest) {
// Geo info is accessible on Edge
const { geo } = request; // { country: string, city: string, ... }

return Response.json({
country: geo?.country,
city: geo?.city,
});
}

Pro Tips

1. Environment variable prefix rules

NEXT_PUBLIC_*: Accessible on client (browser)
All others: Server-only (not included in client bundle)

2. Local env file priority

.env.local          → Local development (not committed to git)
.env.development → Development environment
.env.production → Production environment
.env → All environments (common defaults)

3. Build-time check (next.config.ts)

// next.config.ts
import './src/env'; // Run env validation at build start

const config: NextConfig = { /* ... */ };
export default config;
Advertisement