Migration Guide
Guide to migrating from older FlatWP configurations to the new centralized configuration system.
Overview
The new configuration system (@flatwp/config) provides:
- Type Safety: Zod schema validation with TypeScript types
- Centralization: Single source of truth in
flatwp.config.ts - Validation: Helpful error messages for invalid config
- Runtime Access: Convenient utility functions
Migration Path
Step 1: Install Package
The @flatwp/config package is already included in FlatWP monorepo workspaces. No installation needed.
For standalone projects:
npm install @flatwp/config
# or
pnpm add @flatwp/config
# or
yarn add @flatwp/config
Step 2: Create Configuration File
Create flatwp.config.ts in your app root:
import { defineConfig, validateEnv } from '@flatwp/config';
export default defineConfig({
wordpress: {
graphqlUrl: validateEnv(
'NEXT_PUBLIC_WORDPRESS_API_URL',
process.env.NEXT_PUBLIC_WORDPRESS_API_URL
),
revalidateSecret: validateEnv(
'REVALIDATION_SECRET',
process.env.REVALIDATION_SECRET
),
},
});
Step 3: Update Runtime Config Helper
Create or update lib/config.ts:
import flatwpConfig from '../flatwp.config';
import { getISRConfig } from '@flatwp/config';
export const config = flatwpConfig;
export const wordpress = config.wordpress;
export const features = config.features;
export const site = config.site;
export function getContentISR(contentType: string) {
return getISRConfig(config, contentType);
}
Step 4: Update Page Files
Before:
// Hard-coded or scattered config
export const revalidate = 60;
After:
import { getContentISR } from '@/lib/config';
// Centralized, type-safe configuration
export const revalidate = getContentISR('posts').revalidate;
Step 5: Update API Routes
Before:
export async function POST(request: Request) {
const { secret } = await request.json();
if (secret !== process.env.REVALIDATION_SECRET) {
return Response.json({ error: 'Invalid' }, { status: 401 });
}
// ...
}
After:
import { wordpress } from '@/lib/config';
export async function POST(request: Request) {
const { secret } = await request.json();
// Type-safe, validated configuration
if (secret !== wordpress.revalidateSecret) {
return Response.json({ error: 'Invalid' }, { status: 401 });
}
// ...
}
Breaking Changes
Configuration Structure
Before (scattered):
// Multiple files with hard-coded values
const WORDPRESS_URL = process.env.NEXT_PUBLIC_WORDPRESS_API_URL;
const revalidate = 60; // Hard-coded
After (centralized):
export default defineConfig({
wordpress: {
graphqlUrl: process.env.NEXT_PUBLIC_WORDPRESS_API_URL!,
},
rendering: {
posts: {
revalidate: 60, // Centralized
},
},
});
Rendering Configuration
Before:
// In each page file
export const revalidate = 60;
export const dynamic = 'force-static';
After:
export default defineConfig({
rendering: {
posts: {
strategy: 'isr',
revalidate: 60,
generateStaticParams: true,
},
},
});
import { getContentISR } from '@/lib/config';
export const revalidate = getContentISR('posts').revalidate;
Environment Variable Validation
Before (no validation):
const apiUrl = process.env.NEXT_PUBLIC_WORDPRESS_API_URL;
// Runtime error if undefined
After (validated):
import { validateEnv } from '@flatwp/config';
const apiUrl = validateEnv(
'NEXT_PUBLIC_WORDPRESS_API_URL',
process.env.NEXT_PUBLIC_WORDPRESS_API_URL
);
// Fails fast with helpful error message
Feature Flags
Before:
const hasPreview = !!process.env.PREVIEW_SECRET;
const hasSearch = true; // Hard-coded
After:
export default defineConfig({
features: {
preview: {
enabled: !!process.env.PREVIEW_SECRET,
},
search: {
enabled: true,
provider: 'fuse',
},
},
});
import { isFeatureEnabled } from '@/lib/config';
const hasPreview = isFeatureEnabled('preview');
const hasSearch = isFeatureEnabled('search');
Common Migration Scenarios
Scenario 1: Hard-Coded Revalidation
Before:
export const revalidate = 300;
export default async function BlogArchive() {
// ...
}
After:
- Add to configuration:
export default defineConfig({
rendering: {
archives: {
strategy: 'isr',
revalidate: 300,
},
},
});
- Update page:
import { getContentISR } from '@/lib/config';
export const revalidate = getContentISR('archives').revalidate;
Scenario 2: Multiple Environment Variables
Before:
export const WORDPRESS_URL = process.env.NEXT_PUBLIC_WORDPRESS_API_URL!;
export const WORDPRESS_DOMAIN = process.env.NEXT_PUBLIC_WORDPRESS_DOMAIN!;
export const PREVIEW_SECRET = process.env.PREVIEW_SECRET;
After:
- Centralize in config:
export default defineConfig({
wordpress: {
graphqlUrl: validateEnv(
'NEXT_PUBLIC_WORDPRESS_API_URL',
process.env.NEXT_PUBLIC_WORDPRESS_API_URL
),
domain: process.env.NEXT_PUBLIC_WORDPRESS_DOMAIN,
previewSecret: process.env.PREVIEW_SECRET,
},
});
- Update imports:
import { wordpress } from './config';
export const WORDPRESS_URL = wordpress.graphqlUrl;
export const WORDPRESS_DOMAIN = wordpress.domain;
export const PREVIEW_SECRET = wordpress.previewSecret;
Scenario 3: Custom Post Types
Before:
export const revalidate = 300;
export const dynamic = 'force-static';
After:
- Add custom type to config:
export default defineConfig({
rendering: {
custom: {
product: {
strategy: 'isr',
revalidate: 300,
generateStaticParams: true,
},
},
},
});
- Update page:
import { getContentISR } from '@/lib/config';
export const revalidate = getContentISR('product').revalidate;
Scenario 4: Feature Toggles
Before:
const SEARCH_ENABLED = process.env.NEXT_PUBLIC_SEARCH_ENABLED === 'true';
export function Search() {
if (!SEARCH_ENABLED) return null;
// ...
}
After:
- Configure feature:
export default defineConfig({
features: {
search: {
enabled: process.env.NEXT_PUBLIC_SEARCH_ENABLED === 'true',
provider: 'fuse',
},
},
});
- Update component:
import { isFeatureEnabled } from '@/lib/config';
export function Search() {
if (!isFeatureEnabled('search')) return null;
// ...
}
Validation Errors
Common Errors and Fixes
Missing Required Variable
Error:
ConfigError: Missing required environment variable: NEXT_PUBLIC_WORDPRESS_API_URL
Hint: Set NEXT_PUBLIC_WORDPRESS_API_URL in your .env.local file
Fix:
Add the variable to .env.local:
NEXT_PUBLIC_WORDPRESS_API_URL=https://your-wordpress.com/graphql
Invalid URL
Error:
wordpress.graphqlUrl: Must be a valid URL (e.g., https://example.com)
Fix: Ensure URL includes protocol:
# Wrong
NEXT_PUBLIC_WORDPRESS_API_URL=wordpress.com/graphql
# Right
NEXT_PUBLIC_WORDPRESS_API_URL=https://wordpress.com/graphql
Short Secret
Error:
wordpress.revalidateSecret: Must be at least 16 characters (currently 8)
Fix: Generate a secure secret:
openssl rand -base64 32
Then add to .env.local:
REVALIDATION_SECRET=your-32-character-secret-here
Testing Migration
Verify Configuration
import { validateConfig } from '@flatwp/config';
import config from './flatwp.config';
const result = validateConfig(config);
if (result.success) {
console.log('✅ Configuration valid');
console.log('WordPress URL:', result.data?.wordpress.graphqlUrl);
} else {
console.error('❌ Configuration invalid:');
result.errors?.forEach((error) => console.error(' -', error));
process.exit(1);
}
Run verification:
npx tsx scripts/verify-config.ts
Test Runtime Access
import { config, wordpress, getContentISR } from '@/lib/config';
describe('Configuration', () => {
it('should load WordPress config', () => {
expect(wordpress.graphqlUrl).toBeDefined();
expect(wordpress.graphqlUrl).toMatch(/^https?:\/\//);
});
it('should provide ISR config', () => {
const postsISR = getContentISR('posts');
expect(postsISR.revalidate).toBeDefined();
});
it('should validate secrets', () => {
expect(wordpress.revalidateSecret.length).toBeGreaterThanOrEqual(16);
});
});
Rollback Plan
If you need to rollback:
1. Keep Old Files Temporarily
Don't delete old configuration files until migration is verified:
# Rename instead of delete
mv lib/wordpress.config.ts lib/wordpress.config.ts.old
mv lib/rendering.config.ts lib/rendering.config.ts.old
2. Feature Flag Migration
Roll out gradually using feature flag:
const USE_NEW_CONFIG = process.env.USE_NEW_CONFIG === 'true';
export const config = USE_NEW_CONFIG
? newFlatWPConfig
: oldConfig;
3. Restore Old Config
If issues arise:
# Restore old files
mv lib/wordpress.config.ts.old lib/wordpress.config.ts
git checkout lib/config.ts
Post-Migration Checklist
- Configuration file created (
flatwp.config.ts) - Runtime helper updated (
lib/config.ts) - All pages migrated to use
getContentISR() - API routes use centralized config
- Environment variables validated
- Tests updated and passing
- Development environment tested
- Staging environment tested
- Production deployment verified
- Old configuration files removed
- Team documentation updated
Getting Help
If you encounter issues during migration:
- Check documentation: Configuration Reference
- Review examples: See
apps/webandapps/starterfor reference - Validation errors: Read error messages carefully - they're designed to be helpful
- GitHub issues: Report bugs or ask questions
- Discord community: Get help from other FlatWP users
See Also
- Getting Started - Setup guide
- Configuration Reference - Complete API
- Environment Variables - Env var reference
- Runtime Usage - Using config in code