Category: Engineering

  • Building FlatWP – Our Headless WordPress Journey

    Building FlatWP – Our Headless WordPress Journey

    After building dozens of headless WordPress sites for clients, we kept running into the same problems. Every project started from scratch. Every developer had to figure out ISR strategies, image optimization, and WordPress GraphQL quirks all over again.

    We decided to build FlatWP to solve this once and for all.

    Developer working on code

    The Problem with Current Solutions

    Most WordPress headless starters are either too basic (just fetch and display) or too opinionated (locked into specific frameworks or hosting). Agencies need something that’s production-ready but flexible enough to customize for different clients.

    We wanted a starter that understood real WordPress workflows – ACF, custom fields, WooCommerce, forms – not just blog posts.

    What Makes FlatWP Different

    FlatWP is built with performance as the foundation. Every architectural decision prioritizes speed:

    • Smart ISR: Content updates instantly via webhooks, not on a timer
    • Static by default: Pages that rarely change are fully static
    • Optimized images: Automatic WebP/AVIF conversion and responsive sizing
    • TypeScript throughout: Full type safety from WordPress to React

    But the real differentiator is the WordPress plugin. Instead of just showing you how to query WordPress, we built tooling that makes the entire workflow seamless.

    Modern web development workspace

    Built for Real Projects

    This isn’t a demo or proof-of-concept. FlatWP is designed for production use from day one. We include preview mode for editors, form handling, SEO metadata, and all the unglamorous features that matter when shipping to clients.

    Our goal is simple: let developers focus on building great experiences, not wrestling with infrastructure.

    We’re launching the open-source version this month, with Pro features coming in early 2025. Stay tuned.

  • ISR Deep Dive: Making Headless WordPress Lightning Fast

    ISR Deep Dive: Making Headless WordPress Lightning Fast

    Incremental Static Regeneration (ISR) is the killer feature that makes Next.js perfect for headless WordPress. But understanding when and how to use it can be tricky.

    What is ISR?

    ISR lets you update static pages after build time without rebuilding your entire site. You get the speed of static generation with the freshness of server-side rendering.

    Here’s how it works: Next.js generates static HTML at build time. After deployment, when someone requests a page, they get the cached version. In the background, Next.js regenerates the page and updates the cache.

    Data visualization dashboard

    Time-Based vs On-Demand Revalidation

    Next.js offers two ISR strategies:

    Time-based revalidation regenerates pages after a specified interval:

    export const revalidate = 3600; // 1 hour

    This is great for content that updates predictably, like archive pages or dashboards.

    On-demand revalidation regenerates pages when triggered by an API call. This is what FlatWP uses. When you save a post in WordPress, our plugin immediately triggers revalidation:

    await fetch('/api/revalidate', {
      method: 'POST',
      body: JSON.stringify({ paths: ['/blog/my-post'] })
    });

    Computer code on screen

    The FlatWP Approach

    We use different strategies for different content types:

    • Blog posts: On-demand ISR (update immediately when edited)
    • Static pages: No revalidation (fully static)
    • Archives: Short time-based ISR (5 minutes)
    • Homepage: Very short ISR or server component

    This gives you instant updates where they matter, without sacrificing performance.

    Performance Impact

    With ISR, first-time visitors get sub-100ms page loads. The page is pre-rendered, served from the edge, and cached globally. Subsequent visitors get even faster loads from CDN cache.

    Compare this to server-side rendering, which queries WordPress on every request. ISR gives you the best of both worlds.

  • ACF + TypeScript: Building Type-Safe Flexible Content

    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.

    FlatWP solves this with automatic TypeScript generation from your ACF field groups.

    The Problem

    When you query ACF fields through GraphQL, you get untyped data:

    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.

    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;
      };
      ctaText: string;
      ctaUrl: string;
    }

    Now your components are fully typed:

    export function HeroBlock({ fields }: { fields: HeroBlock }) {
      return (
        <section>
          <h1>{fields.heading}</h1>
          <p>{fields.subheading}</p>
          {/* TypeScript knows exactly what fields exist */}
        </section>
      );
    }

    Flexible Content Blocks

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

    const blockComponents = {
      hero: HeroBlock,
      features: FeaturesBlock,
      testimonial: TestimonialBlock,
    };
    
    export function BlockRenderer({ blocks }: { blocks: ACFBlock[] }) {
      return blocks.map((block) => {
        const Component = blockComponents[block.layout];
        return <Component key={block.id} fields={block.fields} />;
      });
    }

    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.

    You’ll be able to build complex page layouts in WordPress while maintaining full TypeScript safety in your React components.

  • GraphQL vs REST API: Why We Chose GraphQL for FlatWP

    WordPress offers both REST API and GraphQL for headless implementations. We deliberately chose GraphQL for FlatWP, and here’s why.

    The Over-Fetching Problem

    WordPress REST API returns everything about a post, whether you need it or not:

    GET /wp-json/wp/v2/posts/123

    You get the author object, meta fields, embedded media, comment stats, and dozens of other fields you’ll never use. This bloats response sizes and slows down your site.

    GraphQL’s Precision

    With GraphQL, you request exactly what you need:

    query GetPost($id: ID!) {
      post(id: $id) {
        title
        content
        featuredImage {
          url
          alt
        }
      }
    }

    The response contains only those fields. Nothing more, nothing less.

    TypeScript Integration

    GraphQL’s typed schema enables automatic TypeScript generation. Our codegen process creates perfect types from your queries:

    // Auto-generated from GraphQL schema
    interface GetPostQuery {
      post: {
        title: string;
        content: string;
        featuredImage: {
          url: string;
          alt: string;
        };
      };
    }

    This is nearly impossible with REST API without manually maintaining types.

    Nested Data in One Request

    REST API requires multiple requests for nested data:

    // Get post
    GET /wp-json/wp/v2/posts/123
    // Get author
    GET /wp-json/wp/v2/users/5
    // Get categories
    GET /wp-json/wp/v2/categories?post=123

    GraphQL fetches everything in one query:

    query GetPost($id: ID!) {
      post(id: $id) {
        title
        author {
          name
          avatar
        }
        categories {
          name
          slug
        }
      }
    }

    Better Performance

    Fewer requests = faster page loads. We measured:

    • REST API: 3 requests, 45KB total, 280ms
    • GraphQL: 1 request, 12KB, 95ms

    The WPGraphQL Plugin

    WPGraphQL is mature, well-maintained, and has a huge ecosystem:

    • WPGraphQL for ACF
    • WPGraphQL for WooCommerce
    • WPGraphQL for Yoast SEO
    • WPGraphQL JWT Authentication

    Popular plugins have GraphQL extensions, making integration seamless.

    When to Use REST API

    GraphQL isn’t always the answer. Use REST API when:

    • You need file uploads (GraphQL doesn’t handle multipart well)
    • Your WordPress host doesn’t support WPGraphQL
    • You’re building a simple integration with minimal data needs

    But for full-featured headless sites, GraphQL’s benefits are undeniable.

  • Building a Search Experience Without Algolia

    Algolia is great, but $99/month for search on a small site feels excessive. FlatWP includes a fast, free alternative using static generation and client-side search.

    The Static Search Index Approach

    We generate a lightweight JSON index at build time:

    // app/search-index.json/route.ts
    export const revalidate = 3600;
    
    export async function GET() {
      const posts = await fetchAllPosts();
      
      const index = posts.map(post => ({
        id: post.id,
        title: post.title,
        excerpt: post.excerpt,
        slug: post.slug,
        category: post.category.name,
      }));
      
      return Response.json(index);
    }

    This creates a ~50KB JSON file (for 100 posts) that browsers cache.

    Client-Side Search with Fuse.js

    Fuse.js provides fuzzy search on the client:

    import Fuse from 'fuse.js';
    
    const fuse = new Fuse(searchIndex, {
      keys: ['title', 'excerpt', 'category'],
      threshold: 0.3,
      includeScore: true
    });
    
    const results = fuse.search(query);

    Search is instant – no network request needed.

    When Does This Break Down?

    This approach works well up to ~2000 posts. Beyond that:

    • Index size becomes noticeable (~300KB+)
    • Initial download impacts performance
    • Search slowdown on lower-end devices

    At that scale, consider upgrading to a search service.

    Enhancing the Experience

    We add keyboard shortcuts (⌘K), instant results as you type, and proper highlighting:

    <Command.Dialog>
      <Command.Input 
        placeholder="Search posts..."
        value={query}
        onValueChange={setQuery}
      />
      <Command.List>
        {results.map(result => (
          <Command.Item key={result.item.id}>
            <Link href={`/blog/${result.item.slug}`}>
              {highlightMatch(result.item.title, query)}
            </Link>
          </Command.Item>
        ))}
      </Command.List>
    </Command.Dialog>

    Progressive Enhancement

    For FlatWP Pro, we’re adding optional Algolia integration. It’s a feature flag:

    const searchProvider = process.env.SEARCH_PROVIDER || 'static';
    
    if (searchProvider === 'algolia') {
      // Use Algolia
    } else {
      // Use static JSON + Fuse.js
    }

    Start free, upgrade when needed. No lock-in.

    Performance Comparison

    We tested search on a 500-post site:

    • Static + Fuse.js: 15ms, 0 network requests
    • Algolia: 45ms average (includes network latency)
    • WordPress search: 300ms+ (full database query)

    The static approach is actually faster for most use cases.

  • Monorepo Architecture for FlatWP Projects

    FlatWP uses a monorepo to keep Next.js and WordPress plugin development in sync. Here’s why and how it works.

    Why Monorepo?

    In a headless setup, you’re maintaining:

    • Next.js frontend
    • WordPress plugin for webhooks/admin
    • Shared TypeScript types
    • Configuration files

    Keeping these in separate repos means:

    • Types get out of sync
    • Changes require coordinating multiple PRs
    • Testing becomes complicated
    • New developers need to clone multiple repos

    A monorepo solves all of this.

    Our Structure

    flatwp/
    ├── apps/
    │   ├── web/           # Next.js app
    │   └── wp-plugin/     # WordPress plugin
    ├── packages/
    │   ├── types/         # Shared TS types
    │   └── config/        # ESLint, TS configs
    ├── package.json
    └── pnpm-workspace.yaml

    Shared Types in Action

    When you generate GraphQL types, both the Next.js app and WordPress plugin admin UI access them:

    // packages/types/src/wordpress.ts
    export interface Post {
      id: string;
      title: string;
      slug: string;
    }
    
    // Used in apps/web
    import { Post } from '@flatwp/types';
    
    // Used in apps/wp-plugin admin UI
    import { Post } from '@flatwp/types';

    One source of truth, no duplication.

    pnpm Workspaces

    We use pnpm for fast, efficient dependency management:

    # pnpm-workspace.yaml
    packages:
      - 'apps/*'
      - 'packages/*'

    Run commands across all workspaces:

    pnpm dev          # Start all apps in dev mode
    pnpm build        # Build all apps
    pnpm type-check   # Type check everything

    Turborepo for Speed

    Turborepo caches builds and runs tasks in parallel:

    // turbo.json
    {
      "tasks": {
        "build": {
          "dependsOn": ["^build"],
          "outputs": [".next/**", "build/**"]
        },
        "dev": {
          "cache": false,
          "persistent": true
        }
      }
    }

    Second builds are near-instant thanks to caching.

    Benefits We’ve Seen

    • Faster onboarding: One clone, one install
    • Atomic changes: Update types + usage in one commit
    • Better CI: Test everything together
    • Shared tooling: One ESLint config, one Prettier config

    When NOT to Monorepo

    If you’re just starting and want to move fast, skip the monorepo initially. Build the Next.js app first, add the WordPress plugin later.

    But once you’re serious about shipping, the monorepo pays dividends.