Advanced Themer 13 Dev Log: One Day, Fourteen Features, and a Three-Plugin Pileup

Behind the scenes of a full day hardening our in-house Astra toolkit — filters, hooks, hotfixes, and one very satisfying bug hunt.


Advanced Themer 13 is our ground-up rebuild of the toolkit we bolt onto every Astra site we ship. Today it grew up a lot. Here's the full engineering log — not the marketing version, the real one.

The philosophy: filter Astra, don't fight it

Everything AT13 does to the front end goes through Astra's own option pipeline. Astra reads every setting through astra_get_option(), and that function runs each value through a dynamic filter: astra_get_option_{$option}. Hook that filter and you own the setting — no template overrides, no CSS specificity wars, no forked theme files that break on update.

That's the pattern behind today's Blog Layout module. The new Layout tab writes plugin options (at13_blog_layout, at13_blog_columns, at13_blog_content, and friends), and a loop registers a closure on the matching Astra filter for each one that's actually set:

add_filter( "astra_get_option_{$astra_opt}", function () use ( $at13_val, $astra_opt ) {
    return in_array( $astra_opt, [ 'blog-grid', 'blog-post-per-page' ], true )
        ? (int) $at13_val
        : $at13_val;
} );

Anything left on default never registers a filter at all, so the Customizer keeps authority until you deliberately take it. Grid/List/Cover layouts, column counts, excerpt vs. full content, hover effects, and opt-in overrides for Astra's blog-post-structure and blog-meta arrays — all of it rides astra_get_option_*. Zero template files touched.

The same trick killed an annoyance: Astra free injects a permanently-disabled "Astra Menu Settings 🔒" button into every item on Appearance → Menus — pure Pro upsell, and it makes third-party panels like Menu Icons look locked. Turns out the entire upsell layer gates on one option. One line in the child theme:

add_filter( 'astra_get_option_ast-disable-upgrade-notices', '__return_false' );

Locks gone. Nothing was ever actually locked.

Header builder, the surgical version

The header picked up real client-facing controls today, each one wired to the markup layer that actually owns it:

  • CTA button after the menu. Appended via the wp_nav_menu_items filter, scoped to theme_location === 'primary', with label, URL, background color, and an open-in-new-tab toggle (rel="noopener noreferrer" included). Astra stretches .menu-link to full nav height, which left the button text riding the top of a tall block — fixed by flex-centering the <li> and forcing a compact box (line-height:1, height:auto, both !important to out-rank Astra's own menu rules).
  • Phone number with SVG icon. The icon is inline SVG using stroke="currentColor", so it inherits the header text color and the hover color for free — no extra HTTP request, no recoloring assets. The whole phone/social row also got wrapped in Astra's .ast-container, so its right edge now aligns to the same content boundary as the menu instead of drifting to the browser edge.
  • Tagline with honest semantics. The show/hide checkbox now overrides Astra's own "Display Site Tagline" customizer setting in both directions via its option filter — checked shows, unchecked hides, and blank text falls back to Settings → General. Previously an empty tagline rendered an invisible empty <p> and looked like a bug.
  • Site title sizing (default 34px, changeable) and per-element show/hide toggles for the phone, all with sane defaults so existing sites don't shift when the plugin updates.

Content controls with per-page escape hatches

Two requests that every client eventually makes: "kill the comment spam" and "hide the page titles." Both are now global toggles with local overrides:

  • Hide Comments (posts) filters comments_open and get_comments_number to zero on singular posts — Astra sees a closed, empty discussion and renders no comments UI at all. No CSS hiding, no orphaned markup.
  • Hide Titles is split into separate Pages and Posts toggles, implemented through Astra's astra_the_title_enabled filter so the <h1> markup is skipped, not display:none'd. Every editor screen gets an "AT13 Title" meta box whose checkbox defaults to checked (hidden); unchecking writes _at13_show_title = 1 post meta and that one page opts back in. Default-on with opt-out means zero clicks for the common case.

Also new: a Duplicate row action on posts and pages — nonce-verified, capability-checked, clones content, excerpt, taxonomies, and all post meta (minus _edit_lock/_edit_last) into a draft titled "(Copy)". Our AT13 per-page flags ride along with the meta, which is exactly what you want when cloning a landing page.

Tracking scripts done right

Three boxes, three injection points, because that's what the vendors actually specify: Header (wp_head at priority 99 — GA4, GTM loader), Body (wp_body_open at priority 1 — GTM's <noscript> frame, textbook placement), and Footer (wp_footer — chat widgets and anything lazy). The sanitizer preserves raw HTML only for users with unfiltered_html; anyone else gets wp_kses_post'd on save, so the feature can't become a stored-XSS vector through a lesser role.

The CSS tab also gained a Mobile CSS box: write plain rules, set a breakpoint (default 720px), and the plugin wraps output in @media (max-width: …) at priority 999 — after Quick Styles, after Astra, so mobile rules win.

The bug hunt: Visual Composer × WooCommerce × PHP 8.5

Mid-session, the VC editor started white-screening. Site fine, one page uneditable. This is where discipline beats guessing: straight to debug.log — all 201 MB of it, bloated by PHP 8.5 deprecation notices from plugins that haven't caught up yet.

The real fatal, timestamped to the minute we hit the editor:

PHP Fatal error: Uncaught TypeError: strpos(): Argument #1 ($haystack)
must be of type string, int given in
woocommerce/src/Internal/ProductAttributes/VisualAttributeTermAdmin.php:290

The autopsy: Visual Composer's editor swaps in an admin screen object whose ID is an integer — and it does it directly, without firing the current_screen action. WooCommerce later runs strpos( $screen->id, 'edit-pa_' ) on admin_enqueue_scripts, assuming a string. On PHP 7 that mismatch was a shrugging warning; on PHP 8+ it's fatal. Three parties, each individually defensible, collectively a white screen.

The fix is a 20-line mu-plugin that normalizes $screen->id to a string on both current_screen and admin_enqueue_scripts at priority 9 — one tick before WooCommerce reads it at 10. No core files patched, survives every update of both plugins, and becomes a no-op the day either vendor fixes their side. We also truncated the log and added error_reporting( E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED ) to wp-config so real errors stay findable.

Diagnosis to deployed fix: about ten minutes. That's the entire argument for WP_DEBUG_LOG and reading the actual error instead of bisecting plugins for an afternoon.

The graduation: from "this site's hack" to shippable product

The biggest structural change shipped last. All the Astra wiring used to live in the child theme — which meant every new site needed files hand-copied around. It now lives in the plugin (inc/frontend.php), loaded like this:

add_action( 'after_setup_theme', function () {
    if ( get_template() === 'astra' ) {
        require_once AT13_PATH . 'inc/frontend.php';
    }
}, 5 );

Astra active? Full wiring. Any other theme? Silent no-op — the admin pages still work, nothing fatals. An AT13_FRONTEND_LOADED guard plus a defined('AT13_VERSION') check in the child theme means old child-theme copies can coexist as a fallback without ever double-loading.

New site recipe, final form: Astra → Advanced Themer 13 → bare child theme → drop the mu-plugin hotfix in mu-plugins/. Configure from one menu. Ship.


SolarBlu
Scroll to Top