isset( $_GET['date_from'] ) ? sanitize_text_field( wp_unslash( $_GET['date_from'] ) ) : '', 'date_to' => isset( $_GET['date_to'] ) ? sanitize_text_field( wp_unslash( $_GET['date_to'] ) ) : '', // GET key is 'abds_type', not 'post_type': WordPress's own admin // router treats a real post_type GET value as "this is an edit.php // screen for that type" and calls wp_die("Cannot load %s.") because // this settings page isn't registered under that type's parent slug // (confirmed 2026-09-21: choosing a type and clicking Filter broke // the page). Only the URL param name changed -- $filters['post_type'] // (this array's own key) and ABDS_SEO_Reports's expected arg name // are unchanged. 'post_type' => isset( $_GET['abds_type'] ) ? sanitize_key( wp_unslash( $_GET['abds_type'] ) ) : '', 'min_score' => isset( $_GET['min_score'] ) ? sanitize_text_field( wp_unslash( $_GET['min_score'] ) ) : '', 'max_score' => isset( $_GET['max_score'] ) ? sanitize_text_field( wp_unslash( $_GET['max_score'] ) ) : '', 'orderby' => isset( $_GET['orderby'] ) ? sanitize_key( wp_unslash( $_GET['orderby'] ) ) : 'content_score', 'order' => isset( $_GET['order'] ) ? sanitize_key( wp_unslash( $_GET['order'] ) ) : 'asc', 'page' => isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1, ); // phpcs:enable $report = ( new ABDS_SEO_Reports() )->query( $filters ); $status_labels = array( 'good' => __( 'Good', 'abds-seo' ), 'ok' => __( 'OK', 'abds-seo' ), 'poor' => __( 'Poor', 'abds-seo' ), 'not_analyzed' => __( 'Not analyzed', 'abds-seo' ), ); $export_url = wp_nonce_url( add_query_arg( array_merge( array_filter( array( 'date_from' => $filters['date_from'], 'date_to' => $filters['date_to'], // Key here is the URL param the export link carries; // abds_type, not post_type -- see the read side above. 'abds_type' => $filters['post_type'], 'min_score' => $filters['min_score'], 'max_score' => $filters['max_score'], 'orderby' => $filters['orderby'], 'order' => $filters['order'], ) ), array( 'action' => 'abds_seo_export_csv' ) ), admin_url( 'admin-post.php' ) ), 'abds_seo_export_csv' ); // Content-gap analysis is a Pro feature (shipped by the ABDS SEO Pro add-on). // When the add-on is absent its classes aren't loaded, so guard every use and // fall back to a locked teaser rather than fatally instantiating them. The // license check (2026-09-23 fix) matters just as much as the class_exists() // one: without it, an unlicensed site with the add-on merely INSTALLED saw // the real form instead of the blurred teaser -- the functional leak (the // form's own submit handler actually running the analysis for free) is now // closed at its source (ABDS_SEO_Content_Gaps::schedule()), but this gate // keeps the VISIBLE state honest too, matching what the REST/admin-post // layer will actually do when submitted. $gaps_available = class_exists( 'ABDS_SEO_Content_Gaps' ) && ABDS_SEO_License_Accessor::gate()->allows( ABDS_SEO_Feature_Map::FEATURE_CONTENT_GAPS ); if ( $gaps_available ) { $gaps = new ABDS_SEO_Content_Gaps(); $gaps_status = $gaps->get_status(); $gaps_result = $gaps->get_result(); } else { $gaps_status = array( 'status' => 'idle' ); $gaps_result = null; } $serp_ready = class_exists( 'ABDS_SEO_AI_Key_Vault' ) ? ( new ABDS_SEO_AI_Key_Vault( ABDS_SEO_AI_Key_Vault::TYPE_SERP ) )->has_key() : false; // Stage 9.6, Block 3.4: a "blurred preview" upsell — this ONE card, as // the brief's own example, not a pattern rolled out everywhere. Same // direct-to-buy-page contract every other CTA in the plugin uses (see // admin.js's buildBuyUrl()), built by hand here since this template // has no access to the React bundle's helper. $abds_seo_gaps_buy_url = 'https://license.abds-seo.com/buy/?tier=pro&period=monthly¤cy=usd&utm_source=plugin&utm_medium=software&utm_content=reports-content-gaps-blur'; ?>

1 ) : ?>
add_query_arg( 'paged', '%#%' ), 'format' => '', 'current' => $report['page'], 'total' => $report['total_pages'], ) ) ); ?>