Category: Tutorials

  • WordPress Plugin Compatibility: What Works with FlatWP

    Mastering TypeScript with Advanced Custom Fields

    Advanced Custom Fields (ACF) is the go-to solution for flexible WordPress content. But in a TypeScript headless setup, losing type safety on your custom fields is a major pain point. This guide will show you every workaround the proper solution.

    Code editor with TypeScript

    “TypeScript makes you feel like a wizard. ACF without types makes you feel like a detective. FlatWP gives you both.” – A satisfied developer

    The Problem With Untyped ACF

    When you query ACF fields through GraphQL, you get untyped data that looks like this:

    const hero = page.acf.hero; // any type - no autocomplete, no safety

    This means runtime errors, no IDE support, and constant trips to the WordPress admin to check field names. You can read more about this on the official ACF documentation.

    Real-World Problems

    Here are the issues we encountered before implementing type safety:

    1. Typos in field names that don’t show until production
    2. Refactoring field names becomes a search-and-replace nightmare
    3. No way to know what fields exist without checking WordPress admin
    4. Team members constantly asking “what’s the field name for X?”

    TypeScript code on monitor

    Breaking It Down Further

    The issue compounds when you have:

    • Multiple developers on the team
    • Complex field groups with nested repeaters
    • Client-requested field changes mid-project
    • Different ACF field types (relationships, galleries, etc.)
    Nested List Example

    Sometimes you need nested lists to organize information:

    • Frontend Issues
      • No autocomplete in VSCode/Cursor
      • Runtime type errors
      • Manual type guards everywhere
    • Backend Issues
      • Field name mismatches
      • Data structure uncertainty
      • Version control conflicts
    Why h6 Headings Matter

    Even though h6 is rarely used, it’s still valid HTML and should be styled properly. This is the deepest heading level you’ll encounter.


    The FlatWP Solution

    Our WordPress plugin exposes ACF schemas as structured JSON. Our codegen tool transforms these into TypeScript interfaces:

    interface HeroBlock {
      heading: string;
      subheading: string;
      image: {
        url: string;
        alt: string;
        width: number;
        height: number;
      };
      ctaText: string;
      ctaUrl: string;
      backgroundColor?: 'light' | 'dark' | 'accent';
    }

    Now your components are fully typed with intelligent autocomplete:

    export function HeroBlock({ fields }: { fields: HeroBlock }) {
      return (
        <section className={`hero hero--${fields.backgroundColor || 'light'}`}>
          <h1>{fields.heading}</h1>
          <p>{fields.subheading}</p>
          <img 
            src={fields.image.url} 
            alt={fields.image.alt}
            width={fields.image.width}
            height={fields.image.height}
          />
          <a href={fields.ctaUrl}>{fields.ctaText}</a>
        </section>
      );
    }

    Development team collaboration

    Advanced Formatting Examples

    Let’s look at various ways to format content for maximum readability.

    Mixed List Types

    Here’s an ordered list with definitions:

    1. Generate Types – Run the codegen command after ACF changes
    2. Import Types – Use them in your component props
    3. Enjoy Safety – TypeScript catches errors before runtime

    And an unordered list with code examples:

    • npm run generate-types – Generates ACF types
    • npm run build – Builds the Next.js app
    • npm run dev – Runs development server

    Pro tip: Add type generation to your pre-commit hooks so types are always in sync with your ACF configuration!

    Complex Table Example

    ACF Field Type TypeScript Type GraphQL Return Notes
    Text string String Simple text field
    Number number Int/Float Numeric values
    True/False boolean Boolean Checkbox value
    Image WPImage MediaItem Complex object with URL, alt, dimensions
    Repeater Array<T> [Type] Array of typed objects

    Flexible Content Blocks

    ACF’s Flexible Content field type is perfect for page builders. FlatWP provides a block renderer pattern with full type safety:

    const blockComponents = {
      hero: HeroBlock,
      features: FeaturesBlock,
      testimonial: TestimonialBlock,
      cta: CTABlock,
      gallery: GalleryBlock,
    } as const;
    
    type BlockLayout = keyof typeof blockComponents;
    
    interface ACFBlock<T = unknown> {
      id: string;
      layout: BlockLayout;
      fields: T;
    }
    
    export function BlockRenderer({ blocks }: { blocks: ACFBlock[] }) {
      return (
        <>
          {blocks.map((block) => {
            const Component = blockComponents[block.layout];
            if (!Component) {
              console.warn(`Unknown block type: ${block.layout}`);
              return null;
            }
            return <Component key={block.id} fields={block.fields} />;
          })}
        </>
      );
    }

    Visit our documentation site for more examples and implementation details.

    Installation Steps

    Getting started is straightforward:

    1. Install the FlatWP WordPress plugin from the repository
    2. Configure your ACF field groups as usual
    3. Run npm install @flatwp/types in your Next.js project
    4. Execute npm run generate-types to create TypeScript interfaces
    5. Import and use types in your components

    Important: Make sure to run type generation after every ACF schema change to keep your frontend in sync!

    Developer working on laptop

    Coming in FlatWP Pro

    The Pro version will include a library of 20+ pre-built ACF blocks with matching Shadcn components. Hero sections, feature grids, testimonials, pricing tables – all typed, styled, and ready to use.

    Pro Feature Highlights

    • Pre-configured ACF field groups
    • Matching React/Shadcn components
    • Dark mode variants
    • Mobile-responsive layouts
    • Accessibility built-in (ARIA labels, keyboard navigation)
    • Animation variants using Framer Motion

    You’ll be able to build complex page layouts in WordPress while maintaining full TypeScript safety in your React components. Check out our GitHub repository to follow development progress.


    Conclusion: Type Safety Changed Everything

    We shipped our first FlatWP site to production 6 months ago and haven’t looked back. Zero runtime errors related to ACF field access. Confident refactoring. Happy developers. Manual type checking Automatic type checking.

    The investment in proper TypeScript integration pays dividends on every single project.

  • Preview Mode: Let Editors See Drafts Before Publishing

    One of the biggest challenges with headless WordPress is preview functionality. Editors want to see their drafts before publishing, but your static site only shows published content.

    FlatWP’s preview mode solves this elegantly.

    How It Works

    When an editor clicks “Preview” in WordPress, our plugin generates a special URL:

    https://flatwp.com/api/preview?secret=xyz&id=123&type=post

    This hits your Next.js preview API route, which:

    1. Verifies the secret token
    2. Enables Next.js draft mode via cookies
    3. Redirects to the post URL

    Now when Next.js renders the page, it queries WordPress for draft content instead of published content.

    The Implementation

    Your preview API route:

    export async function GET(request: Request) {
      const { searchParams } = new URL(request.url);
      const secret = searchParams.get('secret');
      const id = searchParams.get('id');
    
      // Verify secret
      if (secret !== process.env.PREVIEW_SECRET) {
        return new Response('Invalid token', { status: 401 });
      }
    
      // Enable draft mode
      draftMode().enable();
    
      // Redirect to the post
      redirect(`/blog/${slug}`);
    }

    Then in your page component:

    export default async function Post({ params }) {
      const { isEnabled } = draftMode();
      
      // Fetch draft if preview mode is enabled
      const post = await fetchPost(params.slug, {
        preview: isEnabled
      });
      
      return <PostTemplate post={post} />;
    }

    The WordPress Side

    Our plugin adds a “Preview on Frontend” button to the WordPress editor that generates the preview URL automatically.

    It also handles authentication – only logged-in WordPress users can generate preview links, keeping your drafts secure.

    Exit Preview

    We add a banner to preview pages:

    {draftMode().isEnabled && (
      <div className="bg-yellow-100 p-4">
        <p>You are viewing a preview.</p>
        <a href="/api/exit-preview">Exit Preview</a>
      </div>
    )}

    The exit route simply clears the draft mode cookie.

    Why This Matters

    Without preview mode, editors have to publish content to see how it looks. This breaks their workflow and risks publishing unfinished work.

    With FlatWP’s preview mode, editors can iterate on drafts, share preview links with teammates, and only publish when ready.

    It’s a small feature that makes a huge difference in adoption.

  • Deploying FlatWP: Vercel, Netlify, or Self-Hosted?

    FlatWP works on any platform that supports Next.js, but the deployment choice significantly impacts performance and developer experience.

    Vercel (Recommended)

    Vercel created Next.js, so integration is seamless:

    Pros:

    • Zero-config deployment – just connect GitHub
    • ISR works perfectly out of the box
    • Global edge network for fast delivery
    • On-demand revalidation built-in
    • Excellent free tier (100GB bandwidth, unlimited builds)
    • Preview deployments for every PR

    Cons:

    • Can get expensive at scale ($20/user/month for teams)
    • Vendor lock-in (though you can export)

    Best for: Most projects, especially if you value DX and don’t mind the cost at scale.

    Netlify

    Netlify has excellent Next.js support via their Essential Next.js plugin:

    Pros:

    • Strong free tier (100GB bandwidth)
    • Great build performance
    • Form handling built-in
    • Split testing features
    • Slightly cheaper than Vercel at scale

    Cons:

    • ISR support is newer, less battle-tested
    • Some Next.js features lag behind Vercel
    • Build times can be slower for large sites

    Best for: Teams already on Netlify or those wanting to save costs.

    Cloudflare Pages

    Cloudflare now supports Next.js via their @cloudflare/next-on-pages adapter:

    Pros:

    • Incredibly generous free tier (unlimited requests)
    • Cloudflare’s global network
    • R2 storage for assets
    • Very cost-effective at scale

    Cons:

    • Requires adapter configuration
    • Some Next.js features not supported
    • Newer, less documentation
    • ISR implementation differs from Vercel

    Best for: High-traffic sites on a budget.

    Self-Hosted (Node.js)

    You can deploy Next.js to any Node.js server:

    Pros:

    • Full control
    • No platform fees
    • Can use your existing infrastructure

    Cons:

    • You manage scaling, CDN, caching
    • More DevOps overhead
    • ISR requires Redis or similar for cache
    • No automatic preview deployments

    Best for: Enterprise with existing infrastructure or strict data requirements.

    Our Recommendation

    Start with Vercel. The DX is unmatched and the free tier is generous enough for most projects. Once you’re at scale and costs matter, evaluate Netlify or Cloudflare.

    For the WordPress backend, use any WordPress host – FlatWP doesn’t care where WordPress lives as long as GraphQL is accessible.

  • Form Handling in Headless WordPress: Three Approaches

    Forms are everywhere – contact forms, newsletter signups, lead capture. Here’s how to handle them in a headless WordPress setup.

    Approach 1: WordPress Forms (Recommended)

    Use Contact Form 7 or WPForms in WordPress, submit from Next.js:

    // Server action in Next.js
    async function handleSubmit(formData: FormData) {
      const response = await fetch(
        'https://cms.flatwp.com/wp-json/contact-form-7/v1/contact-forms/123/feedback',
        {
          method: 'POST',
          body: formData,
        }
      );
      
      return response.json();
    }

    Pros:

    • Editors manage form fields in WordPress
    • Email notifications handled by WordPress
    • Submissions stored in WordPress database
    • Works with existing WordPress plugins

    Cons:

    • Extra API call to WordPress
    • WordPress becomes a dependency for forms

    Approach 2: Next.js Server Actions

    Handle forms entirely in Next.js with server actions:

    // app/contact/actions.ts
    'use server'
    
    export async function submitContact(formData: FormData) {
      const data = {
        name: formData.get('name'),
        email: formData.get('email'),
        message: formData.get('message'),
      };
      
      // Send email via Resend, SendGrid, etc.
      await sendEmail(data);
      
      // Store in database if needed
      await db.contacts.create(data);
    }

    Pros:

    • No WordPress dependency
    • Full control over validation and processing
    • Modern server actions pattern
    • Can integrate with any email service

    Cons:

    • Editors can’t manage form fields
    • Need separate storage for submissions
    • More code to maintain

    Approach 3: Third-Party Services

    Use Formspree, Tally, or similar:

    <form action="https://formspree.io/f/your-id" method="POST">
      <input name="email" type="email" />
      <button type="submit">Submit</button>
    </form>

    Pros:

    • Zero backend code
    • Spam protection included
    • Email notifications handled
    • Nice dashboard for submissions

    Cons:

    • Monthly cost ($0-$20)
    • Less customization
    • Another service to manage

    FlatWP’s Approach

    We include adapters for all three approaches. Our default recommendation:

    • Use WordPress forms for marketing sites (editors need control)
    • Use server actions for apps (more dynamic needs)
    • Use third-party for MVPs (fastest to ship)

    The beauty of headless is you’re not locked in. Start with one, switch to another as needs change.

    Validation with Zod

    Regardless of approach, validate with Zod:

    const contactSchema = z.object({
      name: z.string().min(2),
      email: z.string().email(),
      message: z.string().min(10),
    });
    
    export async function submitContact(formData: FormData) {
      const data = contactSchema.parse({
        name: formData.get('name'),
        email: formData.get('email'),
        message: formData.get('message'),
      });
      
      // data is now typed and validated
    }

    Type-safe forms with runtime validation. Beautiful.