Changeset 3789687
- Timestamp:
- 11/07/2026 01:53:21 PM (100 minutes ago)
- Location:
- klusio-builder-bridge
- Files:
-
- 4 added
- 2 edited
-
tags/1.17.0 (added)
-
tags/1.17.0/klusio-builder-bridge.php (added)
-
tags/1.17.0/readme.txt (added)
-
tags/1.17.0/uninstall.php (added)
-
trunk/klusio-builder-bridge.php (modified) (13 diffs)
-
trunk/readme.txt (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
klusio-builder-bridge/trunk/klusio-builder-bridge.php
r3789598 r3789687 3 3 * Plugin Name: Klusio 4 4 * Description: Lets Klusio update texts on your Elementor and Bricks pages after your approval, with a backup of every change and a restore option. Works as a regular plugin and as a must-use plugin. 5 * Version: 1.1 6.05 * Version: 1.17.0 6 6 * Requires at least: 5.6 7 7 * Requires PHP: 7.2 … … 72 72 return; 73 73 } 74 define('KLUSIO_BRIDGE_VERSION', '1.1 6.0');74 define('KLUSIO_BRIDGE_VERSION', '1.17.0'); 75 75 define('KLUSIO_BRIDGE_NAAM', 'klusio-builder-bridge'); 76 76 define('KLUSIO_BRIDGE_BACKUP_PREFIX', '_klusio_el_backup_'); … … 3909 3909 function klusio_bridge_workspace_link($section = 'overview', $id = 0, $page_url = '', $kind = '') 3910 3910 { 3911 $allowed = array('overview','growth','drafts','connection','setup','preferences','notifications','permissions','links','results','improve',' google','ai','opportunity','draft','page_draft');3911 $allowed = array('overview','growth','drafts','connection','setup','preferences','notifications','permissions','links','results','improve','review','google','ai','opportunity','draft','page_draft'); 3912 3912 if (!in_array($section, $allowed, true)) { 3913 3913 $section = 'overview'; … … 4189 4189 echo '<section class="kbw-card"><span class="kbw-status">' . esc_html($check['source']) . '</span><h2>' . esc_html(get_the_title($post) ?: __('Zonder titel', 'klusio-builder-bridge')) . '</h2><p class="kbw-small">' . esc_html__('Bron: opgeslagen WordPress-inhoud, gecontroleerd bij het openen.', 'klusio-builder-bridge') . ' ' . esc_html__('Laatst gewijzigd:', 'klusio-builder-bridge') . ' ' . esc_html(get_the_modified_date('j M Y H:i', $post)) . '</p>'; 4190 4190 klusio_bridge_workspace_local_buttons($post); 4191 echo '</section><d iv class="kbw-grid"><section class="kbw-card"><h2>' . esc_html__('Dit kun je nu nalopen', 'klusio-builder-bridge') . '</h2>';4191 echo '</section><details class="kbw-card kv-stored"><summary>'.esc_html__('Opgeslagen tekst en koppen nalopen','klusio-builder-bridge').'</summary><div class="kbw-grid"><section class="kbw-card"><h2>' . esc_html__('Dit kun je nu nalopen', 'klusio-builder-bridge') . '</h2>'; 4192 4192 $tips = array(); 4193 4193 if ($check['empty_title']) { $tips[] = __('De WordPress-titel is leeg. Geef de pagina een herkenbare naam.', 'klusio-builder-bridge'); } … … 4206 4206 foreach ($check['headings'] as $heading) { echo '<li><span>H' . esc_html((string)$heading['level']) . '</span> ' . esc_html($heading['text']) . '</li>'; } 4207 4207 echo '</ol><p class="kbw-small">' . esc_html__('Een paginatitel uit je thema of builder kan buiten deze tekst staan. Deze lijst is geen beoordeling van de complete openbare pagina.', 'klusio-builder-bridge') . '</p></section></div>'; 4208 echo '</details>'; 4209 klusio_bridge_value_mount($post); 4210 if ($post->post_status === 'publish' && $data) echo '<p><a class="kbw-btn" target="_blank" rel="noopener" href="'.esc_url(klusio_bridge_workspace_link('review',0,get_permalink($post))).'">'.esc_html__('Dezelfde paginastand in Klusio','klusio-builder-bridge').' ↗</a></p>'; 4208 4211 klusio_bridge_native_page((string)get_permalink($post)); 4212 if (current_user_can('manage_options') && klusio_bridge_native_connection()) { return; } 4209 4213 $related = array(); 4210 4214 foreach (array('next','doing','done') as $group) { … … 4302 4306 return; 4303 4307 } 4308 if ($view === 'overview') klusio_bridge_value_start($d); 4304 4309 if (klusio_bridge_native_render($view, $d)) { 4305 4310 // The native workspace is hydrated from the current canonical account state. … … 4494 4499 if(!$c||(int)($s['sender']??0)!==get_current_user_id())return new WP_Error('klusio_native_sender',__('Verbinding niet beschikbaar.', 'klusio-builder-bridge'),array('status'=>403)); 4495 4500 $u=get_user_by('id',(int)$request['id']); 4496 return new WP_REST_Response(array(' grant_id'=>(int)$c['grant_id'],'administrator'=>$u&&user_can($u,'manage_options'),'publish'=>$u&&user_can($u,'publish_posts')&&user_can($u,'publish_pages')));4501 return new WP_REST_Response(array('credential_restricted'=>!empty($GLOBALS['klusio_editorial_identity']),'grant_id'=>(int)$c['grant_id'],'administrator'=>$u&&user_can($u,'manage_options'),'publish'=>$u&&user_can($u,'publish_posts')&&user_can($u,'publish_pages'))); 4497 4502 } 4498 4503 add_action('rest_api_init',function(){ … … 4546 4551 echo '<label><input type="checkbox" name="enabled" value="1" '.checked(get_option('klusio_native_enabled',false),true,false).'> '.esc_html__('Ik wil deze website met de Klusio-werkverbinding verbinden.','klusio-builder-bridge').'</label><p class="kbw-small">'.esc_html__('Bij gebruik worden de gekozen opdrachten, artikelteksten, afbeeldingen, instellingen en je WordPress-gebruikersnummer beveiligd met Klusio uitgewisseld. Alleen beheerders kunnen opdrachten starten.','klusio-builder-bridge').'</p><button class="kbw-btn" type="submit">'.esc_html__('Websitekeuze opslaan','klusio-builder-bridge').'</button></form>'; 4547 4552 if(get_option('klusio_native_enabled',false))echo '<p><a class="kbw-btn kbw-primary" target="_blank" rel="noopener noreferrer" href="'.esc_url(add_query_arg('site',site_url(),'https://klusio.nl/account/wordpress-native-connect.php')).'">'.esc_html($c?__('Accounttoestemming bekijken of intrekken','klusio-builder-bridge'):__('Rechten bevestigen in mijn Klusio-account','klusio-builder-bridge')).' ↗</a></p>'; 4553 echo '<div id="kv-health"><button type="button" class="kbw-btn">'.esc_html__('Verbinding en toegang nu controleren','klusio-builder-bridge').'</button><div role="status"></div></div>'; 4548 4554 if($c)echo '<p class="kbw-small">'.esc_html__('Verbonden tot','klusio-builder-bridge').' '.esc_html(wp_date('j F Y',(int)$c['expires_at'])).'.</p>'; 4549 4555 echo '</section>'; … … 4625 4631 } 4626 4632 root.append(preview(d.content||'',d.title)); 4633 if(d.editable&&cfg.scopes.includes('drafts'))root.append(button('Bekijk in mijn echte websiteopmaak',()=>{if(dirty){status('Sla eerst op zodat je de actuele opgeslagen versie controleert.');return;}if(window.klusioThemePreview)window.klusioThemePreview(d,root);})); 4627 4634 if(d.editable&&cfg.scopes.includes('publish')){const box=card('Publicatie controleren','Bij publiceren krijgt een nieuw artikel de werkelijke publicatiedatum. De oorspronkelijke planningsdatum blijft apart bewaard. Een bestaande live publicatie wordt hiermee niet overschreven.'); 4628 4635 box.append(el('p','Geplande datum: '+(d.intended_publish_at||'niet ingesteld'),'kn-muted'));const t=check('Ik heb de opgeslagen tekst, feiten, foto en alt-tekst gecontroleerd en wil deze versie nu publiceren.');const publish=command('Nu publiceren','draft_publish',{id:Number(d.id),revision:d.revision,publication_sha256:d.publication_sha256,checked:true},true);publish.dataset.publishButton='1';t.input.dataset.publishCheck='1';publish.disabled=true;t.input.onchange=()=>{publish.disabled=!t.input.checked||dirty;if(dirty)status('Sla je wijzigingen eerst op en controleer daarna de opgeslagen versie.');};box.append(t.label,publish);root.append(box);} … … 4634 4641 const s=card(b.title||'Je conceptpagina',b.message);s.append(el('p','Status: '+b.state,'kbw-status'));if(b.edit_url)s.append(link('Bewerken in je WordPress-editor →',b.edit_url));if(b.preview_url){const a=link('Bekijk de echte paginaopmaak ↗',b.preview_url);a.target='_blank';a.rel='noopener';const para=el('p');para.append(a);s.append(para);const f=document.createElement('iframe');try{const u=new URL(b.preview_url);if(u.origin===location.origin){f.src=u.href;f.title='WordPress-paginavoorbeeld';f.className='kn-preview desktop';const tools=el('div',undefined,'kn-toolbar');tools.append(button('Desktop',()=>f.className='kn-preview desktop'),button('Mobiel',()=>f.className='kn-preview mobile'));s.append(tools,f);}}catch(e){}} 4635 4642 if(['queued','building','publishing','attention'].includes(b.state))s.append(button('Actuele stand ophalen',()=>load().catch(e=>status(e.message))));if(['draft','reviewed'].includes(b.state)&&cfg.scopes.includes('publish')){const t=check('Ik heb de tekst, knoppen en de echte paginaopmaak op desktop en mobiel gecontroleerd.');const review=command('Deze versie vastleggen voor publicatie','page_review',{id:Number(root.dataset.id),checked:true},true);review.disabled=true;t.input.onchange=()=>review.disabled=!t.input.checked;s.append(t.label,review);if(b.state==='reviewed'&&b.review_token)s.append(command('Gecontroleerde pagina publiceren','page_publish',{id:Number(root.dataset.id),review_token:b.review_token},true));}if(b.live_url)s.append(link('Bekijk de live pagina →',b.live_url));root.append(s);} 4636 function pageEvidence(d,host){const s=card('Klusio bij deze pagina','Bestaand onderzoek, lopend werk en gemeten resultaten voor deze URL. Zo voorkom je dat dezelfde verbetering dubbel wordt gestart.');for(const o of d.opportunities||[]){const p=el('p');p.append(link(o.title,local('opportunity',o.id)),el('span',' · '+o.status,'kn-muted'));s.append(p);}if(!d.opportunities?.length)s.append(el('p','Er is voor deze URL nog geen voorstel in de huidige selectie.')); 4637 for(const c of d.changes||[]){const box=card(c.status_label,c.message);for(const f of c.fields||[]){const diff=el('div',undefined,'kn-diff');diff.append(el('div','Nu: '+f.old),el('div','Voorstel: '+f.new));box.append(diff);}if(c.opportunity_id)box.append(link('Voorstel en herstel bekijken →',local('opportunity',c.opportunity_id)));s.append(box);} 4638 const measurements=d.measurements;if(measurements?.changes?.length){s.append(el('h3','Google-resultaten bij deze wijziging'),el('p','Bron: '+measurements.source+'. Beschikbaar tot '+(measurements.gsc_end||'nog onbekend')+'. Verschillen bewijzen op zichzelf geen oorzaak.','kn-muted'));for(const m of measurements.changes){const item=el('p',m.label||m.source_label||'Paginawijziging');s.append(item);for(const v of m.milestones||[]){ 4639 if(v.state==='measured'&&v.page){const table=el('table',undefined,'kn-table');const cap=el('caption','Na '+v.milestone+' dagen · '+m.changed_on);table.append(cap);const head=el('tr');head.append(el('th','Google Search Console'),el('th',(v.windows?.before||[]).join(' t/m ')),el('th',(v.windows?.after||[]).join(' t/m ')));table.append(head);for(const [k,label]of Object.entries({impressions:'Vertoningen',clicks:'Klikken',position:'Gemiddelde positie'})){const tr=el('tr');tr.append(el('th',label),el('td',v.page.before?.[k]??'Onbekend'),el('td',v.page.after?.[k]??'Onbekend'));table.append(tr);}s.append(table);if(v.low_data)s.append(el('p','De voormeting bevat weinig vertoningen. Trek nog geen stevige conclusie uit dit verschil.','kn-muted'));} 4640 else s.append(el('p',v.state==='pending'?'Meting na '+v.milestone+' dagen verwacht op '+v.expected_on:'Dit meetmoment is nog niet beschikbaar.','kn-muted'));} 4641 if(m.later_changes?.length)s.append(el('p','Ook gewijzigd op '+m.later_changes.join(', ')+'. Het verschil kan bij meerdere aanpassingen horen.','kn-muted'));}}else s.append(el('p','Er zijn nog geen beschikbare voor- en nametingen voor deze pagina.','kn-muted'));if(d.ai){const ai=card('Deze pagina in AI-antwoorden','Bron: '+d.ai.source+' · '+d.ai.from+' t/m '+d.ai.to+'. Per vraag en AI-systeem zie je de laatste beschikbare meting binnen deze selectie.'); 4642 const labels={chatgpt:'ChatGPT',claude:'Claude',gemini:'Gemini',perplexity:'Perplexity',google_ai_mode:'Google AI-modus'}; 4643 for(const r of d.ai.rows||[]){const row=card(r.question,(labels[r.platform]||r.platform)+' · '+r.checked_at+(r.fresh?'':' · oudere meting'));const chips=el('div',undefined,'kn-actions');chips.append(el('span','Gevonden: '+(r.found===null?'onbekend':r.found?'ja':'niet aangetoond'),'kbw-status'),el('span','Pagina als bron: '+(r.cited?'ja':'nee'),'kbw-status'),el('span','Bedrijf genoemd: '+(r.named?'ja':'nee'),'kbw-status'));row.append(chips);const details=el('details'),summary=el('summary','Antwoordfragment bekijken');details.append(summary,el('p',r.answer));row.append(details);ai.append(row);} 4644 if(!d.ai.rows?.length)ai.append(el('p',d.ai.available?'In deze selectie is geen controleerbare vondst of bronverwijzing naar deze pagina gevonden. Dat zegt niets over alle mogelijke AI-antwoorden.':'De AI-metingen zijn nu niet beschikbaar.')); 4645 ai.append(el('p','Gevonden betekent dat de URL in opgehaalde zoekresultaten stond. Een bronverwijzing bewijst geen aanbeveling; een bedrijfsvermelding geldt voor het antwoord en is geen bewezen gevolg van deze pagina.','kn-muted'));if(d.ai.limited)ai.append(el('p','Selectie beperkt tot de 1.000 recentste metingen.','kn-muted'));s.append(ai);}host.append(s);} 4643 function pageEvidence(d,host){if(window.klusioRenderPageStory){window.klusioRenderPageStory(d,host,id=>local('opportunity',id));}else host.append(card('Paginastand ophalen','Vernieuw dit scherm om de actuele paginastand te bekijken.'));} 4646 4644 function results(d){root.append(card('Wat heeft Klusio gedaan?','Je ziet alleen werk dat live is gecontroleerd. Resultaten houden hun bron en meetperiode; samenloop met andere wijzigingen blijft belangrijk.'));const items=d.snapshot?.done||[];for(const r of items){const s=card(r.title,r.detail);s.append(el('p',r.date,'kn-muted'));if(r.measurement)s.append(el('p',r.measurement));s.append(link('Wijziging en bewijs bekijken →',local(r.kind,r.id)));if(r.page_url)s.append(button('Paginametingen tonen',async()=>{try{const e=await api({action:'read',view:'page',url:r.page_url});pageEvidence(e,s);}catch(e){status(e.message);}}));root.append(s);}if(!items.length)root.append(card('Nog geen live gecontroleerd werk','Nieuwe resultaten verschijnen zodra de bestaande Groeimotor ze heeft geverifieerd.'));} 4647 4645 async function load(){if(!root)return;root.querySelectorAll('textarea.kn-body').forEach(n=>{if(window.wp?.editor?.remove)wp.editor.remove(n.id);});const view=root.dataset.view,kind=root.dataset.kind,id=Number(root.dataset.id);let v=view==='results'?'overview':view;if(view==='detail')v=kind==='customer_task'?'opportunity':kind;const d=await api({action:'read',view:v,id});root.replaceChildren();dirty=false;if(v==='draft')draft(d);else if(v==='opportunity')opportunity(d);else if(v==='page_draft')pageDraft(d);else if(view==='settings')settings(d);else if(view==='history')history(d);else if(view==='results')results(d);else root.append(card('Open het actuele onderdeel','Ga via het menu naar je voorstellen of concepten.'));} … … 4739 4737 $new = WP_Application_Passwords::create_new_application_password(get_current_user_id(), array('name' => 'Klusio ' . gmdate('Y-m-d H:i') . ' ' . substr($f['id'], 0, 8), 'app_id' => wp_generate_uuid4())); 4740 4738 if (is_wp_error($new)) return new WP_Error('klusio_apps', __('WordPress kon de aparte toegang niet aanmaken.', 'klusio-builder-bridge')); 4739 if (!klusio_bridge_value_restrict(get_current_user_id(), $new[1]['uuid'])) { WP_Application_Passwords::delete_application_password(get_current_user_id(), $new[1]['uuid']); return new WP_Error('klusio_scope_store', __('De beperkte toegang kon niet worden opgeslagen. Er is niets naar Klusio verstuurd.', 'klusio-builder-bridge')); } 4741 4740 $f['app_uuid'] = $new[1]['uuid']; $f['state'] = 'checking'; 4742 4741 if (!klusio_bridge_connect_save($f)) { … … 4806 4805 $labels = array('read' => __('Voorstellen, concepten en resultaten lezen', 'klusio-builder-bridge'), 'decisions' => __('Voorstellen beoordelen en feedback geven', 'klusio-builder-bridge'), 'drafts' => __('Concepten maken en bewerken', 'klusio-builder-bridge'), 'publish' => __('Gecontroleerd publiceren en herstellen', 'klusio-builder-bridge'), 'settings' => __('Meldingen en werkwijze aanpassen', 'klusio-builder-bridge')); 4807 4806 foreach ($f['scopes'] as $scope) echo '<li>' . esc_html($labels[$scope]) . '</li>'; 4808 echo '</ul><p>' . esc_html__('WordPress maakt een apart applicatiewachtwoord met jouw bestaande WordPress-rechten en stuurt dat via HTTPS naar Klusio. Dat geeft Klusio technische toegang tot je website. De bovenstaande keuzes bepalen wat WordPress-beheerders vanuit deze plugin in je Klusio-account mogen doen.', 'klusio-builder-bridge') . '</p><p>' . esc_html__('Je pagina’s, vormgeving en publicatiekeuzes blijven behouden. Voor een nieuwe koppeling staat automatisch doorsturen uit. Intrekken kan via Gebruikers → Profiel → Applicatiewachtwoorden.', 'klusio-builder-bridge') . '</p>';4807 echo '</ul><p>' . esc_html__('WordPress maakt een aparte Klusio-toegang en stuurt die via HTTPS naar Klusio. De plugin beperkt die tot website-inhoud en de Klusio-koppeling. Gebruikers, pluginbeheer en algemene instellingen zijn uitgesloten zolang deze plugin actief is. De bovenstaande keuzes bepalen wat WordPress-beheerders vanuit deze plugin in je Klusio-account mogen doen.', 'klusio-builder-bridge') . '</p><p>' . esc_html__('Je pagina’s, vormgeving en publicatiekeuzes blijven behouden. Voor een nieuwe koppeling staat automatisch doorsturen uit. Intrekken kan via Gebruikers → Profiel → Applicatiewachtwoorden.', 'klusio-builder-bridge') . '</p>'; 4809 4808 klusio_bridge_connect_button('approve', __('Bevestigen en verbinden', 'klusio-builder-bridge'), true); 4810 4809 } elseif (($f['state'] ?? '') === 'ready') { 4811 echo '<h3>' . esc_html__('Je website is verbonden', 'klusio-builder-bridge') . '</h3><p>' . esc_html__(' De toegang is gecontroleerd en je Klusio-overzicht is opgehaald. Je kunt nu op beide plekken met dezelfde voorstellen en concepten werken.', 'klusio-builder-bridge') . '</p><a class="kbw-btn kbw-primary" href="' . esc_url(admin_url('admin.php?page=klusio')) . '">' . esc_html__('Open mijn overzicht', 'klusio-builder-bridge') . '</a><p><a href="' . esc_url(klusio_bridge_connect_link()) . '">' . esc_html__('Opnieuw verbinden of rechten kiezen', 'klusio-builder-bridge') . ' →</a></p>';4810 echo '<h3>' . esc_html__('Je website is verbonden', 'klusio-builder-bridge') . '</h3><p>' . esc_html__('Bij het verbinden is de toegang gecontroleerd en je overzicht opgehaald. Controleer hieronder de actuele verbinding of kies in Overzicht je eerste pagina.', 'klusio-builder-bridge') . '</p><a class="kbw-btn kbw-primary" href="' . esc_url(admin_url('admin.php?page=klusio')) . '">' . esc_html__('Open mijn overzicht', 'klusio-builder-bridge') . '</a><p><a href="' . esc_url(klusio_bridge_connect_link()) . '">' . esc_html__('Opnieuw verbinden of rechten kiezen', 'klusio-builder-bridge') . ' →</a></p>'; 4812 4811 } else { 4813 4812 echo '<p>' . esc_html__('Controleer de laatste bevestiging. De plugin maakt bij deze controle geen nieuwe toegang aan.', 'klusio-builder-bridge') . '</p>'; … … 4820 4819 wp_add_inline_script('klusio-connect-capture', "(function(){var m=/^#kc=([a-f0-9]{32}\\.[a-f0-9]{64})$/.exec(location.hash);if(!m)return;var f=document.getElementById('klusio-connect-capture');history.replaceState(null,'',location.pathname+location.search);f.elements.ticket.value=m[1];f.submit();})();"); 4821 4820 } 4821 4822 /** 1.17: useful first steps, theme previews and bounded rendered-page evidence. */ 4823 function klusio_bridge_value_request($in) { 4824 $c = klusio_bridge_native_connection(); 4825 if (!$c) return new WP_Error('klusio_connection', __('Bevestig je werkverbinding via Klusio → Verbinding.', 'klusio-builder-bridge')); 4826 $in['site'] = $c['site']; $in['wp_user_id'] = get_current_user_id(); 4827 $r = wp_remote_post('https://klusio.nl/account/api/wordpress-native.php', array('timeout'=>25,'redirection'=>0,'sslverify'=>true,'limit_response_size'=>2097152,'headers'=>array('Authorization'=>'Klusio '.$c['grant_id'].'.'.$c['token'],'Content-Type'=>'application/json'),'body'=>wp_json_encode($in))); 4828 if (is_wp_error($r)) return new WP_Error('klusio_unavailable', __('De actuele accountstand is niet bereikbaar. Probeer het later opnieuw.', 'klusio-builder-bridge')); 4829 $d = json_decode(wp_remote_retrieve_body($r), true); 4830 if (wp_remote_retrieve_response_code($r) !== 200 || empty($d['success']) || !is_array($d['data'] ?? null)) return new WP_Error('klusio_account', isset($d['error']) ? sanitize_text_field($d['error']) : __('Je verbinding kon niet worden bevestigd.', 'klusio-builder-bridge')); 4831 return $d['data']; 4832 } 4833 function klusio_bridge_value_revision($post) { 4834 return hash('sha256', wp_json_encode(array($post->post_title,$post->post_content,$post->post_excerpt,$post->post_status,$post->post_modified_gmt,get_post_meta($post->ID,'_elementor_data',true),get_post_meta($post->ID,'_bricks_page_content_2',true),get_post_thumbnail_id($post->ID),get_post_meta($post->ID,'_wp_page_template',true)))); 4835 } 4836 function klusio_bridge_value_url($post) { 4837 $url = $post->post_status === 'publish' ? get_permalink($post) : get_preview_post_link($post); 4838 return add_query_arg(array('kl_review'=>$post->ID,'kl_check'=>wp_create_nonce('klusio_render_'.$post->ID.'_'.klusio_bridge_value_revision($post))), $url); 4839 } 4840 function klusio_bridge_value_preview($id, $revision) { 4841 $c = klusio_bridge_native_connection(); 4842 if (!$c || !in_array('drafts',$c['scopes'],true) || !current_user_can('edit_posts')) return new WP_Error('klusio_scope', __('Je hebt toestemming nodig om concepten te maken.', 'klusio-builder-bridge')); 4843 $d = klusio_bridge_value_request(array('action'=>'read','view'=>'draft','id'=>$id)); 4844 if (is_wp_error($d)) return $d; 4845 if (empty($d['editable']) || !hash_equals((string)($d['revision']??''),$revision)) return new WP_Error('klusio_changed', __('Het concept is veranderd of heeft al een WordPress-versie. Laad de nieuwste stand.', 'klusio-builder-bridge')); 4846 $source = $c['account'].':'.$id.':'.get_current_user_id(); 4847 $key = 'klusio_preview_'.hash('sha256',$source); 4848 $prior = (int)get_option($key,0); $post = $prior ? get_post($prior) : null; 4849 if ($post && (get_post_meta($prior,'_klusio_preview_source',true) !== $source || $post->post_status !== 'draft' || !current_user_can('edit_post',$prior) || !hash_equals((string)get_post_meta($prior,'_klusio_preview_hash',true),klusio_bridge_value_revision($post)))) return new WP_Error('klusio_own_changes', __('Dit voorbeeld is in WordPress aangepast. Je eigen werk blijft behouden. Bekijk het in de WordPress-editor.', 'klusio-builder-bridge')); 4850 // A source fingerprint makes retries reuse the same private draft. Never associate it with the publication ID. 4851 if (!$post || get_post_meta($prior,'_klusio_preview_revision',true) !== $revision) { 4852 $values = array('post_type'=>'post','post_status'=>'draft','post_author'=>get_current_user_id(),'post_title'=>sanitize_text_field($d['title']??''),'post_content'=>wp_kses_post($d['content']??''),'comment_status'=>'closed','ping_status'=>'closed'); 4853 // A new saved account revision gets a new private copy. Never update an existing WordPress draft. 4854 $pid = wp_insert_post(wp_slash($values),true); 4855 if (is_wp_error($pid)) return $pid; 4856 update_option($key,$pid,false); 4857 update_post_meta($pid,'_klusio_preview_source',$source); 4858 update_post_meta($pid,'_klusio_preview_revision',$revision); 4859 update_post_meta($pid,'_klusio_preview_created',time()); 4860 $media = attachment_url_to_postid((string)($d['featured_image']??'')); 4861 if ($media && current_user_can('edit_post',$media)) set_post_thumbnail($pid,$media); else delete_post_thumbnail($pid); 4862 update_post_meta($pid,'_klusio_preview_alt',sanitize_text_field($d['featured_image_alt']??'')); 4863 $post=get_post($pid); 4864 update_post_meta($pid,'_klusio_preview_hash',klusio_bridge_value_revision($post)); 4865 if (!$post || $post->post_status!=='draft' || $post->post_content!==$values['post_content']) return new WP_Error('klusio_preview_failed', __('Het voorbeeld kon niet worden gecontroleerd. Publiceer nog niet.', 'klusio-builder-bridge')); 4866 } 4867 return array('post_id'=>$post->ID,'url'=>klusio_bridge_value_url($post),'revision'=>klusio_bridge_value_revision($post),'edit_url'=>get_edit_post_link($post->ID,'raw'),'preview_copy'=>true,'image_available'=>(bool)get_post_thumbnail_id($post->ID)); 4868 } 4869 // Preview copies stay private, including if an editor presses Publish. Own edits are never cleaned up. 4870 add_filter('wp_insert_post_data',function($data,$postarr){if(!empty($postarr['ID']) && get_post_meta((int)$postarr['ID'],'_klusio_preview_source',true) && in_array($data['post_status'],array('publish','future','private'),true))$data['post_status']='draft';return $data;},10,2); 4871 add_filter('wp_get_attachment_image_attributes',function($attr,$attachment){if(is_preview()){$id=get_queried_object_id();if(get_post_meta($id,'_klusio_preview_source',true) && current_user_can('edit_post',$id) && (int)get_post_thumbnail_id($id)===(int)$attachment->ID)$attr['alt']=(string)get_post_meta($id,'_klusio_preview_alt',true);}return $attr;},10,2); 4872 function klusio_bridge_value_report($post) { 4873 $report=get_post_meta($post->ID,'_klusio_render_review',true); 4874 if (!is_array($report)) return array('available'=>false); 4875 $report['available']=true;$report['current']=hash_equals($report['revision'],klusio_bridge_value_revision($post)); 4876 $report['note']=__('Een browsercontrole op het genoemde moment. Thema, templates of externe inhoud kunnen daarna zijn veranderd. Dit is geen volledige toegankelijkheids- of SEO-audit.', 'klusio-builder-bridge'); 4877 return $report; 4878 } 4879 function klusio_bridge_value_save_report($post, $in) { 4880 if (!hash_equals(klusio_bridge_value_revision($post),(string)($in['revision']??''))) return new WP_Error('klusio_stale', __('De pagina is intussen aangepast. Controleer de nieuwste versie.', 'klusio-builder-bridge')); 4881 $runs=$in['runs']??null; 4882 if(!is_array($runs)||count($runs)!==2)return new WP_Error('klusio_review',__('Controleer desktop én mobiel voordat je opslaat.','klusio-builder-bridge')); 4883 $clean=array(); $allowed=array('overflow','image_crop','image_broken','alt_review','heading_jump','heading_missing','field_label','cta_review'); 4884 foreach(array('desktop'=>1280,'mobile'=>390) as $device=>$width){ 4885 $run=$runs[$device]??null; 4886 if(!is_array($run)||(int)($run['width']??0)!==$width||!is_array($run['findings']??null)||count($run['findings'])>60)return new WP_Error('klusio_review',__('De schermcontrole is onvolledig. Probeer opnieuw.','klusio-builder-bridge')); 4887 $items=array();foreach($run['findings'] as $f){ 4888 if(!is_array($f)||!in_array($f['type']??'', $allowed,true))return new WP_Error('klusio_review',__('Een controlepunt is niet herkenbaar.','klusio-builder-bridge')); 4889 $items[]=array('type'=>$f['type'],'text'=>substr(sanitize_text_field($f['text']??''),0,500),'location'=>substr(sanitize_text_field($f['location']??''),0,300)); 4890 } 4891 $clean[$device]=array('width'=>$width,'findings'=>$items,'limited'=>!empty($run['limited'])); 4892 } 4893 $r=array('revision'=>$in['revision'],'checked_at'=>gmdate('c'),'url'=>get_permalink($post),'source'=>'WordPress-browsercontrole','runs'=>$clean,'by'=>get_current_user_id()); 4894 $prior=get_post_meta($post->ID,'_klusio_render_review',true); 4895 if($prior)add_post_meta($post->ID,'_klusio_render_review_backup',$prior); 4896 update_post_meta($post->ID,'_klusio_render_review',$r); 4897 if(get_post_meta($post->ID,'_klusio_render_review',true)!==$r)return new WP_Error('klusio_store',__('De controle is niet opgeslagen. Probeer opnieuw.','klusio-builder-bridge')); 4898 return klusio_bridge_value_report($post); 4899 } 4900 add_action('wp_ajax_klusio_value',function(){ 4901 check_ajax_referer('klusio_value','nonce'); 4902 if(!current_user_can('manage_options'))wp_send_json_error(array('error'=>__('Alleen een beheerder kan deze controle uitvoeren.','klusio-builder-bridge')),403); 4903 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Bounded JSON, each operation validates and sanitizes its fields. 4904 $raw=isset($_POST['request'])?wp_unslash($_POST['request']):''; 4905 $in=is_string($raw)&&strlen($raw)<=60000?json_decode($raw,true,16):null; 4906 if(!is_array($in))wp_send_json_error(array('error'=>__('Ongeldig verzoek.','klusio-builder-bridge')),400); 4907 $action=$in['action']??'';$result=null; 4908 if($action==='health')$result=klusio_bridge_value_health(); 4909 elseif($action==='preview'){ 4910 if(!klusio_bridge_workspace_lock())$result=new WP_Error('klusio_busy',__('Een voorbeeld wordt al voorbereid. Probeer het zo opnieuw.','klusio-builder-bridge')); 4911 else{try{$result=klusio_bridge_value_preview(absint($in['id']??0),(string)($in['revision']??''));}finally{klusio_bridge_workspace_unlock();}} 4912 }else{ 4913 $post=klusio_bridge_workspace_local_post(absint($in['id']??0)); 4914 if(!$post)$result=new WP_Error('klusio_post',__('Deze pagina is niet beschikbaar.','klusio-builder-bridge')); 4915 elseif($action==='inspect')$result=array('post_id'=>$post->ID,'url'=>klusio_bridge_value_url($post),'revision'=>klusio_bridge_value_revision($post),'preview_copy'=>(bool)get_post_meta($post->ID,'_klusio_preview_source',true),'report'=>klusio_bridge_value_report($post)); 4916 elseif($action==='save_review')$result=klusio_bridge_value_save_report($post,$in); 4917 } 4918 if(is_wp_error($result))wp_send_json_error(array('error'=>$result->get_error_message()),409); 4919 if(!is_array($result))wp_send_json_error(array('error'=>__('Onbekende actie.','klusio-builder-bridge')),400); 4920 wp_send_json_success($result); 4921 }); 4922 add_action('template_redirect',function(){ 4923 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Explicit nonce verification below; read-only private frontend inspection. 4924 if(!isset($_GET['kl_review']))return; 4925 $id=is_scalar($_GET['kl_review'])?absint($_GET['kl_review']):0;$post=get_post($id); 4926 $nonce=isset($_GET['kl_check'])&&is_string($_GET['kl_check'])?sanitize_text_field(wp_unslash($_GET['kl_check'])):''; 4927 // phpcs:enable WordPress.Security.NonceVerification.Recommended 4928 nocache_headers();header('X-Robots-Tag: noindex, nofollow',true); 4929 if(!$post||!current_user_can('manage_options')||!current_user_can('edit_post',$id)||get_queried_object_id()!==$id||!wp_verify_nonce($nonce,'klusio_render_'.$id.'_'.klusio_bridge_value_revision($post)))wp_die(esc_html__('Deze privécontrole is verlopen. Open de pagina opnieuw vanuit Klusio in WordPress.','klusio-builder-bridge'),'',array('response'=>403)); 4930 remove_action('template_redirect','redirect_canonical');add_filter('show_admin_bar','__return_false'); 4931 add_action('wp_head',function()use($post){echo '<meta name="klusio-review" content="'.esc_attr($post->ID.':'.klusio_bridge_value_revision($post)).'">';},1); 4932 }); 4933 add_action('rest_api_init',function(){register_rest_route('klusio/v1','/page-review',array('methods'=>'GET','permission_callback'=>'klusio_bridge_workspace_can_receive','callback'=>function($request){ 4934 $url=(string)$request->get_param('url');$a=wp_parse_url($url);$b=wp_parse_url(home_url()); 4935 if(!$a||isset($a['user'])||isset($a['pass'])||($a['host']??'')!==($b['host']??'')||($a['scheme']??'')!==($b['scheme']??'')||($a['port']??null)!==($b['port']??null))return new WP_Error('klusio_site',__('Deze pagina hoort niet bij deze website.','klusio-builder-bridge'),array('status'=>400)); 4936 $post=klusio_bridge_workspace_local_post(url_to_postid($url));if(!$post)return new WP_Error('klusio_post',__('Deze pagina is niet beschikbaar.','klusio-builder-bridge'),array('status'=>404)); 4937 return new WP_REST_Response(klusio_bridge_value_report($post)); 4938 }));}); 4939 4940 /** Restrict only positively identified Klusio application passwords, never the user's login or other integrations. */ 4941 function klusio_bridge_value_credential($user_id,$uuid) { 4942 $ids=(array)get_user_meta($user_id,'_klusio_editorial_credentials',true); 4943 if(in_array($uuid,$ids,true))return true; 4944 // Recognize the UUID saved by the guided 1.16 connection without guessing from a password's display name. 4945 $flow=get_option('klusio_connect_'.$user_id,array()); 4946 return is_array($flow)&&!empty($flow['app_uuid'])&&hash_equals($flow['app_uuid'],(string)$uuid)&&($flow['site']??'')===klusio_bridge_workspace_identity(site_url()); 4947 } 4948 function klusio_bridge_value_restrict($user_id,$uuid) { 4949 $ids=(array)get_user_meta($user_id,'_klusio_editorial_credentials',true); 4950 $ids=array_values(array_unique(array_merge($ids,array($uuid)))); 4951 update_user_meta($user_id,'_klusio_editorial_credentials',$ids); 4952 return get_user_meta($user_id,'_klusio_editorial_credentials',true)===$ids; 4953 } 4954 add_action('wp_authenticate_application_password_errors',function($error,$user,$item){ 4955 if(klusio_bridge_value_credential($user->ID,$item['uuid']??'') && !(defined('REST_REQUEST')&&REST_REQUEST))$error->add('klusio_rest_only',__('Deze Klusio-toegang werkt uitsluitend via de beperkte website-API.','klusio-builder-bridge')); 4956 },10,3); 4957 add_action('application_password_did_authenticate',function($user,$item){ 4958 if(klusio_bridge_value_credential($user->ID,$item['uuid']??''))$GLOBALS['klusio_editorial_identity']=array('user'=>$user->ID,'uuid'=>$item['uuid']); 4959 },10,2); 4960 function klusio_bridge_value_route_allowed($route,$method) { 4961 $read=in_array($method,array('GET','HEAD','OPTIONS'),true); 4962 if($read && in_array($route,array('/','/wp/v2','/wp/v2/users/me','/wp/v2/types','/wp/v2/taxonomies','/yoast/v1','/rankmath/v1','/yoast/v1/bulk_editor/posts'),true))return true; 4963 // Core's batch endpoint, users, settings, plugins, themes and arbitrary namespaces deliberately fail closed. 4964 if(preg_match('~^/wp/v2/(posts|pages|media|categories|tags)(/\d+)?/?$~D',$route))return in_array($method,array('GET','HEAD','OPTIONS','POST','PUT','PATCH','DELETE'),true); 4965 if($read && preg_match('~^/wp/v2/(posts|pages)/\d+/(revisions|autosaves)(/\d+)?/?$~D',$route))return true; 4966 if($read && preg_match('~^/wp/v2/(types|taxonomies)/[a-z0-9_-]+/?$~D',$route))return true; 4967 // Preserve editorial custom post types, using WordPress' registered public REST bases only. 4968 foreach(get_post_types(array('show_in_rest'=>true),'objects') as $type){ 4969 if(empty($type->public))continue; 4970 $base='/'.(!empty($type->rest_namespace)?$type->rest_namespace:'wp/v2').'/'.(!empty($type->rest_base)?$type->rest_base:$type->name); 4971 if(preg_match('~^'.preg_quote($base,'~').'(/\d+)?/?$~D',$route))return in_array($method,array('GET','HEAD','OPTIONS','POST','PUT','PATCH','DELETE'),true); 4972 } 4973 if($method==='POST' && in_array($route,array('/rankmath/v1/updateMeta','/yoast/v1/bulk_editor/update_search'),true))return true; 4974 $get='(?:status|connect-info|connect-proof/[a-f0-9]{32}|native-actor/\d+|page-review|blog/\d+/preview|slot/\d+|media/gebruik|media/\d+/gebruik|elementor/\d+/(?:reservekopieen|gebruik)|bricks/(?:\d+/gebruik|sjablonen))'; 4975 if($read && preg_match('~^/klusio/v1/'.$get.'/?$~D',$route))return true; 4976 $both='(?:workspace|blog-release|redirects|builder-data|elementor/\d+|bricks/(?:\d+|globaal/[a-z0-9]{6})|vertaling/\d+|berichten/[a-z0-9_-]{1,20}(?:/[a-f0-9]{16,64})?)'; 4977 if(($read||$method==='POST')&&preg_match('~^/klusio/v1/'.$both.'/?$~D',$route))return true; 4978 $write='(?:native-connection|blog/\d+/publish|(?:elementor|bricks)/\d+/terugzetten|media/\d+/alt|translatepress/(?:\d+/inspect|(?:media|bericht|meta)/\d+)|berichten/[a-z0-9_-]{1,20}/[a-f0-9]{16,64}/prullenbak)'; 4979 return $method==='POST'&&preg_match('~^/klusio/v1/'.$write.'/?$~D',$route); 4980 } 4981 /** SEO integrations may only touch the existing page title and description, never their general settings. */ 4982 function klusio_bridge_value_seo_allowed($request) { 4983 $route=$request->get_route();$p=$request->get_params(); 4984 if($request->get_method()!=='POST')return true; 4985 $fields=array();$id=0; 4986 if($route==='/rankmath/v1/updateMeta'){ 4987 if(array_diff(array_keys($p),array('objectType','objectID','meta'))||($p['objectType']??'')!=='post'||!is_array($p['meta']??null))return false; 4988 $id=$p['objectID']??0;$fields=$p['meta'];$allowed=array('rank_math_title','rank_math_description'); 4989 }elseif($route==='/yoast/v1/bulk_editor/update_search'){ 4990 if(array_diff(array_keys($p),array('items'))||!is_array($p['items']??null)||count($p['items'])!==1||!is_array($p['items'][0]??null))return false; 4991 $fields=$p['items'][0];$id=$fields['id']??0;unset($fields['id']);$allowed=array('seo_title','meta_description'); 4992 }else return true; 4993 if(!is_scalar($id)||!ctype_digit((string)$id)||(int)$id<1||!get_post((int)$id)||!current_user_can('edit_post',(int)$id)||!$fields||array_diff(array_keys($fields),$allowed))return false; 4994 foreach($fields as $value)if(!is_string($value)||strlen($value)>10000)return false; 4995 return true; 4996 } 4997 add_filter('rest_pre_dispatch',function($result,$server,$request){ 4998 if(!empty($GLOBALS['klusio_editorial_identity']) && (!klusio_bridge_value_route_allowed($request->get_route(),$request->get_method())||!klusio_bridge_value_seo_allowed($request)))return new WP_Error('klusio_editorial_only',__('Deze Klusio-toegang mag alleen website-inhoud en de afgesproken Klusio-koppeling gebruiken. Gebruikers, plugins en algemene instellingen vallen daarbuiten.','klusio-builder-bridge'),array('status'=>403)); 4999 return $result; 5000 },5,3); 5001 function klusio_bridge_value_health() { 5002 $s=klusio_bridge_workspace_settings();$raw=get_option('klusio_native_connection',array());$c=klusio_bridge_native_connection();$snapshot=klusio_bridge_workspace_data(); 5003 $result=array('site'=>site_url(),'checked_at'=>gmdate('c'),'enabled'=>!empty($s['enabled']),'native_enabled'=>(bool)get_option('klusio_native_enabled',false),'local_valid'=>(bool)$c,'expires_at'=>(int)($raw['expires_at']??0),'last_snapshot'=>(int)($snapshot['generated_at']??0),'account_confirmed'=>false,'scopes'=>array(),'credential_restricted'=>null); 5004 if($c){$remote=klusio_bridge_value_request(array('action'=>'read','view'=>'health'));if(is_wp_error($remote))$result['message']=$remote->get_error_message();else{$result['account_confirmed']=true;$result['scopes']=$remote['scopes'];$result['message']=$remote['message'];$result['credential_restricted']=$remote['credential_restricted']??null;}} 5005 else $result['message']=__('Bevestig de werkverbinding opnieuw. Controleer hierboven ook of het accountoverzicht en de werkverbinding zijn ingeschakeld.','klusio-builder-bridge'); 5006 return $result; 5007 } 5008 function klusio_bridge_value_start($data) { 5009 $s=klusio_bridge_workspace_settings();$picked=(int)get_user_meta(get_current_user_id(),'_klusio_focus_page',true);$post=$picked?klusio_bridge_workspace_local_post($picked):null; 5010 echo '<section class="kbw-card kv-start"><span class="kbw-kicker">'.esc_html__('Je eerste concrete verbetering','klusio-builder-bridge').'</span><h2>'.esc_html__('Van een belangrijke pagina naar een gecontroleerd resultaat','klusio-builder-bridge').'</h2><div class="kv-steps">'; 5011 $ready=(bool)klusio_bridge_native_connection(); 5012 $steps=array(array('admin-links',__('Verbind je website','klusio-builder-bridge'),$ready?__('Je werkverbinding staat aan. De actuele bereikbaarheid controleer je bij Verbinding.','klusio-builder-bridge'):__('Bevestig eerst je account en de toegestane acties.','klusio-builder-bridge'),'connection'),array('admin-page',__('Kies je belangrijkste pagina','klusio-builder-bridge'),$post?get_the_title($post):__('Begin met een dienst of product waarvoor je aanvragen wilt krijgen.','klusio-builder-bridge'),'pages'),array('visibility',__('Bekijk en beoordeel','klusio-builder-bridge'),__('Controleer de echte paginaopmaak en bekijk het onderbouwde voorstel. Publiceren blijft een aparte keuze.','klusio-builder-bridge'),'growth')); 5013 foreach($steps as $n=>$step){echo '<div><span class="dashicons dashicons-'.esc_attr($step[0]).'" aria-hidden="true"></span><h3>'.esc_html(($n+1).'. '.$step[1]).'</h3><p>'.esc_html($step[2]).'</p><a href="'.esc_url(admin_url('admin.php?page=klusio&view='.$step[3].($n===1&&$post?'&post='.$post->ID:''))).'">'.esc_html__('Open deze stap','klusio-builder-bridge').' →</a></div>';} 5014 echo '</div>'; 5015 if($data && !empty($data['unavailable']))echo '<p class="kbw-small">'.esc_html__('Een deel van je accountgegevens ontbreekt nog. Controleer Verbinding voordat je op basis van dit overzicht handelt.','klusio-builder-bridge').'</p>'; 5016 echo '</section>'; 5017 } 5018 add_action('admin_post_klusio_focus_page',function(){ 5019 check_admin_referer('klusio_focus_page'); 5020 $id=isset($_POST['post'])&&is_scalar($_POST['post'])?absint($_POST['post']):0; 5021 if(!klusio_bridge_workspace_can_view()||!klusio_bridge_workspace_local_post($id))wp_die(esc_html__('Geen toegang tot deze pagina.','klusio-builder-bridge'),'',array('response'=>403)); 5022 update_user_meta(get_current_user_id(),'_klusio_focus_page',$id); 5023 wp_safe_redirect(admin_url('admin.php?page=klusio&view=pages&post='.$id));exit; 5024 }); 5025 function klusio_bridge_value_mount($post) { 5026 if(!current_user_can('manage_options'))return; 5027 echo '<section class="kbw-card kv-review" data-post="'.esc_attr($post->ID).'"><span class="kbw-symbol dashicons dashicons-desktop" aria-hidden="true"></span><h2>'.esc_html__('Controleer wat je bezoeker ziet','klusio-builder-bridge').'</h2><p>'.esc_html__('Bekijk de echte pagina met je thema, builder en dynamische inhoud. Controleer desktop en mobiel en bewaar de aandachtspunten voor deze pagina.','klusio-builder-bridge').'</p><button type="button" class="kbw-btn kbw-primary kv-open">'.esc_html__('Open de websitecontrole','klusio-builder-bridge').'</button><div class="kv-stage"></div></section>'; 5028 echo '<form method="post" action="'.esc_url(admin_url('admin-post.php')).'" class="kbw-card"><input type="hidden" name="action" value="klusio_focus_page"><input type="hidden" name="post" value="'.esc_attr($post->ID).'">';wp_nonce_field('klusio_focus_page');echo '<button class="kbw-btn" type="submit">'.esc_html__('Deze pagina als mijn eerste stap kiezen','klusio-builder-bridge').'</button></form>'; 5029 } 5030 function klusio_bridge_value_style() { 5031 return <<<'CSS' 5032 .kv-steps{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:24px;margin-top:28px}.kv-steps>div{padding:24px;background:#f6f8fa;border-radius:16px 16px 16px 0}.kv-steps h3{font-size:16px;line-height:1.5;margin:16px 0 10px}.kv-steps p{line-height:1.7;color:#546579}.kv-steps .dashicons{color:#286ba5}.kv-stage{margin-top:24px;min-width:0}.kv-tools{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin:20px 0}.kv-shell{overflow:hidden;background:#f3f5f8;border:1px solid #e1e6ec;border-radius:12px 12px 12px 0;position:relative}.kv-shell iframe{border:0;display:block;background:white;transform-origin:top left;max-width:none!important}.kv-note{font-size:13px;color:#596b7f;line-height:1.7}.kv-message{padding:18px 20px;background:#eff4f9;border-radius:10px 10px 10px 0;line-height:1.7;margin:18px 0}.kv-results{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:24px;margin:24px 0}.kv-result{border:1px solid #e1e6ec;border-radius:14px 14px 14px 0;padding:24px}.kv-result h3{margin-top:0}.kv-result li{margin:18px 0;line-height:1.7}.kv-result small{display:block;overflow-wrap:anywhere;color:#647587}.kv-story{list-style:none;padding:0;margin:24px 0}.kv-story>li{display:grid;grid-template-columns:36px 1fr;gap:16px;padding:22px 0;border-bottom:1px solid #e5eaf0}.kv-number{display:flex;align-items:center;justify-content:center;background:#edf3f9;color:#24689f;width:32px;height:32px;border-radius:50%;font-weight:600}.kv-story h3{font-size:17px;margin:0 0 8px}.kv-story p{line-height:1.7;margin:8px 0}.kv-table{width:100%;border-collapse:collapse;font-size:14px;margin:16px 0}.kv-table th,.kv-table td{text-align:left;border-bottom:1px solid #e4e9ef;padding:12px 8px;overflow-wrap:anywhere}.kv-table caption{text-align:left;line-height:1.7;color:#5a6c7d;margin-bottom:12px}.kv-fact{display:flex;align-items:baseline;justify-content:space-between;gap:16px;padding:14px 0;border-bottom:1px solid #e5eaf0}.kv-health-choices{display:flex;gap:8px;flex-wrap:wrap;margin:16px 0}.kv-health-choices span{background:#f1f4f7;padding:7px 12px;border-radius:7px;font-size:13px}.kv-story-card{padding:28px;border:1px solid #e1e7ed;border-radius:16px 16px 16px 0;background:#fff;margin:24px 0;min-width:0}.kv-story-card h2{font-size:22px;line-height:1.4}.kv-story-card p{line-height:1.8;color:#526579}.kv-story-card details{margin-top:18px}.kv-story-card summary{cursor:pointer;font-weight:600}.kv-account-form{display:flex;align-items:end;gap:12px;flex-wrap:wrap}.kv-account-form label{flex:1;min-width:180px}.kv-account-form input{display:block;width:100%;margin-top:8px;padding:12px}.kv-account-form button{padding:12px 20px}.kv-compact{font-size:13px;color:#5b6b7d}.kv-story-card a{color:#256da5}.kv-start{margin-bottom:24px!important}.kv-start h2{max-width:32ch}.kv-stored>summary{font-size:16px;font-weight:600;cursor:pointer}.kv-stored>.kbw-grid{margin-top:24px}.kv-tools button[aria-pressed=true]{background:#e8eef4;color:#205e91}.kv-tools button:disabled{opacity:.5;cursor:wait}@media(max-width:900px){.kv-steps{grid-template-columns:1fr}.kv-steps>div{padding:20px}.kv-results{grid-template-columns:1fr}.kv-result{padding:18px}.kv-story-card{padding:20px}.kv-table th,.kv-table td{font-size:12px;padding:10px 4px}} 5033 CSS; 5034 } 5035 function klusio_bridge_value_script() { 5036 return <<<'JS' 5037 (function(){'use strict'; 5038 const cfg=window.klusioValue;if(!cfg)return; 5039 const el=(tag,text,cls)=>{const n=document.createElement(tag);if(text!==undefined)n.textContent=text;if(cls)n.className=cls;return n;}; 5040 const btn=(text,fn)=>{const b=el('button',text,'kbw-btn');b.type='button';b.onclick=fn;return b;}; 5041 async function call(data){const body=new FormData();body.set('action','klusio_value');body.set('nonce',cfg.nonce);body.set('request',JSON.stringify(data));const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),35000);try{const response=await fetch(cfg.ajax,{method:'POST',credentials:'same-origin',body,signal:controller.signal});let r;try{r=await response.json();}catch(_){throw Error('De serverbevestiging is niet leesbaar. Vernieuw je login en probeer het opnieuw.');}if(!r?.success)throw Error(r?.data?.error||'De controle is niet beschikbaar.');return r.data;}catch(e){if(e.name==='AbortError')throw Error('De verbinding duurde te lang. Open de controle opnieuw voordat je verdergaat.');throw e;}finally{clearTimeout(timer);}} 5042 function message(host,text){host.textContent=text;host.className='kv-message';host.setAttribute('role','status');} 5043 function report(host,r){host.replaceChildren();if(!r?.available){host.append(el('p','Er is nog geen bewaarde browsercontrole voor deze pagina.','kv-note'));return;}host.append(el('p',r.source+' · '+new Date(r.checked_at).toLocaleString('nl-NL')+(r.current?'':' · de pagina is daarna gewijzigd'),'kv-note'));const grid=el('div',undefined,'kv-results');for(const [device,label]of [['desktop','Desktop · 1280 px'],['mobile','Mobiel · 390 px']]){const s=el('section',undefined,'kv-result');s.append(el('h3',label));const run=r.runs?.[device];if(!run){s.append(el('p','Nog niet gecontroleerd.'));grid.append(s);continue;}const list=el('ul');for(const f of run.findings||[]){const li=el('li',f.text);li.append(el('small',f.location));list.append(li);}if(!run.findings?.length)s.append(el('p','Geen aandachtspunten in deze beperkte controle. Bekijk zelf ook de foto’s, knoppen en inhoud.'));else s.append(list);if(run.limited)s.append(el('p','De pagina bevat meer elementen dan deze controle kan beoordelen.','kv-note'));grid.append(s);}host.append(grid,el('p',r.note||'Dit zijn controlepunten uit de browser, geen bewijs van fouten of SEO-effect.','kv-note'));} 5044 function inspect(doc,width){ 5045 const findings=[],nodes=[...doc.querySelectorAll('body *')],limited=nodes.length>3000; 5046 const visible=n=>{const s=doc.defaultView.getComputedStyle(n),r=n.getBoundingClientRect();return s.display!=='none'&&s.visibility!=='hidden'&&r.width>1&&r.height>1&&!n.closest('#wpadminbar,[aria-hidden="true"]');}; 5047 const location=n=>{const heading=n.closest('section,article,main')?.querySelector('h1,h2,h3');let path=n.tagName.toLowerCase();if(n.id)path+='#'+n.id;else if(n.parentElement){path+=' · element '+([...n.parentElement.children].indexOf(n)+1);}return (heading?.textContent.trim().slice(0,100)||'Pagina')+' · '+path;}; 5048 const add=(type,text,n)=>{if(findings.length<60)findings.push({type,text,location:location(n)});}; 5049 const seen=new Set();let previous=0,cta=false,h1=0; 5050 for(const n of nodes.slice(0,3000)){if(!visible(n))continue;const r=n.getBoundingClientRect(),s=doc.defaultView.getComputedStyle(n);if((r.right>width+3||r.left< -3)&&!n.closest('[role="slider"],.swiper-wrapper,.slick-track')&&!seen.has(n.parentElement)&&s.position!=='fixed'){add('overflow','Dit element valt buiten de schermbreedte. Controleer of de bezoeker horizontaal moet schuiven.',n);seen.add(n);} 5051 if(/^H[1-6]$/.test(n.tagName)){const level=Number(n.tagName[1]);if(level===1)h1++;if(previous&&level>previous+1)add('heading_jump','De kopvolgorde springt van H'+previous+' naar H'+level+'. Controleer de leesvolgorde.',n);previous=level;} 5052 if(n.tagName==='IMG'){if(n.complete&&n.naturalWidth===0)add('image_broken','Deze afbeelding kon tijdens de controle niet worden geladen.',n);if(s.objectFit==='cover'&&n.naturalWidth&&n.naturalHeight){const intrinsic=n.naturalWidth/n.naturalHeight,box=r.width/r.height,visibleRatio=Math.min(intrinsic/box,box/intrinsic);if(visibleRatio<.65)add('image_crop','Deze afbeelding verliest door de uitsnede een groot deel van het beeld. Controleer of het onderwerp volledig zichtbaar blijft.',n);}if(!n.hasAttribute('alt'))add('alt_review','De afbeelding mist een alt-attribuut. Geef betekenisvol beeld een beschrijving; decoratief beeld een lege alt-tekst.',n);} 5053 if(['INPUT','SELECT','TEXTAREA'].includes(n.tagName)&&!['hidden','submit','button','reset','image'].includes(n.type)&&!n.labels?.length&&!n.getAttribute('aria-label')&&!n.getAttribute('aria-labelledby')&&!n.title)add('field_label','Dit zichtbare formulierveld heeft geen herkenbaar label. Controleer de naam voor schermlezers.',n); 5054 if(n.tagName==='FORM'||(['A','BUTTON'].includes(n.tagName)&&(/contact|offerte|gesprek|aanvraag|bestel|winkelwagen|reserver|boek|plan|proberen|start|inloggen/i.test(n.textContent)||/^(tel:|mailto:)|wa\.me|api\.whatsapp/.test(n.getAttribute('href')||''))))cta=true; 5055 } 5056 const main=doc.querySelector('main')||doc.body;if(!h1)add('heading_missing','Er is geen zichtbare H1 aangetroffen. Controleer of de hoofdonderwerp-titel in de paginaopmaak staat.',main);if(!cta)add('cta_review','Er is geen herkenbare contact-, aanvraag- of bestelactie aangetroffen. Controleer welke vervolgstap bij deze pagina past.',main); 5057 return {width,findings,limited:limited||findings.length>=60};} 5058 window.klusioInspectRendered=inspect; 5059 async function openReview(stage,id,initial){ 5060 if(stage.klusioDispose)stage.klusioDispose();stage.replaceChildren();const notice=el('div');stage.append(notice);message(notice,'De eigen websiteopmaak openen…');let info; 5061 try{info=initial||await call({action:'inspect',id});}catch(e){message(notice,e.message);return;} 5062 let url;try{url=new URL(info.url);if(url.origin!==location.origin)throw Error();}catch(_){message(notice,'De website staat op een ander adres. Open het voorbeeld in een apart tabblad en controleer de domeininstelling.');return;} 5063 const tools=el('div',undefined,'kv-tools'),shell=el('div',undefined,'kv-shell'),frame=el('iframe'),results=el('div');frame.title='Echte websiteopmaak';frame.setAttribute('referrerpolicy','same-origin');shell.append(frame);stage.append(tools,shell,results);let device=window.innerWidth<782?'mobile':'desktop',loaded=false,runs={},busy=false; 5064 function size(){const width=device==='desktop'?1280:390,scale=Math.min(1,shell.clientWidth/width);frame.style.width=width+'px';frame.style.height='760px';frame.style.transform='scale('+scale+')';shell.style.height=(760*scale)+'px';} 5065 const observer=new ResizeObserver(size);observer.observe(shell); 5066 function selection(){desktop.setAttribute('aria-pressed',String(device==='desktop'));mobile.setAttribute('aria-pressed',String(device==='mobile'));}const desktop=btn('Desktop',()=>{device='desktop';size();selection();}),mobile=btn('Mobiel',()=>{device='mobile';size();selection();});selection(); 5067 const run=btn('Controleer dit scherm',async()=>{if(!loaded||busy)return;busy=true;run.disabled=true;desktop.disabled=true;mobile.disabled=true;message(notice,'De zichtbare pagina wordt gecontroleerd…');try{await Promise.race([frame.contentDocument.fonts?.ready||Promise.resolve(),new Promise(r=>setTimeout(r,2500))]);await new Promise(r=>setTimeout(r,600));const doc=frame.contentDocument;if(doc.querySelector('meta[name="klusio-review"]')?.content!==info.post_id+':'+info.revision)throw Error('De preview is verlopen of de pagina is intussen veranderd. Open de controle opnieuw.');runs[device]=inspect(doc,device==='desktop'?1280:390);report(results,{available:true,current:true,source:'Nog niet opgeslagen',checked_at:new Date().toISOString(),runs});message(notice,runs.desktop&&runs.mobile?'Beide schermen zijn gecontroleerd. Bekijk de aandachtspunten en bewaar de controle.':'Scherm gecontroleerd. Kies nu het andere scherm en controleer dat ook.');save.disabled=!(runs.desktop&&runs.mobile);}catch(e){message(notice,e.message||'Deze websiteopmaak kon niet worden gecontroleerd.');}finally{busy=false;run.disabled=false;desktop.disabled=false;mobile.disabled=false;}}); 5068 const save=btn('Controle bij de pagina bewaren',async()=>{save.disabled=true;try{const r=await call({action:'save_review',id:info.post_id,revision:info.revision,runs});report(results,r);message(notice,info.preview_copy?'Bewaard bij je privévoorbeeld. Controleer na publicatie ook de live pagina.':'Bewaard bij deze pagina. Dezelfde controle is op te halen in je Klusio-account.');}catch(e){message(notice,e.message);save.disabled=false;}});save.disabled=true;run.disabled=true; 5069 const separate=el('a','Open volledig voorbeeld ↗','kbw-btn');separate.href=url.href;separate.target='_blank';separate.rel='noopener';tools.append(desktop,mobile,run,save,separate); 5070 const timer=setTimeout(()=>{if(!loaded)message(notice,'Het voorbeeld is nog niet geladen. Je kunt het ook in een apart tabblad openen.');},15000); 5071 frame.onload=()=>{clearTimeout(timer);try{loaded=frame.contentDocument.querySelector('meta[name="klusio-review"]')?.content===info.post_id+':'+info.revision;if(!loaded)throw Error();run.disabled=false;message(notice,'Je ziet de echte websiteopmaak. De controle verstuurt geen formulieren en wijzigt geen pagina-inhoud.');size();}catch(_){message(notice,'De privépreview kon niet worden geopend. Log zo nodig opnieuw in op WordPress en open de controle opnieuw.');}}; 5072 stage.klusioDispose=()=>{observer.disconnect();clearTimeout(timer);frame.onload=null;};frame.src=url.href;size();if(info.report)report(results,info.report); 5073 } 5074 window.klusioThemePreview=async(d,host)=>{const box=el('section',undefined,'kbw-card'),stage=el('div',undefined,'kv-stage');box.append(el('h2','Voorbeeld in jouw website'),el('p','Dit is een aparte privéconceptkopie met je echte thema. Publiceer het artikel vanuit Klusio; deze voorbeeldkopie blijft concept.','kv-note'),stage);host.append(box);try{const info=await call({action:'preview',id:Number(d.id),revision:d.revision});await openReview(stage,info.post_id,info);if(d.featured_image&&!info.image_available)box.prepend(el('p','De uitgelichte foto staat nog niet in deze WordPress-mediabibliotheek. Die foto ontbreekt daarom in deze voorbeeldkopie.','kv-message'));}catch(e){message(stage,e.message);}}; 5075 document.querySelectorAll('.kv-review').forEach(root=>{root.querySelector('.kv-open').onclick=()=>openReview(root.querySelector('.kv-stage'),Number(root.dataset.post));}); 5076 const health=document.getElementById('kv-health');if(health)health.querySelector('button').onclick=async()=>{const button=health.querySelector('button'),host=health.querySelector('[role=status]');button.disabled=true;message(host,'Verbinding heen en terug controleren…');try{const d=await call({action:'health'});host.replaceChildren();host.append(el('p',d.message));const rows=[['Accountoverzicht',d.enabled?'Ingeschakeld':'Uitgeschakeld'],['Werkverbinding',d.native_enabled?'Ingeschakeld':'Uitgeschakeld'],['Account en website',d.account_confirmed?'Nu bevestigd':'Niet bevestigd'],['Toestemming geldig tot',d.expires_at?new Date(d.expires_at*1000).toLocaleString('nl-NL'):'Nog niet ingesteld'],['Overzicht bijgewerkt',d.last_snapshot?new Date(d.last_snapshot*1000).toLocaleString('nl-NL'):'Nog niet ontvangen']];for(const [name,value]of rows){const r=el('div',undefined,'kv-fact');r.append(el('strong',name),el('span',value));host.append(r);}const labels={read:'Bekijken',decisions:'Voorstellen beoordelen',drafts:'Concepten bewerken',publish:'Publiceren en herstellen',settings:'Voorkeuren wijzigen'};const scopes=el('div',undefined,'kv-health-choices');for(const s of d.scopes||[])scopes.append(el('span',labels[s]||s));host.append(scopes);if(d.account_confirmed)host.append(el('p',d.credential_restricted===true?'De aparte technische toegang is beperkt tot inhoud en de Klusio-koppeling. Gebruikers, pluginbeheer en algemene instellingen zijn geblokkeerd.':'De bestaande technische toegang heeft nog geen bevestigde beperking. Verbind opnieuw met de begeleide koppeling om een beperkte Klusio-toegang aan te maken.','kv-note'));if(d.expires_at&&d.expires_at*1000-Date.now()<7*86400000)host.append(el('p','Je toestemming verloopt binnenkort of is al verlopen. Bevestig de rechten opnieuw via je Klusio-account.','kv-note'));}catch(e){message(host,e.message);}finally{button.disabled=false;}}; 5077 })(); 5078 JS; 5079 } 5080 add_action('admin_enqueue_scripts',function($hook){ 5081 if($hook!=='toplevel_page_klusio'||!klusio_bridge_workspace_can_view())return; 5082 wp_add_inline_style('klusio-workspace',klusio_bridge_value_style()); 5083 if(!current_user_can('manage_options'))return; 5084 wp_register_script('klusio-value',false,array(),KLUSIO_BRIDGE_VERSION,true);wp_enqueue_script('klusio-value'); 5085 wp_add_inline_script('klusio-value','window.klusioValue='.wp_json_encode(array('ajax'=>admin_url('admin-ajax.php'),'nonce'=>wp_create_nonce('klusio_value'))).';'); 5086 wp_add_inline_script('klusio-value',klusio_bridge_value_script()); 5087 wp_add_inline_script('klusio-value',klusio_bridge_value_story_script()); 5088 },40); 5089 5090 /** Kept byte-identical to the account renderer by the release regression test. */ 5091 function klusio_bridge_value_story_script() { 5092 return <<<'JS' 5093 (function(){'use strict'; 5094 const el=(tag,text,cls)=>{const n=document.createElement(tag);if(text!==undefined)n.textContent=text;if(cls)n.className=cls;return n;}; 5095 function card(title){const n=el('section',undefined,'kv-story-card');n.append(el('h2',title));return n;} 5096 const fmt=v=>new Intl.NumberFormat('nl-NL',{maximumFractionDigits:1}).format(v); 5097 window.klusioRenderPageStory=function(data,host,linkTo){ 5098 const story=data.story||{},s=card('Van aandachtspunt naar resultaat'),timeline=el('ol',undefined,'kv-story'); 5099 const stages=[['Wat zagen we?',(story.steps||[]).filter(s=>s.kind==='proposal')],['Wat is er aangepast?',(story.steps||[]).filter(s=>s.kind==='change')]]; 5100 for(const [i,[label,rows]]of stages.entries()){const li=el('li'),body=el('div');li.append(el('span',i+1,'kv-number'));body.append(el('h3',label));if(!rows.length)body.append(el('p',i===0?'Er staat nog geen onderbouwd voorstel voor deze URL in de huidige selectie.':'Er staat nog geen uitgevoerde wijziging voor deze URL geregistreerd.')); 5101 for(const row of rows){const detail=el('div');detail.append(el('strong',row.title),el('p',row.detail||''));if(row.date)detail.append(el('p','Geregistreerd op '+row.date,'kv-compact'));if(row.rolled_back_at)detail.append(el('p','Teruggedraaid op '+row.rolled_back_at,'kv-compact'));else if(row.checked_at)detail.append(el('p','Live gecontroleerd op '+row.checked_at,'kv-compact'));else if(i===1)detail.append(el('p','Een geslaagde livecontrole is hier nog niet vastgelegd.','kv-compact'));if(row.opportunity_id&&linkTo){const a=el('a','Open voorstel, verschil en vervolgstap →');a.href=linkTo(row.opportunity_id);detail.append(a);}body.append(detail);}li.append(body);timeline.append(li);} 5102 const li=el('li'),b=el('div');li.append(el('span','3','kv-number'));b.append(el('h3','Wat is zichtbaar op de website?'));const review=data.rendered; 5103 if(review?.available){b.append(el('p','WordPress-browsercontrole · '+review.checked_at+(review.current?'':' · pagina daarna gewijzigd'),'kv-compact'));for(const [device,label]of [['desktop','Desktop'],['mobile','Mobiel']]){const run=review.runs?.[device];if(!run)continue;const d=el('details'),summary=el('summary',label+' · '+(run.findings?.length||0)+' controlepunten');d.append(summary);const ul=el('ul');for(const f of run.findings||[]){const item=el('li',f.text);item.append(el('small',f.location,'kv-compact'));ul.append(item);}d.append(ul);if(!run.findings?.length)d.append(el('p','Geen aandachtspunten binnen deze beperkte controle.'));if(run.limited)d.append(el('p','Deze controle is beperkt door de omvang van de pagina.','kv-compact'));b.append(d);}b.append(el('p',review.note||'Dit is een beperkte browsercontrole; controleer de volledige pagina ook zelf.','kv-compact'));} 5104 else b.append(el('p',review?.unreachable?'De browsercontrole kon niet uit WordPress worden opgehaald. Controleer de pluginversie en verbinding.':'Open deze pagina bij Klusio → Pagina’s in WordPress en bewaar een controle op desktop en mobiel.')); 5105 li.append(b);timeline.append(li);s.append(timeline);host.append(s); 5106 const results=card('Wat veranderde er in Google?'),comparisons=story.comparisons||[];results.append(el('p',story.note||'Vergelijkingen beschrijven het gemeten verschil en bewijzen geen oorzaak.','kv-compact')); 5107 if(!comparisons.length)results.append(el('p','Er is voor deze pagina nog geen voor- en nameting beschikbaar. Zodra Klusio een wijziging en voldoende meetgegevens heeft, verschijnt de vergelijking hier.')); 5108 for(const c of comparisons){const block=el('details');block.open=comparisons.length===1;block.append(el('summary','Wijziging '+(c.changed_on||'datum onbekend')+' · '+(c.days||'?')+' dagen'));if(c.comparable&&c.metrics?.length){const table=el('table',undefined,'kv-table');table.append(el('caption','Google Search Console · vóór '+c.before.join(' t/m ')+' · na '+c.after.join(' t/m ')));const head=el('tr');head.append(el('th','Meting'),el('th','Vóór'),el('th','Na'));table.append(head);for(const m of c.metrics){const tr=el('tr');tr.append(el('th',m.label),el('td',fmt(m.before)),el('td',fmt(m.after)));table.append(tr);}block.append(table);if(c.low_data)block.append(el('p','De voormeting bevat weinig gegevens. Dit verschil is nog onvoldoende voor een stevige conclusie.','kv-compact'));if(!c.verified)block.append(el('p','Deze wijziging is als toegepast geregistreerd; uitvoering is niet onafhankelijk bevestigd.','kv-compact'));if(c.later_changes?.length)block.append(el('p','Er zijn ook latere wijzigingen op '+c.later_changes.join(', ')+'. Hun effect is niet afzonderlijk te bepalen.','kv-compact'));} 5109 else block.append(el('p',c.state==='pending'?'Deze nameting is nog niet beschikbaar'+(c.expected_on?'. Verwacht vanaf '+c.expected_on:'.'): 'Er zijn onvoldoende vergelijkbare gegevens voor dit meetmoment.'));results.append(block);}host.append(results); 5110 const ai=card('Hoe komt deze pagina terug in AI?'),e=data.ai||{};ai.append(el('p',(e.source||'Klusio AI-metingen')+' · '+(e.from||'?')+' t/m '+(e.to||'?')+'. Per vraag en AI-systeem de laatste beschikbare meting.','kv-compact'));const platforms={chatgpt:'ChatGPT',claude:'Claude',gemini:'Gemini',perplexity:'Perplexity',google_ai_mode:'Google AI-modus'}; 5111 for(const r of e.rows||[]){const detail=el('details');detail.append(el('summary',r.question),el('p',(platforms[r.platform]||r.platform)+' · '+r.checked_at+(r.fresh?'':' · oudere meting'),'kv-compact'));const badges=el('div',undefined,'kv-health-choices');for(const text of ['Gevonden: '+(r.found===null?'onbekend':r.found?'ja':'niet aangetoond'),'Pagina als bron: '+(r.cited?'ja':'nee'),'Bedrijf genoemd: '+(r.named?'ja':'nee')])badges.append(el('span',text));detail.append(badges,el('p',r.answer||''));ai.append(detail);} 5112 if(!e.rows?.length)ai.append(el('p',e.available===false?'De AI-metingen zijn nu niet beschikbaar.':'In deze selectie is geen controleerbare vondst of bronverwijzing naar deze pagina aangetroffen. Dit zegt niets over alle mogelijke AI-antwoorden.')); 5113 ai.append(el('p','Gevonden is geen bewijs dat AI de hele pagina heeft gelezen. Een bronverwijzing is geen aanbeveling. Een bedrijfsvermelding geldt voor het antwoord en is geen bewezen gevolg van deze pagina.','kv-compact'));if(e.limited)ai.append(el('p','Selectie beperkt tot de 1.000 recentste metingen.','kv-compact'));host.append(ai); 5114 }; 5115 const root=document.getElementById('kw-page-story');if(!root)return;const form=root.querySelector('form'),out=root.querySelector('[data-story-output]');form.onsubmit=async ev=>{ev.preventDefault();const button=form.querySelector('button');button.disabled=true;out.textContent='De paginastand ophalen…';const ctrl=new AbortController(),timer=setTimeout(()=>ctrl.abort(),55000);try{const body=new FormData(form);body.set('csrf_token',document.querySelector('.kw-page')?.dataset.csrf||'');const r=await fetch('/account/api/wordpress-page-story.php',{method:'POST',body,credentials:'same-origin',signal:ctrl.signal});let data;try{data=await r.json();}catch(_){throw Error('De server geeft geen leesbaar antwoord. Vernieuw de pagina en probeer het opnieuw.');}if(!data.success)throw Error(data.error||'De pagina kon niet worden opgehaald.');out.replaceChildren();window.klusioRenderPageStory(data.data,out,id=>'/account/groeimotor#opportunity-'+Number(id));}catch(e){out.textContent=e.name==='AbortError'?'Het ophalen duurt te lang. Probeer het later opnieuw; je website is niet gewijzigd.':e.message||'De paginastand is nu niet beschikbaar.';}finally{clearTimeout(timer);button.disabled=false;}}; 5116 })(); 5117 JS; 5118 } 5119 5120 add_filter('display_post_states',function($states,$post){if(get_post_meta($post->ID,'_klusio_preview_source',true))$states['klusio_preview']=__('Klusio-voorbeeld, publiceer vanuit Klusio','klusio-builder-bridge');return $states;},10,2); 5121 add_filter('rest_pre_insert_post',function($prepared,$request){ 5122 $id=(int)$request->get_param('id'); 5123 if($id && get_post_meta($id,'_klusio_preview_source',true) && in_array($request->get_param('status'),array('publish','future','private'),true))return new WP_Error('klusio_preview_private',__('Dit is een privévoorbeeld. Publiceer het oorspronkelijke artikel vanuit Klusio.','klusio-builder-bridge'),array('status'=>409)); 5124 return $prepared; 5125 },10,2); -
klusio-builder-bridge/trunk/readme.txt
r3789598 r3789687 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.2 7 Stable tag: 1.1 6.07 Stable tag: 1.17.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 13 13 == Description == 14 14 15 [Klusio](https://klusio.nl/) helps small and medium sized businesses get found in Google and in AI assistants. Klusio suggests concrete improvements to the texts on your website: headings, paragraphs, buttons, frequently asked questions and internal links. You approve changes in your Klusio account or, after separate authorization, in WordPress. 16 17 Pages built with Elementor or Bricks keep their text inside the page builder data, so a normal WordPress text update cannot change them without breaking the design. This plugin lets Klusio place an approved change exactly where it belongs, while the layout, styling and other content stay untouched. 18 19 = What the plugin does = 20 21 * Adds REST API routes under `klusio/v1`. Klusio calls them with a WordPress application password of a user on your site. 22 * Reads and writes only pages that this user may edit. Writing also requires the `unfiltered_html` capability, because page builder data contains HTML. 23 * Never overwrites newer work. Every write carries a fingerprint (hash) of the version Klusio read. If the page changed in the meantime, or someone has it open in the editor, nothing is written. 24 * Makes a compressed backup (up to three per page) and a WordPress revision before every change, and reads the result back to verify it. 25 * Restores a backup on request, again only when the page did not change since. 26 * Refreshes the Elementor CSS, the search text and the page cache of the changed page only. 27 * Supports Elementor pages, templates and global widgets, and Bricks pages, headers, footers, templates and global elements. 28 * Optional: when TranslatePress is active, saves the source language and its translation together. 29 * Optional: adds an approved item to a list that a page shows with a query loop, such as frequently asked questions that are each a post with the answer in an ACF field. Only existing categories, only text fields of that post type; restoring moves the item to the trash. 30 * Optional: releases a blog draft that Klusio prepared, only after a user who may publish posts enabled this for one Klusio account. 31 * Optional: on a multilingual site with WPML or Polylang, places the translation of a blog post as its own post in that language, linked to the original post. With TranslatePress, adds the translation of a blog post to the TranslatePress dictionary. A translation that was made on the site itself is never overwritten. 32 * Optional: stores a redirect that you approved in Klusio for an old address that gives a "page not found" error (404), to another address on your website. A redirect is only used when WordPress finds nothing at that address, so it never hides an existing page, post or product, or a redirect made with another plugin. 33 * Optional: with TranslatePress (and its SEO Pack, which translates page titles and descriptions), places the translated page title and meta description that you approved in Klusio for a page in another language. The previous translation is kept, so restoring brings it back. A title that is also a heading or text on the page is never changed this way, because TranslatePress would change that text too. 34 35 = Klusio inside WordPress = 36 37 The Klusio menu contains an overview, growth proposals, drafts and connection settings. Enable the account overview under Klusio, Connection, then open your Klusio account's WordPress settings to send the first overview. The existing Klusio WordPress synchronization refreshes it hourly when the connection is available. The screen shows when it was last updated and warns when the data is older than two hours. 38 39 Only administrators see the overview by default. An administrator can also grant access to editors who may edit and publish all pages. Turning the overview off deletes its local copy. WordPress page and post editors can use "Improve with Klusio" to open a page-specific analysis; access to the Klusio account still requires login and is checked separately. 40 41 Without additional authorization, proposals and drafts open in their existing Klusio account workflow. With the optional scoped work connection, administrators can handle supported actions inside WordPress using the same server-side services. The page editor also shows matching work and warns if the page changed after the last verified action. Local WordPress drafts open in the regular WordPress editor. No new write or publication permission is granted. Existing page content, builder styling, revisions and publication settings are preserved. 42 43 = Guided setup, settings and results = 44 45 Without an account or connected overview, the Klusio menu explains how to create an account, connect your site, enable the overview and verify the first synchronization. Installing alone does not send site data or publish content. 46 47 Administrators can prepare six email preferences and a work-mode choice in WordPress. These choices open a confirmation page in the Klusio account. An active Klusio account administrator must review and confirm them there. WordPress never receives an account login token and cannot change account permissions directly. Local editor access is managed separately in WordPress. 48 49 The Growth screen includes existing internal-link and conversion actions. Their links open the exact existing account workflow, with proposal review, permissions, backups and verification. Results show live-verified work and available Search Console comparisons after 14, 28 and 56 days. Sources, periods and uncertainty remain visible. Missing measurements are never replaced with invented results. 50 51 = Read and work without leaving WordPress = 52 53 The Pages section searches your editable WordPress pages and posts. A read-only check covers the stored title, heading order, empty image alternatives and generic link labels. It does not render shortcodes, run dynamic blocks, crawl links or claim to audit the complete public page. Known static text fields in Elementor and Bricks are included. Templates, image widgets and dynamic sources remain outside the check and are identified as requiring inspection in the real builder. Native edit and preview links respect your WordPress permissions. 54 55 Draft article text is available after the next successful synchronization with a compatible Klusio account. Text previews are bounded and clearly marked when incomplete; embedded images, scripts and trackers are not loaded. Proposal details show the same execution contract used by the account. Changes, final approvals and publishing use the existing checked account services. The optional work connection can bring these controls into WordPress. For full page drafts already present in this WordPress, you can open the current local preview and editor directly. 56 57 = Optional work connection (1.15) = 58 59 After enabling the work connection locally, an active Klusio account administrator confirms the exact website and chooses scopes: read, review proposals, edit drafts, publish/restore and change preferences. Only WordPress administrators can use these actions. Existing editor access remains read-only. Grants expire after 90 days, can be revoked in Klusio and stop when the site, integration credentials or account administrator access changes. Turning off the local connection deletes its local credential. No account browser session is shared with WordPress. 60 61 Supported proposals can be approved, rejected, deferred for seven days or given feedback. Feedback pauses execution until a new approval; it does not promise an automatic rewrite. Full before/after text is required for native on-page approval. Destructive operations such as deleting or merging pages remain outside this workflow. Restore requires the unchanged fingerprint of the applied version. 62 63 Unpublished, unlinked article drafts can be edited using the WordPress editor and media library. A newly selected photo replaces the old alt text with the selected media description, which can be edited before saving. Saving creates a backup and invalidates the previous review. Publishing requires reviewing the exact saved version again. Existing WordPress posts and customer edits are not overwritten by this editor. The actual first publication date is recorded separately from the planned date. A text preview is labelled as such; full page concepts use the real WordPress preview. Scheduling remains in the Klusio account. 64 65 Accepted tasks are stored on Klusio and remain visible in History when the browser closes. Repeating the same request does not create a second task. Interrupted writes require inspection instead of automatic replay. Page results distinguish retrieved AI search results, cited URLs and actual company mentions. Search Console comparisons retain their periods and do not claim causation. 66 67 = What the plugin does not do = 68 69 * The Klusio menu offers an optional account overview, growth proposals, drafts and contextual links to your account. There is no visitor tracking, advertising or unsolicited admin notice. 70 * External requests to Klusio require explicit local consent and separate account authorization. Installing the plugin alone does not enable this connection. 71 * It changes nothing on installation. A connected administrator can request changes within delegated scopes; existing approval, package, backup and conflict checks remain in force. 72 73 A Klusio account is required to use this plugin. 15 [Klusio](https://klusio.nl/) helps small and medium sized businesses improve their visibility in Google and AI assistants. This plugin connects your existing WordPress site to your Klusio account and brings relevant proposals, drafts and results into WordPress. 16 17 A Klusio account is required. Installing the plugin alone does not connect your site, send content or publish anything. 18 19 = Start with a useful improvement = 20 21 The Klusio menu guides you through connecting your account, choosing an important page and reviewing a concrete proposal. Keep one focus page per administrator, inspect the real website and decide what to improve. Existing package, approval and publication rules remain in force. 22 23 Guided setup works from WordPress or your Klusio account. A WordPress administrator confirms the exact website and a Klusio account administrator confirms the account and permissions. The handshake is resumable and verifies both ends before storing a connection. A new connection starts with automatic publication off; reconnecting preserves your existing publication preferences. 24 25 = Your Klusio work inside WordPress = 26 27 * Overview: your goal, selected proposals, current work, drafts and verified results, with a visible synchronization time. 28 * Growth: inspect the same execution contract as in your account. With explicit delegation, approve, reject or defer supported proposals and give feedback. Feedback pauses execution until a new approval. 29 * Drafts: edit unpublished, unlinked articles, choose a local media image and update its alt text. Saving preserves a backup and invalidates the previous review. Publishing requires confirmation of the exact saved version. 30 * Pages: find editable pages and posts, open their native editor and use "Improve with Klusio" for page-specific work. 31 * History: accepted work stays visible on the Klusio server when your browser closes. Repeated submissions do not create a second job; interrupted writes require inspection. 32 * Settings: manage supported notification and work-mode preferences through the same authorized account services. Without a delegated settings scope, choices open an account confirmation screen. 33 * Results: see registered changes, live verification, available Search Console comparisons and AI evidence. Retrieved search results, cited pages and actual company mentions remain separate. Missing measurements are never replaced with invented results. 34 35 The optional account overview refreshes through the existing hourly synchronization. It shows its age and warns after two hours. Administrators can enable read-only editor access locally. Turning the overview off removes its local copy. Account deep links retain your selected page or proposal and still require the correct account login. 36 37 = Preserve your Elementor and Bricks website = 38 39 Klusio can place approved changes in existing builder text fields, headings, buttons, FAQs and internal links. Layout, styling and unrelated content remain intact. Supported objects include Elementor pages, templates and global widgets, and Bricks pages, templates, headers, footers and global elements. 40 41 Before each write the plugin checks the page fingerprint and editing lock. If someone changed the page, it refuses to overwrite their work. It creates a compressed backup and WordPress revision, reads the result back and refreshes the affected page's generated CSS and cache. Restore also checks for newer changes. 42 43 Supported query-loop additions use existing public post types, categories and approved text fields. Optional WPML, Polylang and TranslatePress operations preserve independently edited translations. Approved redirects apply only to genuinely missing addresses, so they do not hide existing pages or other plugins' redirects. Existing Yoast and Rank Math page title and description routes remain compatible. 44 45 = Preview the real theme and inspect mobile = 46 47 Administrators can open an unpublished article as a separate private WordPress draft using the active theme. The saved account revision is the source. A new revision creates a new private copy; previous copies and local edits are retained. Featured images are shown when already present in the local media library; missing images are disclosed. Publish the canonical article from Klusio: these preview copies remain drafts while the plugin's protection is active. 48 49 For existing pages and private previews, inspect the actual rendered frontend at desktop and mobile widths. A bounded browser check identifies possible overflow, strong image cropping, missing image alternatives, heading gaps, unnamed fields and missing next steps. Builders and templates participate through their rendered output. The check submits no forms and changes no page content. 50 51 Save dated observations with a page fingerprint and read them back in WordPress or your account. Earlier reports remain available locally. Changes to theme, templates or external content can still make an observation outdated. This is not a full accessibility, conversion or SEO audit. A separate collapsed check of stored text remains available. 52 53 = Explicit connection and permissions = 54 55 The optional work connection requires local consent and an active account administrator's confirmation. Scopes separately allow reading, reviewing proposals, editing drafts, publishing/restoring and changing preferences. Only WordPress administrators execute delegated actions; editor access remains read-only. Grants expire after 90 days and can be revoked. Changed integration credentials, website identity or account administrator access invalidate the delegation. 56 57 The Connection screen checks WordPress and Klusio in both directions and shows scopes, expiry and the last received overview. New Application Passwords created through guided setup, and exact credentials identified from that setup, are restricted by the active plugin to supported editorial REST routes. User administration, plugin/theme management, general settings, batch calls and unrelated routes are blocked for those credentials. Normal administrator sessions and other integrations remain unchanged. 58 59 This technical restriction depends on the plugin staying active. Revoke the Application Password in WordPress to remove access independently. Older manual credentials are not silently changed; reconnect through guided setup to create a limited credential. Account scopes further limit operations initiated in the plugin. No account browser session or login token is shared with WordPress. 60 61 = Honest evidence, controlled changes = 62 63 Google comparisons show their sources, equal-length periods and data limitations. Low-data and later-change warnings remain visible. A difference after a change does not prove that the change caused it. Only explicitly successful live checks are presented as verified. 64 65 Complete before/after text is required for native on-page approval. Existing customer pages are not overwritten by the article draft editor. Destructive operations such as deleting or merging pages remain outside the proposal workflow. No visitor tracking, advertising or unsolicited customer messages are added. 74 66 75 67 == External services == 76 68 77 The guided connection contacts https://klusio.nl/account/api/wordpress-connect.php only after you choose to check or confirm a connection. It exchanges a short-lived connection ticket, installation URL, account identity, chosen permissions and an automatically generated Application Password for the confirming WordPress administrator. That separate password is sent only in an HTTPS server-to-server request and stored encrypted in Klusio. It has the administrator's existing WordPress permissions; account scopes separately limit actions initiated from the plugin. Revoke technical access in Users > Profile > Application Passwords. Revoke the account delegation in your Klusio account. No customer content is published by connecting.69 The guided connection contacts https://klusio.nl/account/api/wordpress-connect.php only after you choose to check or confirm a connection. It exchanges a short-lived connection ticket, installation URL, account identity, chosen permissions and an automatically generated Application Password for the confirming WordPress administrator. That separate password is sent only in an HTTPS server-to-server request and stored encrypted in Klusio. The active plugin limits positively identified guided credentials to editorial REST routes; the underlying Application Password belongs to the administrator and must be revoked if that protection is no longer wanted. Account scopes separately limit actions initiated from the plugin. Revoke technical access in Users > Profile > Application Passwords. Revoke the account delegation in your Klusio account. No customer content is published by connecting. 78 70 79 71 … … 94 86 * Clicking a link to your Klusio account passes the website address and, where relevant, the selected page address or item ID. Prepared notification and work-mode choices are also passed to the account confirmation screen when you submit that form. These are navigation context only: Klusio still checks your login, company and access to that item. 95 87 88 Saved browser reviews contain the page address, revision fingerprint, observation time and bounded descriptions of elements needing inspection. Form values and visitor data are not collected. Reports remain in WordPress and are fetched by the bound Klusio account on request. Private article preview copies also remain in WordPress. 89 96 90 No data about your visitors is exchanged. 97 91 … … 143 137 144 138 == Changelog == 139 140 = 1.17.0 = 141 * Guided first improvement with an important page, actionable checks and separate publication approval. 142 * Private article previews in the real WordPress theme; saved account revisions use separate copies and preserve local edits. 143 * Desktop and mobile rendered-page checks with dated findings, retained previous reports and shared account evidence. 144 * Round-trip connection health, expiry and explicit account permissions. Guided application passwords are restricted to editorial and Klusio routes while the plugin is active. 145 * One page story across WordPress and Klusio: proposals, changes, live verification and comparable Google/AI evidence. 146 145 147 146 148 = 1.16.0 =
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)