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.
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:
- Node.js Environment: The CLI and doctor require Node.js
^22.18.0 || >=24.11.0. Check System Requirements. - Critical Configuration: Validates
wp.config.ts,package.json, and framework adapters. - Database Blueprint Integration: Validates syntax and presence of
cms/mock-data.ts. - Internal Framework Integrity: Checks all protected blueprints in
lib/blueprints.js. - Tailwind CSS Styling: Verifies
src/index.cssorsrc/app/globals.css. - Selective Hydration Diagnostics: Analyzes compiled island bundle sizes against the 100 kB threshold.
- Static Code Linting: Scans templates for missing CPT metadata declarations, unhydrated loops, and hardcoded relative URLs.
pnpm forgewp doctorforgewp analyze
Generates a visual performance report at .forgewp/analyzer/index.html calculating an overall performance score, total JavaScript payload, and per-island bundle breakdowns.
pnpm forgewp analyzeforgewp 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:
pnpm forgewp repair2. 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
[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
- Wrap in
<Hydrate clientOnly>: For browser-only widgets (Canvas, WebGL, map renderers), bypass SSR entirely:
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}- Use
useEffectfor Browser APIs:useEffecthooks do not execute during Node.js SSR. Shift DOM access and window listeners insideuseEffect:
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}- Check
isDecoupled(): When conditionally rendering browser features:
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
[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 dynamicimport()inside the component so large libraries load asynchronously on demand. - Change Loading Triggers: Change
trigger="load"totrigger="interaction"ortrigger="visible"withpreload="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
[ERROR] Failed to extract block AST from src/blocks/FeatureGrid.tsx:
SyntaxError: Unexpected token, expected "..." (28:14)Remediation
- Use standard
ascasting (val as string) rather than angle-bracket casting (<string>val), which conflicts with JSX tag parsing. - Run
pnpm typecheckbefore 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:
- PHP version older than PHP 8.0 (e.g. PHP 7.4).
- Memory limit exhaustion (
Allowed memory size of ... bytes exhausted). - Missing PHP extensions (
intl,json,mbstring, orgd).
Remediation
- Enable WordPress Debugging: In your hosting environment's
wp-config.php, enable debug logging:
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.
- Increase PHP Memory Limit: In
wp-config.phporphp.ini:
1define('WP_MEMORY_LIMIT', '256M');
2define('WP_MAX_MEMORY_LIMIT', '512M');- Verify PHP 8.0+: Ensure your host runs PHP 8.0, 8.1, 8.2, or 8.3:
php -vCustom 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
- Admin Flush: Go to
Settings → Permalinksin the WordPress admin dashboard and click Save Changes without making modifications. This automatically rebuilds the rewrite rule cache. - WP-CLI Flush: Run via SSH:
wp rewrite flushTip
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
- Tailwind utility classes were constructed with dynamic string interpolation (e.g.
text-${color}-500), which cannot be discovered by Tailwind static scanners. - The CSS file was not enqueued in
functions.php. - An aggressive server caching plugin stripped CSS variables or
--wp--presetcustom properties.
Remediation
- Avoid Dynamic String Interpolation: Always write complete class names or use a mapped lookup table:
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"- Check Generated
functions.php: Verify that the compiled theme folder containsassets/with your bundled CSS file and thatwp_enqueue_style('forgewp-style', ...)references it. - Verify Global Theme CSS: Ensure
src/app/globals.csscontains@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:
[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:
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
- Reserve Layout Dimensions: Apply CSS
min-h-[...]oraspect-ratioon the parent container or<Hydrate>wrapper:
1<div className="min-h-80">
2 <Hydrate trigger="visible" preload="near-visible">
3 <InteractiveWidget />
4 </Hydrate>
5</div>- 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:
- WordPress permalinks are set to Plain (
?p=123), which disables/wp-json/pretty REST routes. - The form was not declared in
wp.config.ts(formssection) orcms/forms/{name}.ts. - Web server rewrite rules (
.htaccesson Apache ortry_filesin Nginx) are missing.
Remediation
- Set Permalinks to Post Name: In
Settings → Permalinks, select Post name (/%postname%/) and save. - Verify Nginx Configuration: Ensure Nginx includes WordPress query forwarding:
1location / {
2 try_files $uri $uri/ /index.php?$args;
3}- Check Form Registration in
wp.config.ts: Verify the form key matches the name passed tosubmitWpForm("contact", data).
Form Submission Returns "Bot Protection Triggered"
Cause
ForgeWP includes automated anti-spam protections:
- Honeypot Trap: An invisible form field (
_forgewp_hp) that must remain empty. Automated bots populate every field they discover. - 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:
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
saveJSX 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
savecomponent, never alter the root wrapper tags or class names without providing adeprecatedblock migration definition.
7. Troubleshooting Checklist
Use this quick checklist to resolve issues methodically:
Step Action Command / Location 1 Run health preflight pnpm forgewp doctor2 Restore damaged blueprints pnpm forgewp repair3 Clean build artifacts and caches pnpm forgewp clean4 Run TypeScript validation pnpm typecheck5 Inspect hydration islands pnpm forgewp analyze6 Flush WordPress permalinks Settings → Permalinks → Save7 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.