ForgeWP

Common Errors & Troubleshooting

Systematic diagnostic workflows, compiler error remedies, runtime debugging, and auto-repair procedures for ForgeWP theme development.

When developing or exporting ForgeWP themes, errors generally fall into three distinct environments: local Vite development, compile-time Node.js static rendering (SSR), or the live WordPress PHP runtime.

This guide details systematic diagnostic workflows, root-cause analyses, and concrete remediation steps for common issues encountered across the development lifecycle.

Flowchart
Rendering diagram...

1. Automated Diagnostic Tooling

Before debugging issues manually, use ForgeWP built-in diagnostic and self-healing tools.

forgewp doctor

Runs preflight system checks across seven core project boundaries:

  1. Node.js Environment: The CLI and doctor require Node.js ^22.18.0 || >=24.11.0. Check System Requirements.
  2. Critical Configuration: Validates wp.config.ts, package.json, and framework adapters.
  3. Database Blueprint Integration: Validates syntax and presence of cms/mock-data.ts.
  4. Internal Framework Integrity: Checks all protected blueprints in lib/blueprints.js.
  5. Tailwind CSS Styling: Verifies src/index.css or src/app/globals.css.
  6. Selective Hydration Diagnostics: Analyzes compiled island bundle sizes against the 100 kB threshold.
  7. Static Code Linting: Scans templates for missing CPT metadata declarations, unhydrated loops, and hardcoded relative URLs.
Terminal
pnpm forgewp doctor

forgewp analyze

Generates a visual performance report at .forgewp/analyzer/index.html calculating an overall performance score, total JavaScript payload, and per-island bundle breakdowns.

Terminal
pnpm forgewp analyze

forgewp repair

Auto-heals damaged or missing framework blueprints without altering custom components or page templates. If cms/mock-data.ts or internal configuration files were inadvertently deleted, repair restores clean defaults:

Terminal
pnpm forgewp repair

2. Compile-Time & SSR Build Failures

During pnpm export, the compiler executes your React components in a Node.js server environment (render-theme.mts) to generate static HTML templates.

ReferenceError: window is not defined / document is not defined

Cause

A component directly accesses browser globals (window, document, navigator, localStorage, HTMLElement, IntersectionObserver) during initial execution or in the top-level module scope before mounting.

Diagnostic Output

Output
[ERROR] Static export failed during template rendering:
ReferenceError: window is not defined
  at CanvasChart (src/components/CanvasChart.tsx:14:5)
  at renderTheme (node_modules/@forgewp/compiler/lib/render-theme.mts:112:20)

Remediation

  1. Wrap in <Hydrate clientOnly>: For browser-only widgets (Canvas, WebGL, map renderers), bypass SSR entirely:
src/components/ChartIsland.tsx
1import { Hydrate } from "@forgewp/react"
2import { CanvasChart } from "./CanvasChart"
3
4export function ChartSection() {
5  return (
6    <Hydrate clientOnly trigger="visible">
7      <CanvasChart />
8    </Hydrate>
9  )
10}
  1. Use useEffect for Browser APIs: useEffect hooks do not execute during Node.js SSR. Shift DOM access and window listeners inside useEffect:
src/components/WindowListener.tsx
1import { useEffect, useState } from "react"
2
3export function WindowTracker() {
4  const [scrollY, setScrollY] = useState(0)
5
6  useEffect(() => {
7    function handleScroll() {
8      setScrollY(window.scrollY)
9    }
10    window.addEventListener("scroll", handleScroll, { passive: true })
11    return () => window.removeEventListener("scroll", handleScroll)
12  }, [])
13
14  return <div>Scroll position: {scrollY}px</div>
15}
  1. Check isDecoupled(): When conditionally rendering browser features:
Conditional Browser Check
1import { isDecoupled } from "@forgewp/react"
2
3if (typeof window !== "undefined") {
4  // Safe to access browser APIs
5}

Heavy Island Bundle Warning (> 100 kB)

Cause

An interactive island component bundles large third-party libraries (e.g. monolithic Lucide sets, Lodash, Moment.js, complex chart packages) into client hydration chunks.

Diagnostic Output

Output
[WARN] PriceCalculator........... 142.4 kB  HEAVY
     Chunk Path: dist/assets/PriceCalculator-Bz4f9L.js
     Recommendations:
        1. Split static markup sections out into parent static templates.
        2. Avoid heavy external packages inside the island (or lazy load them).
        3. Switch to trigger="interaction" or trigger="idle".

Remediation

  • Extract Static Chrome: Keep headings, wrappers, and promotional text in the parent server template. Pass only interactive controls into the island.
  • Dynamic Imports: Use React.lazy() or dynamic import() inside the component so large libraries load asynchronously on demand.
  • Change Loading Triggers: Change trigger="load" to trigger="interaction" or trigger="visible" with preload="near-visible" to prevent render-blocking script downloads.

AST Parser Syntax Error During Block Extraction

Cause

Complex TypeScript type assertions (e.g. const x = (<string>val) or non-standard syntax) in components targeted by @forgewp-block or defineBlock().

Diagnostic Output

Output
[ERROR] Failed to extract block AST from src/blocks/FeatureGrid.tsx:
SyntaxError: Unexpected token, expected "..." (28:14)

Remediation

  • Use standard as casting (val as string) rather than angle-bracket casting (<string>val), which conflicts with JSX tag parsing.
  • Run pnpm typecheck before exporting to verify that all TypeScript syntax is clean.

3. WordPress Server & Runtime Issues

White Screen of Death (WSOD) on Theme Activation

Cause

PHP fatal error during theme initialization in functions.php. Common causes include:

  1. PHP version older than PHP 8.0 (e.g. PHP 7.4).
  2. Memory limit exhaustion (Allowed memory size of ... bytes exhausted).
  3. Missing PHP extensions (intl, json, mbstring, or gd).

Remediation

  1. Enable WordPress Debugging: In your hosting environment's wp-config.php, enable debug logging:
wp-config.php
1define('WP_DEBUG', true);
2define('WP_DEBUG_LOG', true);
3define('WP_DEBUG_DISPLAY', false);

Check wp-content/debug.log for the exact fatal stack trace.

  1. Increase PHP Memory Limit: In wp-config.php or php.ini:
wp-config.php
1define('WP_MEMORY_LIMIT', '256M');
2define('WP_MAX_MEMORY_LIMIT', '512M');
  1. Verify PHP 8.0+: Ensure your host runs PHP 8.0, 8.1, 8.2, or 8.3:
Terminal (SSH)
php -v

Custom Post Type or Archive URLs Return 404

Cause

WordPress caches its URL rewrite rules in the MySQL database (wp_options table under rewrite_rules). When you declare a new post type in wp.config.ts, WordPress does not know about the new permalink structure until rules are flushed.

Remediation

  1. Admin Flush: Go to Settings → Permalinks in the WordPress admin dashboard and click Save Changes without making modifications. This automatically rebuilds the rewrite rule cache.
  2. WP-CLI Flush: Run via SSH:
Terminal
wp rewrite flush
Tip

If a dedicated WordPress Page uses the exact same slug as your custom post type archive (e.g. Page /properties vs CPT archive /properties), set hasArchive: false on the post type in wp.config.ts so the Page owns the root URL without conflict.


Styles or Tailwind Classes Missing in Production

Cause

  1. Tailwind utility classes were constructed with dynamic string interpolation (e.g. text-${color}-500), which cannot be discovered by Tailwind static scanners.
  2. The CSS file was not enqueued in functions.php.
  3. An aggressive server caching plugin stripped CSS variables or --wp--preset custom properties.

Remediation

  1. Avoid Dynamic String Interpolation: Always write complete class names or use a mapped lookup table:
Bad vs Good Tailwind Usage
1// Incorrect: Purged at build time
2const badClass = `bg-${color}-500`
3
4// Correct: Static lookup table
5const COLOR_MAP: Record<string, string> = {
6  blue: "bg-blue-500",
7  emerald: "bg-emerald-500",
8  amber: "bg-amber-500",
9}
10const goodClass = COLOR_MAP[color] || "bg-blue-500"
  1. Check Generated functions.php: Verify that the compiled theme folder contains assets/ with your bundled CSS file and that wp_enqueue_style('forgewp-style', ...) references it.
  2. Verify Global Theme CSS: Ensure src/app/globals.css contains @import "tailwindcss"; or @tailwind utilities;.

4. Interactivity & Hydration Gotchas

Event Handler Does Not Fire Inside <WpQueryLoop>

Cause

<WpQueryLoop> compiles to a native PHP WP_Query loop that runs on the server and generates static HTML with 0 KB client JavaScript. If you attach an onClick or onChange handler inside an unhydrated loop, the browser cannot execute it because no React runtime is attached to those DOM nodes.

forgewp doctor reports this as a static lint warning:

Output
[WARN] src/components/PostList.tsx: Found interactive event handler 'onClick' inside static WpQueryLoop. Wrap in <Hydrate> to enable interactivity.

Remediation

Wrap the interactive portion inside an explicit <Hydrate> island:

src/components/InteractivePostCard.tsx
1import { WpQueryLoop, Hydrate, useWpTitle } from "@forgewp/react"
2import { BookmarkButton } from "./BookmarkButton"
3
4export function PostGrid() {
5  return (
6    <WpQueryLoop postType="post" postsPerPage={6}>
7      <article className="rounded-xl border p-6">
8        <PostTitle />
9        {/* Isolated hydration island for interactive action */}
10        <Hydrate trigger="visible">
11          <BookmarkButton />
12        </Hydrate>
13      </article>
14    </WpQueryLoop>
15  )
16}
17
18function PostTitle() {
19  const title = useWpTitle()
20  return <h3 className="text-xl font-bold">{title}</h3>
21}

Layout Shift (CLS) When Island Hydrates

Cause

An island component renders with different DOM dimensions after hydration (e.g. an accordion starts collapsed on the client but rendered expanded during SSR, or images lack explicit aspect ratio boxes).

Remediation

  1. Reserve Layout Dimensions: Apply CSS min-h-[...] or aspect-ratio on the parent container or <Hydrate> wrapper:
Reserved Layout Box
1<div className="min-h-80">
2  <Hydrate trigger="visible" preload="near-visible">
3    <InteractiveWidget />
4  </Hydrate>
5</div>
  1. Synchronize Initial State: Ensure client components initialize with the same default state rendered during server export.

5. Native Forms & REST API Troubleshooting

Form Submission Returns HTTP 404

Cause

The WordPress REST API route /wp-json/forgewp/v1/forms/{name}/submit is not registered or cannot be reached. Common reasons:

  1. WordPress permalinks are set to Plain (?p=123), which disables /wp-json/ pretty REST routes.
  2. The form was not declared in wp.config.ts (forms section) or cms/forms/{name}.ts.
  3. Web server rewrite rules (.htaccess on Apache or try_files in Nginx) are missing.

Remediation

  1. Set Permalinks to Post Name: In Settings → Permalinks, select Post name (/%postname%/) and save.
  2. Verify Nginx Configuration: Ensure Nginx includes WordPress query forwarding:
/etc/nginx/sites-available/wordpress
1location / {
2    try_files $uri $uri/ /index.php?$args;
3}
  1. Check Form Registration in wp.config.ts: Verify the form key matches the name passed to submitWpForm("contact", data).

Form Submission Returns "Bot Protection Triggered"

Cause

ForgeWP includes automated anti-spam protections:

  1. Honeypot Trap: An invisible form field (_forgewp_hp) that must remain empty. Automated bots populate every field they discover.
  2. Time-Trap Validation: Rejects submissions completed in under 3 seconds from page load (_forgewp_elapsed < 3000ms), catching instant script submissions.

Remediation

  • In automated test suites (e.g. Playwright or Cypress), wait at least 3.5 seconds before submitting the form:
tests/form.spec.ts
1await page.goto("/contact")
2await page.fill('input[name="name"]', "Test User")
3await page.fill('input[name="email"]', "test@example.com")
4// Wait past the 3-second bot time-trap threshold
5await page.waitForTimeout(3200)
6await page.click('button[type="submit"]')
  • Ensure custom CSS does not accidentally hide the honeypot field in a way that tricks browser autofill into filling it.

Notification Emails Not Arriving

Cause

WordPress default wp_mail() relies on the hosting server's local Sendmail/Postfix MTA, which is frequently blocked by modern spam filters (SPF, DKIM, DMARC failures).

Remediation

  • Install and configure an authenticated SMTP plugin (e.g. WP Mail SMTP, FluentSMTP) with a verified transactional service (Postmark, SendGrid, Amazon SES, or Resend).
  • Test email delivery using the WP Mail SMTP built-in test email tool to verify valid headers and SMTP handshakes.

6. Gutenberg Block Editor Diagnostics

"This block contains unexpected or invalid content"

Cause

Gutenberg block validation occurs when the markup generated by a block's save component does not match the raw HTML stored in the WordPress post content (post_content).

Remediation

  • Understand ForgeWP's renderer: ForgeWP generates dynamic blocks with a PHP renderer. The authoring definition may include save JSX that the compiler uses to generate that renderer; do not remove it just because the WordPress block is dynamic. Dynamic rendering avoids some saved-markup mismatch problems, but renamed attributes, changed types, nested blocks, and incompatible data can still need migration.
  • Avoid Modifying Saved HTML Structure: If using static blocks with a save component, never alter the root wrapper tags or class names without providing a deprecated block migration definition.

7. Troubleshooting Checklist

Use this quick checklist to resolve issues methodically:

Table · 3 columns
Step
Action
Command / Location
1
Run health preflight
pnpm forgewp doctor
2
Restore damaged blueprints
pnpm forgewp repair
3
Clean build artifacts and caches
pnpm forgewp clean
4
Run TypeScript validation
pnpm typecheck
5
Inspect hydration islands
pnpm forgewp analyze
6
Flush WordPress permalinks
Settings → Permalinks → Save
7
Inspect WordPress debug log
wp-content/debug.log

Next Steps

Now that you know how to diagnose and resolve errors across ForgeWP projects, proceed to the Frequently Asked Questions to explore common architectural, performance, and hosting questions.