` elements, and a schema screen where the same questions are typed * again as JSON-LD. They drift within a week, and the version Google reads is * the one nobody proof-reads. * * Here the block's attributes are the source. `Services\BlockSchema` parses the * post's blocks and hands the schema builder a FAQPage or a HowTo node built * from exactly what the page shows — so an answer edited in the editor is the * answer in the rich result, without a second screen. * * All three render server-side. A block that renders its markup at save time * freezes it into the database: change a heading and last year's posts keep the * old table of contents, and fixing the markup means re-saving every post. */ final class BlocksModule implements ModuleInterface { public function id(): string { return 'blocks'; } public function register( Container $container ): void {} public function boot( Container $container ): void { add_action( 'init', array( $this, 'register_blocks' ) ); // Anchors for the table of contents to point at — added to the rendered // content, never to what is stored, and only on a page that has the // block. A plugin that rewrites every heading on every page for a // feature nobody used would be a poor guest in somebody else's theme. add_filter( 'the_content', array( $this, 'heading_anchors' ), 9 ); add_filter( 'block_categories_all', array( $this, 'category' ), 10, 1 ); add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue' ) ); } /** * @param array> $categories * @return array> */ public function category( array $categories ): array { foreach ( $categories as $existing ) { if ( 'ai-rank-suite' === ( $existing['slug'] ?? '' ) ) { return $categories; } } array_unshift( $categories, array( 'slug' => 'ai-rank-suite', 'title' => __( 'AI Rank Suite', 'ai-rank-suite' ), ) ); return $categories; } public function register_blocks(): void { register_block_type( 'ai-rank-suite/faq', array( 'api_version' => 2, 'title' => __( 'FAQ', 'ai-rank-suite' ), 'description' => __( 'Questions and answers that also become a FAQPage rich result.', 'ai-rank-suite' ), 'category' => 'ai-rank-suite', 'icon' => 'editor-help', 'attributes' => array( 'title' => array( 'type' => 'string', 'default' => '' ), 'items' => array( 'type' => 'array', 'default' => array() ), 'open' => array( 'type' => 'boolean', 'default' => false ), ), 'render_callback' => array( Services\Render::class, 'faq' ), 'supports' => array( 'html' => false, 'anchor' => true ), ) ); register_block_type( 'ai-rank-suite/howto', array( 'api_version' => 2, 'title' => __( 'How-to', 'ai-rank-suite' ), 'description' => __( 'Numbered steps that also become a HowTo rich result.', 'ai-rank-suite' ), 'category' => 'ai-rank-suite', 'icon' => 'editor-ol', 'attributes' => array( 'name' => array( 'type' => 'string', 'default' => '' ), 'description' => array( 'type' => 'string', 'default' => '' ), 'totalTime' => array( 'type' => 'string', 'default' => '' ), 'supplies' => array( 'type' => 'array', 'default' => array() ), 'tools' => array( 'type' => 'array', 'default' => array() ), 'steps' => array( 'type' => 'array', 'default' => array() ), ), 'render_callback' => array( Services\Render::class, 'howto' ), 'supports' => array( 'html' => false, 'anchor' => true ), ) ); register_block_type( 'ai-rank-suite/toc', array( 'api_version' => 2, 'title' => __( 'Table of contents', 'ai-rank-suite' ), 'description' => __( 'The headings of this page, linked — built when the page is viewed, so it never goes stale.', 'ai-rank-suite' ), 'category' => 'ai-rank-suite', 'icon' => 'list-view', 'attributes' => array( 'title' => array( 'type' => 'string', 'default' => '' ), 'maxLevel' => array( 'type' => 'number', 'default' => 3 ), 'ordered' => array( 'type' => 'boolean', 'default' => false ), ), 'render_callback' => array( Services\Render::class, 'toc' ), 'supports' => array( 'html' => false ), ) ); } /** * @param string $content Rendered post content. */ public function heading_anchors( string $content ): string { $post = get_post(); if ( ! $post instanceof \WP_Post || ! str_contains( (string) $post->post_content, 'wp:ai-rank-suite/toc' ) ) { return $content; } return Services\Headings::add_ids( $content ); } /** * The editor side. Written against the wp.* globals rather than built: * three blocks are not worth a second bundle, and this way the file that * ships is the file that was written. */ public function enqueue(): void { $file = AIRANKSUITE_DIR . 'assets/blocks/blocks.js'; if ( ! file_exists( $file ) ) { return; } wp_enqueue_script( 'airanksuite-blocks', AIRANKSUITE_URL . 'assets/blocks/blocks.js', array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-i18n' ), (string) filemtime( $file ), true ); wp_set_script_translations( 'airanksuite-blocks', 'ai-rank-suite', AIRANKSUITE_DIR . 'languages' ); $css = AIRANKSUITE_DIR . 'assets/blocks/blocks.css'; if ( file_exists( $css ) ) { wp_enqueue_style( 'airanksuite-blocks', AIRANKSUITE_URL . 'assets/blocks/blocks.css', array(), (string) filemtime( $css ) ); } } }