Skip to content

Conversation

josephfusco
Copy link
Member

@josephfusco josephfusco commented Oct 8, 2025

Zero-build toolbar implementation for headless WordPress applications.

Package Structure

@wpengine/hwp-toolbar

  • Pure JavaScript implementation, no transpilation required
  • Framework-agnostic core with React adapter
  • WordPress context management (user session, post state, preview mode)

WordPress Plugins

  • hwp-cors-local – Local development CORS headers
  • hwp-frontend-links – Admin bar frontend routing
  • hwp-wp-env-helpers – Docker environment REST API patches

Implementation

Vanilla

import { Toolbar } from '@wpengine/hwp-toolbar';

const toolbar = new Toolbar({
  position: 'bottom',
  onPreviewChange: (enabled) => console.log('Preview:', enabled)
});

toolbar.setWordPressContext({
  user: { id: 1, name: 'Admin' },
  post: { id: 42, title: 'Hello World' }
});

React

import { useToolbar } from '@wpengine/hwp-toolbar/react';

function MyToolbar() {
  const { state, nodes } = useToolbar(toolbar);
  return <div>User: {state.user?.name}</div>;
}

Technical Details

  • Direct source consumption – No build pipeline, modules served as-is
  • Deterministic port assignment – Hash-based calculation prevents conflicts
  • Modular architecture – Core, renderer, and framework adapters decoupled
  • Composer integration – Reusable plugin distribution
  • Complete examples – Vanilla/Vite and React/Next.js 15 implementations

Running Examples

# Vanilla
cd examples/vanilla/toolbar-demo && npm run example:start  # localhost:3644

# Next.js
cd examples/next/toolbar-demo && npm run example:start     # localhost:3975

Copy link

changeset-bot bot commented Oct 8, 2025

⚠️ No Changeset found

Latest commit: 085b439

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Integrate toolbar demo with real WordPress instance using wp-env:
- Add CORS headers via mu-plugin for localhost:3000
- Fetch real WordPress user via REST API
- Fetch posts using WPGraphQL
- Update demo UI with real data workflow
- Document WordPress integration in README

Updates:
- Use query parameter format for REST API (/?rest_route=)
- Use query parameter format for GraphQL (/?graphql)
- Add lifecycle script for permalink setup
- Update demo instructions for real WordPress workflow
…tion

Switch vanilla example from hardcoded configs to auto-generated approach.
Files like mu-plugin.php, .htaccess, and .wp-env.json are now generated
from centralized templates by the setup-env.js script.

- Add setup-env.js script that generates configs from templates
- Remove mu-plugin.php, .htaccess, .wp-env.json from version control
- Update .gitignore to exclude auto-generated example files
Add comprehensive Next.js App Router example demonstrating the toolbar
with real WordPress integration.

- Complete Next.js example with App Router
- Hash-based port calculation for deterministic ports
- Template-based configuration (mu-plugin, .htaccess, .wp-env.json)
- One-command startup with concurrently
- Real WordPress data integration (no mocks)
- WPGraphQL for post fetching
- Responsive toolbar with real user data
Enhance toolbar with configurable options and example improvements:

Toolbar Package:
- Add position config (top/bottom) with CSS positioning
- Add theme config with custom CSS variables and className
- Add getConfig/setConfig methods for runtime updates
- Improve CSS with position-aware dropdown menus

Vanilla Example:
- Use hash-based port calculation from setup-env.js
- Load ports from generated .env file in Vite config
- Update scripts to use template-based configuration
- Add pnpm workspace configuration
- Remove hardcoded mu-plugin.php (now generated)
Introduces three reusable, single-responsibility plugins for headless WordPress development:

- hwp-cors-local: Enables CORS headers for local development environments
- hwp-frontend-links: Adds "View on Frontend" links to WordPress admin interface
- hwp-wp-env-helpers: Fixes wp-env REST API routing quirks

Each plugin is independently configurable via HEADLESS_FRONTEND_URL constant and can be used standalone or in combination based on project needs.
… plugins

Updates toolbar demo examples to use production-ready configuration:

- WP_HOME points to WordPress instance (not frontend)
- HEADLESS_FRONTEND_URL configured separately for frontend application
- Replaces mu-plugin template approach with modular plugin references
- Updates PHP version to 8.3
- Removes mu-plugin mapping in favor of standard plugin loading

This allows WordPress to remain fully functional while supporting headless architecture, and enables composable plugin usage across different projects.
wp-env cannot resolve relative plugin paths from subdirectories.
Changed setup-env.js to generate absolute paths using path.resolve()
and path.join(), ensuring plugins load correctly in both vanilla
and Next.js examples.

Also added comprehensive implementation breakdown documentation.
Updated example:start scripts in both toolbar demos to use concurrently
properly. WordPress and frontend now start together using concurrently,
with a 5 second delay on frontend to ensure WordPress completes startup.

Changes:
- Separated wp-env setup (npm install) into wp:ensure script
- Modified wp:start to only run wp-env start (no blocking npm install)
- Used concurrently to run both services in parallel
- Added sleep 5 before frontend start to allow wp-env initialization
… DRY

- Remove mu-plugin.php template and generation code
- Use relative paths for plugin references (portable across machines)
- Simplify npm scripts (inline npm install, remove wp:ensure)
- Add .gitignore files to exclude generated files
- Align both toolbar examples with consistent configuration
Refactored plugin to support both single and multiple frontend configurations via HEADLESS_FRONTEND_URL or HWP_FRONTEND_LINKS. Admin bar and post row actions now display a 'View in [Label]' link for each configured frontend, improving flexibility for sites with multiple environments.
@josephfusco josephfusco requested review from ahuseyn and Copilot October 9, 2025 14:22
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a comprehensive headless WordPress toolkit with a modular plugin architecture and production-ready toolbar package. It transforms the previous template-based approach into a maintainable, reusable system with dynamic configuration and comprehensive documentation.

  • Creates three single-responsibility WordPress plugins for CORS, frontend links, and wp-env helpers
  • Introduces the @wpengine/hwp-toolbar package with framework-agnostic design and React hooks
  • Updates examples to use the new plugin architecture with dynamic port calculation
  • Provides complete documentation and production-ready configuration patterns

Reviewed Changes

Copilot reviewed 44 out of 47 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/toolbar/* Core toolbar package with TypeScript, React hooks, and vanilla renderer
plugins/* Three modular WordPress plugins for headless development
examples//toolbar-demo/ Updated demo applications using new architecture
scripts/get-ports.js Dynamic port calculation system for multi-example setup
IMPLEMENTATION_BREAKDOWN.md Comprehensive implementation documentation
Comments suppressed due to low confidence (1)

plugins/hwp-wp-env-helpers/hwp-wp-env-helpers.php:1

  • Direct $_SERVER access should be validated. Consider using sanitize_text_field() or checking if the key exists before accessing.
<?php

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@josephfusco josephfusco marked this pull request as ready for review October 9, 2025 14:23
@josephfusco josephfusco requested a review from a team as a code owner October 9, 2025 14:23
@josephfusco
Copy link
Member Author

@ahuseyn I'm working on resolving these last build issues but i'll mark it ready for review now :)

Copy link

github-actions bot commented Oct 9, 2025

📦 Plugin Artifacts Ready!

Download from GitHub Actions run

Available plugins:

  • ✅ hwp-cli.zip
  • ✅ hwp-cors-local.zip
  • ✅ hwp-frontend-links.zip
  • ✅ hwp-wp-env-helpers.zip

See the "Artifacts" section at the bottom of the Actions run page

@josephfusco josephfusco requested a review from Copilot October 9, 2025 22:09
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 55 out of 58 changed files in this pull request and generated 5 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

const response = await fetch(`${WP_URL}/?rest_route=/wp/v2/users/1`);

if (response.ok) {
const user = await response.json();
Copy link

Copilot AI Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the WordPress connection fails, the error handling only checks response.ok but doesn't provide specific guidance about the likely cause. Consider adding more specific error messages for common scenarios like CORS issues or WordPress not running.

Copilot uses AI. Check for mistakes.

Update plugin initialization to activate when either WP_ENVIRONMENT_TYPE
is 'local' OR WP_DEBUG is true, matching the documented behavior.

Previously, the code only checked WP_ENVIRONMENT_TYPE, creating a
mismatch with the README and IMPLEMENTATION_BREAKDOWN docs which stated
both conditions were supported.

Files modified:
- plugins/hwp-cors-local/hwp-cors-local.php: Add WP_DEBUG check
- plugins/hwp-cors-local/README.md: Clarify OR logic
- IMPLEMENTATION_BREAKDOWN.md: Update feature description
Add complete set of CSS custom properties to match documented
theming capabilities in README. Includes WordPress admin-inspired
default values.

CSS Variables Added:
- --hwp-toolbar-bg: Background color (#23282d)
- --hwp-toolbar-border: Border color (#32373c)
- --hwp-toolbar-text: Text color (#ffffff)
- --hwp-toolbar-text-hover: Hover text color (#72aee6)
- --hwp-toolbar-primary: Primary action color (#0073aa)
- --hwp-toolbar-primary-hover: Primary hover (#005177)
- --hwp-toolbar-danger: Danger action color (#dc3232)
- --hwp-toolbar-danger-hover: Danger hover (#a00)
- --hwp-toolbar-divider: Divider color (#464b50)
- --hwp-toolbar-shadow: Box shadow (rgba(0, 0, 0, 0.3))
- --hwp-toolbar-z-index: Stacking context (9999)
- --hwp-toolbar-font-family: Font stack
- --hwp-toolbar-font-size: Base font size (13px)

Enhancements:
- Applied variables to all toolbar elements
- Added hover states with smooth transitions
- Improved button, link, and dropdown styling
- Added proper borders and shadows
- Enhanced visual feedback for interactive elements
Replace unsafe type casting with proper getConfig() method to access
toolbar configuration. This eliminates the TypeScript 'as any' escape
hatch and provides proper type safety.

Before: const config = (toolbar as any).config;
After:  const config = toolbar.getConfig();

The getConfig() method is already publicly exposed by the Toolbar class
and returns a properly typed ToolbarConfig object.
Add documentation for HWP_FRONTEND_LINKS constant which enables
configuring multiple frontend environments. This feature was already
implemented in the code but not documented.

Users can now easily see how to configure multiple frontend targets
(production, staging, local) which will each appear as separate
"View in [Label]" links in the WordPress admin.

Example configuration added showing three frontends with custom labels.
Add detailed error handling to vanilla toolbar demo login function
with specific guidance for common failure scenarios.

Improvements:
- 404 errors: Guide user to WordPress admin for initial setup
- 500+ errors: Indicate server issues and suggest checking if WP is running
- Network errors: Distinguish between connection failures and provide
  troubleshooting steps (WordPress not running, CORS issues, wrong URL)
- Generic errors: Provide fallback error message with console reference

This addresses Copilot feedback about providing more specific error
messages instead of generic "response.ok" checks.
Add comprehensive documentation explaining the port calculation strategy
and rationale behind PORT_RANGE and PORT_BASE constants.

Documentation now explains:
- Why PORT_RANGE is 900 (space for many examples, manageable range)
- Why PORT_BASE is 100 (avoid common default ports like 3000, 8000)
- The full port calculation algorithm (hash -> decimal -> modulo)
- Expected port ranges (frontend: 3100-3999, WordPress: 8100-8999)
- Key properties: deterministic and collision-resistant

Addresses Copilot feedback about documenting "magic numbers."
Enhance dropdown menus with full keyboard navigation, ARIA attributes,
and click-outside-to-close behavior for improved accessibility and UX.

Accessibility Improvements:
- ARIA attributes: aria-haspopup, aria-expanded, role="menu", role="menuitem"
- Keyboard navigation: Enter/Space to toggle, Escape to close
- Click-outside detection to auto-close dropdown menus
- Auto-close menu after selecting an item

Implementation:
- Proper event listener management to prevent memory leaks
- Cleanup of click-outside handler when menu closes
- Synchronized aria-expanded state with visual state
- Screen reader friendly semantic markup

This addresses accessibility concerns identified in code review.
Fix node filtering logic to correctly distribute nodes into left,
center, and right sections based on their position property.

Previous behavior:
- Filtered out nodes with position='right'
- Put everything else in left section
- Ignored center positioning entirely

New behavior:
- Left section: nodes with no position or position='left'
- Center section: nodes with position='center'
- Right section: nodes with position='right'

Also extracted renderNode helper to reduce duplication and improve
code readability.
Remove non-functional demo-path node that was leftover from development.
This node displayed 'examples/vanilla/toolbar-demo' but had no onClick
handler and served no purpose in the demo.

Keeps the functional 'home' node for navigation.
Eliminate TypeScript compilation requirement and convert to vanilla JS following the CLI package pattern.

Changes:
- Convert src/index.ts → src/core/Toolbar.js (with JSDoc types)
- Convert src/react.ts → src/react.js (with JSDoc types)
- Create modular structure: src/core/Toolbar.js and src/core/VanillaRenderer.js
- Update package.json to point to src/ instead of dist/
- Remove TypeScript configuration (tsconfig.json)
- Remove build scripts and TypeScript dependencies
- Fix Next.js example dev.sh to use npx

Benefits:
✅ No build step required
✅ Direct source consumption
✅ Faster development (no compilation watch)
✅ Simpler package structure
✅ JSDoc provides IDE autocomplete/intellisense
✅ Same pattern as CLI package
@josephfusco josephfusco requested a review from Copilot October 10, 2025 03:20
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 56 out of 59 changed files in this pull request and generated 6 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.


// Click outside to close
const handleClickOutside = (e) => {
if (!container.contains(e.target)) {
Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The event target may not be an Element in all cases. Consider checking if e.target is an Element before calling contains().

Suggested change
if (!container.contains(e.target)) {
if (!(e.target instanceof Element) || !container.contains(e.target)) {

Copilot uses AI. Check for mistakes.

Comment on lines 28 to 33
// Demo: Using user ID 1 (wp-env default admin) for simplicity
// Production: Use /wp/v2/users/me with Application Passwords or OAuth
// Note: This is acceptable in demos where auth setup would add unnecessary complexity
return fetchFromWordPress('/wp/v2/users/1');
}

Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded user ID 1 could expose admin information. Consider using /wp/v2/users/me with proper authentication or implementing user validation.

Suggested change
// Demo: Using user ID 1 (wp-env default admin) for simplicity
// Production: Use /wp/v2/users/me with Application Passwords or OAuth
// Note: This is acceptable in demos where auth setup would add unnecessary complexity
return fetchFromWordPress('/wp/v2/users/1');
}
// Use /wp/v2/users/me to get the current authenticated user.
// Requires proper authentication (e.g., Application Passwords or OAuth).
return fetchFromWordPress('/wp/v2/users/me');
}

Copilot uses AI. Check for mistakes.

header( 'Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With' );

// Handle preflight OPTIONS requests
if ( $_SERVER['REQUEST_METHOD'] === 'OPTIONS' ) {
Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct access to $_SERVER['REQUEST_METHOD'] without sanitization. Consider using WordPress functions like $_SERVER['REQUEST_METHOD'] check after validating the request context.

Suggested change
if ( $_SERVER['REQUEST_METHOD'] === 'OPTIONS' ) {
if ( function_exists( 'get_request_method' ) ? get_request_method() === 'OPTIONS' : ( isset( $_SERVER['REQUEST_METHOD'] ) && $_SERVER['REQUEST_METHOD'] === 'OPTIONS' ) ) {

Copilot uses AI. Check for mistakes.

@josephfusco josephfusco requested a review from Copilot October 10, 2025 04:59
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 56 out of 59 changed files in this pull request and generated 9 comments.


Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +198 to +199
// register(id, onClick)
this.nodes.set(id, { id, type: 'button', label: id, onClick: labelOrNode });
Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using register(id, onClick) pattern, the label is set to the id but labelOrNode is actually the onClick function. The label should remain as id and onClick should be labelOrNode.

Copilot uses AI. Check for mistakes.

/**
* Demo Actions
*/
window.login = async () => {
Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Global function assignment pollutes the global namespace. Consider using a module pattern or attaching to a namespaced object instead of directly to window.

Suggested change
window.login = async () => {
window.demoApp = window.demoApp || {};
window.demoApp.login = async () => {

Copilot uses AI. Check for mistakes.

header( 'Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With' );

// Handle preflight OPTIONS requests
if ( $_SERVER['REQUEST_METHOD'] === 'OPTIONS' ) {
Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct access to $_SERVER superglobal without sanitization. Consider using WordPress functions like get_server_var() or sanitize the input.

Suggested change
if ( $_SERVER['REQUEST_METHOD'] === 'OPTIONS' ) {
if ( get_server_var( 'REQUEST_METHOD' ) === 'OPTIONS' ) {

Copilot uses AI. Check for mistakes.

Comment on lines +291 to +295
const handleClickOutside = (e) => {
if (!container.contains(e.target)) {
closeMenu();
}
};
Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The click outside handler is added/removed on every trigger click but never properly cleaned up if the dropdown is destroyed. This could lead to memory leaks.

Copilot uses AI. Check for mistakes.

@theodesp
Copy link
Member

theodesp commented Oct 10, 2025

@josephfusco awesome job.
Please resolve copilot comments.

josephfusco and others added 6 commits October 10, 2025 06:53
Enhance error messages in Next.js toolbar example to provide specific,
actionable guidance for common WordPress connection failures.

Changes:
- Add comprehensive status code handling (401, 403, 404, 500+) in wordpress.ts
- Parse WordPress error response JSON for detailed error messages
- Detect "No route was found" errors and suggest hwp-wp-env-helpers plugin
- Provide specific troubleshooting steps for CORS, network, and server errors
- Update page.tsx to display detailed error messages instead of generic text

Error messages now include:
- Multiple possible causes for each error type
- Specific commands to resolve issues (npx wp-env start, etc.)
- References to required plugins (hwp-cors-local, hwp-wp-env-helpers)
- WordPress log checking instructions for server errors
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants