How the Compiler Builds a WordPress Theme
An architectural deep dive into the ForgeWP export pipeline, the framework adapter layer, compile-time Node SSR, AST analysis, and how React code transforms into native WordPress PHP templates.
The ForgeWP compiler is a build-time transpilation engine, not a headless Node.js server. When you run pnpm export, the compiler reads your React components, Tailwind styling, CMS schemas, and theme configuration, and translates them directly into a self-contained, native PHP WordPress theme.
You author themes using React, TypeScript, and Vite with Hot Module Replacement (HMR). The exported package contains PHP templates, rendered HTML fragments, styles, and JavaScript where needed. Interactive islands bundle the browser dependencies they use, including React and any shared modules.
The Ten-Stage Export Pipeline
When you execute pnpm export, the pipeline orchestrator (export-theme.js) executes ten sequential stages to transform your project into a production theme:
Stage 1: Preflight Verification and Self-Healing
The compiler inspects your workspace with validateCriticalFiles(). It ensures that user-editable files (wp.config.ts, package.json, index.html, src/main.tsx, src/app/layout.tsx, src/app/routes.tsx) exist and are valid. If any internal framework blueprints are missing or outdated, ForgeWP restores them from pre-compiled templates automatically.
Stage 2: Selective Island Splitting
splitInteractiveIslands() runs static analysis on your components. When a component contains both static markup and interactive event listeners (onClick, useState, or animation libraries), the compiler automatically extracts the interactive subtree into a standalone hydration island in .forgewp-islands/. This leaves the surrounding layout as a lightweight, server-rendered static shell.
Stage 3: Gutenberg Block Wrapper Pre-Scaffolding
scanAndGenerateBlocks() scans components declared with defineBlock(). It pre-scaffolds editor entry points in .forgewp-blocks/ so that Vite and Rollup register them as entry points during asset compilation.
Stage 4: Vite Asset and CSS Compilation
buildAssets() invokes Vite in production mode. It compiles Tailwind CSS into an optimized stylesheet (assets/app-[hash].css), bundles the React client runtime, and generates individual chunks for every hydration island. Vite writes the bundled assets and a .vite/manifest.json file into dist/.
Stage 5: Compile-Time Node SSR Pre-Rendering
renderStaticMarkup() executes render-theme.mts in a headless Node.js process using tsx. It imports your React application and renders it to static HTML fragments for the header, footer, front page, 404 page, single posts, archive loops, and custom page templates.
Stage 6: Asset and Web Font Pipeline
The asset optimizer runs two parallel processes:
- Sharp Image Pipeline: Scans all local images referenced across components, generating responsive WebP variants with configurable quality levels and caching identical files.
- Web Font Orchestration: Downloads remote Google Fonts or custom font definitions declared in
wp.config.ts, stores them as local WOFF2 files inassets/fonts/, and generates local@font-facerules.
Stage 7: WordPress Core & Theme Generation
generateTheme() transforms pre-rendered static HTML and project configuration into native WordPress PHP:
markup-processor.js: Replaces internal compiler tokens with native PHP function calls.php-builders.js: Generates template hierarchy files (index.php,front-page.php,page.php,single.php,archive.php,404.php,header.php,footer.php).functions.php: Emits script and style enqueues, Polylang string registrations, REST form endpoints, menu resolvers, and site options pages.theme.json: Emits WordPress design settings reflecting your tokens. This file also works with PHP-backed themes; it does not make the output a Full Site Editing block theme.
Stage 8: Asset Diagnostics & Manifest Generation
generateAssetManifest() calculates gzipped bundle sizes, audits font loading strategies, and identifies oversized hydration islands. printAssetDiagnosticsReport() outputs this performance summary to your terminal.
Stage 9: Export Validation
If --validate or --strict is enabled, validateExport() verifies that all required PHP files, static fragments, hydration chunks, and stylesheet headers exist and are non-empty. In strict mode, unresolved editable field mappings or missing assets will cause the build to fail with an actionable error.
Stage 10: Packaging and Compression
zipTheme() compresses the .forgewp/out/<theme-slug>/ directory into a production-ready ZIP archive (.forgewp/<theme-slug>.zip) using level-9 zlib compression.
The Framework Adapter Architecture
ForgeWP uses a framework adapter layer to decouple its compiler core from framework-specific rendering and hydration logic. While React is the primary adapter, the compiler core communicates exclusively through standard adapter contracts.
packages/compiler/lib/
├── adapters/
│ ├── react-adapter.js <-- Primary React 19 adapter
│ └── html-adapter.js <-- Static HTML proof-of-concept
├── blocks/
│ ├── php-transpiler.js <-- Transpiles JSX into Gutenberg render.php
│ └── editor-transpiler.js <-- Transpiles JSX into block editor controls
├── hydration/
│ ├── is-interactive.js <-- AST scanner for interactive hooks
│ └── island-split.js <-- Splits static shells from interactive islands
├── markup-processor.js <-- Transforms compiler tokens into PHP tags
├── php-builders.js <-- Assembles standard WordPress template wrappers
└── generate-theme.js <-- Coordinates theme output generationThe Framework Adapter Interface
Every framework adapter implements four required methods:
Adapter Method Description renderStaticMarkup(themeRoot)Renders the theme application to static HTML fragments for each route. scanForHydrationIslands(themeRoot)Discovers all components that require browser-side hydration. findComponentPath(themeRoot, slug)Resolves the absolute file path for a discovered hydration component. getHydrationRollupInputs(themeRoot)Generates a Rollup/Vite input map for all client hydration islands. |
|---|
Compile-Time Node SSR Engine (render-theme.mts)
Pre-rendering a React application inside a headless Node.js environment requires polyfilling browser-specific globals while preventing layout shifts in production. ForgeWP achieves this in render-theme.mts.
1. Resilient Browser Globals Polyfills
Because standard React components and third-party UI libraries frequently access window or document, ForgeWP injects an in-memory polyfill during Node SSR:
1if (typeof globalThis.window === 'undefined') {
2 globalThis.window = {
3 location: { pathname: '/', search: '', href: 'http://localhost/' },
4 navigator: { userAgent: 'Node' },
5 addEventListener: () => {},
6 removeEventListener: () => {},
7 _forgeWpCompileTime: true,
8 } as any;
9
10 globalThis.document = {
11 addEventListener: () => {},
12 removeEventListener: () => {},
13 querySelector: () => null,
14 querySelectorAll: () => [],
15 } as any;
16
17 globalThis.matchMedia = (query: string) => ({
18 matches: false, // Prevents compile-time prefers-reduced-motion baking
19 media: query,
20 addEventListener: () => {},
21 removeEventListener: () => {},
22 }) as any;
23}Note
Why does matchMedia return matches: false during SSR?
If compile-time SSR evaluated prefers-reduced-motion: true, components that dynamically branch on motion preferences would permanently bake stripped-down, no-hover fallbacks into static PHP HTML. By returning false during compilation, the complete CSS layout ships to WordPress, while real client-side motion preferences are respected dynamically in the browser.
2. Module Loader Hook (asset-loader.js)
In standard Node.js, attempting to import non-JavaScript assets (such as .css, .svg, or .png) throws a runtime syntax error. ForgeWP registers an ES module loader hook (asset-loader.js) that intercepts these imports during the SSR pass:
- CSS imports are stubbed out as empty modules so stylesheets do not crash Node.
- SVG imports are resolved to clean inline SVG markup or data URIs.
- Image imports are resolved to static image metadata objects (
{ src, width, height }).
The Markup Processor (markup-processor.js)
During the compile-time SSR pass, ForgeWP data hooks and JSX primitives emit unique intermediate tokens. The Markup Processor scans the resulting HTML and replaces these tokens with native WordPress PHP function calls.
Core Token Replacement Rules
Intermediate Token Emitted WordPress PHP Expression Description __FORGEWP_THE_TITLE__<?php echo html_entity_decode( get_the_title(), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); ?>Post title with HTML entity decoding. __FORGEWP_THE_CONTENT__<?php the_content(); ?>Main WordPress post content. <forgewp-block-area /><?php the_content(); ?>Designated slot for WordPress block editor content. __FORGEWP_THE_PERMALINK__<?php the_permalink(); ?>Current post URL. __FORGEWP_THE_EXCERPT__<?php the_excerpt(); ?>Post summary excerpt. __FORGEWP_THE_DATE__<?php echo esc_html( get_the_date() ); ?>Published post date. __FORGEWP_THE_AUTHOR__<?php echo esc_html( get_the_author() ); ?>Post author display name. __FORGEWP_THE_POST_THUMBNAIL_URL__<?php echo esc_url( get_the_post_thumbnail_url( null, 'large' ) ); ?>Featured image URL. __FORGEWP_I18N_<text>__<?php echo __('text', 'textdomain'); ?>Localized gettext translation string. <forgewp-loop-start /><?php if (have_posts()) : while (have_posts()) : the_post(); ?>Standard WordPress loop opening. <forgewp-loop-end /><?php endwhile; else : echo "<p>No posts found.</p>"; endif; ?>Standard WordPress loop closing. |
|---|
Query Loop Compilation ()
When you use <WpQueryLoop> in your React components, the compiler translates its props into a secure, server-side WP_Query instance in PHP:
1<forgewp-query-loop-start postType="room" postsPerPage="6" orderby="date" order="DESC" />
2 <div class="room-card">...</div>
3<forgewp-query-loop-end />The markup processor transforms this into:
1<?php
2 $query_args = array(
3 'post_type' => 'room',
4 'posts_per_page' => 6,
5 'orderby' => 'date',
6 'order' => 'DESC',
7 );
8 if (function_exists('pll_current_language')) {
9 $query_args['lang'] = pll_current_language();
10 }
11 $custom_query = new WP_Query($query_args);
12 if ($custom_query->have_posts()) : while ($custom_query->have_posts()) : $custom_query->the_post();
13 ?>
14 <div class="room-card">...</div>
15<?php
16 endwhile;
17 wp_reset_postdata();
18 endif;
19?>The compiled query automatically respects language filters (via Polylang integration) and calls wp_reset_postdata() to prevent query pollution.
Where JavaScript Remains in Production
ForgeWP strips JavaScript from components that only render static content. However, browser JavaScript is intentionally preserved in three specific areas:
1. Selective Hydration Islands
Components that use interactive React hooks (useState, useEffect, useReducer), DOM event handlers (onClick, onSubmit), or browser motion libraries are compiled into standalone client chunks in assets/. The lightweight forgewp-hydrator.js script mounts these components in the browser only when their trigger condition (such as visible or click) is met.
2. Gutenberg Block Editor Controls
Custom Gutenberg blocks created with defineBlock() include a dedicated editor bundle (assets/forgewp-editor.js). This bundle runs inside the WordPress admin Block Editor (/wp-admin) to provide sidebar inspector controls, rich text inputs, and block previews.
3. Global Hydration State (window.forgeWpHydration)
functions.php injects runtime configuration data into window.forgeWpHydration:
- Theme asset URLs (
themeUri). - Navigation menu trees (
menus). - Registered page links (
pageLinks). - Theme options and Customizer settings (
siteSettings). - Hydration island chunk mappings (
manifest).
Compiler Boundaries and Limitations
ForgeWP is designed to translate recognized React patterns into WordPress output. It does not attempt to convert arbitrary JavaScript logic into PHP.
Supported Patterns
- Known data hooks:
useWpTitle(),useWpContent(),useWpQuery(),useWpMeta(),useWpI18n(). - Standard JSX elements and structural styling with Tailwind CSS.
- Explicit schema contracts:
defineEditable(),defineBlock(),defineWpForm(). - Array iterations over WordPress post collections (
posts.map(...)).
Unsupported Patterns in Static Server Components
- Browser-only APIs called directly during render (
window.localStorage.getItem(...)without a client guard). - Dynamic JavaScript evaluations in static templates (e.g.
Math.random()to generate random IDs). - Heavy third-party UI components that expect a live browser DOM on first paint.
If a component requires browser-only APIs or dynamic client logic, mark it for hydration using <Hydrate> or add an interactive event listener. ForgeWP will automatically extract it into an island, preserving its complete React runtime behavior in the browser.
Next Steps
Now that you understand the stages of the export pipeline, proceed to Compiler Contracts and Translation Rules to explore the exact mappings for data hooks, editable schemas, Gutenberg dual-host transpilation, and form handling.