Tag: nextjs

  • Why We Built FlatWP: The Headless WordPress Stack We Actually Wanted

    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.

    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.

    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.

  • Understanding ISR: The Secret to Fast Headless WordPress Sites

    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.

    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'] })
    });

    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.

  • Image Optimization in Headless WordPress: Beyond next/image

    WordPress is notorious for bloated images. Editors upload 5MB photos from their phone, and suddenly your site loads like it’s 2010.

    In headless WordPress, you have a unique opportunity to fix this at the framework level.

    The WordPress Media Problem

    Traditional WordPress generates multiple image sizes on upload. But these sizes are often wrong for modern responsive design. You end up with dozens of unused files cluttering your media library.

    Plus, WordPress doesn’t handle modern formats (WebP, AVIF) or responsive srcsets particularly well.

    The Next.js Solution

    Next.js Image component handles optimization automatically:

    • Serves modern formats (WebP/AVIF) to supporting browsers
    • Generates responsive srcsets on-demand
    • Lazy loads by default
    • Prevents layout shift with automatic sizing

    But you need to configure it correctly for WordPress.

    FlatWP’s Approach

    We create a custom image loader that:

    1. Pulls the original image from WordPress
    2. Proxies it through Next.js optimization
    3. Caches the optimized versions globally
    4. Serves from Vercel’s edge network

    The result? A 5MB WordPress upload becomes a 50KB WebP delivered in milliseconds.

    Configuration

    In your next.config.js:

    images: {
      domains: ['cms.flatwp.com/'],
      formats: ['image/avif', 'image/webp'],
      deviceSizes: [640, 750, 828, 1080, 1200, 1920],
    }

    Then use it in components:

    <Image
      src={post.featuredImage.url}
      alt={post.featuredImage.alt}
      width={1200}
      height={630}
      priority={isAboveFold}
    />

    WordPress-Side Optimization

    Even better: prevent bloated uploads in the first place. Our WordPress plugin includes:

    • Upload size limits
    • Automatic compression before upload
    • Warnings when images exceed recommended dimensions
    • Bulk optimization tools for existing media

    Performance Impact

    We measured a typical blog post with 5 images:

    • Traditional WordPress: 2.3MB transferred, 4.2s load
    • FlatWP optimized: 180KB transferred, 0.8s load

    That’s a 92% reduction in bandwidth and 5x faster load times. For free.

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

  • SEO in Headless WordPress: Better Than Traditional?

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

    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.

  • 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://dev-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.

  • WooCommerce Headless: Coming to FlatWP Pro

    E-commerce is the #1 requested feature for FlatWP. We’re building a production-ready headless WooCommerce storefront for the Pro version.

    Why Headless WooCommerce?

    Traditional WooCommerce sites are slow. Product pages take 3-5 seconds to load, cart updates require full page refreshes, and checkout flows are clunky.

    A headless storefront gives you:

    • Instant navigation: Products load in <500ms
    • Smooth cart updates: Add to cart without page reload
    • Modern checkout: One-page, optimized flows
    • Better mobile UX: App-like experience

    Speed directly impacts conversion. Amazon found 100ms slower = 1% sales drop. For a $1M/year store, that’s $10k annually.

    The Technical Approach

    We’re using WPGraphQL for WooCommerce to query products, categories, and orders:

    query GetProducts {
      products(first: 20) {
        nodes {
          id
          name
          slug
          price
          image {
            url
          }
          ... on SimpleProduct {
            stockQuantity
          }
        }
      }
    }

    For cart and checkout, we’re using WooCommerce’s REST API with JWT authentication.

    What’s Included

    FlatWP Pro’s WooCommerce integration will include:

    • Product catalog: Grid/list views with filtering and sorting
    • Single product pages: Gallery, variants, add to cart
    • Cart: Persistent cart with quantity updates
    • Checkout: One-page checkout with Stripe integration
    • Account pages: Order history, address management
    • Search: Product search with filters

    All built with Shadcn components, fully typed with TypeScript.

    Performance Targets

    We’re targeting:

    • Product list: <1s LCP
    • Product detail: <1.5s LCP
    • Add to cart: <200ms
    • Checkout page: <2s LCP

    These are 3-5x faster than typical WooCommerce sites.

    Launch Timeline

    We’re targeting Q1 2025 for the WooCommerce beta. It’ll be included with FlatWP Pro ($299 one-time or $99/year).

    If you’re interested in early access, join our waitlist.

    Why Not Free?

    E-commerce is complex. We’re investing significant engineering time to make it production-ready:

    • Proper cart state management
    • Payment gateway integration
    • Tax calculations
    • Shipping methods
    • Order processing

    This is Pro-tier functionality. The revenue funds ongoing development and support.

    For DIY developers, we’ll document how to build WooCommerce integration yourself using the free version.

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