Skip to content

WordPress systems / Case 28

Using ?debug as a controlled live-preview feature flag during a WordPress rebuild

A major location-template update needed real production content and integrations for QA, but unfinished ACF fields, sections, styles, and layout changes could not replace the stable public page until the rebuild was complete.

WordPressFeature flagsPHPACFRelease workflow

What this solved for the business or user

Normal visitors kept seeing the existing production page while the new version could be opened intentionally with a preview URL. That made it possible to review the rebuild against real content, forms, and production behavior before switching everyone to the new implementation.

What was happening

A live WordPress location template was being rebuilt while its content model was also changing. New ACF fields, reusable sections, responsive layouts, section-specific SCSS, and content fallbacks needed to be tested against the actual production records. Publishing the new template directly would expose partially migrated content and unfinished styling to visitors. A completely separate local copy, on the other hand, could miss live data, language behavior, form integrations, or content combinations that only existed in production.

Why the obvious solution was not enough

The preview had to use the same WordPress page, post ID, production content, plugins, and integrations as the public route while keeping the default request stable. It also needed to avoid a common failure mode: loading the new PHP branch but forgetting its matching CSS or JavaScript, which makes the preview look broken even when the template logic is correct. The query parameter could control presentation and rendering, but it could not be treated as authentication or as a safe place to run destructive data changes.

How the solution works

  1. Define one small preview-state helper instead of checking $_GET['debug'] independently throughout the theme. The helper becomes the single source of truth for whether the request should use the in-development path.
  2. Keep the ordinary URL on the stable implementation. Only a deliberate preview request such as ?debug=1 selects the new template branch while development is in progress.
  3. Use the same preview state when enqueueing assets. If the new template requires a new SCSS build, JavaScript behavior, or temporary diagnostic stylesheet, load those assets only for the preview request so the markup and styling cannot drift apart.
  4. Build the new template around real production data but retain defensive fallbacks for ACF fields that have not been populated yet. This lets content migration and template development happen incrementally rather than requiring every record to be updated before the first QA pass.
  5. Use the preview URL for developer, QA, editor, and stakeholder review. Because the URL points to the real page, reviewers can test actual long titles, missing fields, language variants, Gravity Forms, responsive behavior, and integration edge cases instead of a hand-created sample page.
  6. Prevent the preview state from becoming a second permanent product. Once the new implementation passes QA and content migration is complete, switch the normal request to the new template and remove or disable the temporary preview branch.
  7. For internal-only previews, strengthen the flag with WordPress authentication and capability checks. A query string by itself is discoverable and should never protect sensitive content or privileged functionality.
  8. Send no-cache headers for preview requests and make sure any CDN or page cache varies by the query string or bypasses the preview. Otherwise a cached preview response can leak into the ordinary URL or an old preview can hide a code change during QA.
  9. Keep preview URLs out of search results. Mark preview responses noindex and preserve the normal canonical URL so the temporary state does not become an indexable duplicate of the production page.
  10. Do not use the flag to perform destructive migrations, payments, irreversible writes, or security-sensitive actions. The flag is best for selecting a rendering path, asset bundle, integration version, or extra diagnostics while the underlying data writes remain explicit and controlled.
Safer WordPress preview-gate pattern
function project_is_preview_mode(): bool {
    if (!isset($_GET['debug']) || $_GET['debug'] !== '1') {
        return false;
    }

    // Best practice for an internal preview.
    return is_user_logged_in() && current_user_can('edit_pages');
}

add_action('wp_enqueue_scripts', function () {
    if (!is_singular('wpseo_locations')) {
        return;
    }

    if (project_is_preview_mode()) {
        wp_enqueue_style(
            'location-preview',
            get_theme_file_uri('/assets/css/is_location_preview.css'),
            [],
            filemtime(get_theme_file_path('/assets/css/is_location_preview.css'))
        );
    } else {
        wp_enqueue_style(
            'location-live',
            get_theme_file_uri('/assets/css/is_location.css')
        );
    }
});

add_filter('wp_robots', function (array $robots): array {
    if (project_is_preview_mode()) {
        $robots['noindex'] = true;
        $robots['nofollow'] = true;
    }
    return $robots;
});

if (project_is_preview_mode()) {
    nocache_headers();
    get_template_part('template-parts/location', 'next');
} else {
    get_template_part('template-parts/location', 'current');
}

What should be verified before shipping

  • Open the ordinary location URL in a logged-out browser and confirm the current production template and assets remain unchanged.
  • Open the same URL with ?debug=1 using an authorized account and confirm the new template, CSS, and JavaScript all load together.
  • Test records where new ACF fields are complete, partially migrated, and completely empty so the preview does not depend on perfect content.
  • Check English and translated versions, forms, dynamic tokens, and downstream integrations because the value of this preview is that it exercises the real production environment.
  • Inspect response headers and cache behavior to confirm a preview response is not reused for a normal request and that code changes are visible immediately during QA.
  • Inspect rendered robots directives and canonical output so the temporary preview URL is not treated as a separate indexable page.
  • Try ?debug=1 while logged out and confirm the hardened implementation does not expose internal-only preview behavior.
  • After launch, remove the old branch and temporary preview assets rather than leaving two implementations to drift indefinitely.

What changed

The location rebuild could be developed and reviewed against real production content without replacing the stable public experience before it was ready. The same mechanism also made content migration safer because new fields and sections could be populated gradually while the public template continued using its established path.

Reusable lesson

A query-controlled preview is useful as a lightweight release switch, not as a security boundary. Centralize the flag, keep rendering and assets on the same branch, account for caching and indexing, and remove the temporary path once the new implementation becomes the default.

How I would evaluate this today

The original production technique was intentionally lightweight: a debug query parameter selected work that should not yet appear to normal visitors. For a longer-running or sensitive project, the stronger version is an authenticated preview, password-protected staging environment, or a proper feature-flag system. The useful engineering idea is the same: separate deployment from public activation so code can exist in the environment before every visitor receives it.

Official documentation and standards