diff --git a/assets/dist/seo.js b/assets/dist/seo.js index fc65529..d22d92a 100644 --- a/assets/dist/seo.js +++ b/assets/dist/seo.js @@ -1 +1 @@ -const V={count:":chars characters · :px / :max px",good:"Good length",emptyTitle:"Empty: the title template is used",emptyDescription:"Empty: the template or the excerpt is used",shortTitle:"Short: aim for 30–60 characters",shortDescription:"Short: aim for 120–160 characters",longTitle:"Too long: Google cuts titles after about 580 px",longDescription:"Too long: Google cuts descriptions after about 920 px on desktop and 680 px on mobile",indexing:"Indexing",links:"Links",inherit:"Inherit",siteDefault:"Site default",index:"Index",noindex:"Noindex",follow:"Follow",nofollow:"Nofollow",advanced:"Advanced directives",advancedSet:{one:":count set",other:":count set"},maxSnippet:"Maximum snippet length (characters)",maxSnippetHint:"Empty: no limit. 0: no snippet. -1: no limit.",maxImagePreview:"Image preview size",notSet:"Not set",none:"None",standard:"Standard",large:"Large",maxVideoPreview:"Maximum video preview (seconds)",noarchive:"Do not show a cached copy (noarchive)",nosnippet:"Do not show a snippet (nosnippet)",noimageindex:"Do not index images (noimageindex)",unavailableAfter:"Remove from results after",invalidNumber:"Enter a whole number, -1 or more.",desktop:"Desktop",mobile:"Mobile",device:"Preview device",language:"Language: :locale",fromTemplate:"Title from the template “:template”",fromContent:"Description from the content excerpt",noDescription:"No description: Google picks text from the page.",noUrl:"The address appears after the first save.",savedNote:"The address and the H1 come from the saved record. Save to refresh them.",checking:"Checking for duplicates…",dupTitle:{one:"Same title as :count other page: :list",other:"Same title as :count other pages: :list"},dupDescription:{one:"Same description as :count other page: :list",other:"Same description as :count other pages: :list"},andMore:"and :count more",keyphrase:"Focus keyphrase “:phrase”",kpTitle:"In the SEO title",kpTitleNo:"Not in the SEO title",kpH1:"In the H1",kpH1No:"Not in the H1",kpDesc:"In the meta description",kpDescNo:"Not in the meta description",kpUrl:"In the address",kpUrlNo:"Not in the address",kpLength:"Text length: :count words",kpLengthNo:"Text length: :count words, aim for at least 300",contextFailed:"The preview data could not be loaded.",imageUpload:"Upload image",imageReplace:"Replace image",imageRemove:"Remove",imageUploading:"Uploading…",imageFailed:"The image could not be uploaded.",imageAlt:"Social image preview",size:":size of 500 KB",clean:"No problems found.",unknown:"Line :line: “:word” is not a robots.txt directive.",unknownSuggest:"Line :line: “:word” is not a robots.txt directive. Did you mean “:suggestion”?",noGroup:"Line :line: this rule has no User-agent line above it, so crawlers ignore it.",badPath:"Line :line: a path must start with / or *.",noColon:"Line :line: write the directive as “Name: value”.",ownSitemap:"Line :line: Sitemap lines are added automatically. This one is kept as written.",crawlDelay:"Line :line: Google ignores Crawl-delay. Bing and Yandex use it.",moreFindings:"and :count more",tooLarge:"Over 500 KB: the file cannot be saved.",url:"Address",urlPlaceholder:"/konto/ustawienia",agent:"Crawler",allowedRule:"Allowed for :agent. Rule: line :line “:rule”.",allowedNone:"Allowed for :agent: no rule matches this address.",blocked:"Blocked for :agent by line :line “:rule”.",blockedBot:"Blocked for :agent by its switch on the AI bots tab.",unsaved:"Tests the text in the editor and the AI bot switches, including unsaved changes.",stagingNote:"On this environment every address is blocked. The result shows the production behaviour.",urlRequired:"Enter an address starting with /.",exportHeading:"Export",importHeading:"Import",dataset:"Data",dsRedirects:"Redirects",dsRules:"URL rules",dsRecords:"SEO of records",download:"Download CSV",chooseFile:"Choose CSV file",noFile:"No file chosen",summary:":new new, :changed changed, :unchanged unchanged, :refused refused",bNew:"New",bChanged:"Changed",bUnchanged:"Unchanged",bRefused:"Refused",bWarning:"Warning",colLine:"Line",colChange:"Change",colAddress:"Address",colField:"Field",colBefore:"Before",colAfter:"After",colNote:"Note",rowCap:"Showing the first 200 of :count rows. All rows are applied.",refusedBlocks:"Fix the refused rows in the file and preview it again. Nothing is applied while a row is refused.",nothing:"The file changes nothing.",discard:"Discard preview",fileTooLarge:"This file is larger than :limit. Split it into smaller files, or import it through MCP.",readFailed:"The file could not be read.",openHistory:"Open History",applyCount:{one:"Apply :count change",other:"Apply :count changes"}},ee={count:"Znaki: :chars · :px / :max px",good:"Dobra długość",emptyTitle:"Puste: używany jest szablon tytułu",emptyDescription:"Puste: używany jest szablon lub zajawka",shortTitle:"Krótki: celuj w 30–60 znaków",shortDescription:"Krótki: celuj w 120–160 znaków",longTitle:"Za długi: Google ucina tytuły po ok. 580 px",longDescription:"Za długi: Google ucina opisy po ok. 920 px na komputerze i 680 px na telefonie",indexing:"Indeksowanie",links:"Linki",inherit:"Dziedzicz",siteDefault:"Domyślne",index:"Indeksuj",noindex:"Nie indeksuj",follow:"Śledź",nofollow:"Nie śledź",advanced:"Dyrektywy zaawansowane",advancedSet:{one:"ustawiona :count",few:"ustawione :count",many:"ustawionych :count",other:"ustawione :count"},maxSnippet:"Maksymalna długość fragmentu (znaki)",maxSnippetHint:"Puste: bez limitu. 0: bez fragmentu. -1: bez limitu.",maxImagePreview:"Rozmiar podglądu obrazu",notSet:"Nie ustawiono",none:"Brak",standard:"Standardowy",large:"Duży",maxVideoPreview:"Maksymalny podgląd wideo (sekundy)",noarchive:"Nie pokazuj kopii (noarchive)",nosnippet:"Nie pokazuj fragmentu (nosnippet)",noimageindex:"Nie indeksuj obrazów (noimageindex)",unavailableAfter:"Usuń z wyników po dniu",invalidNumber:"Wpisz liczbę całkowitą, -1 lub większą.",desktop:"Komputer",mobile:"Telefon",device:"Urządzenie podglądu",language:"Język: :locale",fromTemplate:"Tytuł z szablonu „:template”",fromContent:"Opis z zajawki treści",noDescription:"Brak opisu: Google wybierze tekst ze strony.",noUrl:"Adres pojawi się po pierwszym zapisie.",savedNote:"Adres i H1 pochodzą z zapisanego rekordu. Zapisz, aby je odświeżyć.",checking:"Sprawdzanie duplikatów…",dupTitle:{one:"Ten sam tytuł co :count inna strona: :list",few:"Ten sam tytuł co :count inne strony: :list",many:"Ten sam tytuł co :count innych stron: :list",other:"Ten sam tytuł co :count innej strony: :list"},dupDescription:{one:"Ten sam opis co :count inna strona: :list",few:"Ten sam opis co :count inne strony: :list",many:"Ten sam opis co :count innych stron: :list",other:"Ten sam opis co :count innej strony: :list"},andMore:"i :count więcej",keyphrase:"Fraza kluczowa „:phrase”",kpTitle:"W tytule SEO",kpTitleNo:"Brak w tytule SEO",kpH1:"W nagłówku H1",kpH1No:"Brak w nagłówku H1",kpDesc:"W opisie meta",kpDescNo:"Brak w opisie meta",kpUrl:"W adresie",kpUrlNo:"Brak w adresie",kpLength:"Długość tekstu: :count słów",kpLengthNo:"Długość tekstu: :count słów, celuj w co najmniej 300",contextFailed:"Nie udało się wczytać danych podglądu.",imageUpload:"Wgraj obraz",imageReplace:"Zmień obraz",imageRemove:"Usuń",imageUploading:"Wgrywanie…",imageFailed:"Nie udało się wgrać obrazu.",imageAlt:"Podgląd obrazu w mediach społecznościowych",size:":size z 500 KB",clean:"Nie znaleziono problemów.",unknown:"Wiersz :line: „:word” nie jest dyrektywą robots.txt.",unknownSuggest:"Wiersz :line: „:word” nie jest dyrektywą robots.txt. Czy chodziło o „:suggestion”?",noGroup:"Wiersz :line: nad tą regułą nie ma wiersza User-agent, więc roboty ją ignorują.",badPath:"Wiersz :line: ścieżka musi zaczynać się od / lub *.",noColon:"Wiersz :line: zapisz dyrektywę jako „Nazwa: wartość”.",ownSitemap:"Wiersz :line: wiersze Sitemap są dodawane automatycznie. Ten zostaje bez zmian.",crawlDelay:"Wiersz :line: Google ignoruje Crawl-delay. Bing i Yandex go stosują.",moreFindings:"i :count więcej",tooLarge:"Ponad 500 KB: nie można zapisać pliku.",url:"Adres",urlPlaceholder:"/konto/ustawienia",agent:"Robot",allowedRule:"Dozwolone dla :agent. Reguła: wiersz :line „:rule”.",allowedNone:"Dozwolone dla :agent: żadna reguła nie pasuje do tego adresu.",blocked:"Zablokowane dla :agent przez wiersz :line „:rule”.",blockedBot:"Zablokowane dla :agent przełącznikiem w zakładce Boty AI.",unsaved:"Sprawdza tekst w edytorze i przełączniki botów AI, także niezapisane zmiany.",stagingNote:"W tym środowisku każdy adres jest zablokowany. Wynik pokazuje zachowanie na produkcji.",urlRequired:"Wpisz adres zaczynający się od /.",exportHeading:"Eksport",importHeading:"Import",dataset:"Dane",dsRedirects:"Przekierowania",dsRules:"Reguły adresów",dsRecords:"SEO rekordów",download:"Pobierz CSV",chooseFile:"Wybierz plik CSV",noFile:"Nie wybrano pliku",summary:"Nowe: :new, zmienione: :changed, bez zmian: :unchanged, odrzucone: :refused",bNew:"Nowy",bChanged:"Zmieniony",bUnchanged:"Bez zmian",bRefused:"Odrzucony",bWarning:"Ostrzeżenie",colLine:"Wiersz",colChange:"Zmiana",colAddress:"Adres",colField:"Pole",colBefore:"Przed",colAfter:"Po",colNote:"Uwagi",rowCap:"Pokazano pierwsze 200 z :count wierszy. Zastosowane zostaną wszystkie.",refusedBlocks:"Popraw odrzucone wiersze w pliku i ponownie wyświetl podgląd. Nic nie zostanie zastosowane, dopóki jakiś wiersz jest odrzucony.",nothing:"Plik niczego nie zmienia.",discard:"Odrzuć podgląd",fileTooLarge:"Ten plik jest większy niż :limit. Podziel go na mniejsze pliki lub zaimportuj przez MCP.",readFailed:"Nie udało się odczytać pliku.",openHistory:"Otwórz Historię zmian",applyCount:{one:"Zastosuj :count zmianę",few:"Zastosuj :count zmiany",many:"Zastosuj :count zmian",other:"Zastosuj :count zmiany"}},te={en:V,pl:ee};function y(i){return(i.getAttribute("ui-locale")||document.documentElement.lang||"en").toLowerCase().split(/[-_]/)[0]||"en"}function L(i){return te[i]??V}function v(i,e={}){const t=Object.keys(e).sort((n,r)=>r.length-n.length);let s=i;for(const n of t)s=s.split(":"+n).join(String(e[n]));return s}function B(i,e,t,s={}){let n="other";try{n=new Intl.PluralRules(t).select(e)}catch{n=e===1?"one":"other"}const r=n==="one"&&i.one||n==="few"&&i.few||n==="many"&&i.many||i.other;return v(r,{...s,count:new Intl.NumberFormat(t).format(e)})}function A(i,e){try{return new Intl.NumberFormat(e).format(i)}catch{return String(i)}}const H="http://www.w3.org/2000/svg",se={info:["M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z","M12 16v-4","M12 8h.01"],"triangle-alert":["m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3","M12 9v4","M12 17h.01"],"circle-check":["M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z","m9 12 2 2 4-4"],"circle-alert":["M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z","M12 8v4","M12 16h.01"],"chevron-down":["m6 9 6 6 6-6"]};function S(i,e=14){const t=document.createElementNS(H,"svg");t.setAttribute("width",String(e)),t.setAttribute("height",String(e)),t.setAttribute("viewBox","0 0 24 24"),t.setAttribute("fill","none"),t.setAttribute("stroke","currentColor"),t.setAttribute("stroke-width","2"),t.setAttribute("stroke-linecap","round"),t.setAttribute("stroke-linejoin","round"),t.setAttribute("aria-hidden","true"),t.setAttribute("class","seo-icon seo-icon--"+i);for(const s of se[i]??[]){const n=document.createElementNS(H,"path");n.setAttribute("d",s),t.appendChild(n)}return t}function o(i,e="",t){const s=document.createElement(i);return e&&(s.className=e),t!==void 0&&(s.textContent=t),s}function I(i,e,t){const s=i.getAttribute(e);if(s===null||s==="")return t;try{return JSON.parse(s)}catch{return t}}function T(i,e,t){i.dispatchEvent(new CustomEvent(e,{detail:t,bubbles:!0,composed:!0}))}let W=0;function E(i){return W+=1,`${i}-${W}`}const ne="golem15-seo-image";function ie(i){return/^\/(?!\/)/.test(i)||/^https?:\/\//i.test(i)}class oe extends HTMLElement{static observedAttributes=["disabled","invalid","ui-locale"];current="";pending="";error="";onUploadResult=e=>{const t=e.detail;!t||t.id!==this.pending||(this.pending="",typeof t.url=="string"&&t.url!==""?(this.current=t.url,this.error="",T(this,"summer-change",{value:this.current})):this.error=t.error||this.labels().imageFailed,this.render())};get value(){return this.current}set value(e){this.current=typeof e=="string"?e:"",this.render()}connectedCallback(){this.addEventListener("summer-upload-result",this.onUploadResult),this.render()}disconnectedCallback(){this.removeEventListener("summer-upload-result",this.onUploadResult)}attributeChangedCallback(){this.isConnected&&this.render()}labels(){return L(y(this))}render(){const e=this.labels(),t=this.hasAttribute("disabled"),s=[];if(this.current&&ie(this.current)){const r=o("div","seo-image__preview"),a=o("img","seo-image__img");a.src=this.current,a.alt=e.imageAlt,a.width=240,a.height=126,r.append(a),s.push(r)}if(!t){const r=o("div","seo-image__actions"),a=E("seo-image-file"),l=o("input","seo-visually-hidden");l.type="file",l.accept="image/jpeg,image/png,image/gif,image/webp",l.id=a,l.addEventListener("change",()=>{const d=l.files?.[0];d&&(this.pending=E("seo-image-upload"),this.error="",this.render(),T(this,"summer-upload",{id:this.pending,file:d}))});const c=o("label","seo-image__button",this.current?e.imageReplace:e.imageUpload);if(c.htmlFor=a,r.append(l,c),this.current){const d=o("button","seo-image__button seo-image__button--secondary",e.imageRemove);d.type="button",d.addEventListener("click",()=>{this.current="",this.error="",T(this,"summer-change",{value:""}),this.render()}),r.append(d)}s.push(r)}const n=o("div","seo-image__status"+(this.error?" seo-image__status--error":""));n.setAttribute("role","status"),n.textContent=this.pending?e.imageUploading:this.error,s.push(n),this.classList.toggle("seo-image--invalid",this.hasAttribute("invalid")),this.replaceChildren(...s)}}const re="golem15-seo-robots";function M(){return{index:"",follow:"",noarchive:!1,nosnippet:!1,noimageindex:!1,maxSnippet:null,maxImagePreview:"",maxVideoPreview:null,unavailableAfter:""}}function ae(i){const e=M(),t=i.trim();if(t==="")return e;const s=new Set;for(const n of t.split(",")){const r=n.trim().toLowerCase();if(r==="")return null;const a=r.indexOf(":"),l=(a<0?r:r.slice(0,a)).trim(),c=a<0?null:r.slice(a+1).trim(),d=l==="noindex"?"index":l==="nofollow"?"follow":l;if(s.has(d))return null;switch(s.add(d),l){case"index":case"noindex":if(c!==null)return null;e.index=l;break;case"follow":case"nofollow":if(c!==null)return null;e.follow=l;break;case"noarchive":case"nosnippet":case"noimageindex":if(c!==null)return null;e[l]=!0;break;case"max-snippet":case"max-video-preview":{if(c===null||!/^-?\d+$/.test(c))return null;const h=Number.parseInt(c,10);if(h<-1)return null;l==="max-snippet"?e.maxSnippet=h:e.maxVideoPreview=h;break}case"max-image-preview":if(c!=="none"&&c!=="standard"&&c!=="large")return null;e.maxImagePreview=c;break;case"unavailable_after":if(c===null||!/^\d{4}-\d{2}-\d{2}$/.test(c)||Number.isNaN(Date.parse(c+"T00:00:00Z")))return null;e.unavailableAfter=c;break;default:return null}}return e}function le(i){const e=[];return i.index&&e.push(i.index),i.follow&&e.push(i.follow),i.noarchive&&e.push("noarchive"),i.nosnippet&&e.push("nosnippet"),i.noimageindex&&e.push("noimageindex"),i.maxSnippet!==null&&e.push("max-snippet:"+i.maxSnippet),i.maxImagePreview&&e.push("max-image-preview:"+i.maxImagePreview),i.maxVideoPreview!==null&&e.push("max-video-preview:"+i.maxVideoPreview),i.unavailableAfter&&e.push("unavailable_after:"+i.unavailableAfter),e.join(",")}function D(i){return[i.noarchive,i.nosnippet,i.noimageindex,i.maxSnippet!==null,i.maxImagePreview!=="",i.maxVideoPreview!==null,i.unavailableAfter!==""].filter(Boolean).length}class ce extends HTMLElement{static observedAttributes=["options","disabled","invalid","ui-locale","locale"];current="";model=M();open=!1;get value(){return this.current}set value(e){const t=typeof e=="string"?e:e==null?"":String(e);t===this.current&&this.childElementCount>0||(this.current=t,this.model=ae(t)??M(),this.open=this.open||D(this.model)>0,this.render())}connectedCallback(){this.open=D(this.model)>0,this.render()}attributeChangedCallback(){this.isConnected&&this.render()}labels(){return L(y(this))}commit(){this.current=le(this.model),T(this,"summer-change",{value:this.current}),this.render()}render(){const e=this.labels(),t=this.hasAttribute("disabled"),n=I(this,"options",{}).scope==="type"?e.siteDefault:e.inherit;this.classList.toggle("seo-robots--invalid",this.hasAttribute("invalid"));const r=document.activeElement&&this.contains(document.activeElement)?document.activeElement.dataset.focusKey:void 0,a=o("div","seo-robots__groups");a.append(this.radioGroup(e.indexing,"index",[["",n,"inherit"],["index",e.index,"ok"],["noindex",e.noindex,"danger"]],t),this.radioGroup(e.links,"follow",[["",n,"inherit"],["follow",e.follow,"ok"],["nofollow",e.nofollow,"danger"]],t));const l=E("seo-robots-advanced"),c=o("button","seo-robots__toggle");c.type="button",c.setAttribute("aria-expanded",this.open?"true":"false"),c.setAttribute("aria-controls",l),c.dataset.focusKey="toggle",c.append(o("span","",e.advanced),S("chevron-down")),c.addEventListener("click",()=>{this.open=!this.open,this.render()});const d=o("div","seo-robots__advanced-row");d.append(c);const h=D(this.model);h>0&&d.append(o("span","seo-robots__summary",B(e.advancedSet,h,y(this))));const u=o("div","seo-robots__panel");u.id=l,u.hidden=!this.open,u.append(this.numberField(e.maxSnippet,e.maxSnippetHint,"maxSnippet",t,e),this.selectField(e,t),this.numberField(e.maxVideoPreview,"","maxVideoPreview",t,e),this.dateField(e,t));const g=o("div","seo-robots__checks");for(const b of["noarchive","nosnippet","noimageindex"]){const p=o("label","seo-robots__check"),f=o("input");f.type="checkbox",f.checked=this.model[b],f.disabled=t,f.dataset.focusKey=b,f.addEventListener("change",()=>{this.model[b]=f.checked,this.commit()}),p.append(f,o("span","",e[b])),g.append(p)}u.append(g),this.replaceChildren(a,d,u),r&&this.querySelector(`[data-focus-key="${r}"]`)?.focus()}radioGroup(e,t,s,n){const r=o("div","seo-robots__group"),a=E("seo-robots-caption"),l=o("span","seo-robots__caption",e);l.id=a;const c=o("div","seo-robots__track");c.setAttribute("role","radiogroup"),c.setAttribute("aria-labelledby",a);const d=this.model[t];return s.forEach(([h,u,g],b)=>{const p=o("button","seo-robots__segment seo-robots__segment--"+g,u);p.type="button",p.setAttribute("role","radio");const f=d===h;p.setAttribute("aria-checked",f?"true":"false"),p.tabIndex=f?0:-1,p.disabled=n,p.dataset.focusKey=t+"-"+b,p.addEventListener("click",()=>this.choose(t,h)),p.addEventListener("keydown",w=>{const x=w.key==="ArrowRight"||w.key==="ArrowDown"?1:w.key==="ArrowLeft"||w.key==="ArrowUp"?-1:0;if(x===0||n)return;w.preventDefault();const m=(b+x+s.length)%s.length;this.choose(t,s[m][0]),this.querySelector(`[data-focus-key="${t}-${m}"]`)?.focus()}),c.append(p)}),r.append(l,c),r}choose(e,t){this.hasAttribute("disabled")||(e==="index"?this.model.index=t:this.model.follow=t,this.commit())}numberField(e,t,s,n,r){const a=o("label","seo-robots__field");a.append(o("span","seo-robots__label",e));const l=o("input","seo-robots__input");l.type="number",l.min="-1",l.step="1",l.disabled=n,l.dataset.focusKey=s;const c=this.model[s];l.value=c===null?"":String(c);const d=o("span","seo-robots__error");return d.setAttribute("role","status"),l.addEventListener("change",()=>{const h=l.value.trim();if(h===""){this.model[s]=null,this.commit();return}if(!/^-?\d+$/.test(h)||Number.parseInt(h,10)<-1){d.textContent=r.invalidNumber;return}this.model[s]=Number.parseInt(h,10),this.commit()}),a.append(l),t&&a.append(o("span","seo-robots__hint",t)),a.append(d),a}selectField(e,t){const s=o("label","seo-robots__field");s.append(o("span","seo-robots__label",e.maxImagePreview));const n=o("select","seo-robots__input");n.disabled=t,n.dataset.focusKey="maxImagePreview";for(const[r,a]of[["",e.notSet],["none",e.none],["standard",e.standard],["large",e.large]]){const l=o("option","",a);l.value=r,l.selected=this.model.maxImagePreview===r,n.append(l)}return n.addEventListener("change",()=>{this.model.maxImagePreview=n.value,this.commit()}),s.append(n),s}dateField(e,t){const s=o("label","seo-robots__field");s.append(o("span","seo-robots__label",e.unavailableAfter));const n=o("input","seo-robots__input");return n.type="date",n.disabled=t,n.dataset.focusKey="unavailableAfter",n.value=this.model.unavailableAfter,n.addEventListener("change",()=>{this.model.unavailableAfter=/^\d{4}-\d{2}-\d{2}$/.test(n.value)?n.value:"",this.commit()}),s.append(n),s}}const de="golem15-seo-robotstester",G=["Googlebot","Googlebot-Image","Bingbot","*"];function ue(i,e){const t={agent:e.agent,line:e.line,rule:e.rule};return e.allowed?e.line>0?v(i.allowedRule,t):v(i.allowedNone,t):e.bot?v(i.blockedBot,t):v(i.blocked,t)}class he extends HTMLElement{static observedAttributes=["busy","state","ui-locale","disabled","label","busy-label"];context=null;result=null;error="";inflight=null;queued=null;connected=!1;input=null;select=null;button=null;line=null;notes=null;builtFor="";onResult=e=>{const s=e.detail?.data;this.inflight=null,s&&s.op==="context"?(this.context=s,this.build(!0)):s&&s.op==="test"&&(this.result=s,this.error="",this.renderResult()),this.flush()};connectedCallback(){this.connected=!0,this.addEventListener("summer-result",this.onResult),this.build(!1),this.request({op:"context"})}disconnectedCallback(){this.connected=!1,this.removeEventListener("summer-result",this.onResult)}attributeChangedCallback(e,t,s){if(this.connected){if(e==="busy"&&s===null&&this.flush(),e==="state"&&s==="error"&&(this.inflight=null),e==="ui-locale"){this.build(!0);return}this.syncControls()}}labels(){return L(y(this))}request(e){if(this.hasAttribute("busy")||this.inflight){this.queued=e;return}this.inflight=e,T(this,"summer-action",{payload:e})}flush(){if(this.hasAttribute("busy")||this.inflight||!this.queued)return;const e=this.queued;this.queued=null,this.request(e)}build(e){const t=y(this)+"|"+(this.context?.agents??G).join(",");if(!e&&this.builtFor===t&&this.input){this.syncControls();return}this.builtFor=t;const s=this.labels(),n=this.input?.value??"",r=this.select?.value??"Googlebot";this.replaceChildren();const a=o("div","seo-robotstester__row"),l=E("seo-robotstester-url"),c=o("div","seo-robotstester__field seo-robotstester__field--url"),d=o("label","seo-robotstester__label",s.url);d.htmlFor=l;const h=o("input","seo-robotstester__input");h.id=l,h.type="text",h.placeholder=s.urlPlaceholder,h.value=n,h.addEventListener("keydown",m=>{m.key==="Enter"&&(m.preventDefault(),this.test())}),c.append(d,h);const u=E("seo-robotstester-agent"),g=o("div","seo-robotstester__field seo-robotstester__field--agent"),b=o("label","seo-robotstester__label",s.agent);b.htmlFor=u;const p=o("select","seo-robotstester__select");p.id=u;for(const m of this.context?.agents??G){const k=o("option","",m);k.value=m,p.append(k)}p.value=r,p.value!==r&&(p.value="Googlebot"),g.append(b,p);const f=o("button","seo-robotstester__button");f.type="button",f.addEventListener("click",()=>this.test()),a.append(c,g,f);const w=o("div","seo-robotstester__result");w.setAttribute("role","status");const x=o("div","seo-robotstester__notes");this.input=h,this.select=p,this.button=f,this.line=w,this.notes=x,this.append(a,w,x),this.syncControls(),this.renderResult()}syncControls(){if(!this.button||!this.input||!this.select)return;const e=this.hasAttribute("disabled"),t=this.hasAttribute("busy");this.input.readOnly=e,this.select.disabled=e,this.button.hidden=e,this.button.disabled=t,this.button.textContent=t?this.getAttribute("busy-label")||this.getAttribute("label")||"":this.getAttribute("label")||""}test(){if(this.hasAttribute("disabled")||!this.input||!this.select)return;const e=this.input.value.trim();if(!e.startsWith("/")){this.error=this.labels().urlRequired,this.result=null,this.renderResult();return}this.error="",this.request({op:"test",url:e,agent:this.select.value})}renderResult(){if(!this.line||!this.notes)return;const e=this.labels();if(this.line.className="seo-robotstester__result",this.error)this.line.classList.add("seo-robotstester__result--error"),this.line.replaceChildren(o("span","",this.error));else if(this.result){const s=this.result.allowed;this.line.classList.add(s?"seo-robotstester__result--allowed":"seo-robotstester__result--blocked"),this.line.replaceChildren(S(s?"circle-check":"triangle-alert"),o("span","",ue(e,this.result)))}else this.line.replaceChildren();const t=[];(this.context?.staging||I(this,"data",{}).staging)&&t.push(o("p","seo-robotstester__note",e.stagingNote)),t.push(o("p","seo-robotstester__note",e.unsaved)),this.notes.replaceChildren(...t)}}const pe="golem15-seo-robotstxt",fe=500*1024,F=10,me=["User-agent","Allow","Disallow","Sitemap","Crawl-delay","Host","Clean-param"];function ge(i,e){const t=Array.from({length:e.length+1},(s,n)=>n);for(let s=1;s<=i.length;s++){let n=t[0];t[0]=s;for(let r=1;r<=e.length;r++){const a=t[r];t[r]=Math.min(t[r]+1,t[r-1]+1,n+(i[s-1]===e[r-1]?0:1)),n=a}}return t[e.length]}function be(i){const e=i.toLowerCase();let t="",s=3;for(const n of me){const r=ge(e,n.toLowerCase());r{const r=n+1,a=s.indexOf("#"),l=(a>=0?s.slice(0,a):s).trim();if(l==="")return;const c=l.indexOf(":");if(c<0){e.push({kind:"noColon",line:r});return}const d=l.slice(0,c).trim(),h=l.slice(c+1).trim();switch(d.toLowerCase()){case"user-agent":t=!0;return;case"allow":case"disallow":t?h!==""&&!h.startsWith("/")&&!h.startsWith("*")&&e.push({kind:"badPath",line:r}):e.push({kind:"noGroup",line:r});return;case"sitemap":e.push({kind:"ownSitemap",line:r});return;case"crawl-delay":e.push({kind:"crawlDelay",line:r});return;case"host":case"clean-param":return}const u=be(d);e.push(u?{kind:"unknownSuggest",line:r,word:d,suggestion:u}:{kind:"unknown",line:r,word:d})}),e}function ve(i){return new TextEncoder().encode(i).length}function _e(i,e){return v(i[e.kind],{line:e.line,word:e.word??"",suggestion:e.suggestion??""})}class ye extends HTMLElement{static observedAttributes=["disabled","invalid","ui-locale"];current="";control=null;panel=null;meter=null;builtFor="";get value(){return this.current}set value(e){this.current=typeof e=="string"?e:e==null?"":String(e),this.control&&this.control.value!==this.current&&(this.control.value=this.current),this.update()}connectedCallback(){this.build()}attributeChangedCallback(e){if(this.isConnected){if(e==="ui-locale"){this.build();return}this.sync()}}labels(){return L(y(this))}build(){const e=y(this);if(this.builtFor===e&&this.control){this.sync();return}this.builtFor=e,this.replaceChildren();const t=E("seo-robotstxt-lint"),s=o("textarea","seo-robotstxt__control");s.spellcheck=!1,s.setAttribute("wrap","off"),s.value=this.current,s.setAttribute("aria-describedby",t);const n=this.getAttribute("field-name");n&&s.setAttribute("aria-label",n),s.addEventListener("input",()=>{this.hasAttribute("disabled")||(this.current=s.value,this.update(),T(this,"summer-change",{value:this.current}))});const r=o("div","seo-robotstxt__lint");r.id=t,r.setAttribute("role","status");const a=o("div","seo-robotstxt__size");this.control=s,this.panel=r,this.meter=a,this.append(s,r,a),this.sync()}sync(){this.control&&(this.control.readOnly=this.hasAttribute("disabled"),this.classList.toggle("seo-robotstxt--invalid",this.hasAttribute("invalid")),this.control.setAttribute("aria-invalid",this.hasAttribute("invalid")?"true":"false"),this.update())}update(){if(!this.panel||!this.meter)return;const e=this.labels(),t=y(this),s=we(this.current),n=[];if(s.length===0){const c=o("div","seo-robotstxt__finding seo-robotstxt__finding--clean");c.append(S("circle-check"),o("span","",e.clean)),n.push(c)}else{for(const c of s.slice(0,F)){const d=o("div","seo-robotstxt__finding");d.append(S("triangle-alert"),o("span","",_e(e,c))),n.push(d)}s.length>F&&n.push(o("div","seo-robotstxt__more",v(e.moreFindings,{count:A(s.length-F,t)})))}this.panel.replaceChildren(...n);const r=ve(this.current),a=new Intl.NumberFormat(t,{maximumFractionDigits:1}).format(r/1024)+" KB";this.meter.replaceChildren(o("span","",v(e.size,{size:a})));const l=r>fe;this.classList.toggle("seo-robotstxt--over",l),l&&this.meter.append(o("span","seo-robotstxt__over",e.tooLarge))}}const $="20px Arial, sans-serif",X="14px Arial, sans-serif",Y=580,J=920,xe=680,ke=30,Ae=120;let N;function ze(){if(N!==void 0)return N;try{const e=document.createElement("canvas").getContext("2d");N=e&&typeof e.measureText=="function"?e:null}catch{N=null}return N}function Te(i,e){let t=0;for(const s of i)/[ilIj.,:;'!|]/.test(s)?t+=.28:/[mwMW@]/.test(s)?t+=.85:/[A-Z]/.test(s)?t+=.67:s===" "?t+=.28:t+=.53;return t*e}function O(i,e){const t=ze();if(t){t.font=e;const n=t.measureText(i).width;if(Number.isFinite(n)&&(n>0||i===""))return n}const s=Number.parseFloat(e)||14;return Te(i,s)}const j=" …";function q(i,e,t){if(O(i,e)<=t)return i;const s=u=>O(u+j,e)<=t,n=Array.from(i),r=[];for(let u=1;u>1;s(n.slice(0,r[u]).join(""))?(a=u,l=u+1):c=u-1}if(a>=0)return n.slice(0,r[a]).join("")+j;let d=0,h=n.length;for(;ds?c="long":a=n}function R(i,e){const t=K(i);if(t.length===0)return!1;const s=K(e);for(let n=0;n+t.length<=s.length;n++){let r=!0;for(let a=0;a{const s=e.detail?.data;this.inflight=null,s&&s.op==="context"?(this.context=s,this.contextFailed=!1,this.render(),this.scheduleDuplicates(!0)):s&&s.op==="duplicates"&&(this.duplicates.set(s.locale,s),this.checking=!1,this.render()),this.flush()};onLocale=e=>{const t=e.detail;typeof t=="string"&&t!==""&&t!==this.shownLocale&&(this.shownLocale=t,this.render(),this.scheduleDuplicates(!1))};connectedCallback(){this.connected=!0,this.addEventListener("summer-result",this.onResult),window.addEventListener("summer:ml-locale",this.onLocale),this.render(),this.request({op:"context"})}disconnectedCallback(){this.connected=!1,this.removeEventListener("summer-result",this.onResult),window.removeEventListener("summer:ml-locale",this.onLocale),clearTimeout(this.timer)}attributeChangedCallback(e,t,s){if(this.connected){if(e==="busy"&&s===null){this.flush();return}if(e==="state"&&s==="error"){this.inflight?.op==="context"&&(this.contextFailed=!0),this.checking=!1,this.inflight=null,this.render();return}if(e==="fill-values"){this.render(),this.scheduleDuplicates(!1);return}this.render()}}labels(){return L(y(this))}request(e){if(this.hasAttribute("busy")||this.inflight){this.queued=e;return}this.inflight=e,T(this,"summer-action",{payload:e})}flush(){if(this.hasAttribute("busy")||this.inflight||!this.queued)return;const e=this.queued;this.queued=null,this.request(e)}fill(){return I(this,"fill-values",{})}locale(){const e=this.context?.locales??[];return e.length===0?null:e.find(t=>t.locale===this.shownLocale)??e[0]}scheduleDuplicates(e){const t=this.locale();if(!t)return;const s=this.fill(),n=z(s,"seo_title",t.locale)||z(s,"title",t.locale),r=z(s,"seo_description",t.locale)||z(s,"description",t.locale),a=JSON.stringify([t.locale,n,r]);if(a===this.lastChecked)return;clearTimeout(this.timer);const l=()=>{this.lastChecked=a,this.checking=!0,this.render(),this.request({op:"duplicates",locale:t.locale,title:n,description:r})};e?l():this.timer=setTimeout(l,Le)}render(){const e=this.labels(),t=y(this);if(!this.context&&!this.contextFailed){this.setAttribute("aria-busy","true"),this.replaceChildren(o("div","seo-serp__skeleton"));return}this.removeAttribute("aria-busy");const s=this.locale(),n=s?.locale??this.shownLocale,r=this.fill(),a=(z(r,"seo_title",n)||z(r,"title",n)).trim(),l=(z(r,"seo_description",n)||z(r,"description",n)).trim(),c=z(r,"seo_keyphrase",n).trim(),d=s?.siteName??"",h=a||Re(s?.titleTemplate??"",{title:s?.defaultTitle??"",site:d})||s?.defaultTitle||"",u=l||s?.defaultDescription||"",g=o("div","seo-serp__header"),b=o("div","seo-serp__track");b.setAttribute("role","radiogroup"),b.setAttribute("aria-label",e.device);for(const _ of["desktop","mobile"]){const C=o("button","seo-serp__segment",e[_]);C.type="button",C.setAttribute("role","radio"),C.setAttribute("aria-checked",this.device===_?"true":"false"),C.addEventListener("click",()=>{this.device=_,this.render()}),b.append(C)}g.append(b),n&&g.append(o("span","seo-serp__language",v(e.language,{locale:n.toUpperCase()})));const p=o("div","serp-card"+(this.device==="mobile"?" serp-card--mobile":"")),f=o("div","serp-card__site");f.append(o("span","serp-card__favicon",Array.from(d||this.context?.host||"?")[0]?.toUpperCase()??"?"));const w=o("div","serp-card__names");if(w.append(o("span","serp-card__sitename",d||this.context?.host||"")),w.append(o("span","serp-card__crumbs",je(this.context?.host??"",s?.url??null))),f.append(w),p.append(f),p.append(o("div","serp-card__title",q(h,$,Y))),u){const _=this.device==="mobile"?xe:J;p.append(o("div","serp-card__description",q(u,X,_)))}else p.append(o("div","serp-card__description serp-card__description--missing",e.noDescription));const x=o("div","seo-serp__notes");if(this.contextFailed){const _=o("div","seo-serp__error");_.setAttribute("role","status"),_.append(S("circle-alert"),o("span","",e.contextFailed)),x.append(_)}!a&&s?.titleTemplate&&x.append(o("div","",v(e.fromTemplate,{template:s.titleTemplate}))),!l&&s?.defaultDescription&&s.descriptionSource==="content"&&x.append(o("div","",e.fromContent)),s&&s.url===null&&x.append(o("div","",e.noUrl)),(this.getAttribute("record-id")??"")!==""&&s&&x.append(o("div","",e.savedNote));const m=o("div","seo-serp__checks");if(m.setAttribute("role","status"),this.checking)m.append(o("div","seo-serp__checking",e.checking));else{const _=this.duplicates.get(n);if(_)for(const[C,Q]of[[_.title,e.dupTitle],[_.description,e.dupDescription]])C.length>0&&m.append(Pe(C,Q,e,t))}const k=[g,p,x,m];c&&s&&k.push(Me(c,{title:h,description:u,h1:s.h1,url:s.url,bodyWords:s.bodyWords},e,t)),this.replaceChildren(...k)}}function je(i,e){const t=(e??"").split("?")[0].split("/").filter(Boolean).map(s=>{try{return decodeURIComponent(s)}catch{return s}});return[i,...t].filter(Boolean).join(" › ")}function Pe(i,e,t,s){const n=o("div","seo-serp__check seo-serp__check--warn");n.append(S("triangle-alert"));const r=o("span"),a="\0list\0",l=B(e,i.length,s,{list:a}),[c,d]=l.split(a);return r.append(document.createTextNode(c??"")),i.slice(0,P).forEach((h,u)=>{if(u>0&&r.append(document.createTextNode(", ")),h.url.startsWith("/")&&!h.url.startsWith("//")){const g=o("a","seo-serp__link",h.label);g.href=h.url,g.target="_blank",g.rel="noopener",r.append(g)}else r.append(document.createTextNode(h.label))}),i.length>P&&r.append(document.createTextNode(" "+v(t.andMore,{count:i.length-P}))),r.append(document.createTextNode(d??"")),n.append(r),n}function Me(i,e,t,s){const n=o("div","seo-serp__keyphrase");n.append(o("div","seo-serp__keyphrase-title",v(t.keyphrase,{phrase:i})));const r=(a,l,c)=>{const d=o("div","seo-serp__check "+(a?"seo-serp__check--ok":"seo-serp__check--warn"));d.append(S(a?"circle-check":"triangle-alert"),o("span","",a?l:c)),n.append(d)};if(r(R(i,e.title),t.kpTitle,t.kpTitleNo),e.h1!==null&&r(R(i,e.h1),t.kpH1,t.kpH1No),r(R(i,e.description),t.kpDesc,t.kpDescNo),e.url!==null&&r(R(i,e.url.replace(/[-_/]+/g," ")),t.kpUrl,t.kpUrlNo),e.bodyWords!==null){const a=new Intl.NumberFormat(s).format(e.bodyWords);r(e.bodyWords>=Ne,v(t.kpLength,{count:a}),v(t.kpLengthNo,{count:a}))}return n}const Oe="golem15-seo-text",Ue={empty:"info",short:"triangle-alert",long:"triangle-alert",good:"circle-check"};class Be extends HTMLElement{static observedAttributes=["options","disabled","invalid","ui-locale","locale"];current="";control=null;statusIcon=null;statusText=null;countText=null;live=null;lastStatus=null;builtFor="";get value(){return this.current}set value(e){this.current=typeof e=="string"?e:e==null?"":String(e),this.control&&this.control.value!==this.current&&(this.control.value=this.current),this.update(!1)}connectedCallback(){this.build()}attributeChangedCallback(e){if(this.isConnected){if(e==="options"||e==="ui-locale"){this.build();return}this.sync()}}measure(){return I(this,"options",{}).measure==="description"?"description":"title"}labels(){return L(y(this))}build(){const e=this.measure()+"|"+y(this);if(this.builtFor===e&&this.control){this.sync();return}this.builtFor=e,this.replaceChildren();const t=E("seo-text-counter"),n=this.measure()==="description"?o("textarea","seo-text__control seo-text__control--area"):o("input","seo-text__control");n instanceof HTMLTextAreaElement?n.rows=3:n.type="text",n.value=this.current,n.setAttribute("aria-describedby",t);const r=this.getAttribute("field-name");r&&n.setAttribute("aria-label",r),n.addEventListener("input",()=>{this.hasAttribute("disabled")||(this.current=n.value.replace(/\s+$/u,""),this.update(!0),T(this,"summer-change",{value:this.current}))});const a=o("div","seo-text__counter");a.id=t;const l=o("span","seo-text__status");this.statusIcon=o("span","seo-text__icon"),this.statusText=o("span","seo-text__status-text"),l.append(this.statusIcon,this.statusText),this.countText=o("span","seo-text__count"),a.append(l,this.countText),this.live=o("span","seo-visually-hidden"),this.live.setAttribute("aria-live","polite"),this.live.setAttribute("role","status"),this.control=n,this.lastStatus=null,this.append(n,a,this.live),this.sync()}sync(){if(!this.control)return;const e=this.hasAttribute("disabled");this.control.readOnly=e,this.classList.toggle("seo-text--invalid",this.hasAttribute("invalid")),this.control.setAttribute("aria-invalid",this.hasAttribute("invalid")?"true":"false"),this.update(!1)}update(e){if(!this.statusText||!this.countText||!this.statusIcon)return;const t=this.labels(),s=y(this),n=this.measure(),r=Ce(this.current,n),a=He(t,r.status,n);this.statusIcon.replaceChildren(S(Ue[r.status])),this.statusText.textContent=a,this.dataset.status=r.status,this.countText.textContent=v(t.count,{chars:A(r.chars,s),px:A(r.px,s),max:A(r.maxPx,s)}),e&&this.live&&r.status!==this.lastStatus&&(this.live.textContent=a),this.lastStatus=r.status}}function He(i,e,t){switch(e){case"empty":return t==="title"?i.emptyTitle:i.emptyDescription;case"short":return t==="title"?i.shortTitle:i.shortDescription;case"long":return t==="title"?i.longTitle:i.longDescription;default:return i.good}}const We="golem15-seo-transfer",Z=60*1024,U=200;function Ge(i){return i.summary.refused>0?"refused":i.count>0?"ready":"nothing"}function qe(i){const e=i.filter(s=>s.change==="refused"),t=i.filter(s=>s.change!=="refused");return[...e,...t].slice(0,U)}function Ke(i,e){const t=new Blob([e],{type:"text/csv;charset=utf-8"}),s=URL.createObjectURL(t),n=document.createElement("a");n.href=s,n.download=i,n.rel="noopener",document.body.append(n),n.click(),n.remove(),setTimeout(()=>URL.revokeObjectURL(s),0)}function Ze(){return window.location.pathname.replace(/\/transfer(\/.*)?$/,"/history")}class Ve extends HTMLElement{static observedAttributes=["busy","state","ui-locale","disabled","label","busy-label"];connected=!1;pending=null;exportDataset="redirects";importDataset="redirects";fileName="";csv="";fileError="";importError="";exportError="";applyError="";preview=null;applied=!1;onResult=e=>{const s=e.detail?.data;if(this.pending=null,!s){this.render();return}s.error!==void 0?s.op==="export"?this.exportError=s.message:s.op==="preview"?(this.importError=s.message,this.preview=null):this.applyError=s.message:s.op==="export"?(this.exportError="",Ke(s.filename,s.csv)):s.op==="preview"?(this.importError="",this.applyError="",this.applied=!1,this.preview=s):s.op==="apply"&&(this.preview=null,this.applyError="",this.applied=!0),this.render()};connectedCallback(){this.connected=!0,this.addEventListener("summer-result",this.onResult),this.render()}disconnectedCallback(){this.connected=!1,this.removeEventListener("summer-result",this.onResult)}attributeChangedCallback(e,t,s){this.connected&&(e==="state"&&s==="error"&&(this.pending=null),e==="busy"&&s===null&&(this.pending=null),this.render())}labels(){return L(y(this))}request(e){this.hasAttribute("disabled")||this.hasAttribute("busy")||this.pending||(this.pending=e.op,T(this,"summer-action",{payload:e}),this.render())}busyLabel(e,t){return this.pending===e&&this.getAttribute("busy-label")||t}datasetSelect(e,t,s){const n=o("div","seo-transfer__field"),r=E("seo-transfer-dataset"),a=o("label","seo-transfer__label",e.dataset);a.htmlFor=r;const l=o("select","seo-transfer__select");l.id=r;const c=[["redirects",e.dsRedirects],["rules",e.dsRules],["records",e.dsRecords]];for(const[d,h]of c){const u=o("option","",h);u.value=d,l.append(u)}return l.value=t,l.disabled=this.hasAttribute("disabled"),l.addEventListener("change",()=>s(l.value)),n.append(a,l),n}button(e,t,s){const n=o("button","seo-transfer__button seo-transfer__button--"+t,e);return n.type="button",n.addEventListener("click",s),n}message(e,t){const s=o("p",t,e);return s.setAttribute("role","status"),s}async chooseFile(e){const t=e.files?.[0];if(this.preview=null,this.applied=!1,this.importError="",this.applyError="",this.csv="",!t){this.fileName="",this.fileError="",this.render();return}if(this.fileName=t.name,t.size>Z){this.fileError=v(this.labels().fileTooLarge,{limit:A(Z/1024,y(this))+" KB"}),this.render();return}try{this.csv=await t.text(),this.fileError=""}catch{this.fileError=this.labels().readFailed}this.render()}render(){const e=this.labels(),t=y(this),s=this.hasAttribute("disabled"),n=this.hasAttribute("busy")||this.pending!==null,r=this.getAttribute("label")||e.chooseFile,a=o("section","seo-transfer__section");a.append(o("h3","seo-transfer__heading",e.exportHeading));const l=o("div","seo-transfer__row");l.append(this.datasetSelect(e,this.exportDataset,f=>this.exportDataset=f));const c=this.button(this.busyLabel("export",e.download),"outline",()=>this.request({op:"export",dataset:this.exportDataset}));c.disabled=n,c.hidden=s,l.append(c),a.append(l),this.exportError&&a.append(this.message(this.exportError,"seo-transfer__error"));const d=o("section","seo-transfer__section seo-transfer__section--import");d.append(o("h3","seo-transfer__heading",e.importHeading));const h=o("div","seo-transfer__row");h.append(this.datasetSelect(e,this.importDataset,f=>{this.importDataset=f,this.preview=null,this.render()}));const u=o("input","seo-transfer__file");u.type="file",u.accept=".csv,text/csv",u.hidden=!0,u.addEventListener("change",()=>{this.chooseFile(u)});const g=this.button(e.chooseFile,"outline",()=>u.click());g.hidden=s,g.disabled=n;const b=o("span",this.fileName?"seo-transfer__filename":"seo-transfer__filename seo-transfer__filename--none",this.fileName||e.noFile),p=this.button(this.busyLabel("preview",r),"outline",()=>this.request({op:"preview",dataset:this.importDataset,csv:this.csv}));if(p.disabled=n||this.csv===""||this.fileError!=="",p.hidden=s,h.append(u,g,b,p),d.append(h),(this.fileError||this.importError)&&d.append(this.message(this.fileError||this.importError,"seo-transfer__error")),this.applied){const f=o("p","seo-transfer__done"),w=o("a","seo-transfer__link",e.openHistory);w.href=Ze(),f.append(w),d.append(f)}this.preview&&d.append(this.renderPreview(e,t,n,s)),this.replaceChildren(a,d)}renderPreview(e,t,s,n){const r=this.preview,a=o("div","seo-transfer__preview"),l=r.summary;a.append(this.message(v(e.summary,{new:A(l.new,t),changed:A(l.changed,t),unchanged:A(l.unchanged,t),refused:A(l.refused,t)}),"seo-transfer__summary"));const c=o("div","summer-table-wrap seo-transfer__table-wrap"),d=o("table","summer-table seo-transfer__table"),h=o("tr");for(const m of[e.colLine,e.colChange,e.colAddress,e.colField,e.colBefore,e.colAfter,e.colNote])h.append(o("th","",m));const u=o("thead");u.append(h);const g=o("tbody"),b={new:e.bNew,changed:e.bChanged,unchanged:e.bUnchanged,refused:e.bRefused};for(const m of qe(r.rows)){const k=o("tr","seo-transfer__tr--"+m.change);k.append(o("td","seo-transfer__num",A(m.line,t)));const _=o("td");_.append(o("span","seo-transfer__badge seo-transfer__badge--"+m.change,b[m.change])),m.warning&&_.append(o("span","seo-transfer__badge seo-transfer__badge--warning",e.bWarning)),k.append(_),k.append(o("td","seo-transfer__mono",m.address)),k.append(o("td","",m.field)),k.append(o("td","",m.before)),k.append(o("td","",m.after)),k.append(o("td","",m.note)),g.append(k)}d.append(u,g),c.append(d),a.append(c),(r.total>U||r.rows.length>U)&&a.append(o("p","seo-transfer__note",v(e.rowCap,{count:A(Math.max(r.total,r.rows.length),t)})));const p=Ge(r);p==="refused"?a.append(this.message(e.refusedBlocks,"seo-transfer__error")):p==="nothing"&&a.append(this.message(e.nothing,"seo-transfer__note")),this.applyError&&a.append(this.message(this.applyError,"seo-transfer__error"));const f=o("div","seo-transfer__actions"),w=this.button(e.discard,"ghost",()=>{this.preview=null,this.applyError="",this.render()});w.disabled=s;const x=this.button(this.busyLabel("apply",B(e.applyCount,r.count,t)),"primary",()=>this.request({op:"apply",token:r.token}));return x.disabled=s||p!=="ready",f.append(w,x),f.hidden=n,a.append(f),a}}const $e=[[Oe,Be],[re,ce],[Se,Fe],[ne,oe],[pe,ye],[de,he],[We,Ve]];for(const[i,e]of $e)customElements.get(i)||customElements.define(i,e);export{ne as IMAGE_TAG,de as ROBOTSTESTER_TAG,pe as ROBOTSTXT_TAG,re as ROBOTS_TAG,Se as SERP_TAG,oe as SeoImageElement,ce as SeoRobotsElement,he as SeoRobotsTesterElement,ye as SeoRobotsTxtElement,Fe as SeoSerpElement,Be as SeoTextElement,Ve as SeoTransferElement,Oe as TEXT_TAG,We as TRANSFER_TAG}; +const V={count:":chars characters · :px / :max px",good:"Good length",emptyTitle:"Empty: the title template is used",emptyDescription:"Empty: the template or the excerpt is used",shortTitle:"Short: aim for 30–60 characters",shortDescription:"Short: aim for 120–160 characters",longTitle:"Too long: Google cuts titles after about 580 px",longDescription:"Too long: Google cuts descriptions after about 920 px on desktop and 680 px on mobile",indexing:"Indexing",links:"Links",inherit:"Inherit",siteDefault:"Site default",index:"Index",noindex:"Noindex",follow:"Follow",nofollow:"Nofollow",advanced:"Advanced directives",advancedSet:{one:":count set",other:":count set"},maxSnippet:"Maximum snippet length (characters)",maxSnippetHint:"Empty: no limit. 0: no snippet. -1: no limit.",maxImagePreview:"Image preview size",notSet:"Not set",none:"None",standard:"Standard",large:"Large",maxVideoPreview:"Maximum video preview (seconds)",noarchive:"Do not show a cached copy (noarchive)",nosnippet:"Do not show a snippet (nosnippet)",noimageindex:"Do not index images (noimageindex)",unavailableAfter:"Remove from results after",invalidNumber:"Enter a whole number, -1 or more.",desktop:"Desktop",mobile:"Mobile",device:"Preview device",language:"Language: :locale",fromTemplate:"Title from the template “:template”",fromContent:"Description from the content excerpt",noDescription:"No description: Google picks text from the page.",noUrl:"The address appears after the first save.",savedNote:"The address and the H1 come from the saved record. Save to refresh them.",checking:"Checking for duplicates…",dupTitle:{one:"Same title as :count other page: :list",other:"Same title as :count other pages: :list"},dupDescription:{one:"Same description as :count other page: :list",other:"Same description as :count other pages: :list"},andMore:"and :count more",keyphrase:"Focus keyphrase “:phrase”",kpTitle:"In the SEO title",kpTitleNo:"Not in the SEO title",kpH1:"In the H1",kpH1No:"Not in the H1",kpDesc:"In the meta description",kpDescNo:"Not in the meta description",kpUrl:"In the address",kpUrlNo:"Not in the address",kpLength:"Text length: :count words",kpLengthNo:"Text length: :count words, aim for at least 300",contextFailed:"The preview data could not be loaded.",imageUpload:"Upload image",imageReplace:"Replace image",imageRemove:"Remove",imageUploading:"Uploading…",imageFailed:"The image could not be uploaded.",imageAlt:"Social image preview",size:":size of 500 KB",clean:"No problems found.",unknown:"Line :line: “:word” is not a robots.txt directive.",unknownSuggest:"Line :line: “:word” is not a robots.txt directive. Did you mean “:suggestion”?",noGroup:"Line :line: this rule has no User-agent line above it, so crawlers ignore it.",badPath:"Line :line: a path must start with / or *.",noColon:"Line :line: write the directive as “Name: value”.",ownSitemap:"Line :line: Sitemap lines are added automatically. This one is kept as written.",crawlDelay:"Line :line: Google ignores Crawl-delay. Bing and Yandex use it.",moreFindings:"and :count more",tooLarge:"Over 500 KB: the file cannot be saved.",url:"Address",urlPlaceholder:"/konto/ustawienia",agent:"Crawler",allowedRule:"Allowed for :agent. Rule: line :line “:rule”.",allowedNone:"Allowed for :agent: no rule matches this address.",blocked:"Blocked for :agent by line :line “:rule”.",blockedBot:"Blocked for :agent by its switch on the AI bots tab.",unsaved:"Tests the text in the editor and the AI bot switches, including unsaved changes.",stagingNote:"On this environment every address is blocked. The result shows the production behaviour.",urlRequired:"Enter an address starting with /.",testFailed:"The test could not be run. Try again.",exportHeading:"Export",importHeading:"Import",dataset:"Data",dsRedirects:"Redirects",dsRules:"URL rules",dsRecords:"SEO of records",download:"Download CSV",chooseFile:"Choose CSV file",noFile:"No file chosen",summary:":new new, :changed changed, :unchanged unchanged, :refused refused",bNew:"New",bChanged:"Changed",bUnchanged:"Unchanged",bRefused:"Refused",bWarning:"Warning",colLine:"Line",colChange:"Change",colAddress:"Address",colField:"Field",colBefore:"Before",colAfter:"After",colNote:"Note",rowCap:"Showing the first 200 of :count rows. All rows are applied.",refusedBlocks:"Fix the refused rows in the file and preview it again. Nothing is applied while a row is refused.",nothing:"The file changes nothing.",discard:"Discard preview",fileTooLarge:"This file is larger than :limit. Split it into smaller files, or import it through MCP.",readFailed:"The file could not be read.",openHistory:"Open History",applyCount:{one:"Apply :count change",other:"Apply :count changes"}},te={count:"Znaki: :chars · :px / :max px",good:"Dobra długość",emptyTitle:"Puste: używany jest szablon tytułu",emptyDescription:"Puste: używany jest szablon lub zajawka",shortTitle:"Krótki: celuj w 30–60 znaków",shortDescription:"Krótki: celuj w 120–160 znaków",longTitle:"Za długi: Google ucina tytuły po ok. 580 px",longDescription:"Za długi: Google ucina opisy po ok. 920 px na komputerze i 680 px na telefonie",indexing:"Indeksowanie",links:"Linki",inherit:"Dziedzicz",siteDefault:"Domyślne",index:"Indeksuj",noindex:"Nie indeksuj",follow:"Śledź",nofollow:"Nie śledź",advanced:"Dyrektywy zaawansowane",advancedSet:{one:"ustawiona :count",few:"ustawione :count",many:"ustawionych :count",other:"ustawione :count"},maxSnippet:"Maksymalna długość fragmentu (znaki)",maxSnippetHint:"Puste: bez limitu. 0: bez fragmentu. -1: bez limitu.",maxImagePreview:"Rozmiar podglądu obrazu",notSet:"Nie ustawiono",none:"Brak",standard:"Standardowy",large:"Duży",maxVideoPreview:"Maksymalny podgląd wideo (sekundy)",noarchive:"Nie pokazuj kopii (noarchive)",nosnippet:"Nie pokazuj fragmentu (nosnippet)",noimageindex:"Nie indeksuj obrazów (noimageindex)",unavailableAfter:"Usuń z wyników po dniu",invalidNumber:"Wpisz liczbę całkowitą, -1 lub większą.",desktop:"Komputer",mobile:"Telefon",device:"Urządzenie podglądu",language:"Język: :locale",fromTemplate:"Tytuł z szablonu „:template”",fromContent:"Opis z zajawki treści",noDescription:"Brak opisu: Google wybierze tekst ze strony.",noUrl:"Adres pojawi się po pierwszym zapisie.",savedNote:"Adres i H1 pochodzą z zapisanego rekordu. Zapisz, aby je odświeżyć.",checking:"Sprawdzanie duplikatów…",dupTitle:{one:"Ten sam tytuł co :count inna strona: :list",few:"Ten sam tytuł co :count inne strony: :list",many:"Ten sam tytuł co :count innych stron: :list",other:"Ten sam tytuł co :count innej strony: :list"},dupDescription:{one:"Ten sam opis co :count inna strona: :list",few:"Ten sam opis co :count inne strony: :list",many:"Ten sam opis co :count innych stron: :list",other:"Ten sam opis co :count innej strony: :list"},andMore:"i :count więcej",keyphrase:"Fraza kluczowa „:phrase”",kpTitle:"W tytule SEO",kpTitleNo:"Brak w tytule SEO",kpH1:"W nagłówku H1",kpH1No:"Brak w nagłówku H1",kpDesc:"W opisie meta",kpDescNo:"Brak w opisie meta",kpUrl:"W adresie",kpUrlNo:"Brak w adresie",kpLength:"Długość tekstu: :count słów",kpLengthNo:"Długość tekstu: :count słów, celuj w co najmniej 300",contextFailed:"Nie udało się wczytać danych podglądu.",imageUpload:"Wgraj obraz",imageReplace:"Zmień obraz",imageRemove:"Usuń",imageUploading:"Wgrywanie…",imageFailed:"Nie udało się wgrać obrazu.",imageAlt:"Podgląd obrazu w mediach społecznościowych",size:":size z 500 KB",clean:"Nie znaleziono problemów.",unknown:"Wiersz :line: „:word” nie jest dyrektywą robots.txt.",unknownSuggest:"Wiersz :line: „:word” nie jest dyrektywą robots.txt. Czy chodziło o „:suggestion”?",noGroup:"Wiersz :line: nad tą regułą nie ma wiersza User-agent, więc roboty ją ignorują.",badPath:"Wiersz :line: ścieżka musi zaczynać się od / lub *.",noColon:"Wiersz :line: zapisz dyrektywę jako „Nazwa: wartość”.",ownSitemap:"Wiersz :line: wiersze Sitemap są dodawane automatycznie. Ten zostaje bez zmian.",crawlDelay:"Wiersz :line: Google ignoruje Crawl-delay. Bing i Yandex go stosują.",moreFindings:"i :count więcej",tooLarge:"Ponad 500 KB: nie można zapisać pliku.",url:"Adres",urlPlaceholder:"/konto/ustawienia",agent:"Robot",allowedRule:"Dozwolone dla :agent. Reguła: wiersz :line „:rule”.",allowedNone:"Dozwolone dla :agent: żadna reguła nie pasuje do tego adresu.",blocked:"Zablokowane dla :agent przez wiersz :line „:rule”.",blockedBot:"Zablokowane dla :agent przełącznikiem w zakładce Boty AI.",unsaved:"Sprawdza tekst w edytorze i przełączniki botów AI, także niezapisane zmiany.",stagingNote:"W tym środowisku każdy adres jest zablokowany. Wynik pokazuje zachowanie na produkcji.",urlRequired:"Wpisz adres zaczynający się od /.",testFailed:"Nie udało się uruchomić testu. Spróbuj ponownie.",exportHeading:"Eksport",importHeading:"Import",dataset:"Dane",dsRedirects:"Przekierowania",dsRules:"Reguły adresów",dsRecords:"SEO rekordów",download:"Pobierz CSV",chooseFile:"Wybierz plik CSV",noFile:"Nie wybrano pliku",summary:"Nowe: :new, zmienione: :changed, bez zmian: :unchanged, odrzucone: :refused",bNew:"Nowy",bChanged:"Zmieniony",bUnchanged:"Bez zmian",bRefused:"Odrzucony",bWarning:"Ostrzeżenie",colLine:"Wiersz",colChange:"Zmiana",colAddress:"Adres",colField:"Pole",colBefore:"Przed",colAfter:"Po",colNote:"Uwagi",rowCap:"Pokazano pierwsze 200 z :count wierszy. Zastosowane zostaną wszystkie.",refusedBlocks:"Popraw odrzucone wiersze w pliku i ponownie wyświetl podgląd. Nic nie zostanie zastosowane, dopóki jakiś wiersz jest odrzucony.",nothing:"Plik niczego nie zmienia.",discard:"Odrzuć podgląd",fileTooLarge:"Ten plik jest większy niż :limit. Podziel go na mniejsze pliki lub zaimportuj przez MCP.",readFailed:"Nie udało się odczytać pliku.",openHistory:"Otwórz Historię zmian",applyCount:{one:"Zastosuj :count zmianę",few:"Zastosuj :count zmiany",many:"Zastosuj :count zmian",other:"Zastosuj :count zmiany"}},se={en:V,pl:te};function y(n){return(n.getAttribute("ui-locale")||document.documentElement.lang||"en").toLowerCase().split(/[-_]/)[0]||"en"}function L(n){return se[n]??V}function v(n,e={}){const t=Object.keys(e).sort((i,r)=>r.length-i.length);let s=n;for(const i of t)s=s.split(":"+i).join(String(e[i]));return s}function B(n,e,t,s={}){let i="other";try{i=new Intl.PluralRules(t).select(e)}catch{i=e===1?"one":"other"}const r=i==="one"&&n.one||i==="few"&&n.few||i==="many"&&n.many||n.other;return v(r,{...s,count:new Intl.NumberFormat(t).format(e)})}function A(n,e){try{return new Intl.NumberFormat(e).format(n)}catch{return String(n)}}const H="http://www.w3.org/2000/svg",ie={info:["M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z","M12 16v-4","M12 8h.01"],"triangle-alert":["m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3","M12 9v4","M12 17h.01"],"circle-check":["M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z","m9 12 2 2 4-4"],"circle-alert":["M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z","M12 8v4","M12 16h.01"],"chevron-down":["m6 9 6 6 6-6"]};function S(n,e=14){const t=document.createElementNS(H,"svg");t.setAttribute("width",String(e)),t.setAttribute("height",String(e)),t.setAttribute("viewBox","0 0 24 24"),t.setAttribute("fill","none"),t.setAttribute("stroke","currentColor"),t.setAttribute("stroke-width","2"),t.setAttribute("stroke-linecap","round"),t.setAttribute("stroke-linejoin","round"),t.setAttribute("aria-hidden","true"),t.setAttribute("class","seo-icon seo-icon--"+n);for(const s of ie[n]??[]){const i=document.createElementNS(H,"path");i.setAttribute("d",s),t.appendChild(i)}return t}function o(n,e="",t){const s=document.createElement(n);return e&&(s.className=e),t!==void 0&&(s.textContent=t),s}function D(n,e,t){const s=n.getAttribute(e);if(s===null||s==="")return t;try{return JSON.parse(s)}catch{return t}}function C(n,e,t){n.dispatchEvent(new CustomEvent(e,{detail:t,bubbles:!0,composed:!0}))}let W=0;function T(n){return W+=1,`${n}-${W}`}const ne="golem15-seo-image";function oe(n){return/^\/(?!\/)/.test(n)||/^https?:\/\//i.test(n)}class re extends HTMLElement{static observedAttributes=["disabled","invalid","ui-locale"];current="";pending="";error="";onUploadResult=e=>{const t=e.detail;!t||t.id!==this.pending||(this.pending="",typeof t.url=="string"&&t.url!==""?(this.current=t.url,this.error="",C(this,"summer-change",{value:this.current})):this.error=t.error||this.labels().imageFailed,this.render())};get value(){return this.current}set value(e){this.current=typeof e=="string"?e:"",this.render()}connectedCallback(){this.addEventListener("summer-upload-result",this.onUploadResult),this.render()}disconnectedCallback(){this.removeEventListener("summer-upload-result",this.onUploadResult)}attributeChangedCallback(){this.isConnected&&this.render()}labels(){return L(y(this))}render(){const e=this.labels(),t=this.hasAttribute("disabled"),s=[];if(this.current&&oe(this.current)){const r=o("div","seo-image__preview"),a=o("img","seo-image__img");a.src=this.current,a.alt=e.imageAlt,a.width=240,a.height=126,r.append(a),s.push(r)}if(!t){const r=o("div","seo-image__actions"),a=T("seo-image-file"),l=o("input","seo-visually-hidden");l.type="file",l.accept="image/jpeg,image/png,image/gif,image/webp",l.id=a,l.addEventListener("change",()=>{const d=l.files?.[0];d&&(this.pending=T("seo-image-upload"),this.error="",this.render(),C(this,"summer-upload",{id:this.pending,file:d}))});const c=o("label","seo-image__button",this.current?e.imageReplace:e.imageUpload);if(c.htmlFor=a,r.append(l,c),this.current){const d=o("button","seo-image__button seo-image__button--secondary",e.imageRemove);d.type="button",d.addEventListener("click",()=>{this.current="",this.error="",C(this,"summer-change",{value:""}),this.render()}),r.append(d)}s.push(r)}const i=o("div","seo-image__status"+(this.error?" seo-image__status--error":""));i.setAttribute("role","status"),i.textContent=this.pending?e.imageUploading:this.error,s.push(i),this.classList.toggle("seo-image--invalid",this.hasAttribute("invalid")),this.replaceChildren(...s)}}const ae="golem15-seo-robots";function M(){return{index:"",follow:"",noarchive:!1,nosnippet:!1,noimageindex:!1,maxSnippet:null,maxImagePreview:"",maxVideoPreview:null,unavailableAfter:""}}function le(n){const e=M(),t=n.trim();if(t==="")return e;const s=new Set;for(const i of t.split(",")){const r=i.trim().toLowerCase();if(r==="")return null;const a=r.indexOf(":"),l=(a<0?r:r.slice(0,a)).trim(),c=a<0?null:r.slice(a+1).trim(),d=l==="noindex"?"index":l==="nofollow"?"follow":l;if(s.has(d))return null;switch(s.add(d),l){case"index":case"noindex":if(c!==null)return null;e.index=l;break;case"follow":case"nofollow":if(c!==null)return null;e.follow=l;break;case"noarchive":case"nosnippet":case"noimageindex":if(c!==null)return null;e[l]=!0;break;case"max-snippet":case"max-video-preview":{if(c===null||!/^-?\d+$/.test(c))return null;const h=Number.parseInt(c,10);if(h<-1)return null;l==="max-snippet"?e.maxSnippet=h:e.maxVideoPreview=h;break}case"max-image-preview":if(c!=="none"&&c!=="standard"&&c!=="large")return null;e.maxImagePreview=c;break;case"unavailable_after":if(c===null||!/^\d{4}-\d{2}-\d{2}$/.test(c)||Number.isNaN(Date.parse(c+"T00:00:00Z")))return null;e.unavailableAfter=c;break;default:return null}}return e}function ce(n){const e=[];return n.index&&e.push(n.index),n.follow&&e.push(n.follow),n.noarchive&&e.push("noarchive"),n.nosnippet&&e.push("nosnippet"),n.noimageindex&&e.push("noimageindex"),n.maxSnippet!==null&&e.push("max-snippet:"+n.maxSnippet),n.maxImagePreview&&e.push("max-image-preview:"+n.maxImagePreview),n.maxVideoPreview!==null&&e.push("max-video-preview:"+n.maxVideoPreview),n.unavailableAfter&&e.push("unavailable_after:"+n.unavailableAfter),e.join(",")}function I(n){return[n.noarchive,n.nosnippet,n.noimageindex,n.maxSnippet!==null,n.maxImagePreview!=="",n.maxVideoPreview!==null,n.unavailableAfter!==""].filter(Boolean).length}class de extends HTMLElement{static observedAttributes=["options","disabled","invalid","ui-locale","locale"];current="";model=M();open=!1;get value(){return this.current}set value(e){const t=typeof e=="string"?e:e==null?"":String(e);t===this.current&&this.childElementCount>0||(this.current=t,this.model=le(t)??M(),this.open=this.open||I(this.model)>0,this.render())}connectedCallback(){this.open=I(this.model)>0,this.render()}attributeChangedCallback(){this.isConnected&&this.render()}labels(){return L(y(this))}commit(){this.current=ce(this.model),C(this,"summer-change",{value:this.current}),this.render()}render(){const e=this.labels(),t=this.hasAttribute("disabled"),i=D(this,"options",{}).scope==="type"?e.siteDefault:e.inherit;this.classList.toggle("seo-robots--invalid",this.hasAttribute("invalid"));const r=document.activeElement&&this.contains(document.activeElement)?document.activeElement.dataset.focusKey:void 0,a=o("div","seo-robots__groups");a.append(this.radioGroup(e.indexing,"index",[["",i,"inherit"],["index",e.index,"ok"],["noindex",e.noindex,"danger"]],t),this.radioGroup(e.links,"follow",[["",i,"inherit"],["follow",e.follow,"ok"],["nofollow",e.nofollow,"danger"]],t));const l=T("seo-robots-advanced"),c=o("button","seo-robots__toggle");c.type="button",c.setAttribute("aria-expanded",this.open?"true":"false"),c.setAttribute("aria-controls",l),c.dataset.focusKey="toggle",c.append(o("span","",e.advanced),S("chevron-down")),c.addEventListener("click",()=>{this.open=!this.open,this.render()});const d=o("div","seo-robots__advanced-row");d.append(c);const h=I(this.model);h>0&&d.append(o("span","seo-robots__summary",B(e.advancedSet,h,y(this))));const u=o("div","seo-robots__panel");u.id=l,u.hidden=!this.open,u.append(this.numberField(e.maxSnippet,e.maxSnippetHint,"maxSnippet",t,e),this.selectField(e,t),this.numberField(e.maxVideoPreview,"","maxVideoPreview",t,e),this.dateField(e,t));const g=o("div","seo-robots__checks");for(const b of["noarchive","nosnippet","noimageindex"]){const p=o("label","seo-robots__check"),f=o("input");f.type="checkbox",f.checked=this.model[b],f.disabled=t,f.dataset.focusKey=b,f.addEventListener("change",()=>{this.model[b]=f.checked,this.commit()}),p.append(f,o("span","",e[b])),g.append(p)}u.append(g),this.replaceChildren(a,d,u),r&&this.querySelector(`[data-focus-key="${r}"]`)?.focus()}radioGroup(e,t,s,i){const r=o("div","seo-robots__group"),a=T("seo-robots-caption"),l=o("span","seo-robots__caption",e);l.id=a;const c=o("div","seo-robots__track");c.setAttribute("role","radiogroup"),c.setAttribute("aria-labelledby",a);const d=this.model[t];return s.forEach(([h,u,g],b)=>{const p=o("button","seo-robots__segment seo-robots__segment--"+g,u);p.type="button",p.setAttribute("role","radio");const f=d===h;p.setAttribute("aria-checked",f?"true":"false"),p.tabIndex=f?0:-1,p.disabled=i,p.dataset.focusKey=t+"-"+b,p.addEventListener("click",()=>this.choose(t,h)),p.addEventListener("keydown",w=>{const k=w.key==="ArrowRight"||w.key==="ArrowDown"?1:w.key==="ArrowLeft"||w.key==="ArrowUp"?-1:0;if(k===0||i)return;w.preventDefault();const m=(b+k+s.length)%s.length;this.choose(t,s[m][0]),this.querySelector(`[data-focus-key="${t}-${m}"]`)?.focus()}),c.append(p)}),r.append(l,c),r}choose(e,t){this.hasAttribute("disabled")||(e==="index"?this.model.index=t:this.model.follow=t,this.commit())}numberField(e,t,s,i,r){const a=o("label","seo-robots__field");a.append(o("span","seo-robots__label",e));const l=o("input","seo-robots__input");l.type="number",l.min="-1",l.step="1",l.disabled=i,l.dataset.focusKey=s;const c=this.model[s];l.value=c===null?"":String(c);const d=o("span","seo-robots__error");return d.setAttribute("role","status"),l.addEventListener("change",()=>{const h=l.value.trim();if(h===""){this.model[s]=null,this.commit();return}if(!/^-?\d+$/.test(h)||Number.parseInt(h,10)<-1){d.textContent=r.invalidNumber;return}this.model[s]=Number.parseInt(h,10),this.commit()}),a.append(l),t&&a.append(o("span","seo-robots__hint",t)),a.append(d),a}selectField(e,t){const s=o("label","seo-robots__field");s.append(o("span","seo-robots__label",e.maxImagePreview));const i=o("select","seo-robots__input");i.disabled=t,i.dataset.focusKey="maxImagePreview";for(const[r,a]of[["",e.notSet],["none",e.none],["standard",e.standard],["large",e.large]]){const l=o("option","",a);l.value=r,l.selected=this.model.maxImagePreview===r,i.append(l)}return i.addEventListener("change",()=>{this.model.maxImagePreview=i.value,this.commit()}),s.append(i),s}dateField(e,t){const s=o("label","seo-robots__field");s.append(o("span","seo-robots__label",e.unavailableAfter));const i=o("input","seo-robots__input");return i.type="date",i.disabled=t,i.dataset.focusKey="unavailableAfter",i.value=this.model.unavailableAfter,i.addEventListener("change",()=>{this.model.unavailableAfter=/^\d{4}-\d{2}-\d{2}$/.test(i.value)?i.value:"",this.commit()}),s.append(i),s}}class ${constructor(e,t){this.host=e,this.onDropped=t}host;onDropped;inflight=null;queued=null;retry=null;retried=!1;timer;get current(){return this.inflight}request(e){if(this.host.hasAttribute("busy")||this.inflight!==null){this.queued=e;return}this.send(e)}settle(){const e=this.inflight;return this.inflight=null,this.flush(),e}flush(){if(this.host.hasAttribute("busy")||this.inflight!==null)return;const e=this.retry??this.queued;e!==null&&(this.retry!==null?this.retry=null:this.queued=null,this.send(e))}reset(){clearTimeout(this.timer),this.timer=void 0,this.inflight=null,this.retry=null,this.retried=!1}send(e){if(this.inflight=e,C(this.host,"summer-action",{payload:e}),this.host.hasAttribute("busy")||this.inflight!==e){this.retried=!1;return}if(this.inflight=null,this.retried){this.retried=!1,this.onDropped(e);return}this.retried=!0,this.retry=e,clearTimeout(this.timer),this.timer=setTimeout(()=>{this.timer=void 0,this.flush()},0)}}const ue="golem15-seo-robotstester",G=["Googlebot","Googlebot-Image","Bingbot","*"];function he(n,e){const t={agent:e.agent,line:e.line,rule:e.rule};return e.allowed?e.line>0?v(n.allowedRule,t):v(n.allowedNone,t):e.bot?v(n.blockedBot,t):v(n.blocked,t)}class pe extends HTMLElement{static observedAttributes=["busy","state","ui-locale","disabled","label","busy-label"];context=null;result=null;error="";actions=new $(this,e=>this.failed(e));connected=!1;input=null;select=null;button=null;line=null;notes=null;builtFor="";onResult=e=>{const s=e.detail?.data;this.actions.settle(),s&&s.op==="context"?(this.context=s,this.build(!0)):s&&s.op==="test"&&(this.result=s,this.error="",this.renderResult())};connectedCallback(){this.connected=!0,this.addEventListener("summer-result",this.onResult),this.build(!1),queueMicrotask(()=>{this.connected&&!this.context&&this.request({op:"context"})})}disconnectedCallback(){this.connected=!1,this.removeEventListener("summer-result",this.onResult),this.actions.reset()}attributeChangedCallback(e,t,s){if(this.connected){if(e==="busy"&&s===null&&this.actions.flush(),e==="state"&&s==="error"&&this.actions.settle(),e==="ui-locale"){this.build(!0);return}this.syncControls()}}labels(){return L(y(this))}request(e){this.actions.request(e)}failed(e){e.op==="test"&&(this.error=this.labels().testFailed,this.result=null,this.renderResult())}build(e){const t=y(this)+"|"+(this.context?.agents??G).join(",");if(!e&&this.builtFor===t&&this.input){this.syncControls();return}this.builtFor=t;const s=this.labels(),i=this.input?.value??"",r=this.select?.value??"Googlebot";this.replaceChildren();const a=o("div","seo-robotstester__row"),l=T("seo-robotstester-url"),c=o("div","seo-robotstester__field seo-robotstester__field--url"),d=o("label","seo-robotstester__label",s.url);d.htmlFor=l;const h=o("input","seo-robotstester__input");h.id=l,h.type="text",h.placeholder=s.urlPlaceholder,h.value=i,h.addEventListener("keydown",m=>{m.key==="Enter"&&(m.preventDefault(),this.test())}),c.append(d,h);const u=T("seo-robotstester-agent"),g=o("div","seo-robotstester__field seo-robotstester__field--agent"),b=o("label","seo-robotstester__label",s.agent);b.htmlFor=u;const p=o("select","seo-robotstester__select");p.id=u;for(const m of this.context?.agents??G){const x=o("option","",m);x.value=m,p.append(x)}p.value=r,p.value!==r&&(p.value="Googlebot"),g.append(b,p);const f=o("button","seo-robotstester__button");f.type="button",f.addEventListener("click",()=>this.test()),a.append(c,g,f);const w=o("div","seo-robotstester__result");w.setAttribute("role","status");const k=o("div","seo-robotstester__notes");this.input=h,this.select=p,this.button=f,this.line=w,this.notes=k,this.append(a,w,k),this.syncControls(),this.renderResult()}syncControls(){if(!this.button||!this.input||!this.select)return;const e=this.hasAttribute("disabled"),t=this.hasAttribute("busy");this.input.readOnly=e,this.select.disabled=e,this.button.hidden=e,this.button.disabled=t,this.button.textContent=t?this.getAttribute("busy-label")||this.getAttribute("label")||"":this.getAttribute("label")||""}test(){if(this.hasAttribute("disabled")||!this.input||!this.select)return;const e=this.input.value.trim();if(!e.startsWith("/")){this.error=this.labels().urlRequired,this.result=null,this.renderResult();return}this.error="",this.request({op:"test",url:e,agent:this.select.value})}renderResult(){if(!this.line||!this.notes)return;const e=this.labels();if(this.line.className="seo-robotstester__result",this.error)this.line.classList.add("seo-robotstester__result--error"),this.line.replaceChildren(o("span","",this.error));else if(this.result){const s=this.result.allowed;this.line.classList.add(s?"seo-robotstester__result--allowed":"seo-robotstester__result--blocked"),this.line.replaceChildren(S(s?"circle-check":"triangle-alert"),o("span","",he(e,this.result)))}else this.line.replaceChildren();const t=[];(this.context?.staging||D(this,"data",{}).staging)&&t.push(o("p","seo-robotstester__note",e.stagingNote)),t.push(o("p","seo-robotstester__note",e.unsaved)),this.notes.replaceChildren(...t)}}const fe="golem15-seo-robotstxt",me=500*1024,F=10,ge=["User-agent","Allow","Disallow","Sitemap","Crawl-delay","Host","Clean-param"];function be(n,e){const t=Array.from({length:e.length+1},(s,i)=>i);for(let s=1;s<=n.length;s++){let i=t[0];t[0]=s;for(let r=1;r<=e.length;r++){const a=t[r];t[r]=Math.min(t[r]+1,t[r-1]+1,i+(n[s-1]===e[r-1]?0:1)),i=a}}return t[e.length]}function we(n){const e=n.toLowerCase();let t="",s=3;for(const i of ge){const r=be(e,i.toLowerCase());r{const r=i+1,a=s.indexOf("#"),l=(a>=0?s.slice(0,a):s).trim();if(l==="")return;const c=l.indexOf(":");if(c<0){e.push({kind:"noColon",line:r});return}const d=l.slice(0,c).trim(),h=l.slice(c+1).trim();switch(d.toLowerCase()){case"user-agent":t=!0;return;case"allow":case"disallow":t?h!==""&&!h.startsWith("/")&&!h.startsWith("*")&&e.push({kind:"badPath",line:r}):e.push({kind:"noGroup",line:r});return;case"sitemap":e.push({kind:"ownSitemap",line:r});return;case"crawl-delay":e.push({kind:"crawlDelay",line:r});return;case"host":case"clean-param":return}const u=we(d);e.push(u?{kind:"unknownSuggest",line:r,word:d,suggestion:u}:{kind:"unknown",line:r,word:d})}),e}function _e(n){return new TextEncoder().encode(n).length}function ye(n,e){return v(n[e.kind],{line:e.line,word:e.word??"",suggestion:e.suggestion??""})}class ke extends HTMLElement{static observedAttributes=["disabled","invalid","ui-locale"];current="";control=null;panel=null;meter=null;builtFor="";get value(){return this.current}set value(e){this.current=typeof e=="string"?e:e==null?"":String(e),this.control&&this.control.value!==this.current&&(this.control.value=this.current),this.update()}connectedCallback(){this.build()}attributeChangedCallback(e){if(this.isConnected){if(e==="ui-locale"){this.build();return}this.sync()}}labels(){return L(y(this))}build(){const e=y(this);if(this.builtFor===e&&this.control){this.sync();return}this.builtFor=e,this.replaceChildren();const t=T("seo-robotstxt-lint"),s=o("textarea","seo-robotstxt__control");s.spellcheck=!1,s.setAttribute("wrap","off"),s.value=this.current,s.setAttribute("aria-describedby",t);const i=this.getAttribute("field-name");i&&s.setAttribute("aria-label",i),s.addEventListener("input",()=>{this.hasAttribute("disabled")||(this.current=s.value,this.update(),C(this,"summer-change",{value:this.current}))});const r=o("div","seo-robotstxt__lint");r.id=t,r.setAttribute("role","status");const a=o("div","seo-robotstxt__size");this.control=s,this.panel=r,this.meter=a,this.append(s,r,a),this.sync()}sync(){this.control&&(this.control.readOnly=this.hasAttribute("disabled"),this.classList.toggle("seo-robotstxt--invalid",this.hasAttribute("invalid")),this.control.setAttribute("aria-invalid",this.hasAttribute("invalid")?"true":"false"),this.update())}update(){if(!this.panel||!this.meter)return;const e=this.labels(),t=y(this),s=ve(this.current),i=[];if(s.length===0){const c=o("div","seo-robotstxt__finding seo-robotstxt__finding--clean");c.append(S("circle-check"),o("span","",e.clean)),i.push(c)}else{for(const c of s.slice(0,F)){const d=o("div","seo-robotstxt__finding");d.append(S("triangle-alert"),o("span","",ye(e,c))),i.push(d)}s.length>F&&i.push(o("div","seo-robotstxt__more",v(e.moreFindings,{count:A(s.length-F,t)})))}this.panel.replaceChildren(...i);const r=_e(this.current),a=new Intl.NumberFormat(t,{maximumFractionDigits:1}).format(r/1024)+" KB";this.meter.replaceChildren(o("span","",v(e.size,{size:a})));const l=r>me;this.classList.toggle("seo-robotstxt--over",l),l&&this.meter.append(o("span","seo-robotstxt__over",e.tooLarge))}}const X="20px Arial, sans-serif",Y="14px Arial, sans-serif",J=580,Q=920,xe=680,Ae=30,ze=120;let N;function Te(){if(N!==void 0)return N;try{const e=document.createElement("canvas").getContext("2d");N=e&&typeof e.measureText=="function"?e:null}catch{N=null}return N}function Ee(n,e){let t=0;for(const s of n)/[ilIj.,:;'!|]/.test(s)?t+=.28:/[mwMW@]/.test(s)?t+=.85:/[A-Z]/.test(s)?t+=.67:s===" "?t+=.28:t+=.53;return t*e}function O(n,e){const t=Te();if(t){t.font=e;const i=t.measureText(n).width;if(Number.isFinite(i)&&(i>0||n===""))return i}const s=Number.parseFloat(e)||14;return Ee(n,s)}const j=" …";function q(n,e,t){if(O(n,e)<=t)return n;const s=u=>O(u+j,e)<=t,i=Array.from(n),r=[];for(let u=1;u>1;s(i.slice(0,r[u]).join(""))?(a=u,l=u+1):c=u-1}if(a>=0)return i.slice(0,r[a]).join("")+j;let d=0,h=i.length;for(;ds?c="long":a=i}function R(n,e){const t=K(n);if(t.length===0)return!1;const s=K(e);for(let i=0;i+t.length<=s.length;i++){let r=!0;for(let a=0;athis.failed(e));duplicates=new Map;checking=!1;timer;lastChecked="";connected=!1;onResult=e=>{const s=e.detail?.data;this.actions.settle(),s&&s.op==="context"?(this.context=s,this.contextFailed=!1,this.render(),this.scheduleDuplicates(!0)):s&&s.op==="duplicates"&&(this.duplicates.set(s.locale,s),this.checking=!1,this.render())};onLocale=e=>{const t=e.detail;typeof t=="string"&&t!==""&&t!==this.shownLocale&&(this.shownLocale=t,this.render(),this.scheduleDuplicates(!1))};connectedCallback(){this.connected=!0,this.addEventListener("summer-result",this.onResult),window.addEventListener("summer:ml-locale",this.onLocale),this.render(),queueMicrotask(()=>{this.connected&&(this.context?this.scheduleDuplicates(!0):this.request({op:"context"}))})}disconnectedCallback(){this.connected=!1,this.removeEventListener("summer-result",this.onResult),window.removeEventListener("summer:ml-locale",this.onLocale),clearTimeout(this.timer),this.actions.reset(),this.checking=!1,this.lastChecked=""}attributeChangedCallback(e,t,s){if(this.connected){if(e==="busy"&&s===null){this.actions.flush();return}if(e==="state"&&s==="error"){const i=this.actions.settle();i?this.failed(i):(this.checking=!1,this.render());return}if(e==="fill-values"){this.render(),this.scheduleDuplicates(!1);return}this.render()}}labels(){return L(y(this))}request(e){this.actions.request(e)}failed(e){e.op==="context"?this.contextFailed=!0:this.lastChecked="",this.checking=!1,this.render()}fill(){return D(this,"fill-values",{})}locale(){const e=this.context?.locales??[];return e.length===0?null:e.find(t=>t.locale===this.shownLocale)??e[0]}scheduleDuplicates(e){const t=this.locale();if(!t)return;const s=this.fill(),i=z(s,"seo_title",t.locale)||z(s,"title",t.locale),r=z(s,"seo_description",t.locale)||z(s,"description",t.locale),a=JSON.stringify([t.locale,i,r]);if(a===this.lastChecked)return;clearTimeout(this.timer);const l=()=>{this.lastChecked=a,this.checking=!0,this.render(),this.request({op:"duplicates",locale:t.locale,title:i,description:r})};e?l():this.timer=setTimeout(l,Ne)}render(){const e=this.labels(),t=y(this);if(!this.context&&!this.contextFailed){this.setAttribute("aria-busy","true"),this.replaceChildren(o("div","seo-serp__skeleton"));return}this.removeAttribute("aria-busy");const s=this.locale(),i=s?.locale??this.shownLocale,r=this.fill(),a=(z(r,"seo_title",i)||z(r,"title",i)).trim(),l=(z(r,"seo_description",i)||z(r,"description",i)).trim(),c=z(r,"seo_keyphrase",i).trim(),d=s?.siteName??"",h=a||De(s?.titleTemplate??"",{title:s?.defaultTitle??"",site:d})||s?.defaultTitle||"",u=l||s?.defaultDescription||"",g=o("div","seo-serp__header"),b=o("div","seo-serp__track");b.setAttribute("role","radiogroup"),b.setAttribute("aria-label",e.device);for(const _ of["desktop","mobile"]){const E=o("button","seo-serp__segment",e[_]);E.type="button",E.setAttribute("role","radio"),E.setAttribute("aria-checked",this.device===_?"true":"false"),E.addEventListener("click",()=>{this.device=_,this.render()}),b.append(E)}g.append(b),i&&g.append(o("span","seo-serp__language",v(e.language,{locale:i.toUpperCase()})));const p=o("div","serp-card"+(this.device==="mobile"?" serp-card--mobile":"")),f=o("div","serp-card__site");f.append(o("span","serp-card__favicon",Array.from(d||this.context?.host||"?")[0]?.toUpperCase()??"?"));const w=o("div","serp-card__names");if(w.append(o("span","serp-card__sitename",d||this.context?.host||"")),w.append(o("span","serp-card__crumbs",Pe(this.context?.host??"",s?.url??null))),f.append(w),p.append(f),p.append(o("div","serp-card__title",q(h,X,J))),u){const _=this.device==="mobile"?xe:Q;p.append(o("div","serp-card__description",q(u,Y,_)))}else p.append(o("div","serp-card__description serp-card__description--missing",e.noDescription));const k=o("div","seo-serp__notes");if(this.contextFailed){const _=o("div","seo-serp__error");_.setAttribute("role","status"),_.append(S("circle-alert"),o("span","",e.contextFailed)),k.append(_)}!a&&s?.titleTemplate&&k.append(o("div","",v(e.fromTemplate,{template:s.titleTemplate}))),!l&&s?.defaultDescription&&s.descriptionSource==="content"&&k.append(o("div","",e.fromContent)),s&&s.url===null&&k.append(o("div","",e.noUrl)),(this.getAttribute("record-id")??"")!==""&&s&&k.append(o("div","",e.savedNote));const m=o("div","seo-serp__checks");if(m.setAttribute("role","status"),this.checking)m.append(o("div","seo-serp__checking",e.checking));else{const _=this.duplicates.get(i);if(_)for(const[E,ee]of[[_.title,e.dupTitle],[_.description,e.dupDescription]])E.length>0&&m.append(Me(E,ee,e,t))}const x=[g,p,k,m];c&&s&&x.push(Oe(c,{title:h,description:u,h1:s.h1,url:s.url,bodyWords:s.bodyWords},e,t)),this.replaceChildren(...x)}}function Pe(n,e){const t=(e??"").split("?")[0].split("/").filter(Boolean).map(s=>{try{return decodeURIComponent(s)}catch{return s}});return[n,...t].filter(Boolean).join(" › ")}function Me(n,e,t,s){const i=o("div","seo-serp__check seo-serp__check--warn");i.append(S("triangle-alert"));const r=o("span"),a="\0list\0",l=B(e,n.length,s,{list:a}),[c,d]=l.split(a);return r.append(document.createTextNode(c??"")),n.slice(0,P).forEach((h,u)=>{if(u>0&&r.append(document.createTextNode(", ")),h.url.startsWith("/")&&!h.url.startsWith("//")){const g=o("a","seo-serp__link",h.label);g.href=h.url,g.target="_blank",g.rel="noopener",r.append(g)}else r.append(document.createTextNode(h.label))}),n.length>P&&r.append(document.createTextNode(" "+v(t.andMore,{count:n.length-P}))),r.append(document.createTextNode(d??"")),i.append(r),i}function Oe(n,e,t,s){const i=o("div","seo-serp__keyphrase");i.append(o("div","seo-serp__keyphrase-title",v(t.keyphrase,{phrase:n})));const r=(a,l,c)=>{const d=o("div","seo-serp__check "+(a?"seo-serp__check--ok":"seo-serp__check--warn"));d.append(S(a?"circle-check":"triangle-alert"),o("span","",a?l:c)),i.append(d)};if(r(R(n,e.title),t.kpTitle,t.kpTitleNo),e.h1!==null&&r(R(n,e.h1),t.kpH1,t.kpH1No),r(R(n,e.description),t.kpDesc,t.kpDescNo),e.url!==null&&r(R(n,e.url.replace(/[-_/]+/g," ")),t.kpUrl,t.kpUrlNo),e.bodyWords!==null){const a=new Intl.NumberFormat(s).format(e.bodyWords);r(e.bodyWords>=Re,v(t.kpLength,{count:a}),v(t.kpLengthNo,{count:a}))}return i}const Ue="golem15-seo-text",Be={empty:"info",short:"triangle-alert",long:"triangle-alert",good:"circle-check"};class He extends HTMLElement{static observedAttributes=["options","disabled","invalid","ui-locale","locale"];current="";control=null;statusIcon=null;statusText=null;countText=null;live=null;lastStatus=null;builtFor="";get value(){return this.current}set value(e){this.current=typeof e=="string"?e:e==null?"":String(e),this.control&&this.control.value!==this.current&&(this.control.value=this.current),this.update(!1)}connectedCallback(){this.build()}attributeChangedCallback(e){if(this.isConnected){if(e==="options"||e==="ui-locale"){this.build();return}this.sync()}}measure(){return D(this,"options",{}).measure==="description"?"description":"title"}labels(){return L(y(this))}build(){const e=this.measure()+"|"+y(this);if(this.builtFor===e&&this.control){this.sync();return}this.builtFor=e,this.replaceChildren();const t=T("seo-text-counter"),i=this.measure()==="description"?o("textarea","seo-text__control seo-text__control--area"):o("input","seo-text__control");i instanceof HTMLTextAreaElement?i.rows=3:i.type="text",i.value=this.current,i.setAttribute("aria-describedby",t);const r=this.getAttribute("field-name");r&&i.setAttribute("aria-label",r),i.addEventListener("input",()=>{this.hasAttribute("disabled")||(this.current=i.value.replace(/\s+$/u,""),this.update(!0),C(this,"summer-change",{value:this.current}))});const a=o("div","seo-text__counter");a.id=t;const l=o("span","seo-text__status");this.statusIcon=o("span","seo-text__icon"),this.statusText=o("span","seo-text__status-text"),l.append(this.statusIcon,this.statusText),this.countText=o("span","seo-text__count"),a.append(l,this.countText),this.live=o("span","seo-visually-hidden"),this.live.setAttribute("aria-live","polite"),this.live.setAttribute("role","status"),this.control=i,this.lastStatus=null,this.append(i,a,this.live),this.sync()}sync(){if(!this.control)return;const e=this.hasAttribute("disabled");this.control.readOnly=e,this.classList.toggle("seo-text--invalid",this.hasAttribute("invalid")),this.control.setAttribute("aria-invalid",this.hasAttribute("invalid")?"true":"false"),this.update(!1)}update(e){if(!this.statusText||!this.countText||!this.statusIcon)return;const t=this.labels(),s=y(this),i=this.measure(),r=Se(this.current,i),a=We(t,r.status,i);this.statusIcon.replaceChildren(S(Be[r.status])),this.statusText.textContent=a,this.dataset.status=r.status,this.countText.textContent=v(t.count,{chars:A(r.chars,s),px:A(r.px,s),max:A(r.maxPx,s)}),e&&this.live&&r.status!==this.lastStatus&&(this.live.textContent=a),this.lastStatus=r.status}}function We(n,e,t){switch(e){case"empty":return t==="title"?n.emptyTitle:n.emptyDescription;case"short":return t==="title"?n.shortTitle:n.shortDescription;case"long":return t==="title"?n.longTitle:n.longDescription;default:return n.good}}const Ge="golem15-seo-transfer",Z=60*1024,U=200;function qe(n){return n.summary.refused>0?"refused":n.count>0?"ready":"nothing"}function Ke(n){const e=n.filter(s=>s.change==="refused"),t=n.filter(s=>s.change!=="refused");return[...e,...t].slice(0,U)}function Ze(n,e){const t=new Blob([e],{type:"text/csv;charset=utf-8"}),s=URL.createObjectURL(t),i=document.createElement("a");i.href=s,i.download=n,i.rel="noopener",document.body.append(i),i.click(),i.remove(),setTimeout(()=>URL.revokeObjectURL(s),0)}function Ve(){return window.location.pathname.replace(/\/transfer(\/.*)?$/,"/history")}class $e extends HTMLElement{static observedAttributes=["busy","state","ui-locale","disabled","label","busy-label"];connected=!1;pending=null;exportDataset="redirects";importDataset="redirects";fileName="";csv="";fileError="";importError="";exportError="";applyError="";preview=null;applied=!1;onResult=e=>{const s=e.detail?.data;if(this.pending=null,!s){this.render();return}s.error!==void 0?s.op==="export"?this.exportError=s.message:s.op==="preview"?(this.importError=s.message,this.preview=null):this.applyError=s.message:s.op==="export"?(this.exportError="",Ze(s.filename,s.csv)):s.op==="preview"?(this.importError="",this.applyError="",this.applied=!1,this.preview=s):s.op==="apply"&&(this.preview=null,this.applyError="",this.applied=!0),this.render()};connectedCallback(){this.connected=!0,this.addEventListener("summer-result",this.onResult),this.render()}disconnectedCallback(){this.connected=!1,this.removeEventListener("summer-result",this.onResult)}attributeChangedCallback(e,t,s){this.connected&&(e==="state"&&s==="error"&&(this.pending=null),e==="busy"&&s===null&&(this.pending=null),this.render())}labels(){return L(y(this))}request(e){this.hasAttribute("disabled")||this.hasAttribute("busy")||this.pending||(this.pending=e.op,C(this,"summer-action",{payload:e}),this.render())}busyLabel(e,t){return this.pending===e&&this.getAttribute("busy-label")||t}datasetSelect(e,t,s){const i=o("div","seo-transfer__field"),r=T("seo-transfer-dataset"),a=o("label","seo-transfer__label",e.dataset);a.htmlFor=r;const l=o("select","seo-transfer__select");l.id=r;const c=[["redirects",e.dsRedirects],["rules",e.dsRules],["records",e.dsRecords]];for(const[d,h]of c){const u=o("option","",h);u.value=d,l.append(u)}return l.value=t,l.disabled=this.hasAttribute("disabled"),l.addEventListener("change",()=>s(l.value)),i.append(a,l),i}button(e,t,s){const i=o("button","seo-transfer__button seo-transfer__button--"+t,e);return i.type="button",i.addEventListener("click",s),i}message(e,t){const s=o("p",t,e);return s.setAttribute("role","status"),s}async chooseFile(e){const t=e.files?.[0];if(this.preview=null,this.applied=!1,this.importError="",this.applyError="",this.csv="",!t){this.fileName="",this.fileError="",this.render();return}if(this.fileName=t.name,t.size>Z){this.fileError=v(this.labels().fileTooLarge,{limit:A(Z/1024,y(this))+" KB"}),this.render();return}try{this.csv=await t.text(),this.fileError=""}catch{this.fileError=this.labels().readFailed}this.render()}render(){const e=this.labels(),t=y(this),s=this.hasAttribute("disabled"),i=this.hasAttribute("busy")||this.pending!==null,r=this.getAttribute("label")||e.chooseFile,a=o("section","seo-transfer__section");a.append(o("h3","seo-transfer__heading",e.exportHeading));const l=o("div","seo-transfer__row");l.append(this.datasetSelect(e,this.exportDataset,f=>this.exportDataset=f));const c=this.button(this.busyLabel("export",e.download),"outline",()=>this.request({op:"export",dataset:this.exportDataset}));c.disabled=i,c.hidden=s,l.append(c),a.append(l),this.exportError&&a.append(this.message(this.exportError,"seo-transfer__error"));const d=o("section","seo-transfer__section seo-transfer__section--import");d.append(o("h3","seo-transfer__heading",e.importHeading));const h=o("div","seo-transfer__row");h.append(this.datasetSelect(e,this.importDataset,f=>{this.importDataset=f,this.preview=null,this.render()}));const u=o("input","seo-transfer__file");u.type="file",u.accept=".csv,text/csv",u.hidden=!0,u.addEventListener("change",()=>{this.chooseFile(u)});const g=this.button(e.chooseFile,"outline",()=>u.click());g.hidden=s,g.disabled=i;const b=o("span",this.fileName?"seo-transfer__filename":"seo-transfer__filename seo-transfer__filename--none",this.fileName||e.noFile),p=this.button(this.busyLabel("preview",r),"outline",()=>this.request({op:"preview",dataset:this.importDataset,csv:this.csv}));if(p.disabled=i||this.csv===""||this.fileError!=="",p.hidden=s,h.append(u,g,b,p),d.append(h),(this.fileError||this.importError)&&d.append(this.message(this.fileError||this.importError,"seo-transfer__error")),this.applied){const f=o("p","seo-transfer__done"),w=o("a","seo-transfer__link",e.openHistory);w.href=Ve(),f.append(w),d.append(f)}this.preview&&d.append(this.renderPreview(e,t,i,s)),this.replaceChildren(a,d)}renderPreview(e,t,s,i){const r=this.preview,a=o("div","seo-transfer__preview"),l=r.summary;a.append(this.message(v(e.summary,{new:A(l.new,t),changed:A(l.changed,t),unchanged:A(l.unchanged,t),refused:A(l.refused,t)}),"seo-transfer__summary"));const c=o("div","summer-table-wrap seo-transfer__table-wrap"),d=o("table","summer-table seo-transfer__table"),h=o("tr");for(const m of[e.colLine,e.colChange,e.colAddress,e.colField,e.colBefore,e.colAfter,e.colNote])h.append(o("th","",m));const u=o("thead");u.append(h);const g=o("tbody"),b={new:e.bNew,changed:e.bChanged,unchanged:e.bUnchanged,refused:e.bRefused};for(const m of Ke(r.rows)){const x=o("tr","seo-transfer__tr--"+m.change);x.append(o("td","seo-transfer__num",A(m.line,t)));const _=o("td");_.append(o("span","seo-transfer__badge seo-transfer__badge--"+m.change,b[m.change])),m.warning&&_.append(o("span","seo-transfer__badge seo-transfer__badge--warning",e.bWarning)),x.append(_),x.append(o("td","seo-transfer__mono",m.address)),x.append(o("td","",m.field)),x.append(o("td","",m.before)),x.append(o("td","",m.after)),x.append(o("td","",m.note)),g.append(x)}d.append(u,g),c.append(d),a.append(c),(r.total>U||r.rows.length>U)&&a.append(o("p","seo-transfer__note",v(e.rowCap,{count:A(Math.max(r.total,r.rows.length),t)})));const p=qe(r);p==="refused"?a.append(this.message(e.refusedBlocks,"seo-transfer__error")):p==="nothing"&&a.append(this.message(e.nothing,"seo-transfer__note")),this.applyError&&a.append(this.message(this.applyError,"seo-transfer__error"));const f=o("div","seo-transfer__actions"),w=this.button(e.discard,"ghost",()=>{this.preview=null,this.applyError="",this.render()});w.disabled=s;const k=this.button(this.busyLabel("apply",B(e.applyCount,r.count,t)),"primary",()=>this.request({op:"apply",token:r.token}));return k.disabled=s||p!=="ready",f.append(w,k),f.hidden=i,a.append(f),a}}const Xe=[[Ue,He],[ae,de],[Le,je],[ne,re],[fe,ke],[ue,pe],[Ge,$e]];for(const[n,e]of Xe)customElements.get(n)||customElements.define(n,e);export{ne as IMAGE_TAG,ue as ROBOTSTESTER_TAG,fe as ROBOTSTXT_TAG,ae as ROBOTS_TAG,Le as SERP_TAG,re as SeoImageElement,de as SeoRobotsElement,pe as SeoRobotsTesterElement,ke as SeoRobotsTxtElement,je as SeoSerpElement,He as SeoTextElement,$e as SeoTransferElement,Ue as TEXT_TAG,Ge as TRANSFER_TAG}; diff --git a/assets/src/elements/actions.ts b/assets/src/elements/actions.ts new file mode 100644 index 0000000..beb3c3f --- /dev/null +++ b/assets/src/elements/actions.ts @@ -0,0 +1,101 @@ +import { emit } from './labels' + +/** + * Sends a widget element's summer-action payloads one at a time. + * + * The admin's widget host takes an action by setting the element's `busy` + * attribute synchronously, inside the summer-action listener, and removes + * it when the request settles (after it dispatched summer-result, or set + * `state=error`). A payload sent while a request runs is queued (one at + * most, the newest wins) and sent when `busy` goes. + * + * The queue does not trust that an emitted action was taken: when `busy` + * is still absent after the event returns, the host dropped it (for + * example an action sent from connectedCallback before the host wired the + * element). The payload is then retried once on the next task; a second + * drop is reported through onDropped, so the element shows an error rather + * than waiting forever. + */ +export class ActionQueue

{ + private inflight: P | null = null + private queued: P | null = null + private retry: P | null = null + private retried = false + private timer: ReturnType | undefined + + constructor( + private readonly host: HTMLElement, + private readonly onDropped: (payload: P) => void, + ) {} + + /** The payload the host is working on, null when none. */ + get current(): P | null { + return this.inflight + } + + /** Sends payload, or queues it while a request runs. */ + request(payload: P): void { + if (this.host.hasAttribute('busy') || this.inflight !== null) { + this.queued = payload + return + } + this.send(payload) + } + + /** Clears the request that just settled (summer-result or state=error) and sends what waits. */ + settle(): P | null { + const done = this.inflight + this.inflight = null + this.flush() + return done + } + + /** Sends the waiting payload when the host is free. */ + flush(): void { + if (this.host.hasAttribute('busy') || this.inflight !== null) { + return + } + const next = this.retry ?? this.queued + if (next === null) { + return + } + if (this.retry !== null) { + this.retry = null + } else { + this.queued = null + } + this.send(next) + } + + /** Forgets the request in flight and any pending retry (the element left the page). */ + reset(): void { + clearTimeout(this.timer) + this.timer = undefined + this.inflight = null + this.retry = null + this.retried = false + } + + private send(payload: P): void { + this.inflight = payload + emit(this.host, 'summer-action', { payload }) + if (this.host.hasAttribute('busy') || this.inflight !== payload) { + // Taken (or already answered synchronously). + this.retried = false + return + } + this.inflight = null + if (this.retried) { + this.retried = false + this.onDropped(payload) + return + } + this.retried = true + this.retry = payload + clearTimeout(this.timer) + this.timer = setTimeout(() => { + this.timer = undefined + this.flush() + }, 0) + } +} diff --git a/assets/src/elements/labels.ts b/assets/src/elements/labels.ts index 7494cc1..2bbc33d 100644 --- a/assets/src/elements/labels.ts +++ b/assets/src/elements/labels.ts @@ -100,6 +100,7 @@ export interface Labels { unsaved: string stagingNote: string urlRequired: string + testFailed: string // E6 exportHeading: string importHeading: string @@ -217,6 +218,7 @@ const en: Labels = { unsaved: 'Tests the text in the editor and the AI bot switches, including unsaved changes.', stagingNote: 'On this environment every address is blocked. The result shows the production behaviour.', urlRequired: 'Enter an address starting with /.', + testFailed: 'The test could not be run. Try again.', exportHeading: 'Export', importHeading: 'Import', dataset: 'Data', @@ -343,6 +345,7 @@ const pl: Labels = { unsaved: 'Sprawdza tekst w edytorze i przełączniki botów AI, także niezapisane zmiany.', stagingNote: 'W tym środowisku każdy adres jest zablokowany. Wynik pokazuje zachowanie na produkcji.', urlRequired: 'Wpisz adres zaczynający się od /.', + testFailed: 'Nie udało się uruchomić testu. Spróbuj ponownie.', exportHeading: 'Eksport', importHeading: 'Import', dataset: 'Dane', diff --git a/assets/src/elements/robotstester.ts b/assets/src/elements/robotstester.ts index 20ed148..bcf7d32 100644 --- a/assets/src/elements/robotstester.ts +++ b/assets/src/elements/robotstester.ts @@ -1,4 +1,5 @@ -import { el, emit, fmt, icon, jsonAttr, labelsFor, uid, uiLocaleOf, type Labels } from './labels' +import { ActionQueue } from './actions' +import { el, fmt, icon, jsonAttr, labelsFor, uid, uiLocaleOf, type Labels } from './labels' export const ROBOTSTESTER_TAG = 'golem15-seo-robotstester' @@ -49,8 +50,7 @@ export class SeoRobotsTesterElement extends HTMLElement { private context: ContextData | null = null private result: TestData | null = null private error = '' - private inflight: Payload | null = null - private queued: Payload | null = null + private readonly actions = new ActionQueue(this, (payload) => this.failed(payload)) private connected = false private input: HTMLInputElement | null = null private select: HTMLSelectElement | null = null @@ -62,7 +62,7 @@ export class SeoRobotsTesterElement extends HTMLElement { private readonly onResult = (ev: Event): void => { const detail = (ev as CustomEvent).detail as { data?: unknown } | undefined const data = detail?.data as ContextData | TestData | undefined - this.inflight = null + this.actions.settle() if (data && data.op === 'context') { this.context = data this.build(true) @@ -71,19 +71,25 @@ export class SeoRobotsTesterElement extends HTMLElement { this.error = '' this.renderResult() } - this.flush() } connectedCallback(): void { this.connected = true this.addEventListener('summer-result', this.onResult) this.build(false) - this.request({ op: 'context' }) + // Deferred: the host may wire the element only after connecting it. + queueMicrotask(() => { + if (this.connected && !this.context) { + this.request({ op: 'context' }) + } + }) } disconnectedCallback(): void { this.connected = false this.removeEventListener('summer-result', this.onResult) + // A result that arrives while disconnected is lost; start over on reconnect. + this.actions.reset() } attributeChangedCallback(name: string, _old: string | null, value: string | null): void { @@ -91,10 +97,10 @@ export class SeoRobotsTesterElement extends HTMLElement { return } if (name === 'busy' && value === null) { - this.flush() + this.actions.flush() } if (name === 'state' && value === 'error') { - this.inflight = null + this.actions.settle() } if (name === 'ui-locale') { this.build(true) @@ -108,21 +114,16 @@ export class SeoRobotsTesterElement extends HTMLElement { } private request(payload: Payload): void { - if (this.hasAttribute('busy') || this.inflight) { - this.queued = payload - return - } - this.inflight = payload - emit(this, 'summer-action', { payload }) + this.actions.request(payload) } - private flush(): void { - if (this.hasAttribute('busy') || this.inflight || !this.queued) { - return + /** A request the host never took: the tester keeps the default crawlers, a test says it failed. */ + private failed(payload: Payload): void { + if (payload.op === 'test') { + this.error = this.labels().testFailed + this.result = null + this.renderResult() } - const next = this.queued - this.queued = null - this.request(next) } private build(force: boolean): void { diff --git a/assets/src/elements/serp.ts b/assets/src/elements/serp.ts index 2aceae9..487fd86 100644 --- a/assets/src/elements/serp.ts +++ b/assets/src/elements/serp.ts @@ -1,4 +1,5 @@ -import { el, emit, fmt, icon, jsonAttr, labelsFor, plural, uiLocaleOf, type Labels } from './labels' +import { ActionQueue } from './actions' +import { el, fmt, icon, jsonAttr, labelsFor, plural, uiLocaleOf, type Labels } from './labels' import { DESCRIPTION_FONT, DESCRIPTION_MAX_PX, DESCRIPTION_MOBILE_MAX_PX, TITLE_FONT, TITLE_MAX_PX, truncate } from './measure' export const SERP_TAG = 'golem15-seo-serp' @@ -135,8 +136,7 @@ export class SeoSerpElement extends HTMLElement { private contextFailed = false private device: 'desktop' | 'mobile' = 'desktop' private shownLocale = '' - private inflight: Payload | null = null - private queued: Payload | null = null + private readonly actions = new ActionQueue(this, (payload) => this.failed(payload)) private duplicates = new Map() private checking = false private timer: ReturnType | undefined @@ -146,7 +146,7 @@ export class SeoSerpElement extends HTMLElement { private readonly onResult = (ev: Event): void => { const detail = (ev as CustomEvent).detail as { data?: unknown } | undefined const data = detail?.data as ContextData | DuplicatesData | undefined - this.inflight = null + this.actions.settle() if (data && data.op === 'context') { this.context = data this.contextFailed = false @@ -157,7 +157,6 @@ export class SeoSerpElement extends HTMLElement { this.checking = false this.render() } - this.flush() } private readonly onLocale = (ev: Event): void => { @@ -174,7 +173,17 @@ export class SeoSerpElement extends HTMLElement { this.addEventListener('summer-result', this.onResult) window.addEventListener('summer:ml-locale', this.onLocale) this.render() - this.request({ op: 'context' }) + // Deferred: the host may wire the element only after connecting it. + queueMicrotask(() => { + if (!this.connected) { + return + } + if (this.context) { + this.scheduleDuplicates(true) + } else { + this.request({ op: 'context' }) + } + }) } disconnectedCallback(): void { @@ -182,6 +191,10 @@ export class SeoSerpElement extends HTMLElement { this.removeEventListener('summer-result', this.onResult) window.removeEventListener('summer:ml-locale', this.onLocale) clearTimeout(this.timer) + // A result that arrives while disconnected is lost; start over on reconnect. + this.actions.reset() + this.checking = false + this.lastChecked = '' } attributeChangedCallback(name: string, _old: string | null, value: string | null): void { @@ -189,16 +202,17 @@ export class SeoSerpElement extends HTMLElement { return } if (name === 'busy' && value === null) { - this.flush() + this.actions.flush() return } if (name === 'state' && value === 'error') { - if (this.inflight?.op === 'context') { - this.contextFailed = true + const failed = this.actions.settle() + if (failed) { + this.failed(failed) + } else { + this.checking = false + this.render() } - this.checking = false - this.inflight = null - this.render() return } if (name === 'fill-values') { @@ -215,21 +229,18 @@ export class SeoSerpElement extends HTMLElement { /** Sends payload, or queues it (one at most) while a request runs. */ private request(payload: Payload): void { - if (this.hasAttribute('busy') || this.inflight) { - this.queued = payload - return - } - this.inflight = payload - emit(this, 'summer-action', { payload }) + this.actions.request(payload) } - private flush(): void { - if (this.hasAttribute('busy') || this.inflight || !this.queued) { - return + /** A request the host refused (state=error) or never took. */ + private failed(payload: Payload): void { + if (payload.op === 'context') { + this.contextFailed = true + } else { + this.lastChecked = '' } - const next = this.queued - this.queued = null - this.request(next) + this.checking = false + this.render() } private fill(): Record { diff --git a/tests/elements/host.ts b/tests/elements/host.ts new file mode 100644 index 0000000..7bf29f3 --- /dev/null +++ b/tests/elements/host.ts @@ -0,0 +1,114 @@ +// A fake of the admin's widget host (summercms.go +// admin/src/components/form/fields/WidgetField.vue), so element tests drive +// the real host contract instead of dispatching summer-result by hand: +// +// - the host listens for summer-action on the element and ignores it +// while it has no element reference or is already busy; +// - it sets `busy` and clears `state` synchronously, inside the listener; +// - the request settles asynchronously: on success it dispatches +// summer-result with {data, fill, message}, on failure it sets +// state=error; then it removes `busy`. +// +// `order` reproduces how the host wires the element: 'append-first' is the +// v0.5.1 host, which appends the element (running connectedCallback) before +// it keeps the reference, so an action sent from connectedCallback is +// dropped; 'assign-first' keeps the reference before appending. + +export type Answer = (payload: unknown) => unknown + +export interface FakeHost { + node: HTMLElement + /** Every summer-action payload the element emitted. */ + emitted: unknown[] + /** The payloads the host took and posted. */ + posted: unknown[] + /** Makes the host ignore the next n actions (as a host without a reference does). */ + drop(n: number): void + /** Whether a request is running. */ + busy(): boolean +} + +/** A reason the host's request fails with (it sets state=error). */ +export class HostFailure extends Error {} + +export interface MountOptions { + order?: 'append-first' | 'assign-first' + attrs?: Record + /** Answers a posted payload with the response data; throw HostFailure to fail it. */ + answer: Answer + /** When false the host never keeps a reference (every action is dropped). */ + wired?: boolean +} + +export function mountWidget(tag: string, options: MountOptions): FakeHost { + const node = document.createElement(tag) + for (const [name, value] of Object.entries(options.attrs ?? {})) { + node.setAttribute(name, value) + } + let element: HTMLElement | null = null + let busy = false + let dropping = 0 + const emitted: unknown[] = [] + const posted: unknown[] = [] + + async function onAction(payload: unknown): Promise { + const target = element + if (!target || busy) { + return + } + if (dropping > 0) { + dropping-- + return + } + busy = true + target.setAttribute('busy', '') + target.removeAttribute('state') + posted.push(payload) + try { + await Promise.resolve() + const data = options.answer(payload) + target.setAttribute('data', JSON.stringify(data)) + target.dispatchEvent(new CustomEvent('summer-result', { detail: { data, fill: {}, message: undefined } })) + } catch (err) { + if (!(err instanceof HostFailure)) { + throw err + } + target.setAttribute('state', 'error') + } finally { + busy = false + element?.removeAttribute('busy') + } + } + + node.addEventListener('summer-action', (event) => { + const detail = (event as CustomEvent).detail as { payload?: unknown } | undefined + emitted.push(detail?.payload) + void onAction(detail?.payload) + }) + const wired = options.wired ?? true + if (options.order === 'assign-first' && wired) { + element = node + document.body.append(node) + } else { + document.body.append(node) + if (wired) { + element = node + } + } + return { + node, + emitted, + posted, + drop: (n) => { + dropping = n + }, + busy: () => busy, + } +} + +/** Lets microtasks and zero-delay timers run (the host's request and the elements' retries). */ +export async function settle(rounds = 5): Promise { + for (let i = 0; i < rounds; i++) { + await new Promise((resolve) => setTimeout(resolve, 0)) + } +} diff --git a/tests/elements/robotstester.test.ts b/tests/elements/robotstester.test.ts new file mode 100644 index 0000000..b236646 --- /dev/null +++ b/tests/elements/robotstester.test.ts @@ -0,0 +1,144 @@ +import { afterEach, beforeAll, describe, expect, it } from 'vitest' +import { labelsFor } from '../../assets/src/elements/labels' +import { HostFailure, mountWidget, settle, type MountOptions } from './host' + +// E5 golem15-seo-robotstester (UI-SPEC C10) through the widget host +// contract: the context request from connectedCallback, the queue behind +// `busy`, state=error, the address check and a host that drops actions. + +beforeAll(async () => { + await import('../../assets/src/elements/index') +}) + +afterEach(() => { + document.body.replaceChildren() +}) + +type Payload = { op: 'context' } | { op: 'test'; url: string; agent: string } + +function answer(payload: unknown): unknown { + const p = payload as Payload + if (p.op === 'context') { + return { op: 'context', agents: ['Googlebot', 'Bingbot', 'GPTBot', '*'], bots: ['GPTBot'], staging: false } + } + return { op: 'test', url: p.url, allowed: p.url !== '/admin', line: p.url === '/admin' ? 2 : 0, rule: p.url === '/admin' ? 'Disallow: /admin' : '', bot: false, agent: p.agent, staging: false } +} + +function tester(options: Partial = {}) { + return mountWidget('golem15-seo-robotstester', { + attrs: { 'ui-locale': 'en', label: 'Test URL', 'busy-label': 'Testing…' }, + answer, + ...options, + }) +} + +function testURL(node: HTMLElement, url: string): void { + const input = node.querySelector('.seo-robotstester__input')! + input.value = url + node.querySelector('.seo-robotstester__button')!.click() +} + +function resultLine(node: HTMLElement): string { + return node.querySelector('.seo-robotstester__result')?.textContent ?? '' +} + +function agents(node: HTMLElement): string[] { + return Array.from(node.querySelectorAll('.seo-robotstester__select option')).map((o) => o.textContent ?? '') +} + +describe('E5 host protocol', () => { + it('loads the crawler list from a host that appends the element before wiring it (CR-04)', async () => { + const host = tester({ order: 'append-first' }) + await settle() + expect(host.posted[0]).toEqual({ op: 'context' }) + expect(agents(host.node)).toEqual(['Googlebot', 'Bingbot', 'GPTBot', '*']) + }) + + it('a test asked while the context loads is queued and sent after busy clears', async () => { + const host = tester({ order: 'assign-first' }) + await Promise.resolve() + expect(host.busy()).toBe(true) + // The button is disabled while busy; Enter in the address field still asks. + const input = host.node.querySelector('.seo-robotstester__input')! + input.value = '/admin' + input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })) + expect(host.posted).toEqual([{ op: 'context' }]) + await settle() + expect(host.posted).toEqual([{ op: 'context' }, { op: 'test', url: '/admin', agent: 'Googlebot' }]) + expect(resultLine(host.node)).toContain('Disallow: /admin') + }) + + it('the button shows the busy label while a test runs', async () => { + const host = tester() + await settle() + host.node.setAttribute('busy', '') + expect(host.node.querySelector('.seo-robotstester__button')?.textContent).toBe('Testing…') + host.node.removeAttribute('busy') + expect(host.node.querySelector('.seo-robotstester__button')?.textContent).toBe('Test URL') + }) + + it('state=error unblocks the next test', async () => { + let failNext = true + const host = tester({ + answer: (payload) => { + if ((payload as Payload).op === 'test' && failNext) { + failNext = false + throw new HostFailure('test') + } + return answer(payload) + }, + }) + await settle() + testURL(host.node, '/admin') + await settle() + expect(host.node.getAttribute('state')).toBe('error') + testURL(host.node, '/blog') + await settle() + expect(host.posted.slice(-1)).toEqual([{ op: 'test', url: '/blog', agent: 'Googlebot' }]) + expect(resultLine(host.node)).not.toBe('') + }) + + it('retries a test the host dropped once', async () => { + const host = tester() + await settle() + host.drop(1) + testURL(host.node, '/admin') + await settle() + expect(host.emitted.filter((p) => (p as Payload).op === 'test')).toHaveLength(2) + expect(host.posted.slice(-1)).toEqual([{ op: 'test', url: '/admin', agent: 'Googlebot' }]) + expect(resultLine(host.node)).toContain('Disallow: /admin') + }) + + it('says the test failed when the host never takes it', async () => { + const host = tester({ wired: false }) + await settle() + expect(agents(host.node)).toEqual(['Googlebot', 'Googlebot-Image', 'Bingbot', '*']) + testURL(host.node, '/admin') + await settle() + expect(resultLine(host.node)).toBe(labelsFor('en').testFailed) + }) + + it('an address without a leading slash is refused without an action', async () => { + const host = tester() + await settle() + const before = host.emitted.length + testURL(host.node, 'admin') + await settle() + expect(host.emitted.length).toBe(before) + expect(resultLine(host.node)).toBe(labelsFor('en').urlRequired) + }) + + it('a request in flight when the element is disconnected does not block it after reconnecting', async () => { + const host = tester() + await Promise.resolve() + expect(host.busy()).toBe(true) + host.node.remove() + await settle() + document.body.append(host.node) + await settle() + expect(agents(host.node)).toEqual(['Googlebot', 'Bingbot', 'GPTBot', '*']) + testURL(host.node, '/admin') + await settle() + expect(resultLine(host.node)).toContain('Disallow: /admin') + }) +}) diff --git a/tests/elements/serp.test.ts b/tests/elements/serp.test.ts index cfd0591..8f4df9e 100644 --- a/tests/elements/serp.test.ts +++ b/tests/elements/serp.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeAll, describe, expect, it } from 'vitest' +import { HostFailure, mountWidget, settle } from './host' // E3 golem15-seo-serp (UI-SPEC C6): the effective title, truncation at a // word boundary within the pixel budget (mocked canvas: 10 px per @@ -23,17 +24,22 @@ afterEach(() => { document.body.replaceChildren() }) -/** A serp element answered with one context locale. */ -function preview(locale: Partial, fill: Record = {}): HTMLElement { +type ContextLocale = import('../../assets/src/elements/serp').ContextLocale + +function contextLocale(locale: Partial = {}): ContextLocale { + return { locale: 'en', url: '/blog/post', h1: null, siteName: 'Atlas', titleTemplate: '', defaultTitle: 'Post', defaultDescription: null, descriptionSource: 'content', bodyWords: null, ...locale } +} + +/** + * A serp element given one context locale directly (rendering tests); the + * host protocol is covered by 'E3 host protocol' below. + */ +function preview(locale: Partial, fill: Record = {}): HTMLElement { const node = document.createElement('golem15-seo-serp') node.setAttribute('ui-locale', 'en') node.setAttribute('fill-values', JSON.stringify(fill)) - const actions: unknown[] = [] - node.addEventListener('summer-action', (ev) => actions.push((ev as CustomEvent).detail.payload)) document.body.append(node) - expect(actions).toEqual([{ op: 'context' }]) - const loc = { locale: 'en', url: '/blog/post', h1: null, siteName: 'Atlas', titleTemplate: '', defaultTitle: 'Post', defaultDescription: null, descriptionSource: 'content', bodyWords: null, ...locale } - node.dispatchEvent(new CustomEvent('summer-result', { detail: { data: { op: 'context', host: 'example.test', locales: [loc] } } })) + node.dispatchEvent(new CustomEvent('summer-result', { detail: { data: { op: 'context', host: 'example.test', locales: [contextLocale(locale)] } } })) return node } @@ -146,3 +152,100 @@ describe('E3 keyphrase stem matching (Q-4)', () => { expect(serp.wordsMatch('kot', 'kot')).toBe(true) }) }) + +describe('E3 host protocol', () => { + const answer = (payload: unknown): unknown => { + const p = payload as { op: string; locale?: string } + if (p.op === 'context') { + return { op: 'context', host: 'example.test', locales: [contextLocale()] } + } + return { op: 'duplicates', locale: p.locale, title: [], description: [] } + } + + it('loads its context from a host that appends the element before wiring it (CR-04)', async () => { + const host = mountWidget('golem15-seo-serp', { order: 'append-first', attrs: { 'ui-locale': 'en' }, answer }) + await settle() + expect(host.posted[0]).toEqual({ op: 'context' }) + expect(host.node.hasAttribute('aria-busy')).toBe(false) + expect(shownTitle(host.node)).toBe('Post') + expect(host.posted.filter((p) => (p as { op: string }).op === 'context')).toHaveLength(1) + }) + + it('loads its context from a host that wires the element first, once', async () => { + const host = mountWidget('golem15-seo-serp', { order: 'assign-first', attrs: { 'ui-locale': 'en' }, answer }) + await settle() + expect(host.emitted.filter((p) => (p as { op: string }).op === 'context')).toHaveLength(1) + expect(shownTitle(host.node)).toBe('Post') + }) + + it('retries an action the host dropped once', async () => { + const host = mountWidget('golem15-seo-serp', { order: 'assign-first', attrs: { 'ui-locale': 'en' }, answer }) + host.drop(1) + await settle() + expect(host.emitted.slice(0, 2)).toEqual([{ op: 'context' }, { op: 'context' }]) + expect(host.posted[0]).toEqual({ op: 'context' }) + expect(shownTitle(host.node)).toBe('Post') + }) + + it('shows the context error when the host never takes the action', async () => { + const host = mountWidget('golem15-seo-serp', { wired: false, attrs: { 'ui-locale': 'en' }, answer }) + await settle() + expect(host.emitted).toEqual([{ op: 'context' }, { op: 'context' }]) + expect(host.node.hasAttribute('aria-busy')).toBe(false) + expect(host.node.querySelector('.seo-serp__error')?.textContent).toBe(labels.labelsFor('en').contextFailed) + }) + + it('a duplicate check asked while a request runs is sent after busy clears', async () => { + const host = mountWidget('golem15-seo-serp', { attrs: { 'ui-locale': 'en', 'fill-values': JSON.stringify({ seo_title: { en: 'First' } }) }, answer }) + await settle() + const before = host.posted.length + expect(host.posted[before - 1]).toMatchObject({ op: 'duplicates', title: 'First' }) + host.node.setAttribute('busy', '') + host.node.setAttribute('fill-values', JSON.stringify({ seo_title: { en: 'Second' } })) + await new Promise((resolve) => setTimeout(resolve, 900)) + expect(host.posted.length).toBe(before) + host.node.removeAttribute('busy') + await settle() + expect(host.posted[host.posted.length - 1]).toMatchObject({ op: 'duplicates', title: 'Second' }) + }) + + it('state=error unblocks the next request', async () => { + let failNext = false + const host = mountWidget('golem15-seo-serp', { + attrs: { 'ui-locale': 'en', 'fill-values': JSON.stringify({ seo_title: { en: 'First' } }) }, + answer: (payload) => { + if (failNext && (payload as { op: string }).op === 'duplicates') { + failNext = false + throw new HostFailure('duplicates') + } + return answer(payload) + }, + }) + await settle() + failNext = true + host.node.setAttribute('fill-values', JSON.stringify({ seo_title: { en: 'Second' } })) + await new Promise((resolve) => setTimeout(resolve, 900)) + await settle() + expect(host.node.getAttribute('state')).toBe('error') + expect(host.posted[host.posted.length - 1]).toMatchObject({ op: 'duplicates', title: 'Second' }) + expect(host.node.querySelector('.seo-serp__checking')).toBeNull() + host.node.setAttribute('fill-values', JSON.stringify({ seo_title: { en: 'Third' } })) + await new Promise((resolve) => setTimeout(resolve, 900)) + await settle() + expect(host.posted[host.posted.length - 1]).toMatchObject({ op: 'duplicates', title: 'Third' }) + expect(host.node.hasAttribute('state')).toBe(false) + }) + + it('a request in flight when the element is disconnected does not block it after reconnecting', async () => { + const host = mountWidget('golem15-seo-serp', { attrs: { 'ui-locale': 'en' }, answer }) + await Promise.resolve() + expect(host.busy()).toBe(true) + host.node.remove() + await settle() + expect(host.node.hasAttribute('aria-busy')).toBe(true) + document.body.append(host.node) + await settle() + expect(host.posted.filter((p) => (p as { op: string }).op === 'context')).toHaveLength(2) + expect(shownTitle(host.node)).toBe('Post') + }) +}, 5000)