Plugin Directory

Changeset 3789687


Ignore:
Timestamp:
11/07/2026 01:53:21 PM (100 minutes ago)
Author:
klusio
Message:

Release Klusio 1.17.0: guided first improvement, private theme previews, rendered page reviews, scoped technical credentials and shared page evidence

Location:
klusio-builder-bridge
Files:
4 added
2 edited

Legend:

Unmodified
Added
Removed
  • klusio-builder-bridge/trunk/klusio-builder-bridge.php

    r3789598 r3789687  
    33 * Plugin Name: Klusio
    44 * 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.16.0
     5 * Version: 1.17.0
    66 * Requires at least: 5.6
    77 * Requires PHP: 7.2
    … …  
    7272    return;
    7373}
    74 define('KLUSIO_BRIDGE_VERSION', '1.16.0');
     74define('KLUSIO_BRIDGE_VERSION', '1.17.0');
    7575define('KLUSIO_BRIDGE_NAAM', 'klusio-builder-bridge');
    7676define('KLUSIO_BRIDGE_BACKUP_PREFIX', '_klusio_el_backup_');
    … …  
    39093909function klusio_bridge_workspace_link($section = 'overview', $id = 0, $page_url = '', $kind = '')
    39103910{
    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');
    39123912    if (!in_array($section, $allowed, true)) {
    39133913        $section = 'overview';
    … …  
    41894189        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>';
    41904190        klusio_bridge_workspace_local_buttons($post);
    4191         echo '</section><div 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>';
    41924192        $tips = array();
    41934193        if ($check['empty_title']) { $tips[] = __('De WordPress-titel is leeg. Geef de pagina een herkenbare naam.', 'klusio-builder-bridge'); }
    … …  
    42064206        foreach ($check['headings'] as $heading) { echo '<li><span>H' . esc_html((string)$heading['level']) . '</span> ' . esc_html($heading['text']) . '</li>'; }
    42074207        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>';
    42084211        klusio_bridge_native_page((string)get_permalink($post));
     4212        if (current_user_can('manage_options') && klusio_bridge_native_connection()) { return; }
    42094213        $related = array();
    42104214        foreach (array('next','doing','done') as $group) {
    … …  
    43024306        return;
    43034307    }
     4308    if ($view === 'overview') klusio_bridge_value_start($d);
    43044309    if (klusio_bridge_native_render($view, $d)) {
    43054310        // The native workspace is hydrated from the current canonical account state.
    … …  
    44944499    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));
    44954500    $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')));
    44974502}
    44984503add_action('rest_api_init',function(){
    … …  
    45464551    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>';
    45474552    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>';
    45484554    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>';
    45494555    echo '</section>';
    … …  
    46254631}
    46264632root.append(preview(d.content||'',d.title));
     4633if(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);}));
    46274634if(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.');
    46284635box.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);}
    … …  
    46344641const 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){}}
    46354642if(['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);}
     4643function 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.'));}
    46464644function 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.'));}
    46474645async 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.'));}
    … …  
    47394737        $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()));
    47404738        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')); }
    47414740        $f['app_uuid'] = $new[1]['uuid']; $f['state'] = 'checking';
    47424741        if (!klusio_bridge_connect_save($f)) {
    … …  
    48064805        $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'));
    48074806        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>';
    48094808        klusio_bridge_connect_button('approve', __('Bevestigen en verbinden', 'klusio-builder-bridge'), true);
    48104809    } 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>';
    48124811    } else {
    48134812        echo '<p>' . esc_html__('Controleer de laatste bevestiging. De plugin maakt bij deze controle geen nieuwe toegang aan.', 'klusio-builder-bridge') . '</p>';
    … …  
    48204819    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();})();");
    48214820}
     4821
     4822/** 1.17: useful first steps, theme previews and bounded rendered-page evidence. */
     4823function 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}
     4833function 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}
     4836function 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}
     4840function 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.
     4870add_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);
     4871add_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);
     4872function 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}
     4879function 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}
     4900add_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});
     4922add_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});
     4933add_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. */
     4941function 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}
     4948function 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}
     4954add_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);
     4957add_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);
     4960function 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. */
     4982function 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}
     4997add_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);
     5001function 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}
     5008function 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}
     5018add_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});
     5025function 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}
     5030function 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}}
     5033CSS;
     5034}
     5035function klusio_bridge_value_script() {
     5036    return <<<'JS'
     5037(function(){'use strict';
     5038const cfg=window.klusioValue;if(!cfg)return;
     5039const el=(tag,text,cls)=>{const n=document.createElement(tag);if(text!==undefined)n.textContent=text;if(cls)n.className=cls;return n;};
     5040const btn=(text,fn)=>{const b=el('button',text,'kbw-btn');b.type='button';b.onclick=fn;return b;};
     5041async 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);}}
     5042function message(host,text){host.textContent=text;host.className='kv-message';host.setAttribute('role','status');}
     5043function 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'));}
     5044function inspect(doc,width){
     5045const findings=[],nodes=[...doc.querySelectorAll('body *')],limited=nodes.length>3000;
     5046const 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"]');};
     5047const 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;};
     5048const add=(type,text,n)=>{if(findings.length<60)findings.push({type,text,location:location(n)});};
     5049const seen=new Set();let previous=0,cta=false,h1=0;
     5050for(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);}
     5051if(/^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;}
     5052if(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);}
     5053if(['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);
     5054if(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}
     5056const 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);
     5057return {width,findings,limited:limited||findings.length>=60};}
     5058window.klusioInspectRendered=inspect;
     5059async function openReview(stage,id,initial){
     5060if(stage.klusioDispose)stage.klusioDispose();stage.replaceChildren();const notice=el('div');stage.append(notice);message(notice,'De eigen websiteopmaak openen…');let info;
     5061try{info=initial||await call({action:'inspect',id});}catch(e){message(notice,e.message);return;}
     5062let 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;}
     5063const 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;
     5064function 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';}
     5065const observer=new ResizeObserver(size);observer.observe(shell);
     5066function 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();
     5067const 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;}});
     5068const 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;
     5069const 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);
     5070const timer=setTimeout(()=>{if(!loaded)message(notice,'Het voorbeeld is nog niet geladen. Je kunt het ook in een apart tabblad openen.');},15000);
     5071frame.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.');}};
     5072stage.klusioDispose=()=>{observer.disconnect();clearTimeout(timer);frame.onload=null;};frame.src=url.href;size();if(info.report)report(results,info.report);
     5073}
     5074window.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);}};
     5075document.querySelectorAll('.kv-review').forEach(root=>{root.querySelector('.kv-open').onclick=()=>openReview(root.querySelector('.kv-stage'),Number(root.dataset.post));});
     5076const 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})();
     5078JS;
     5079}
     5080add_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. */
     5091function klusio_bridge_value_story_script() {
     5092    return <<<'JS'
     5093(function(){'use strict';
     5094const el=(tag,text,cls)=>{const n=document.createElement(tag);if(text!==undefined)n.textContent=text;if(cls)n.className=cls;return n;};
     5095function card(title){const n=el('section',undefined,'kv-story-card');n.append(el('h2',title));return n;}
     5096const fmt=v=>new Intl.NumberFormat('nl-NL',{maximumFractionDigits:1}).format(v);
     5097window.klusioRenderPageStory=function(data,host,linkTo){
     5098const story=data.story||{},s=card('Van aandachtspunt naar resultaat'),timeline=el('ol',undefined,'kv-story');
     5099const stages=[['Wat zagen we?',(story.steps||[]).filter(s=>s.kind==='proposal')],['Wat is er aangepast?',(story.steps||[]).filter(s=>s.kind==='change')]];
     5100for(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.'));
     5101for(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);}
     5102const 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;
     5103if(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'));}
     5104else 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.'));
     5105li.append(b);timeline.append(li);s.append(timeline);host.append(s);
     5106const 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'));
     5107if(!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.'));
     5108for(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'));}
     5109else 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);
     5110const 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'};
     5111for(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);}
     5112if(!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.'));
     5113ai.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};
     5115const 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})();
     5117JS;
     5118}
     5119
     5120add_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);
     5121add_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  
    55Tested up to: 7.1
    66Requires PHP: 7.2
    7 Stable tag: 1.16.0
     7Stable tag: 1.17.0
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    1313== Description ==
    1414
    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
     17A 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
     21The 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
     23Guided 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
     35The 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
     39Klusio 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
     41Before 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
     43Supported 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
     47Administrators 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
     49For 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
     51Save 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
     55The 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
     57The 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
     59This 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
     63Google 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
     65Complete 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.
    7466
    7567== External services ==
    7668
    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.
     69The 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.
    7870
    7971
    … …  
    9486* 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.
    9587
     88Saved 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
    9690No data about your visitors is exchanged.
    9791
    … …  
    143137
    144138== 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
    145147
    146148= 1.16.0 =
Note: See TracChangeset for help on using the changeset viewer.