';
echo wp_kses( HelpIcon::render( 'seo_title' ), KsesTags::help_icon() );
if ( $this->ai_enabled() )
{
printf(
' ',
esc_html__( 'Generate with AI', 'acez-seo' )
);
}
/**
* Extra AI actions for this meta field, rendered right after the base
* plugin's own "Generate with AI" button (M69 Etap 5).
*
* The "Translate from the original" button (M35) used to sit here
* behind a licence check; it lives in acez-seo-pro now. A listener
* that outputs a `button.acez-seo-ai-generate` with data-field and
* data-target is wired up automatically — meta-box-ai.js binds by
* class, so the addon needs no JS of its own for this editor.
*
* @param \WP_Post $post
* @param string $field 'title' or 'description'.
* @param string $target DOM id of the input the result should fill.
*/
do_action( 'acez_seo_metabox_meta_field_actions', $post, 'title', 'acez_seo_title' );
echo '
';
printf( '', esc_attr( $title ) );
echo '
';
echo wp_kses( HelpIcon::render( 'seo_description' ), KsesTags::help_icon() );
if ( $this->ai_enabled() )
{
printf(
' ',
esc_html__( 'Generate with AI', 'acez-seo' )
);
}
/**
* Extra AI actions for this meta field, rendered right after the base
* plugin's own "Generate with AI" button (M69 Etap 5).
*
* The "Translate from the original" button (M35) used to sit here
* behind a licence check; it lives in acez-seo-pro now. A listener
* that outputs a `button.acez-seo-ai-generate` with data-field and
* data-target is wired up automatically — meta-box-ai.js binds by
* class, so the addon needs no JS of its own for this editor.
*
* @param \WP_Post $post
* @param string $field 'title' or 'description'.
* @param string $target DOM id of the input the result should fill.
*/
do_action( 'acez_seo_metabox_meta_field_actions', $post, 'description', 'acez_seo_description' );
echo '
',
esc_html__( 'Generate product description with AI', 'acez-seo' ),
esc_html__( 'The result goes into the "Meta description" field above (also used as the product description in schema.org) — review before saving.', 'acez-seo' )
);
}
if ( $using_wc )
{
echo '
' . esc_html__( 'WooCommerce detected — price, currency, SKU, and availability are pulled automatically from the product. The fields below are ignored.', 'acez-seo' ) . '
';
echo wp_kses( HelpIcon::render( 'schema_type_override' ), KsesTags::help_icon() );
echo ' ' . esc_html__( 'When automatic detection gets it wrong (e.g. a page with a blog built using a third-party builder module), pick the correct type for the main node — its properties will still be filled in automatically from the post\'s data.', 'acez-seo' ) . ' ';
echo '
';
echo '' . esc_html__( 'If filled in, this replaces the ENTIRE auto-generated graph (the entries below are then ignored).', 'acez-seo' ) . ' ';
printf(
'
',
esc_html__( 'Or generate a node proposal with AI (it lands as a new row above — review and adjust before saving):', 'acez-seo' ),
esc_attr__( 'FAQ (AI proposal)', 'acez-seo' ),
esc_html__( 'Generate FAQ node with AI', 'acez-seo' ),
esc_attr__( 'HowTo (AI proposal)', 'acez-seo' ),
esc_html__( 'Generate HowTo node with AI', 'acez-seo' ),
wp_kses( HelpIcon::render( 'ai_schema_nodes' ), KsesTags::help_icon() )
);
}
}
/**
* M51 — the free "Lite" analysis: keyphrase field + live lights. Renders
* NOTHING when the Pro addon is active (its full analysis, with its own
* keyphrase field bound to the SAME meta key, replaces this section).
*/
private function render_lite_section( \WP_Post $post ): void
{
if ( ! \AcezSeo\Analysis\LiteAnalyzer::enabled() )
{
return;
}
$keyphrase = (string) $this->post_meta->get( $post->ID, MetaFields::KEYPHRASE );
echo '
',
esc_html__( 'The Pro version understands Polish inflection and synonyms, analyzes readability, and runs an AI content review.', 'acez-seo' ),
esc_attr__( 'Hide this hint', 'acez-seo' )
);
echo '
';
}
/**
* M66 part D — manual property overrides for the FORCED main node.
*
* The fields themselves live in acez-seo-pro since M69 Etap 3: a licence
* unlocking code that ships in the WordPress.org package is exactly what
* Guideline 5 forbids. What stays here is the extension point and, when
* no addon answers it, a static teaser — marketing copy, which Guideline
* 11 allows on our own editor screen.
*
* Which of the two renders is decided by has_action(), deliberately NOT
* by a licence check (M69 Etap 2 doctrine): the base plugin no longer
* knows about licences at all. With the addon installed but unlicensed,
* the addon draws its own signpost — otherwise the hint would vanish for
* precisely the users it is meant to reach.
*/
private function render_forced_schema_fields( \WP_Post $post, string $type_override ): void
{
$hidden = '' === $type_override || 'none' === $type_override;
if ( ! has_action( 'acez_seo_metabox_schema_fields' ) )
{
$this->render_pro_teaser_locked_fields( $hidden );
return;
}
/**
* Renders the forced node's property fields inside this meta box.
*
* The addon must reuse `id="acez-seo-forced-schema-fields"` and honour
* $hidden: the base plugin's own custom-schema-repeater.js toggles
* that element live when the type dropdown changes, so the fields
* appear and disappear without a page reload exactly as before.
*
* @param \WP_Post $post
* @param bool $hidden Whether no real type is forced yet, so the block starts hidden.
* @param string $type_override The forced type name ('' or 'none' when unset).
*/
do_action( 'acez_seo_metabox_schema_fields', $post, $hidden, $type_override );
}
/**
* M66 (option d) — disabled preview of the part-D fields, shown when no
* addon answers `acez_seo_metabox_schema_fields`. Field markup mirrors the
* real fields acez-seo-pro renders, minus `name=""` (never posted) and
* plus `disabled` — same labels, so what is being sold is obvious. Since
* M69 Etap 3 this is genuinely dead markup: there is no code path here
* that could store or apply these values, whatever a client posts.
*/
private function render_pro_teaser_locked_fields( bool $hidden ): void
{
echo '
';
}
/**
* M66 part E — the "add a node from a form" builder for the custom-nodes
* repeater.
*
* Pure convenience UI: the builder only COMPOSES the JSON, which is then
* appended as an ordinary repeater row and travels the existing save and
* sanitization path — a form-built node is indistinguishable from a
* hand-pasted one. That is exactly why it could be sold as Pro with a
* render-time gate and nothing else; and exactly why, since M69 Etap 4,
* the form itself lives in acez-seo-pro instead: shipping it here and
* unlocking it with a licence is what Guideline 5 forbids.
*
* Unlike the forced-node fields, this is NOT tied to the type-override
* state — a custom node is useful whether or not the main node's type is
* forced — so the teaser shows unconditionally when no addon answers.
*/
private function render_node_builder(): void
{
if ( ! has_action( 'acez_seo_metabox_node_builder' ) )
{
$this->render_pro_teaser_locked_node_builder();
return;
}
/**
* Renders the form-based node builder under the custom-nodes repeater.
*
* The addon appends finished rows through the repeater's own JS API
* (`window.AcezSeoSchemaRepeater.addRow`), never by touching the
* or container directly — the repeater stays owned here.
*/
do_action( 'acez_seo_metabox_node_builder' );
}
/**
* M66 (option d) — disabled preview of the node builder (part E), shown
* when no addon answers `acez_seo_metabox_node_builder`. Same field set as
* the real builder acez-seo-pro renders, disabled, with no JS wired to it
* — dead markup by construction: there is no code path here that could
* turn these fields into a repeater row.
*/
private function render_pro_teaser_locked_node_builder(): void
{
echo '