` that becomes something else — * reaches every post that ever used the block, instead of only the ones somebody * re-saves, and no post is ever left with an invalid block because the markup * moved on. * * The summary block renders with the `acid-aeo-summary` class on purpose: * `Generators\SchemaGraphBuilder` names that selector in the `speakable` node, * so the two have to agree. */ final class Blocks { /** * Block whose `items` attribute holds the key takeaways. * * The other two block names live on `Data\ContentData`, which reads them out * of post content; this one is only ever rendered, so it belongs here. * * @var string */ public const TAKEAWAYS_BLOCK = 'acid-aeo/key-takeaways'; /** * Handle of the editor script that registers the blocks in JavaScript. * * @var string */ public const EDITOR_SCRIPT = 'acid-aeo-blocks'; /** * Handle of the stylesheet that styles the blocks inside the editor. * * @var string */ public const EDITOR_STYLE = 'acid-aeo-blocks-editor'; /** * Handle of the stylesheet that styles the blocks on the front end. * * @var string */ public const STYLE = 'acid-aeo-blocks'; /** * Hooks the block registration into WordPress. * * The assets are registered first, at an earlier `init`, because * `register_block_type()` records handles and WordPress enqueues them later * by name — a handle that is not registered by then is silently dropped. * * @return void */ public function register(): void { add_action( 'init', [ $this, 'register_assets' ], 5 ); add_action( 'init', [ $this, 'register_blocks' ] ); } /** * Registers the editor script and the two stylesheets. * * @return void */ public function register_assets(): void { wp_register_script( self::EDITOR_SCRIPT, ACID_AEO_URL . 'assets/editor/blocks.js', [ 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-i18n' ], ACID_AEO_VERSION, true ); wp_set_script_translations( self::EDITOR_SCRIPT, 'acid-aeo' ); wp_register_style( self::EDITOR_STYLE, ACID_AEO_URL . 'assets/editor/blocks.css', [], ACID_AEO_VERSION ); wp_register_style( self::STYLE, ACID_AEO_URL . 'assets/public/blocks.css', [], ACID_AEO_VERSION ); } /** * Registers the three block types. * * @return void */ public function register_blocks(): void { register_block_type( ContentData::SUMMARY_BLOCK, [ 'api_version' => 3, 'title' => __( 'AI Summary (TL;DR)', 'acid-aeo' ), 'description' => __( 'The short answer this page leads with. Answer engines quote it and the schema marks it speakable.', 'acid-aeo' ), 'category' => 'text', 'icon' => 'excerpt-view', 'keywords' => [ __( 'summary', 'acid-aeo' ), __( 'tldr', 'acid-aeo' ), __( 'abstract', 'acid-aeo' ) ], 'attributes' => [ 'label' => [ 'type' => 'string', 'default' => __( 'In short', 'acid-aeo' ), ], 'text' => [ 'type' => 'string', 'default' => '', ], ], 'editor_script' => self::EDITOR_SCRIPT, 'editor_style' => self::EDITOR_STYLE, 'style' => self::STYLE, 'render_callback' => [ $this, 'render_summary' ], 'supports' => [ 'html' => false, 'multiple' => false, ], ] ); register_block_type( ContentData::FAQ_BLOCK, [ 'api_version' => 3, 'title' => __( 'AI FAQ', 'acid-aeo' ), 'description' => __( 'Question and answer pairs, published as an FAQPage node and listed in llms.txt.', 'acid-aeo' ), 'category' => 'text', 'icon' => 'editor-help', 'keywords' => [ __( 'faq', 'acid-aeo' ), __( 'questions', 'acid-aeo' ) ], 'attributes' => [ 'heading' => [ 'type' => 'string', 'default' => __( 'Frequently asked questions', 'acid-aeo' ), ], 'items' => [ 'type' => 'array', 'default' => [], ], ], 'editor_script' => self::EDITOR_SCRIPT, 'editor_style' => self::EDITOR_STYLE, 'style' => self::STYLE, 'render_callback' => [ $this, 'render_faq' ], 'supports' => [ 'html' => false ], ] ); register_block_type( self::TAKEAWAYS_BLOCK, [ 'api_version' => 3, 'title' => __( 'Key takeaways', 'acid-aeo' ), 'description' => __( 'A short list of what the reader should remember, easy for a model to lift verbatim.', 'acid-aeo' ), 'category' => 'text', 'icon' => 'editor-ul', 'keywords' => [ __( 'takeaways', 'acid-aeo' ), __( 'highlights', 'acid-aeo' ) ], 'attributes' => [ 'heading' => [ 'type' => 'string', 'default' => __( 'Key takeaways', 'acid-aeo' ), ], 'items' => [ 'type' => 'array', 'default' => [], ], ], 'editor_script' => self::EDITOR_SCRIPT, 'editor_style' => self::EDITOR_STYLE, 'style' => self::STYLE, 'render_callback' => [ $this, 'render_takeaways' ], 'supports' => [ 'html' => false ], ] ); } /** * Renders the TL;DR block. * * A block with no text renders nothing: an empty box says less than no box, * and it would still be picked up as the page's speakable summary. * * @param mixed $attributes Block attributes, as WordPress passes them. * @return string HTML, empty when there is nothing to say. */ public function render_summary( $attributes = [] ): string { $attributes = is_array( $attributes ) ? $attributes : []; $label = self::text( $attributes['label'] ?? __( 'In short', 'acid-aeo' ) ); $text = self::text( $attributes['text'] ?? '' ); if ( '' === $text ) { return ''; } $html = '
' . esc_html( $label ) . '
'; } return $html . '' . esc_html( $text ) . '