'Header','footer'=>'Footer','single-post'=>'Single post','single-page'=>'Single page','single-product'=>'WooCommerce product','archive'=>'Blog and archives','search'=>'Search results','404'=>'404 page']; public function __construct(){ add_action('init',[$this,'register'],5); add_filter('digibl_public_dynamic_meta_keys',static function($keys,$type){return array_values(array_unique(array_merge((array)$keys,self::options()['publicMeta'][$type]??[])));},10,2); add_action('admin_menu',[$this,'menu']); add_action('admin_post_digibl_theme_save',[$this,'save']); add_action('admin_post_digibl_theme_create',[$this,'create']); add_action('template_redirect',[$this,'protect_template'],1); add_filter('template_include',[$this,'template'],100); add_action('wp_enqueue_scripts',[$this,'assets']); add_action('wp',[$this,'select']); add_filter('render_block_core/template-part',[$this,'block_part'],20,2); add_filter('wp_sitemaps_post_types',static function($types){unset($types['digibl_template']);return $types;}); } public function register(): void { $caps=array_fill_keys(['edit_post','read_post','delete_post','edit_posts','edit_others_posts','publish_posts','read_private_posts','delete_posts','delete_private_posts','delete_published_posts','delete_others_posts','edit_private_posts','edit_published_posts','create_posts'],'manage_options'); register_post_type('digibl_template',['label'=>'Digiter site templates','public'=>false,'publicly_queryable'=>true,'show_ui'=>false,'show_in_rest'=>false,'exclude_from_search'=>true,'rewrite'=>false,'query_var'=>false,'supports'=>['title','editor','revisions'],'capabilities'=>$caps,'map_meta_cap'=>false]); } public function menu(): void {add_submenu_page('digiter-builder',Language::text('Site templates'),Language::text('Site templates'),'manage_options','digibl-site-templates',[$this,'screen']);} public static function default_colors(): array {return ['accent'=>'#2667b8','ink'=>'#19324c','surface'=>'#ffffff','soft'=>'#edf5ff','border'=>'#d5e5f6','highlight'=>'#f4c95d'];} public static function view_areas(): array { return ['archive'=>'Blog / home','search'=>'Search results','404'=>'404 page']; } public static function area_label(string $area): string { return self::AREAS[$area]??ucwords(str_replace('-',' ',$area)); } public static function assigned_source(string $area): int { if(!array_key_exists($area,self::AREAS)){return 0;}$settings=self::options();$id=absint($settings['areas'][$area]['source']??0); return !empty($settings['enabled'])&&$id&&get_post_type($id)==='digibl_template'&&get_post_status($id)==='publish'&&get_post_meta($id,'_digibl_published',true)?$id:0; } /** True only for a published template configured as a live site region. */ public static function is_assigned_source(int $id): bool { if($id<1){return false;}$settings=self::options();if(empty($settings['enabled'])){return false;} foreach(array_keys(self::AREAS) as $area){if(absint($settings['areas'][$area]['source']??0)===$id){return true;}} return false; } public static function template_document(string $kind,string $name): array { $kind=Document::choice($kind,array_merge(array_keys(self::AREAS),['loop','section']),'section');$doc=Document::blank();$doc['title']=$name;$doc['settings']['canvas']=true; $node=static fn($id,$type,$props=[])=>['id'=>$id,'type'=>$type,'props'=>$props,'children'=>[]]; if(in_array($kind,['single-post','single-page','single-product'],true)){$doc['nodes']=[$node('templateTitle','dynamic-field',['source'=>'post-title']),$node('templateContent','post-content')];} elseif(in_array($kind,['archive','search'],true)){$doc['nodes']=[$node('archiveTitle','archive-info'),$node('archivePosts','posts',['source'=>'current-query']),$node('archivePages','pagination')];} elseif($kind==='header'){$doc['nodes']=[$node('siteName','dynamic-field',['source'=>'site-title']),$node('siteMenu','nav-menu')];} elseif($kind==='footer'){$doc['nodes']=[$node('siteName','dynamic-field',['source'=>'site-title']),$node('footerText','text',['content'=>'

© '.gmdate('Y').' '.esc_html(get_bloginfo('name')).'

'])];} elseif($kind==='loop'){$doc['nodes']=[$node('postImage','featured-image'),$node('postTitle','dynamic-field',['source'=>'post-title']),$node('postExcerpt','dynamic-field',['source'=>'post-excerpt'])];} else{$doc['nodes']=[$node('templateHeading','heading',['text'=>$kind==='404'?'Page not found':$name]),$node('templateText','text')];} return Document::clean($doc); } public static function prepare_view_template(string $area,string $name) { if(!isset(self::view_areas()[$area])){return new \WP_Error('digibl_view_area','This site view cannot be edited as a layout.',['status'=>400]);} $assigned=self::assigned_source($area);if($assigned){return $assigned;} $existing=get_posts(['post_type'=>'digibl_template','post_status'=>['draft','pending'],'author'=>get_current_user_id(),'numberposts'=>1,'orderby'=>'modified','order'=>'DESC','meta_query'=>[['key'=>'_digibl_pending_view_area','value'=>$area]]]); if($existing){return (int)$existing[0]->ID;} $doc=self::template_document($area,$name);if(is_wp_error($doc)){return $doc;} $id=wp_insert_post(wp_slash(['post_type'=>'digibl_template','post_status'=>'draft','post_title'=>$name,'post_author'=>get_current_user_id()]),true);if(is_wp_error($id)){return $id;} if(!add_post_meta($id,'_digibl_draft',wp_slash(wp_json_encode(['revision'=>wp_generate_uuid4(),'doc'=>$doc])),true)){wp_delete_post($id,true);return new \WP_Error('digibl_view_storage','The site-view draft could not be created.',['status'=>500]);} update_post_meta($id,'_digibl_saved_at',gmdate('Y-m-d H:i:s'));update_post_meta($id,'_digibl_template_kind',$area);update_post_meta($id,'_digibl_pending_view_area',$area);return $id; } public static function assign_view_template(string $area,int $id) { if(!isset(self::view_areas()[$area])){return new \WP_Error('digibl_view_area','This site view cannot be assigned.',['status'=>400]);} if(get_post_type($id)!=='digibl_template'||get_post_status($id)!=='publish'||!get_post_meta($id,'_digibl_published',true)){return new \WP_Error('digibl_view_template','Publish the Digiter site template before assigning it.',['status'=>409]);} $current=self::options();$next=$current;$next['enabled']=true;$next['areas'][$area]=['source'=>$id,'include'=>[],'exclude'=>[]]; $result=self::commit($next,(string)$current['revision']);if(!is_wp_error($result)){delete_post_meta($id,'_digibl_pending_view_area');}return $result; } public static function options(): array { $raw=get_option('digibl_site_templates',[]); $settings=wp_parse_args(is_array($raw)?$raw:[],['enabled'=>false,'areas'=>[],'accent'=>'','bodySize'=>0,'headingFont'=>'inherit','revision'=>'','publicMeta'=>[],'colors'=>self::default_colors(),'typography'=>Typography::defaults()]); $settings['colors']=wp_parse_args(is_array($settings['colors']??null)?$settings['colors']:[],self::default_colors()); foreach(self::default_colors() as $key=>$fallback){$settings['colors'][$key]=Document::color($settings['colors'][$key]??'')?:$fallback;} try{$settings['typography']=Typography::clean($settings['typography']);}catch(\InvalidArgumentException $e){$settings['typography']=Typography::defaults();} return $settings; } public static function colors(): array {return self::options()['colors'];} public function screen(): void { if(!current_user_can('manage_options')){return;}$settings=self::options();$templates=get_posts(['post_type'=>'digibl_template','post_status'=>['publish','draft'],'numberposts'=>100,'orderby'=>'title','order'=>'ASC']); echo ('

' . esc_html(Language::text('Digiter Site Templates')) . '

' . esc_html(Language::text('Create a template in Studio, publish it, then assign it below. Enabling site templates replaces the selected theme regions. Keep all areas at “Theme default” to retain the active theme.')) . '

'); if(isset($_GET['saved'])){echo ('

' . esc_html(Language::text('Site template settings saved.')) . '

');} if(!ThemeSupport::supported()){echo ('

' . esc_html(Language::text('The active theme has no dedicated header/footer adapter. Its original header and footer are retained. Page content editing remains available.')) . '

');} echo '
';wp_nonce_field('digibl_theme_save'); echo '

'); foreach(self::AREAS as $area=>$label){$rule=$settings['areas'][$area]??[];echo '';} echo ('
' . esc_html(Language::text('Region')) . '' . esc_html(Language::text('Published template')) . '' . esc_html(Language::text('Include post/page IDs (empty = all)')) . '' . esc_html(Language::text('Exclude post/page IDs')) . '
'.esc_html(Language::text($label)).'

' . esc_html(Language::text('Global design defaults')) . '

' . esc_html(Language::text('Applied to documents that have not set an explicit override. Existing palettes remain available.')) . '

'; echo ('

' . esc_html(Language::text('Linked global colours')) . '

' . esc_html(Language::text('Modules can link to these tokens. Changing a token updates every linked module without editing each one.')) . '

'); foreach(['accent'=>'Accent','ink'=>'Text / ink','surface'=>'Surface','soft'=>'Soft surface','border'=>'Border','highlight'=>'Highlight'] as $key=>$label){echo '';} echo '
--dbs-global-'.esc_html($key).'
'; $metaLines=[];foreach($settings['publicMeta'] as $type=>$keys){foreach($keys as $key){$metaLines[]=$type.':'.$key;}}echo ('

' . esc_html(Language::text('Public dynamic data')) . '

' . esc_html(Language::text('Approve custom fields for public rendering, including ACF repeaters. One post-type:field-key pair per line. Never approve private or sensitive data. Protected keys remain blocked.')) . '

'; submit_button(Language::text('Save site templates'));echo ('

' . esc_html(Language::text('Create a site template')) . '

';wp_nonce_field('digibl_theme_create');echo ('

' . esc_html(Language::text('Your templates')) . '

'; } private function authorize(string $action): void {if(!current_user_can('manage_options')){wp_die(esc_html(Language::text('Access denied.')),'',['response'=>403]);}check_admin_referer($action);} public function save(): void { $this->authorize('digibl_theme_save');$revision=sanitize_text_field(wp_unslash($_POST['revision']??'')); if(!empty($_POST['restore'])){$next=get_option('digibl_site_templates_previous',[]);if(!$next){wp_die(esc_html(Language::text('No previous settings are available.')));}} else{$raw=wp_unslash($_POST);$rawColors=is_array($raw['colors']??null)?$raw['colors']:[];$next=['enabled'=>!empty($raw['enabled']),'areas'=>[],'accent'=>Document::color($raw['accent']??''),'bodySize'=>Document::number($raw['bodySize']??0,0,32),'headingFont'=>Document::choice($raw['headingFont']??'', ['inherit','serif','sans-serif','monospace'],'inherit'),'colors'=>[]];foreach(self::default_colors() as $key=>$fallback){$next['colors'][$key]=Document::color($rawColors[$key]??'')?:$fallback;}foreach(self::AREAS as $area=>$label){$r=is_array($raw['areas'][$area]??null)?$raw['areas'][$area]:[];$id=absint($r['source']??0);if($id&&(get_post_type($id)!=='digibl_template'||get_post_status($id)!=='publish'||post_password_required($id)||!get_post_meta($id,'_digibl_published',true))){wp_die(esc_html(Language::text('Publish the selected template before assigning it.')));}$ids=static fn($v)=>array_slice(array_values(array_unique(array_filter(array_map('absint',explode(',',is_scalar($v)?(string)$v:''))))),0,100);$next['areas'][$area]=['source'=>$id,'include'=>$ids($r['include']??''),'exclude'=>$ids($r['exclude']??'')];}} if(empty($_POST['restore'])){$next['publicMeta']=[];foreach(array_slice(preg_split('/\r?\n/',sanitize_textarea_field(wp_unslash($_POST['publicMeta']??''))),0,100) as $line){$parts=explode(':',trim($line),2);if(count($parts)===2){[$type,$key]=array_map('trim',$parts);$object=get_post_type_object($type);if($object&&$object->public&&preg_match('/^[a-zA-Z][a-zA-Z0-9_-]{0,79}$/D',$key)&&!is_protected_meta($key,'post')){$next['publicMeta'][$type][]=$key;}}}} if(empty($_POST['restore'])){$next['typography']=self::options()['typography'];} $result=self::commit($next,$revision);if(is_wp_error($result)){wp_die(esc_html($result->get_error_message()),'',['response'=>(int)($result->get_error_data()['status']??500)]);} wp_safe_redirect(admin_url('admin.php?page=digibl-site-templates&saved=1'));exit; } /** Serializes publication + assignment and settings writers in the same request. */ public static function with_settings_lock(callable $callback) { if(self::$settings_locked){return $callback();} $key='digibl_theme_settings_lock';$lock=\Digiter\Diagnostics\ProtocolV1::lock($key,120); if(!$lock){return new \WP_Error('digibl_theme_busy','Site templates are being updated. Reload and try again.',['status'=>409]);} self::$settings_locked=true; try{return $callback();} finally{self::$settings_locked=false;\Digiter\Diagnostics\ProtocolV1::unlock($key,$lock);} } public static function commit(array $next,string $revision) { return self::with_settings_lock(static function()use($next,$revision){ wp_cache_delete('digibl_site_templates','options');$current=self::options(); if(!hash_equals((string)$current['revision'],$revision)){return new \WP_Error('digibl_theme_conflict','The settings changed in another session. Reload before saving.',['status'=>409]);} $next['revision']=wp_generate_uuid4();update_option('digibl_site_templates_previous',$current,false); if(!update_option('digibl_site_templates',$next,false)){return new \WP_Error('digibl_theme_storage','Site templates could not be saved.',['status'=>500]);} do_action('digibl_site_templates_changed',$next);return $next; }); } public function create(): void { $this->authorize('digibl_theme_create');$name=Document::text(wp_unslash($_POST['name']??''),100);$kind=Document::choice($_POST['kind']??'',array_merge(array_keys(self::AREAS),['loop','section']),'section'); if(!$name){wp_die(esc_html(Language::text('Enter a template name.')));}$doc=self::template_document($kind,$name);if(is_wp_error($doc)){wp_die(esc_html($doc->get_error_message()));}$id=wp_insert_post(wp_slash(['post_type'=>'digibl_template','post_status'=>'draft','post_title'=>$name,'post_author'=>get_current_user_id()]),true);if(is_wp_error($id)){wp_die(esc_html($id->get_error_message()));} update_post_meta($id,'_digibl_draft',wp_slash(wp_json_encode(['revision'=>wp_generate_uuid4(),'doc'=>$doc])));update_post_meta($id,'_digibl_saved_at',gmdate('Y-m-d H:i:s'));update_post_meta($id,'_digibl_template_kind',$kind); wp_safe_redirect((new PluginLinks())->url($id));exit; } public function protect_template(): void { if(!is_singular('digibl_template')){return;}nocache_headers();header('X-Robots-Tag: noindex, nofollow',true); if(!current_user_can('manage_options')){status_header(404);wp_die(esc_html(Language::text('Page not found.')),'',['response'=>404]);} } public function select(): void { self::$active=[];$options=self::options();if(!$options['enabled']||is_admin()||is_feed()||is_singular('digibl_template')||isset($_GET['digibl_builder'])){return;} $context=is_singular('product')?'single-product':(is_singular('post')?'single-post':(is_singular('page')?'single-page':(is_search()?'search':(is_404()?'404':((is_archive()||is_home())?'archive':''))))); foreach(['header','footer',isset($_GET['digibl_frame'])?'':$context] as $area){$r=$options['areas'][$area]??[];$source=(int)($r['source']??0);$id=is_singular()?get_queried_object_id():0;if(!$source||!get_post_meta($source,'_digibl_published',true)||get_post_status($source)!=='publish'||get_post_type($source)!=='digibl_template'||post_password_required($source)||(!$id&&!empty($r['include']))||($id&&!empty($r['include'])&&!in_array($id,$r['include'],true))||($id&&in_array($id,$r['exclude']??[],true))){continue;}self::$active[$area]=$source;} if(self::$active){if(!defined('DONOTCACHEPAGE')){define('DONOTCACHEPAGE',true);}nocache_headers();} } public function template($path){$main=array_diff_key(self::$active,['header'=>true,'footer'=>true]);return $main?DIGIBL_PATH.'templates/site.php':$path;} public function block_part($content,$block){$area=$block['attrs']['area']??($block['attrs']['slug']??'');if(in_array($area,['header','footer'],true)&&isset(self::$active[$area])){return '<'.$area.' class="dbs-site-'.$area.'">'.self::render(self::$active[$area],is_singular()?get_queried_object_id():0,$area).'';}return $content;} public function assets(): void {if(self::$active){Assets::styles();}} public static function render(int $source,int $context=0,string $area='main'): string {$doc=Document::clean(json_decode(get_post_meta($source,'_digibl_published',true),true));if(is_wp_error($doc)){return '';}$prefix='t'.substr(hash('sha256',$source.'|'.$area),0,10);$doc['nodes']=Document::scope_nodes($doc['nodes'],$prefix);return (new Renderer(false,$context,false,$source))->render($doc);} } /** No hook registration is needed when a manager links to the existing editor. */ final class PluginLinks { public function url(int $id): string {return add_query_arg(['digibl_builder'=>$id,'_digibl_nonce'=>wp_create_nonce('digibl_editor_'.$id)],get_permalink($id));} }