Author: FlatWP Team

  • 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.

  • 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.

  • Image Optimization in Headless WordPress: Beyond next/image

    Full-Stack TypeScript: WordPress Meets Next.js

    TypeScript transformed our Next.js + WordPress setup. Here’s everything we learned shipping type-safe headless sites for real clients with zero compromises maximum confidence.

    TypeScript code in IDE

    The Promise of TypeScript

    When WordPress meets Next.js, you’re dealing with two systems that speak different languages. WordPress returns PHP-shaped data; Next.js expects JavaScript objects. TypeScript bridges this gap with compile-time safety.

    “Without types, you catch bugs in production. With types, you catch them before your first commit.” – Every developer who’s been burned by runtime errors

    Type Generation from GraphQL

    The magic happens when you generate TypeScript types directly from your GraphQL schema. Here’s how we do it with graphql-codegen:

    // codegen.yml
    schema: 'https://cms.flatwp.com/graphql'
    generates:
      src/types/graphql.ts:
        plugins:
          - 'typescript'
          - 'typescript-operations'
          - 'typed-document-node'
        config:
          skipTypename: false
          withHooks: true
          withComponent: false
    

    Now every GraphQL query in your Next.js app is fully typed. When WordPress admins add fields, developers get instant type errors if components aren’t updated. Check out the GraphQL Code Generator docs for more details.

    Developer reviewing code

    Example: Typed Post Query

    Here’s what a typical WordPress post query looks like with TypeScript:

    import { useQuery } from '@apollo/client';
    import { GetPostDocument, GetPostQuery } from '@/types/graphql';
    
    interface PostPageProps {
      slug: string;
    }
    
    export function PostPage({ slug }: PostPageProps) {
      const { data, loading, error } = useQuery<GetPostQuery>(
        GetPostDocument,
        { variables: { slug } }
      );
    
      if (loading) return <Skeleton />;
      if (error) return <ErrorBoundary error={error} />;
      
      const post = data?.post;
      if (!post) return <NotFound />;
    
      // TypeScript knows exactly what properties exist!
      return (
        <article>
          <h1>{post.title}</h1>
          <time dateTime={post.date}>{formatDate(post.date)}</time>
          <div dangerouslySetInnerHTML={{ __html: post.content }} />
        </article>
      );
    }

    Advanced Type Patterns

    Here are some advanced TypeScript patterns we use throughout FlatWP:

    1. Discriminated Unions – Perfect for ACF flexible content blocks
    2. Mapped Types – Transform WP data structures safely
    3. Conditional Types – Handle optional ACF fields elegantly
    4. Template Literal Types – Type-safe route generation

    Code on multiple monitors

    Discriminated Union Example

    Here’s how we handle flexible content blocks with type safety:

    type ContentBlock =
      | { type: 'hero'; heading: string; image: WPImage }
      | { type: 'features'; items: Feature[] }
      | { type: 'testimonial'; quote: string; author: string }
      | { type: 'cta'; text: string; url: string };
    
    function renderBlock(block: ContentBlock) {
      switch (block.type) {
        case 'hero':
          // TypeScript knows block.heading and block.image exist
          return <HeroBlock heading={block.heading} image={block.image} />;
        case 'features':
          // TypeScript knows block.items exists
          return <FeaturesGrid items={block.items} />;
        // ... other cases
      }
    }
    Utility Types for WordPress

    We’ve created several utility types that make working with WordPress data much cleaner. These are included in the FlatWP starter:

    • WPImage – Standardized image object with URL, alt, dimensions
    • WPPost<T> – Generic post type with custom field support
    • WPCategory – Taxonomy term with full hierarchy
    • WPMenu – Navigation menu with nested items

    Strict Mode Configuration

    We run TypeScript in strict mode with additional checks enabled:

    {
      "compilerOptions": {
        "strict": true,
        "noUncheckedIndexedAccess": true,
        "noImplicitReturns": true,
        "noFallthroughCasesInSwitch": true,
        "forceConsistentCasingInFileNames": true,
        "isolatedModules": true
      }
    }

    This configuration catches edge cases that would otherwise slip through. Yes, it requires more upfront work, but the elimination of runtime errors is worth it.

    Key principle: Make invalid states unrepresentable. If WordPress can’t return null for a required field, your TypeScript types shouldn’t allow null either.

    Handling WordPress Nullability

    WordPress is infamous for returning null or undefined unexpectedly. Here’s how we handle it:

    // Bad: Optimistic typing
    interface Post {
      title: string;  // Might actually be null!
      content: string; // Might be empty string OR null
    }
    
    // Good: Defensive typing
    interface Post {
      title: string | null;
      content: string | null;
    }
    
    // Better: Use type guards
    function isValidPost(post: Post): post is Required<Post> {
      return post.title !== null && post.content !== null;
    }
    
    // Usage
    if (isValidPost(post)) {
      // TypeScript knows title and content are strings
      return <h1>{post.title}</h1>;
    }

    TypeScript error checking

    Testing Strategy

    TypeScript dramatically improves our testing strategy. Here’s what we test:

    Test Type Coverage Tools Frequency
    Type Checking 100% tsc –noEmit Pre-commit
    Unit Tests 80%+ Vitest PR
    Integration Critical paths Playwright Pre-deploy
    Type Coverage 95%+ type-coverage Weekly

    Example Test Suite

    Here’s how we test a typed WordPress component:

    import { describe, it, expect } from 'vitest';
    import { render, screen } from '@testing-library/react';
    import { PostCard } from './PostCard';
    import type { Post } from '@/types/graphql';
    
    describe('PostCard', () => {
      it('renders post with all required fields', () => {
        const post: Post = {
          id: '1',
          title: 'Test Post',
          excerpt: 'Test excerpt',
          date: '2024-01-01',
          author: { name: 'John Doe' },
          featuredImage: {
            node: {
              sourceUrl: '/test.jpg',
              altText: 'Test image',
            },
          },
        };
    
        render(<PostCard post={post} />);
        
        expect(screen.getByText('Test Post')).toBeInTheDocument();
        expect(screen.getByAltText('Test image')).toBeInTheDocument();
      });
    
      it('handles missing optional fields gracefully', () => {
        const post: Post = {
          id: '2',
          title: 'Minimal Post',
          excerpt: null,  // TypeScript allows this
          date: '2024-01-01',
          author: { name: 'Jane Doe' },
          featuredImage: null,  // Also allowed
        };
    
        render(<PostCard post={post} />);
        expect(screen.getByText('Minimal Post')).toBeInTheDocument();
      });
    });

    Real-World Performance Impact

    After implementing strict TypeScript across 10+ client projects, we measured the impact:

    • 90% reduction in production runtime errors
    • 40% faster feature development (thanks to autocomplete + refactoring)
    • Zero “undefined is not a function” errors in 6 months
    • 50% less time spent in code review catching type issues

    The upfront investment in TypeScript configuration pays for itself within the first sprint. Don’t skip this step!

    Team celebrating success

    Resources & Next Steps

    Want to dive deeper? Here are our recommended resources:

    1. Read the official TypeScript handbook
    2. Explore WPGraphQL documentation
    3. Check out the FlatWP repository for examples
    4. Join our Discord community for help

    Conclusion

    TypeScript + WordPress + Next.js is a powerful combination. With proper type safety, you get the flexibility of WordPress with the developer experience of modern React development.

    The FlatWP starter includes all of this configuration out-of-the-box, so you can focus on building features instead of fighting with types. Ship faster. Ship safer.

  • 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.

  • 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.

  • 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.

  • SEO in Headless WordPress: Better Than Traditional?

    One concern we hear about headless WordPress: “What about SEO?”

    SEO analytics and performance metrics

    The truth is, headless WordPress can be better for SEO than traditional WordPress. Here’s why.

    Performance is an SEO Factor

    Google’s Core Web Vitals directly impact rankings. FlatWP’s static/ISR approach delivers:

    • LCP < 1s: Pages load in under a second (vs 3-5s for traditional WP)
    • CLS near 0: No layout shift from lazy-loaded elements
    • FID < 50ms: Instant interactivity

    These metrics give you a ranking advantage over slower traditional sites.

    Server-Side Rendering

    Unlike single-page apps that struggle with SEO, Next.js renders full HTML on the server. Crawlers see complete, rendered pages – no JavaScript execution required.

    This means:

    • Content is immediately available to bots
    • Social media crawlers see proper Open Graph data
    • No SEO penalties for client-side rendering

    Meta Data from WordPress

    FlatWP pulls SEO metadata directly from Yoast or Rank Math:

    export async function generateMetadata({ params }) {
      const post = await fetchPost(params.slug);
      
      return {
        title: post.seo.title,
        description: post.seo.metaDesc,
        openGraph: {
          images: [post.seo.opengraphImage],
        },
      };
    }

    Editors manage SEO in WordPress. Next.js renders it perfectly.

    Automatic Sitemaps

    FlatWP generates XML sitemaps dynamically from WordPress content:

    export default async function sitemap() {
      const posts = await fetchAllPosts();
      
      return posts.map(post => ({
        url: `https://flatwp.com/blog/${post.slug}`,
        lastModified: post.modifiedDate,
      }));
    }

    When content updates, the sitemap updates. Search engines stay in sync.

    Schema Markup

    We include proper JSON-LD schema for articles:

    {
      "@context": "https://schema.org",
      "@type": "Article",
      "headline": post.title,
      "datePublished": post.date,
      "author": {
        "@type": "Person",
        "name": post.author.name
      }
    }

    This helps Google understand your content structure.

    No Bloat

    Traditional WordPress sites load unnecessary plugins, tracking scripts, and theme bloat. This slows everything down.

    With FlatWP, you control exactly what JavaScript loads. Most pages need zero client-side JS for content display.

    The Bottom Line

    Headless WordPress with Next.js is better for SEO than traditional WordPress because:

    1. Faster page loads = better rankings
    2. Perfect SSR = happy crawlers
    3. Clean HTML = no bloat penalty
    4. Modern image formats = faster LCP

    You get WordPress’s content management with Next.js’s performance. That’s an SEO win-win.

  • 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.