Skip to main content

10 posts tagged with "Bug Fix"

View all tags

WordPress Block Theme Changes Not Taking Effect? FSE Development Troubleshooting Guide

· 7 min read

Encountered these five issues repeatedly while developing WordPress Block Themes for clients. Each one took significant debugging time. This guide covers the root causes and provides ready-to-use solutions.

TL;DR

Five issues ranked by frequency: file changes not applying (database cache overrides files), block nesting errors (unclosed comments), child theme content not rendering (missing post-content block), SVG icons disappearing (WP_Filesystem polluted by plugins), and WP-CLI mail failures (SMTP plugins don't hook in CLI). Each scenario includes copy-paste diagnostic commands.

Scenario 1: Changed Theme Files, But the Page Looks the Same

Problem

You modified theme.json, templates/*.html, or parts/*.html in your theme directory, but the page shows no change. Even after git pull updates the code, the frontend still renders the old version.

Root Cause

FSE themes have their templates and global styles saved to the database via the Site Editor—stored as wp_template, wp_template_part, and wp_global_styles custom post types. The database version takes priority over file versions. Even if you update the file, as long as a database record exists, WordPress uses that instead.

Solution

Different file types require different cleanup:

Modified contentCleanup method
templates/*.htmlClear wp_template
parts/*.htmlClear wp_template_part
theme.jsonClear wp_global_styles + flush cache
patterns/*.phpTakes effect immediately, no cleanup needed

One-command cleanup for all database template caches:

# Local Docker environment
docker exec wp_cli bash -c 'wp post delete $(wp post list --post_type=wp_template --format=ids --allow-root) --force --allow-root'
docker exec wp_cli bash -c 'wp post delete $(wp post list --post_type=wp_template_part --format=ids --allow-root) --force --allow-root'
docker exec wp_cli bash -c 'wp post delete $(wp post list --post_type=wp_global_styles --format=ids --allow-root) --force --allow-root'
docker exec wp_cli wp cache flush --allow-root

If theme.json changes still don't apply, verify the JSON has no syntax errors (e.g., trailing commas are invalid in JSON):

docker exec wp_cli wp eval 'echo json_encode(json_decode(file_get_contents(get_template_directory() . "/theme.json")));' --allow-root
# Empty output → JSON syntax error

Important

  • Never save in the Site Editor during development—this prevents database overrides
  • Before clearing in production, verify no custom modifications need to be preserved from the Site Editor
  • patterns/*.php is unaffected—Pattern registration runs through PHP code, not the database
  • Corrupted wp_global_styles JSON from the Site Editor can cause site-wide WP_Theme_JSON_Resolver errors, breaking all page styles

Scenario 2: Site Editor Shows "Attempt Recovery" and the Layout Breaks

Problem

The Site Editor shows "Attempt Recovery" for a Pattern. After saving, the page layout is completely broken. Some blocks are incorrectly nested inside others, and the hierarchy doesn't match the source code.

Root Cause

WordPress block editor uses HTML comments to mark block boundaries:

<!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group">
<!-- content -->
<!-- /wp:group -->

When a container block (like wp:group or wp:columns) is missing its closing comment <!-- /wp:group -->, Gutenberg's parse_blocks() treats all subsequent blocks as children of that container. The consequences:

  1. The parent block's save output is empty
  2. Block validation fails
  3. All subsequent blocks have incorrect nesting

Solution

Diagnose: Check block tree hierarchy in the browser console:

wp.data.select('core/block-editor').getBlocks()

Inspect the returned block tree. If a group block contains blocks that shouldn't be inside it, a previous container is likely missing its closing comment.

Fix: Open the Pattern source file and verify every <!-- wp:xxx --> has a matching <!-- /wp:xxx -->. Search for <!-- wp: and <!-- /wp: and count to confirm the numbers match.

Prevention Tip

Use an editor with bracket matching (like VS Code) with a Block Comment highlight extension to catch unclosed comments immediately. For complex Patterns, write the skeleton first (all open/close comment pairs), then fill in the content.

Scenario 3: Child Theme Override Hides Page Editor Content

Problem

After creating a child theme to override a parent template, content entered in the WordPress page editor (text, images, etc.) is completely blank on the frontend. However, hardcoded Patterns in the template (hero sections, CTAs) display normally.

Root Cause

FSE templates render the page editor's post_content through the <!-- wp:post-content /--> block. If the child theme's template file doesn't include this block, WordPress has nowhere to output the page content.

The result: the template's fixed structure (header, hero, sidebar) renders correctly, but everything written in the editor is lost.

Solution

Ensure your child theme template includes the post-content block:

<!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group">

<!-- Template fixed structure (hero, sidebar, etc.) -->

<!-- wp:post-content {"layout":{"type":"constrained"}} /-->

<!-- More fixed structure (CTA, footer reference, etc.) -->

</div>
<!-- /wp:group -->

When troubleshooting "changes not showing", confirm in this order:

  1. Does the template file include <!-- wp:post-content /-->?
  2. Are you modifying the template file or the page content? They control different content areas
  3. Inline cover blocks in the template are controlled by the template file, not the database post_content

Scenario 4: SVG Icons Disappeared But the Files Are Still There

Problem

Your theme uses WP_Filesystem to read SVG icon files. Suddenly all SVG icons disappear. Accessing the SVG file URL directly returns normal content, but the icon positions on pages are empty.

Root Cause

WordPress's $wp_filesystem global defaults to WP_Filesystem_Direct (direct local file access). Some plugins (backup, security) replace $wp_filesystem with WP_Filesystem_ftpsockets or WP_Filesystem_SSH2 during initialization.

FTP/SSH adapters read files through remote connections. For local paths (like /var/www/html/wp-content/themes/...), they can't access the files correctly and return empty strings. Since the replacement happens in the global scope, all theme and plugin code using WP_Filesystem is affected.

Solution

Step 1 — Diagnose: Check the actual $wp_filesystem type:

# Local Docker environment
docker exec wp_cli wp eval 'global $wp_filesystem; echo get_class($wp_filesystem);' --allow-root

# Returns WP_Filesystem_Direct → normal
# Returns WP_Filesystem_ftpsockets or other → polluted

Step 2 — Identify the source: Disable plugins one by one to find which one replaces the adapter:

docker exec wp_cli wp plugin deactivate <plugin-name> --allow-root
docker exec wp_cli wp eval 'global $wp_filesystem; echo get_class($wp_filesystem);' --allow-root
# Repeat until it returns WP_Filesystem_Direct

Step 3 — Code fallback: Add file_get_contents() as a safety net in your theme:

function mytheme_get_svg( $path ) {
global $wp_filesystem;

// Prefer WP_Filesystem
if ( $wp_filesystem && method_exists( $wp_filesystem, 'get_contents' ) ) {
$content = $wp_filesystem->get_contents( $path );
if ( $content ) {
return $content;
}
}

// Fallback to direct file read
if ( file_exists( $path ) ) {
return file_get_contents( $path );
}

return '';
}

Important

  • file_get_contents() may be disabled via disable_functions on restricted hosts, but VPS and Docker environments typically support it
  • The root fix is to identify and handle the polluting plugin; the code fallback is a temporary measure
  • This issue is deceptive—the SVG files are intact and accessible via URL, but return empty only when read through WP_Filesystem in PHP

Scenario 5: Email Sending Fails from Command Line, Works from Browser

Problem

Calling wp_mail() via wp eval on the command line always fails. However, emails triggered by web requests (user registration, contact forms) send normally. SMTP plugins like WP Mail SMTP are correctly configured.

Root Cause

SMTP plugins intercept wp_mail() via hooks to switch the sending channel from PHP sendmail to an SMTP service. But these hooks depend on WordPress's full bootstrap sequence—particularly stages after wp_loaded.

WP-CLI's wp eval loads the WordPress core, but some plugin hooks don't register in the CLI environment. wp_mail() falls back to PHP sendmail, and most servers don't have sendmail configured, causing the failure.

Solution

Method 1 — Web request test: Add a temporary test route in your theme, trigger via browser:

// Add to functions.php temporarily, remove after testing
add_action( 'wp_loaded', function() {
if ( ! isset( $_GET['test_mail'] ) ) return;
if ( '1' !== $_GET['test_mail'] ) return;

$result = wp_mail( '[email protected]', 'SMTP Test', 'Test email body' );
var_dump( $result ); // true = sent successfully
exit;
} );

Visit https://yoursite.com/?test_mail=1 to trigger the test.

Method 2 — eval-file with full bootstrap:

cat > /tmp/test-smtp.php << 'EOF'
<?php
require_once ABSPATH . 'wp-load.php';
do_action('wp_loaded');

$result = wp_mail('[email protected]', 'CLI SMTP Test', 'Test body');
echo $result ? "Sent\n" : "Failed\n";
EOF

docker exec wp_cli wp eval-file /tmp/test-smtp.php --allow-root

Best Practice

In production, always test email sending through web requests. WP-CLI is great for cron jobs and batch operations, but not for verifying features that depend on the full WordPress hook chain (email, cache warming, etc.).


Embedding a Timeline in SITE123 Event Pages? First Dodge These 5 Platform Limits

· 5 min read

TL;DR

Embedding a self-hosted timeline into a SITE123 event page hits 5 platform walls: Custom Code can't target a page or position, scripts run before DOM is ready, selectors hit hidden elements, float layout pushes adjacent blocks out of place, and platform cache injects the script twice. Fix: JS DOM manipulation + horizontal fishbone layout + DOMContentLoaded wrapper.


The Problem

The client runs their event site on SITE123 and needed a timeline below the Hero banner showing key dates. The Timeline service was already deployed (timeline.aidevhub.ai), but after injecting the embed script via SITE123 Custom Code:

  • Timeline didn't appear
  • Timeline showed up at the page footer
  • After inserting the timeline, the Tickets section got pushed below

Root Causes

Limit 1: Custom Code Has No Precise Positioning

SITE123 Custom Code offers only 4 injection spots (head start, head end, body start, body end). You can't target a specific page and can't insert between sections.

This is the fundamental constraint with no workaround—only JS DOM manipulation after page load can compensate.

Limit 2: Script Runs Before DOM Is Ready

If the script is placed in "Before closing head tag", document.body is null at execution time. Stranger still: changing the injection position in SITE123 dashboard sometimes doesn't reflect in the actual page source.

Fix: Wrap everything in DOMContentLoaded regardless of where the script ends up.

Limit 3: Selector Matches a Hidden Element

tl.js prioritizes .container, but on SITE123 event pages .container is the breadcrumb area with visibility: hidden—the timeline gets inserted before the hidden element and stays invisible.

querySelector takes the first match only, and .container is a generic class name that easily collides.

Lesson: Always verify selectors in the browser console on the actual page. Never rely on assumptions.

Limit 4: Float Layout Conflict

SITE123 event pages use Bootstrap float layout (col-sm-5 + col-sm-7, exactly 12 columns). Inserting a full-width block before .product-container pushes the Tickets column to the next row.

This is a structural conflict—inside a float container, you cannot insert a full-row element without breaking the layout.

Fix: Switch to a horizontal fishbone layout (贯穿横线 + 圆点 + alternating cards), which uses width:100% naturally and doesn't participate in float calculation.

Limit 5: Platform Cache Injects Script Twice

After changing the Custom Code position, the page source showed two timeline.aidevhub.ai entries—one script injected twice. SITE123 platform bug, root cause unknown, doesn't affect the final result.


Solutions

1. Execution Timing Safeguard

Wrap all insertion logic in DOMContentLoaded so it works regardless of SITE123's placement:

document.addEventListener('DOMContentLoaded', function() {
// insertion logic
});

2. Always Verify Selectors in Console

Three-step browser console checklist:

// Step 1: Confirm element exists and is visible
const el = document.querySelector('.product-container');
console.log(el.getBoundingClientRect());

// Step 2: Confirm uniqueness (cross-page, should return 1)
const all = document.querySelectorAll('.product-container');
console.log(all.length);

// Step 3: Insert a red test block to verify position visually
const test = document.createElement('div');
test.style.cssText = 'background:red;height:100px;';
el.parentNode.insertBefore(test, el);

3. Layout Choice

Horizontal fishbone: a single horizontal line with dots positioned on it and cards alternating above and below. Vertical layouts easily break in float-based structures; horizontal layouts naturally don't participate in float calculation.

4. Insertion Target

Verified working target: .product-container (parent of event details and Tickets). Insert before it—the timeline lands below Hero, above event details.

5. Deployment

After modifying routes/script.js, run pm2 restart timeline and it takes effect immediately. No need to update the SITE123 embed code.


Notes

Always Verify Selectors in Console

SITE123 page structure is undocumented—never assume. .container is a generic class that may match the breadcrumb nav instead of your target. Always use the browser console on the actual page to confirm.

Custom Code Position Settings Are Unreliable

Changing the injection position in the SITE123 dashboard may not reflect in the actual page source. Make your script self-sufficient with DOMContentLoaded—don't rely on platform settings.

No Full-Width Elements Inside Float Layouts

Inserting a width:100% block inside or before a Bootstrap float container (col-*) pushes subsequent columns to the next row. Workaround: insert outside the container, or switch to a layout that doesn't rely on floats.


Self-Hosted Timeline Service—What Server to Use?

This project runs on Express + sql.js deployed on Vultr. A $5/month instance is sufficient. PM2 manages the process. Includes one-command setup script—running in 5 minutes.

Vultr — From $5/month →

Key Takeaways

  1. SITE123 Custom Code can't target pages or positions—JS DOM manipulation is the only workaround
  2. Wrap scripts in DOMContentLoaded to be self-sufficient, don't trust platform position settings
  3. Verify selectors with querySelectorAll + getBoundingClientRect in the browser console
  4. Full-width elements inside float layouts cause structural conflicts—horizontal layout bypasses this
  5. After modifying server code, pm2 restart timeline takes effect immediately, no need to touch the embed code

Fix React List Key Duplication Causing DOM Errors

· 2 min read

Encountered this issue while building an AI Agent chat interface. Here's the root cause and solution.

TL;DR

Date.now() millisecond timestamps can duplicate within the same millisecond. When used as React list keys, this causes DOM errors. Fix by adding a random suffix or using crypto.randomUUID().

Problem

When rapidly sending messages in a chat interface, the console shows:

Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.

Messages disappear or render incorrectly.

Root Cause

Original code used Date.now() for message IDs:

// ❌ Problematic code
const id = `msg-${Date.now()}-user`

Date.now() returns millisecond timestamps (e.g., 1742345678001). The problem:

  1. Same millisecond = same value — JavaScript's event loop executes synchronous code much faster than 1ms
  2. Rapid operations trigger multiple calls — Fast message sending, SSE streaming creating multiple messages simultaneously
  3. Duplicate keys break reconciliation — React treats same-key elements as identical, causing DOM operation errors

Example: User sends two messages within 1ms, both get key msg-1742345678001-user.

Solution

// ✅ Fixed
const id = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}-user`
  • Math.random().toString(36) generates base-36 random string
  • .slice(2, 9) extracts 7 characters for sufficient uniqueness
  • Timestamp + random string combination has extremely low collision probability

Option 2: Use crypto.randomUUID()

// ✅ More robust (requires modern browser or Node 15.6+)
const id = crypto.randomUUID() // e.g., "550e8400-e29b-41d4-a716-446655440000"
  • Cryptographically secure unique identifier
  • Collision-free guarantee
  • Compatibility: Chrome 92+, Firefox 95+, Safari 15.4+

Option 3: Counter + Timestamp

let counter = 0
const id = `msg-${Date.now()}-${counter++}-user`
  • Simple and reliable
  • Requires maintaining counter state

Complete Example

// Message creation in Zustand store
interface ChatMessage {
id: string
role: 'user' | 'assistant'
content: string
timestamp: number
}

export const useChatStore = create<ChatState>((set) => ({
messages: [],

addUserMessage: (content: string) => {
// ✅ Timestamp + random suffix
const id = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}-user`
const message: ChatMessage = {
id,
role: 'user',
content,
timestamp: Date.now(),
}
set((state) => ({
messages: [...state.messages, message],
}))
return id
},
}))

Key Principles

  1. Keys must be unique and stable — Same element's key cannot duplicate among siblings
  2. Avoid using index as key — Causes issues when list order changes
  3. Avoid timestamp-only keys — Millisecond precision insufficient; microsecond (performance.now()) also unreliable

Interested in similar solutions? Contact us

Debugging Frontend Deployment Not Updating on Production

· 3 min read

TL;DR

Frontend code pushed to Git but new feature missing on production? The root cause is usually outdated build artifacts on the server. Compare local and server dist/ directory timestamps to confirm, then run npm run build on the server.

Problem

New button/feature works locally (npm run dev) but not visible on production:

  • Browser refresh doesn't help
  • Clearing browser cache doesn't help
  • Code logic looks correct
  • Git confirms code was pushed

Root Cause

Frontend static files are typically served directly by Nginx:

Git Push → Server git pull → Server npm run build → Nginx serves dist/

The problem is between step 2 and 3: Code was git pulled, but npm run build wasn't executed or failed.

Common scenarios:

  1. Auto-deploy script syncs code but doesn't trigger build
  2. Manual deploy forgot to run build command
  3. Build ran but output to wrong directory

Solution

Step 1: Compare Build Artifact Timestamps

# Local
ls -la dist/assets/ | head -5

# Server
ssh user@server "ls -la /path/to/project/dist/assets/ | head -5"

Output comparison:

# Local (latest build)
Mar 7 22:55 index-28dFGXhX.js ← contains new feature

# Server (old build)
Mar 7 20:18 index-DsFdnylh.js ← missing new feature

Different filenames (hash changed) means content changed, different timestamps means build not synced.

Step 2: Rebuild on Server

ssh user@server "cd /path/to/project && npm run build"

Step 3: Verify Build Artifacts Updated

ssh user@server "ls -la /path/to/project/dist/assets/"

Confirm timestamps and filenames match local.

Step 4: Refresh Page

Since Vite/Webpack generates new filenames with hashes (e.g., index-28dFGXhX.js), index.html references the new file. Users just need a normal refresh, no forced cache clearing needed.

Complete Debug Script

#!/bin/bash
# Save as check-deploy.sh

SERVER="user@server"
PROJECT_PATH="/path/to/project"

echo "=== Latest Local Commit ==="
git log --oneline -1

echo -e "\n=== Latest Server Commit ==="
ssh $SERVER "cd $PROJECT_PATH && git log --oneline -1"

echo -e "\n=== Local Build Time ==="
ls -la dist/assets/ | head -3

echo -e "\n=== Server Build Time ==="
ssh $SERVER "ls -la $PROJECT_PATH/dist/assets/ | head -3"

echo -e "\n=== Server vs Remote Diff ==="
ssh $SERVER "cd $PROJECT_PATH && git fetch origin && git log HEAD..origin/main --oneline"

FAQ

Q: Why is production still showing old code after Git push?

A: Git push only updates source code. Frontend requires npm run build to generate static files. If your deploy process doesn't automatically trigger a build, the server's dist/ directory remains outdated. Nginx serves static files directly and won't auto-execute builds.

Q: Why doesn't clearing browser cache work?

A: The problem isn't browser cache - the server's static files themselves are old. Even with forced refresh, Nginx returns the old JS/CSS files. The correct fix is updating build artifacts on the server.

Q: How to avoid forgetting to rebuild?

A: Two options: 1) Configure CI/CD for automatic builds (e.g., GitHub Actions); 2) Use git hooks on the server to auto-run npm run build after git pull.

Q: Why does Vite add hash to build filenames?

A: Vite adds content hash to bundle filenames by default (e.g., index-28dFGXhX.js). When content changes, hash changes. This is a cache busting strategy ensuring users always get the latest version while maintaining long-term cache capability.