'multipify-seo','title'=>'Multipify SEO','icon'=>null]); return $cats; } static function register(){ if(!self::enabled()||!function_exists('register_block_type'))return; wp_register_script('mpseo-blocks',plugins_url('assets/blocks-v53.js',MPSEO_FILE),['wp-blocks','wp-element','wp-components','wp-block-editor','wp-server-side-render','wp-i18n'],MPSEO_VERSION,true); wp_localize_script('mpseo-blocks','MPSEOBlocks',['l'=>[ 'faq'=>__('FAQ (Multipify)','multipify-seo-boost'),'faqDesc'=>__('Questions and answers published as FAQPage structured data.','multipify-seo-boost'), 'question'=>__('Question','multipify-seo-boost'),'answer'=>__('Answer','multipify-seo-boost'),'addQuestion'=>__('Add a question','multipify-seo-boost'), 'remove'=>__('Remove','multipify-seo-boost'),'up'=>__('Move up','multipify-seo-boost'),'down'=>__('Move down','multipify-seo-boost'), 'howto'=>__('How-to (Multipify)','multipify-seo-boost'),'howtoDesc'=>__('Numbered steps published as HowTo structured data.','multipify-seo-boost'), 'step'=>__('Step','multipify-seo-boost'),'stepName'=>__('Step title','multipify-seo-boost'),'stepText'=>__('Instructions','multipify-seo-boost'),'addStep'=>__('Add a step','multipify-seo-boost'), 'totalTime'=>__('Total time (minutes)','multipify-seo-boost'),'intro'=>__('Introduction (optional)','multipify-seo-boost'), 'toc'=>__('Table of contents (Multipify)','multipify-seo-boost'),'tocDesc'=>__('Automatic list of the H2 and H3 headings of the content.','multipify-seo-boost'),'tocPreview'=>__('The table of contents is generated from your H2/H3 headings when the page is displayed.','multipify-seo-boost'), 'title'=>__('Title','multipify-seo-boost'), 'crumbs'=>__('Breadcrumbs (Multipify)','multipify-seo-boost'),'crumbsDesc'=>__('Breadcrumb trail of the current page.','multipify-seo-boost'), 'sitemap'=>__('HTML sitemap (Multipify)','multipify-seo-boost'),'sitemapDesc'=>__('List of your published pages, posts and categories.','multipify-seo-boost'), 'open'=>__('Show answers open','multipify-seo-boost'),'settings'=>__('Settings','multipify-seo-boost'), ]]); $common=['editor_script'=>'mpseo-blocks','category'=>'multipify-seo']; register_block_type('multipify/faq',$common+['render_callback'=>[__CLASS__,'render_faq'],'attributes'=>['items'=>['type'=>'array','default'=>[]],'open'=>['type'=>'boolean','default'=>false]]]); register_block_type('multipify/howto',$common+['render_callback'=>[__CLASS__,'render_howto'],'attributes'=>['steps'=>['type'=>'array','default'=>[]],'intro'=>['type'=>'string','default'=>''],'totalTime'=>['type'=>'number','default'=>0]]]); register_block_type('multipify/toc',$common+['render_callback'=>[__CLASS__,'render_toc'],'attributes'=>['title'=>['type'=>'string','default'=>'']]]); register_block_type('multipify/breadcrumbs',$common+['render_callback'=>[__CLASS__,'render_crumbs']]); register_block_type('multipify/sitemap',$common+['render_callback'=>[__CLASS__,'render_sitemap']]); } /** Allowed markup inside answers and steps (simple formatting only). */ static function kses($html){return wp_kses((string)$html,['a'=>['href'=>true,'rel'=>true,'target'=>true],'strong'=>[],'b'=>[],'em'=>[],'i'=>[],'br'=>[],'code'=>[],'ul'=>[],'ol'=>[],'li'=>[]]);} static function clean_items($items,$a,$b){ $out=[];foreach((array)$items as $it){if(!is_array($it))continue;$x=trim(wp_strip_all_tags((string)($it[$a]??'')));$y=trim((string)($it[$b]??''));if($x!==''&&trim(wp_strip_all_tags($y))!=='')$out[]=[$x,$y];} return $out; } static function render_faq(...$args){return wp_kses_post((string)self::render_faq_html(...$args));} static function render_faq_html($attr){ $items=self::clean_items($attr['items']??[],'q','a');if(!$items)return ''; $open=!empty($attr['open']); $o='
'; foreach($items as $it)$o.='
'.esc_html($it[0]).'
'.wpautop(self::kses($it[1])).'
'; return $o.'
'; } static function render_howto(...$args){return wp_kses_post((string)self::render_howto_html(...$args));} static function render_howto_html($attr){ $steps=self::clean_items($attr['steps']??[],'name','text');if(!$steps)return ''; $o='
'; if(!empty($attr['intro']))$o.='

'.self::kses($attr['intro']).'

'; $min=(int)($attr['totalTime']??0); /* translators: %d: number of minutes. */ if($min>0)$o.='

'.esc_html(sprintf(__('Total time: %d min','multipify-seo-boost'),$min)).'

'; $o.='
    '; foreach($steps as $i=>$s)$o.='
  1. '.esc_html($s[0]).'
    '.wpautop(self::kses($s[1])).'
  2. '; return $o.'
'; } static function render_toc(...$args){return wp_kses_post((string)self::render_toc_html(...$args));} static function render_toc_html($attr){ if(class_exists('MPSEO_V52_Options')&&!MPSEO_V52_Options::on('toc_enabled'))return ''; // The placeholder is replaced by MPSEO_V52_Options::content() with the headings of the page. return ''; } static function render_crumbs(...$args){return wp_kses_post((string)self::render_crumbs_html(...$args));} static function render_crumbs_html(){return class_exists('MPSEO_Core')?MPSEO_Core::breadcrumbs():'';} static function render_sitemap(...$args){return wp_kses_post((string)self::render_sitemap_html(...$args));} static function render_sitemap_html(){return class_exists('MPSEO_Index_Controls')&&method_exists('MPSEO_Index_Controls','html_sitemap')?MPSEO_Index_Controls::html_sitemap([]):do_shortcode('[multipify_html_sitemap]');} /* ===================== Structured data ===================== */ /** FAQ and How-to blocks found in a post. */ static function blocks_of($post,$name){ if(!($post instanceof WP_Post)||!has_block('multipify/'.$name,$post))return []; $found=[]; $walk=function($blocks)use(&$walk,&$found,$name){foreach($blocks as $b){if(($b['blockName']??'')==='multipify/'.$name)$found[]=$b['attrs']??[];if(!empty($b['innerBlocks']))$walk($b['innerBlocks']);}}; $walk(parse_blocks($post->post_content)); return $found; } /** Questions found in headings ending with "?" followed by a paragraph. */ static function auto_faq($post){ if(!preg_match_all('~]*>(.*?)\s*(?:\s*)*]*>(.*?)

~is',(string)$post->post_content,$m,PREG_SET_ORDER))return []; $out=[];foreach($m as $x){$q=trim(wp_strip_all_tags($x[1]));if($q!==''&&substr($q,-1)==='?')$out[]=[$q,trim(wp_strip_all_tags($x[2]))];} return array_slice($out,0,30); } static function schema($graph){ if(!is_singular()||!is_array($graph))return $graph; $post=get_queried_object();if(!($post instanceof WP_Post))return $graph; $url=get_permalink($post); $faq=[];foreach(self::blocks_of($post,'faq') as $a)$faq=array_merge($faq,self::clean_items($a['items']??[],'q','a')); $type=get_post_meta($post->ID,MPSEO_Meta::S,true); if(!$faq&&$type==='FAQPage'&&(!class_exists('MPSEO_V52_Options')||MPSEO_V52_Options::on('schema_faq_auto')))$faq=self::auto_faq($post); $howto=[];$howto_attr=[];foreach(self::blocks_of($post,'howto') as $a){$howto=array_merge($howto,self::clean_items($a['steps']??[],'name','text'));$howto_attr=$howto_attr?:$a;} $faq_done=false;$howto_done=false; foreach($graph as &$node){ if(!is_array($node)||($node['@id']??'')!==$url.'#primary')continue; if(($node['@type']??'')==='FAQPage'&&$faq){$node['mainEntity']=self::faq_entities($faq);$faq_done=true;} if(($node['@type']??'')==='HowTo'&&$howto){$node=array_merge($node,self::howto_props($howto,$howto_attr,$url));$howto_done=true;} } unset($node); // A page of another type can still contain an FAQ or a how-to: they become their own nodes. if($faq&&!$faq_done)$graph[]=['@type'=>'FAQPage','@id'=>$url.'#faq','url'=>$url,'mainEntity'=>self::faq_entities($faq)]; if($howto&&!$howto_done)$graph[]=array_merge(['@type'=>'HowTo','@id'=>$url.'#howto','name'=>get_the_title($post),'url'=>$url],self::howto_props($howto,$howto_attr,$url)); return $graph; } static function faq_entities($faq){ $o=[];foreach($faq as $f)$o[]=['@type'=>'Question','name'=>$f[0],'acceptedAnswer'=>['@type'=>'Answer','text'=>trim(wp_strip_all_tags($f[1]))]]; return $o; } static function howto_props($steps,$attr,$url){ $p=['step'=>[]]; foreach($steps as $i=>$s)$p['step'][]=['@type'=>'HowToStep','position'=>$i+1,'name'=>$s[0],'text'=>trim(wp_strip_all_tags($s[1])),'url'=>$url.'#step-'.($i+1)]; $min=(int)($attr['totalTime']??0);if($min>0)$p['totalTime']='PT'.$min.'M'; return $p; } } MPSEO_V53_Blocks::init();