Framework Integration Guide
Learn how to integrate FieldTest with Astro, Next.js, and other modern frameworks.
What You'll Learn
- Setting up FieldTest in Astro projects
- Using FieldTest with Next.js
- Framework-specific validation patterns
- Build-time content validation
- Error handling strategies
Astro Integration
Installation
bash
pnpm add @watthem/fieldtest
Content Collections with Validation
Astro's content collections work perfectly with FieldTest:
src/content/config.ts:
typescript
import { defineCollection, z } from 'astro:content';
import { validateAstroContent, loadUserSchema } from '@watthem/fieldtest';
import type { StandardSchemaV1 } from '@watthem/fieldtest';
// Define your FieldTest schema
const blogPostSchema: StandardSchemaV1 = {
version: '1',
name: 'blog-post',
fields: {
title: { type: 'string', required: true },
description: { type: 'string', required: true },
publishedAt: { type: 'date', required: true },
author: { type: 'string', required: true },
tags: { type: 'string', array: true }
}
};
// Load the schema
const fieldTestSchema = loadUserSchema(blogPostSchema);
// Define collection with Astro + FieldTest validation
const blog = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
publishedAt: z.date(),
author: z.string(),
tags: z.array(z.string()).optional()
}).refine((data) => {
// Additional FieldTest validation
const content = `---
title: ${data.title}
description: ${data.description}
publishedAt: ${data.publishedAt.toISOString()}
author: ${data.author}
${data.tags ? `tags: [${data.tags.map(t => `"${t}"`).join(', ')}]` : ''}
---`;
const result = validateAstroContent(content, fieldTestSchema);
if (!result.valid) {
throw new Error(`FieldTest validation failed: ${result.errors.map(e => e.message).join(', ')}`);
}
return true;
})
});
export const collections = { blog };
Validating During Build
Create a validation script that runs during build:
scripts/validate-content.ts:
typescript
import { getCollection } from 'astro:content';
import { validateAstroContent, loadUserSchema } from '@watthem/fieldtest';
import { blogPostSchema } from '../src/schemas';
async function validateContent() {
const schema = loadUserSchema(blogPostSchema);
const posts = await getCollection('blog');
let hasErrors = false;
for (const post of posts) {
const content = post.body; // Full markdown content
const result = validateAstroContent(content, schema);
if (!result.valid) {
console.error(`\n❌ ${post.id} failed validation:`);
result.errors.forEach(error => {
console.error(` - ${error.field}: ${error.message}`);
});
hasErrors = true;
} else {
console.log(`✓ ${post.id}`);
}
}
if (hasErrors) {
process.exit(1);
}
console.log('\n✓ All content validated successfully!');
}
validateContent();
package.json:
json
{
"scripts": {
"build": "pnpm validate-content && astro build",
"validate-content": "tsx scripts/validate-content.ts"
}
}
Dynamic Pages with Validation
Validate content when generating dynamic pages:
src/pages/blog/[slug].astro:
astro
---
import { getCollection } from 'astro:content';
import { validateAstroContent, loadUserSchema } from '@watthem/fieldtest';
import { blogPostSchema } from '../../schemas';
export async function getStaticPaths() {
const schema = loadUserSchema(blogPostSchema);
const posts = await getCollection('blog');
return posts.map(post => {
// Validate each post
const result = validateAstroContent(post.body, schema);
if (!result.valid) {
throw new Error(`Invalid post ${post.id}: ${result.errors.map(e => e.message).join(', ')}`);
}
return {
params: { slug: post.slug },
props: { post }
};
});
}
const { post } = Astro.props;
const { Content } = await post.render();
---
<article>
<h1>{post.data.title}</h1>
<p class="author">By {post.data.author}</p>
<Content />
</article>
Next.js Integration
App Router (Next.js 13+)
app/blog/[slug]/page.tsx:
typescript
import { validateNextContent, loadUserSchema } from '@watthem/fieldtest';
import type { StandardSchemaV1 } from '@watthem/fieldtest';
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
const blogPostSchema: StandardSchemaV1 = {
version: '1',
name: 'blog-post',
fields: {
title: { type: 'string', required: true },
author: { type: 'string', required: true },
publishedAt: { type: 'date', required: true }
}
};
const schema = loadUserSchema(blogPostSchema);
export async function generateStaticParams() {
const postsDirectory = path.join(process.cwd(), 'content/posts');
const filenames = fs.readdirSync(postsDirectory);
return filenames.map(filename => ({
slug: filename.replace('.md', '')
}));
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const postsDirectory = path.join(process.cwd(), 'content/posts');
const fullPath = path.join(postsDirectory, `${params.slug}.md`);
const fileContents = fs.readFileSync(fullPath, 'utf-8');
// Validate content
const result = validateNextContent(fileContents, schema);
if (!result.valid) {
throw new Error(`Content validation failed: ${result.errors.map(e => e.message).join(', ')}`);
}
const { data, content } = matter(fileContents);
return (
<article>
<h1>{data.title}</h1>
<p>By {data.author}</p>
<div dangerouslySetInnerHTML={{ __html: content }} />
</article>
);
}
Pages Router (Next.js 12 and earlier)
pages/blog/[slug].tsx:
typescript
import { GetStaticPaths, GetStaticProps } from 'next';
import { validateNextContent, loadUserSchema } from '@watthem/fieldtest';
import { blogPostSchema } from '../../schemas';
import fs from 'fs';
import path from 'path';
interface BlogPostProps {
title: string;
author: string;
content: string;
}
export const getStaticPaths: GetStaticPaths = async () => {
const postsDirectory = path.join(process.cwd(), 'content/posts');
const filenames = fs.readdirSync(postsDirectory);
const paths = filenames.map(filename => ({
params: { slug: filename.replace('.md', '') }
}));
return { paths, fallback: false };
};
export const getStaticProps: GetStaticProps<BlogPostProps> = async ({ params }) => {
const schema = loadUserSchema(blogPostSchema);
const postsDirectory = path.join(process.cwd(), 'content/posts');
const fullPath = path.join(postsDirectory, `${params!.slug}.md`);
const fileContents = fs.readFileSync(fullPath, 'utf-8');
// Validate during build
const result = validateNextContent(fileContents, schema);
if (!result.valid) {
throw new Error(`Invalid content in ${params!.slug}: ${result.errors.map(e => e.message).join(', ')}`);
}
const { data, content } = matter(fileContents);
return {
props: {
title: data.title,
author: data.author,
content
}
};
};
export default function BlogPost({ title, author, content }: BlogPostProps) {
return (
<article>
<h1>{title}</h1>
<p>By {author}</p>
<div dangerouslySetInnerHTML={{ __html: content }} />
</article>
);
}
API Routes
Validate content in API endpoints:
app/api/validate/route.ts (App Router):
typescript
import { NextRequest, NextResponse } from 'next/server';
import { validateNextContent, loadUserSchema } from '@watthem/fieldtest';
import { blogPostSchema } from '../../../schemas';
export async function POST(request: NextRequest) {
try {
const { content } = await request.json();
const schema = loadUserSchema(blogPostSchema);
const result = validateNextContent(content, schema);
if (result.valid) {
return NextResponse.json({ valid: true });
} else {
return NextResponse.json(
{ valid: false, errors: result.errors },
{ status: 400 }
);
}
} catch (error) {
return NextResponse.json(
{ error: 'Validation failed' },
{ status: 500 }
);
}
}
Other Frameworks
Remix
typescript
import { json, LoaderFunction } from '@remix-run/node';
import { validateWithSchema, loadUserSchema } from '@watthem/fieldtest';
import { blogPostSchema } from '../schemas';
export const loader: LoaderFunction = async ({ params }) => {
const schema = loadUserSchema(blogPostSchema);
const content = await loadMarkdownFile(params.slug);
const result = validateWithSchema(content, schema);
if (!result.valid) {
throw new Response('Invalid content', { status: 400 });
}
return json({ content });
};
SvelteKit
typescript
import type { Load } from '@sveltejs/kit';
import { validateWithSchema, loadUserSchema } from '@watthem/fieldtest';
import { blogPostSchema } from '$lib/schemas';
export const load: Load = async ({ params }) => {
const schema = loadUserSchema(blogPostSchema);
const content = await loadMarkdownFile(params.slug);
const result = validateWithSchema(content, schema);
if (!result.valid) {
return {
status: 400,
error: new Error('Invalid content')
};
}
return { content };
};
Nuxt 3
typescript
export default defineEventHandler(async (event) => {
const { validateWithSchema, loadUserSchema } = await import('@watthem/fieldtest');
const { blogPostSchema } = await import('~/schemas');
const slug = event.context.params.slug;
const schema = loadUserSchema(blogPostSchema);
const content = await loadMarkdownFile(slug);
const result = validateWithSchema(content, schema);
if (!result.valid) {
throw createError({
statusCode: 400,
message: 'Invalid content'
});
}
return { content };
});
Error Handling Strategies
Development vs Production
typescript
import { validateWithSchema, loadUserSchema } from '@watthem/fieldtest';
const isDevelopment = process.env.NODE_ENV === 'development';
function validateContent(content: string, schema: StandardSchema) {
const result = validateWithSchema(content, schema);
if (!result.valid) {
if (isDevelopment) {
// Show detailed errors in development
console.error('❌ Validation failed:');
result.errors.forEach(e => {
console.error(` ${e.field}: ${e.message}`);
});
throw new Error('Content validation failed - see console for details');
} else {
// Log but don't expose details in production
console.error('Validation failed:', result.errors);
throw new Error('Content validation failed');
}
}
return result;
}
Graceful Degradation
typescript
function validateContentSafe(content: string, schema: StandardSchema) {
try {
const result = validateWithSchema(content, schema);
if (!result.valid) {
// Log errors but don't fail
console.warn('Content has validation issues:', result.errors);
return { valid: false, errors: result.errors };
}
return { valid: true, errors: [] };
} catch (error) {
console.error('Validation error:', error);
return { valid: false, errors: [{ field: 'unknown', message: 'Validation failed', code: 'UNKNOWN' }] };
}
}
Best Practices
1. Validate at Build Time
Catch errors early by validating during the build process, not at runtime.
2. Use TypeScript
Combine FieldTest with TypeScript for maximum type safety:
typescript
import type { StandardSchemaV1, FieldTestDocument } from '@watthem/fieldtest';
const schema: StandardSchemaV1 = {
// TypeScript ensures correct structure
version: '1',
name: 'blog-post',
fields: {
title: { type: 'string', required: true }
}
};
3. Create Reusable Validation Utilities
typescript
// lib/validation.ts
import { validateWithSchema, loadUserSchema } from '@watthem/fieldtest';
import type { StandardSchemaV1 } from '@watthem/fieldtest';
export function createValidator(schema: StandardSchemaV1) {
const loadedSchema = loadUserSchema(schema);
return function validate(content: string) {
const result = validateWithSchema(content, loadedSchema);
if (!result.valid) {
throw new Error(
`Validation failed:\n${result.errors.map(e => ` - ${e.field}: ${e.message}`).join('\n')}`
);
}
return result;
};
}
// Usage
const validateBlogPost = createValidator(blogPostSchema);
validateBlogPost(content);
4. Cache Loaded Schemas
typescript
const schemaCache = new Map();
function getCachedSchema(schemaDefinition: StandardSchemaV1) {
const key = schemaDefinition.name;
if (!schemaCache.has(key)) {
schemaCache.set(key, loadUserSchema(schemaDefinition));
}
return schemaCache.get(key);
}
Next Steps
- 📚 API Reference — Complete API documentation
- 🎓 Schema Validation Guide — Deep dive into schemas
- 💡 Examples — Real-world use cases