From 10ff628f6a1268cce7c7751117a4b9299507e5b4 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Wed, 4 Nov 2026 14:37:49 +0100 Subject: [PATCH] feat: structured data, graph preview and primary category in the SEO tab - E8 golem15-seo-schema: the type per content kind, the Service grid with price and currency checks, custom JSON-LD with a 300 ms parse status - E9 golem15-seo-graph: nodes and findings from the seo_graph action, the JSON, the Rich Results Test link, 800 ms re-check on fill changes, the ML locale switch, one queued request, the failure line - golem15-seo-parent: the primary category select over the record's parents; the seoparent Decode accepts the loaded value sent back - the rebuilt element bundle and vitest smoke tests --- assets/dist/seo.css | 2 +- assets/dist/seo.js | 3 +- assets/src/elements/graph.ts | 240 +++++++++++++++++++++ assets/src/elements/index.ts | 12 ++ assets/src/elements/labels.ts | 153 ++++++++++++- assets/src/elements/parent.ts | 97 +++++++++ assets/src/elements/schema.ts | 384 +++++++++++++++++++++++++++++++++ assets/src/elements/styles.css | 276 ++++++++++++++++++++++++ classes/field_types.go | 7 +- tests/elements/schema.test.ts | 180 ++++++++++++++++ 10 files changed, 1350 insertions(+), 4 deletions(-) create mode 100644 assets/src/elements/graph.ts create mode 100644 assets/src/elements/parent.ts create mode 100644 assets/src/elements/schema.ts create mode 100644 tests/elements/schema.test.ts diff --git a/assets/dist/seo.css b/assets/dist/seo.css index 9bb4c0f..e851e10 100644 --- a/assets/dist/seo.css +++ b/assets/dist/seo.css @@ -1 +1 @@ -golem15-seo-text,golem15-seo-robots,golem15-seo-serp,golem15-seo-image,golem15-seo-robotstxt,golem15-seo-robotstester,golem15-seo-transfer{display:block;color:var(--c-text);font-size:14px}golem15-seo-text .seo-visually-hidden,golem15-seo-image .seo-visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}golem15-seo-text .seo-text__control{box-sizing:border-box;width:100%;height:44px;padding:0 14px;border:1px solid var(--c-border-strong);border-radius:10px;background:var(--c-surface);color:var(--c-text);font:inherit;font-size:14px;transition:border-color .15s,box-shadow .15s}golem15-seo-text .seo-text__control--area{height:auto;min-height:96px;padding:10px 14px;resize:vertical}golem15-seo-text .seo-text__control:focus{outline:none;border-color:var(--c-primary);box-shadow:0 0 0 3px var(--c-ring)}golem15-seo-text.seo-text--invalid .seo-text__control{border-color:var(--c-danger)}golem15-seo-text .seo-text__counter{display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-top:8px;font-size:13px}golem15-seo-text .seo-text__status{display:inline-flex;align-items:center;gap:6px}golem15-seo-text .seo-text__icon{display:inline-flex}golem15-seo-text .seo-text__count{color:var(--c-muted);font-variant-numeric:tabular-nums}golem15-seo-text[data-status=empty] .seo-text__status{color:var(--c-muted)}golem15-seo-text[data-status=good] .seo-text__status{color:var(--c-ok-text, var(--c-text))}golem15-seo-robots .seo-robots__groups{display:flex;flex-wrap:wrap;gap:24px}golem15-seo-robots .seo-robots__group{display:flex;flex-direction:column;gap:6px}golem15-seo-robots .seo-robots__caption,golem15-seo-robots .seo-robots__summary,golem15-seo-robots .seo-robots__hint{color:var(--c-muted);font-size:13px}golem15-seo-robots .seo-robots__track,golem15-seo-serp .seo-serp__track{display:inline-flex;gap:4px;padding:4px;border:1px solid var(--c-border);border-radius:12px;background:var(--c-subtle)}golem15-seo-robots .seo-robots__segment,golem15-seo-serp .seo-serp__segment{height:34px;padding:0 16px;border:0;border-radius:9px;background:transparent;color:var(--c-muted);font:inherit;font-size:13px;cursor:pointer;transition:background-color .15s,color .15s}golem15-seo-robots .seo-robots__segment[aria-checked=true],golem15-seo-serp .seo-serp__segment[aria-checked=true]{font-size:14px;font-weight:600;background:var(--c-surface);color:var(--c-text);box-shadow:var(--shadow-tab, none)}golem15-seo-robots .seo-robots__segment--ok[aria-checked=true]{background:var(--c-ok-bg, var(--c-surface));color:var(--c-ok-text, var(--c-text))}golem15-seo-robots .seo-robots__segment--danger[aria-checked=true]{background:var(--c-danger-soft);color:var(--c-danger)}golem15-seo-robots .seo-robots__segment:focus-visible,golem15-seo-robots .seo-robots__toggle:focus-visible,golem15-seo-serp .seo-serp__segment:focus-visible,golem15-seo-serp .seo-serp__link:focus-visible,golem15-seo-image .seo-image__button:focus-visible{outline:3px solid var(--c-ring);outline-offset:2px}golem15-seo-robots .seo-robots__advanced-row{display:flex;align-items:center;gap:12px;margin-top:12px}golem15-seo-robots .seo-robots__toggle{display:inline-flex;align-items:center;gap:6px;padding:0;border:0;background:none;color:var(--c-text);font:inherit;font-size:14px;font-weight:600;cursor:pointer}golem15-seo-robots .seo-robots__toggle[aria-expanded=true] .seo-icon{transform:rotate(180deg)}golem15-seo-robots .seo-robots__panel{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:12px}golem15-seo-robots .seo-robots__panel[hidden]{display:none}golem15-seo-robots .seo-robots__field{display:flex;flex-direction:column;gap:6px}golem15-seo-robots .seo-robots__input{box-sizing:border-box;height:44px;padding:0 14px;border:1px solid var(--c-border-strong);border-radius:10px;background:var(--c-surface);color:var(--c-text);font:inherit}golem15-seo-robots .seo-robots__input:focus{outline:none;border-color:var(--c-primary);box-shadow:0 0 0 3px var(--c-ring)}golem15-seo-robots .seo-robots__error{color:var(--c-danger);font-size:13px}golem15-seo-robots .seo-robots__checks{grid-column:1 / -1;display:flex;flex-direction:column;gap:8px}golem15-seo-robots .seo-robots__check{display:inline-flex;align-items:center;gap:8px}golem15-seo-robots .seo-robots__check input{width:20px;height:20px;border-radius:5px;accent-color:var(--c-primary)}golem15-seo-robots .seo-robots__unparsed{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:12px;color:var(--c-danger);font-size:13px}golem15-seo-robots .seo-robots__reset{min-height:32px;padding:0 12px;border:1px solid var(--c-border);border-radius:8px;background:transparent;color:inherit;font:inherit;cursor:pointer}golem15-seo-robots.seo-robots--invalid .seo-robots__track{border-color:var(--c-danger)}golem15-seo-serp .seo-serp__skeleton{max-width:600px;height:132px;border-radius:12px;background:var(--c-skel)}golem15-seo-serp .seo-serp__header{display:flex;justify-content:space-between;align-items:center;gap:16px;flex-wrap:wrap;margin-bottom:12px}golem15-seo-serp .seo-serp__language,golem15-seo-serp .seo-serp__notes{color:var(--c-muted);font-size:13px}golem15-seo-serp .serp-card{--serp-bg: #ffffff;--serp-site: #202124;--serp-title: #1a0dab;--serp-text: #4d5156;box-sizing:border-box;max-width:600px;padding:16px;border:1px solid var(--c-border);border-radius:12px;background:var(--serp-bg);font-family:Arial,sans-serif;overflow-wrap:anywhere}golem15-seo-serp .serp-card--mobile{max-width:360px}golem15-seo-serp .serp-card__site{display:flex;align-items:center;gap:10px}golem15-seo-serp .serp-card__favicon{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border:1px solid var(--c-border);border-radius:50%;background:var(--c-subtle);color:var(--c-muted);font-size:14px;font-weight:600}golem15-seo-serp .serp-card__names{display:flex;flex-direction:column;color:var(--serp-site)}golem15-seo-serp .serp-card__sitename{font-size:14px}golem15-seo-serp .serp-card__crumbs{font-size:12px}golem15-seo-serp .serp-card__title{margin-top:6px;color:var(--serp-title);font-size:20px;line-height:1.3;white-space:nowrap}golem15-seo-serp .serp-card--mobile .serp-card__title{white-space:normal}golem15-seo-serp .serp-card__description{margin-top:4px;color:var(--serp-text);font-size:14px;line-height:1.58}golem15-seo-serp .serp-card__description--missing{font-style:italic}golem15-seo-serp .seo-serp__notes{display:flex;flex-direction:column;gap:8px;margin-top:8px}golem15-seo-serp .seo-serp__error{display:inline-flex;align-items:center;gap:6px;color:var(--c-danger)}golem15-seo-serp .seo-serp__checks,golem15-seo-serp .seo-serp__keyphrase{display:flex;flex-direction:column;gap:6px;margin-top:12px;font-size:13px}golem15-seo-serp .seo-serp__checking{color:var(--c-muted)}golem15-seo-serp .seo-serp__keyphrase-title{font-size:14px;font-weight:600}golem15-seo-serp .seo-serp__check{display:inline-flex;align-items:flex-start;gap:6px}golem15-seo-serp .seo-serp__check--ok{color:var(--c-ok-text, var(--c-text))}golem15-seo-serp .seo-serp__check .seo-icon{flex:none;margin-top:2px}golem15-seo-serp .seo-serp__link{color:var(--c-text);text-decoration:underline}golem15-seo-image .seo-image__preview{margin-bottom:8px}golem15-seo-image .seo-image__img{display:block;width:240px;height:126px;object-fit:cover;border:1px solid var(--c-border);border-radius:10px;background:var(--c-subtle)}golem15-seo-image .seo-image__actions{display:flex;gap:8px}golem15-seo-image .seo-image__button{display:inline-flex;align-items:center;height:36px;padding:0 14px;border:1px solid var(--c-border-strong);border-radius:10px;background:var(--c-surface);color:var(--c-text);font:inherit;font-size:14px;cursor:pointer}golem15-seo-image .seo-image__status{margin-top:6px;color:var(--c-muted);font-size:13px}golem15-seo-image .seo-image__status--error{color:var(--c-danger)}golem15-seo-image.seo-image--invalid .seo-image__img{border-color:var(--c-danger)}golem15-seo-robotstxt .seo-robotstxt__control{box-sizing:border-box;display:block;width:100%;min-height:320px;padding:12px 14px;border:1px solid var(--c-border-strong);border-radius:10px;background:var(--c-surface);color:var(--c-text);font-family:DM Mono,ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;line-height:1.6;white-space:pre;overflow-x:auto;resize:vertical;transition:border-color .15s,box-shadow .15s}golem15-seo-robotstxt .seo-robotstxt__control:focus{outline:none;border-color:var(--c-primary);box-shadow:0 0 0 3px var(--c-ring)}golem15-seo-robotstxt.seo-robotstxt--invalid .seo-robotstxt__control,golem15-seo-robotstxt.seo-robotstxt--over .seo-robotstxt__control{border-color:var(--c-danger)}golem15-seo-robotstxt .seo-robotstxt__lint{margin-top:8px;padding:12px 14px;border:1px solid var(--c-border);border-radius:10px;background:var(--c-subtle);font-size:13px}golem15-seo-robotstxt .seo-robotstxt__finding{display:flex;align-items:flex-start;gap:6px;overflow-wrap:anywhere}golem15-seo-robotstxt .seo-robotstxt__finding+.seo-robotstxt__finding{margin-top:4px}golem15-seo-robotstxt .seo-robotstxt__finding .seo-icon{flex:none;margin-top:3px}golem15-seo-robotstxt .seo-robotstxt__finding--clean{color:var(--c-ok-text, var(--c-text))}golem15-seo-robotstxt .seo-robotstxt__more{margin-top:4px;color:var(--c-muted)}golem15-seo-robotstxt .seo-robotstxt__size{display:flex;justify-content:flex-end;gap:12px;margin-top:6px;color:var(--c-muted);font-size:13px;font-variant-numeric:tabular-nums}golem15-seo-robotstxt .seo-robotstxt__over{color:var(--c-danger)}golem15-seo-robotstester .seo-robotstester__row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:8px}golem15-seo-robotstester .seo-robotstester__field{display:flex;flex-direction:column;gap:4px}golem15-seo-robotstester .seo-robotstester__field--url{flex:1 1 240px}golem15-seo-robotstester .seo-robotstester__field--agent{min-width:200px}golem15-seo-robotstester .seo-robotstester__label{color:var(--c-muted);font-size:13px}golem15-seo-robotstester .seo-robotstester__input,golem15-seo-robotstester .seo-robotstester__select{box-sizing:border-box;height:44px;padding:0 14px;border:1px solid var(--c-border-strong);border-radius:10px;background:var(--c-surface);color:var(--c-text);font:inherit;font-size:14px;transition:border-color .15s,box-shadow .15s}golem15-seo-robotstester .seo-robotstester__input:focus,golem15-seo-robotstester .seo-robotstester__select:focus{outline:none;border-color:var(--c-primary);box-shadow:0 0 0 3px var(--c-ring)}golem15-seo-robotstester .seo-robotstester__button{height:42px;padding:0 16px;border:1px solid var(--c-border-strong);border-radius:10px;background:var(--c-surface);color:var(--c-text);font:inherit;font-size:14px;cursor:pointer}golem15-seo-robotstester .seo-robotstester__button:focus-visible{outline:none;box-shadow:0 0 0 3px var(--c-ring)}golem15-seo-robotstester .seo-robotstester__button:disabled{cursor:default;opacity:.6}golem15-seo-robotstester .seo-robotstester__result{display:flex;align-items:flex-start;gap:6px;margin-top:10px;font-size:14px;overflow-wrap:anywhere}golem15-seo-robotstester .seo-robotstester__result .seo-icon{flex:none;margin-top:3px}golem15-seo-robotstester .seo-robotstester__result--allowed{color:var(--c-ok-text, var(--c-text))}golem15-seo-robotstester .seo-robotstester__result--blocked{color:var(--c-text)}golem15-seo-robotstester .seo-robotstester__result--error{color:var(--c-danger)}golem15-seo-robotstester .seo-robotstester__note{margin:6px 0 0;color:var(--c-muted);font-size:13px}golem15-seo-transfer .seo-transfer__section{display:flex;flex-direction:column;gap:12px}golem15-seo-transfer .seo-transfer__section--import{margin-top:24px;padding-top:24px;border-top:1px solid var(--c-border)}golem15-seo-transfer .seo-transfer__heading{margin:0;color:var(--c-text);font-size:14px;font-weight:600}golem15-seo-transfer .seo-transfer__row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:8px}golem15-seo-transfer .seo-transfer__field{display:flex;flex-direction:column;gap:4px}golem15-seo-transfer .seo-transfer__label{color:var(--c-muted);font-size:13px}golem15-seo-transfer .seo-transfer__select{box-sizing:border-box;min-width:240px;height:44px;padding:0 14px;border:1px solid var(--c-border-strong);border-radius:10px;background:var(--c-surface);color:var(--c-text);font:inherit;font-size:14px;transition:border-color .15s,box-shadow .15s}golem15-seo-transfer .seo-transfer__select:focus{outline:none;border-color:var(--c-primary);box-shadow:0 0 0 3px var(--c-ring)}golem15-seo-transfer .seo-transfer__button{height:42px;padding:0 16px;border:1px solid var(--c-border-strong);border-radius:10px;background:var(--c-surface);color:var(--c-text);font:inherit;font-size:14px;cursor:pointer}golem15-seo-transfer .seo-transfer__button--ghost{border-color:transparent;background:transparent}golem15-seo-transfer .seo-transfer__button--primary{border-color:var(--c-primary);background:var(--c-primary);color:var(--c-on-primary, var(--c-surface))}golem15-seo-transfer .seo-transfer__button:focus-visible{outline:none;box-shadow:0 0 0 3px var(--c-ring)}golem15-seo-transfer .seo-transfer__button:disabled{cursor:default;opacity:.6}golem15-seo-transfer .seo-transfer__filename{align-self:center;color:var(--c-text);font-size:14px;overflow-wrap:anywhere}golem15-seo-transfer .seo-transfer__filename--none{color:var(--c-muted)}golem15-seo-transfer .seo-transfer__error{margin:0;color:var(--c-danger);font-size:14px}golem15-seo-transfer .seo-transfer__note,golem15-seo-transfer .seo-transfer__done{margin:0;color:var(--c-muted);font-size:13px}golem15-seo-transfer .seo-transfer__link{color:var(--c-primary)}golem15-seo-transfer .seo-transfer__preview{display:flex;flex-direction:column;gap:12px}golem15-seo-transfer .seo-transfer__summary{margin:0;font-size:14px;font-weight:600}golem15-seo-transfer .seo-transfer__table-wrap{max-height:480px;overflow:auto}golem15-seo-transfer .seo-transfer__num{font-variant-numeric:tabular-nums;text-align:right}golem15-seo-transfer .seo-transfer__mono{font-family:DM Mono,ui-monospace,monospace;overflow-wrap:anywhere}golem15-seo-transfer .seo-transfer__badge{display:inline-flex;align-items:center;height:24px;margin-right:4px;padding:0 10px;border-radius:999px;font-size:12px;white-space:nowrap}golem15-seo-transfer .seo-transfer__badge--new{background:var(--c-ok-bg);color:var(--c-ok-text)}golem15-seo-transfer .seo-transfer__badge--changed,golem15-seo-transfer .seo-transfer__badge--warning{background:var(--c-sel);color:var(--c-text)}golem15-seo-transfer .seo-transfer__badge--unchanged{background:var(--c-subtle);color:var(--c-muted)}golem15-seo-transfer .seo-transfer__badge--refused{background:var(--c-danger-soft);color:var(--c-danger)}golem15-seo-transfer .seo-transfer__actions{display:flex;justify-content:flex-end;gap:10px}@media(max-width:600px){golem15-seo-robots .seo-robots__panel{grid-template-columns:1fr}golem15-seo-serp .serp-card,golem15-seo-serp .serp-card--mobile{max-width:none}}@media(prefers-reduced-motion:reduce){golem15-seo-robotstxt .seo-robotstxt__control,golem15-seo-robotstester .seo-robotstester__input,golem15-seo-robotstester .seo-robotstester__select,golem15-seo-transfer .seo-transfer__select,golem15-seo-text .seo-text__control,golem15-seo-robots .seo-robots__segment,golem15-seo-serp .seo-serp__segment{transition:none}} +golem15-seo-text,golem15-seo-robots,golem15-seo-serp,golem15-seo-image,golem15-seo-robotstxt,golem15-seo-robotstester,golem15-seo-transfer{display:block;color:var(--c-text);font-size:14px}golem15-seo-text .seo-visually-hidden,golem15-seo-image .seo-visually-hidden{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}golem15-seo-text .seo-text__control{box-sizing:border-box;width:100%;height:44px;padding:0 14px;border:1px solid var(--c-border-strong);border-radius:10px;background:var(--c-surface);color:var(--c-text);font:inherit;font-size:14px;transition:border-color .15s,box-shadow .15s}golem15-seo-text .seo-text__control--area{height:auto;min-height:96px;padding:10px 14px;resize:vertical}golem15-seo-text .seo-text__control:focus{outline:none;border-color:var(--c-primary);box-shadow:0 0 0 3px var(--c-ring)}golem15-seo-text.seo-text--invalid .seo-text__control{border-color:var(--c-danger)}golem15-seo-text .seo-text__counter{display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-top:8px;font-size:13px}golem15-seo-text .seo-text__status{display:inline-flex;align-items:center;gap:6px}golem15-seo-text .seo-text__icon{display:inline-flex}golem15-seo-text .seo-text__count{color:var(--c-muted);font-variant-numeric:tabular-nums}golem15-seo-text[data-status=empty] .seo-text__status{color:var(--c-muted)}golem15-seo-text[data-status=good] .seo-text__status{color:var(--c-ok-text, var(--c-text))}golem15-seo-robots .seo-robots__groups{display:flex;flex-wrap:wrap;gap:24px}golem15-seo-robots .seo-robots__group{display:flex;flex-direction:column;gap:6px}golem15-seo-robots .seo-robots__caption,golem15-seo-robots .seo-robots__summary,golem15-seo-robots .seo-robots__hint{color:var(--c-muted);font-size:13px}golem15-seo-robots .seo-robots__track,golem15-seo-serp .seo-serp__track{display:inline-flex;gap:4px;padding:4px;border:1px solid var(--c-border);border-radius:12px;background:var(--c-subtle)}golem15-seo-robots .seo-robots__segment,golem15-seo-serp .seo-serp__segment{height:34px;padding:0 16px;border:0;border-radius:9px;background:transparent;color:var(--c-muted);font:inherit;font-size:13px;cursor:pointer;transition:background-color .15s,color .15s}golem15-seo-robots .seo-robots__segment[aria-checked=true],golem15-seo-serp .seo-serp__segment[aria-checked=true]{font-size:14px;font-weight:600;background:var(--c-surface);color:var(--c-text);box-shadow:var(--shadow-tab, none)}golem15-seo-robots .seo-robots__segment--ok[aria-checked=true]{background:var(--c-ok-bg, var(--c-surface));color:var(--c-ok-text, var(--c-text))}golem15-seo-robots .seo-robots__segment--danger[aria-checked=true]{background:var(--c-danger-soft);color:var(--c-danger)}golem15-seo-robots .seo-robots__segment:focus-visible,golem15-seo-robots .seo-robots__toggle:focus-visible,golem15-seo-serp .seo-serp__segment:focus-visible,golem15-seo-serp .seo-serp__link:focus-visible,golem15-seo-image .seo-image__button:focus-visible{outline:3px solid var(--c-ring);outline-offset:2px}golem15-seo-robots .seo-robots__advanced-row{display:flex;align-items:center;gap:12px;margin-top:12px}golem15-seo-robots .seo-robots__toggle{display:inline-flex;align-items:center;gap:6px;padding:0;border:0;background:none;color:var(--c-text);font:inherit;font-size:14px;font-weight:600;cursor:pointer}golem15-seo-robots .seo-robots__toggle[aria-expanded=true] .seo-icon{transform:rotate(180deg)}golem15-seo-robots .seo-robots__panel{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:12px}golem15-seo-robots .seo-robots__panel[hidden]{display:none}golem15-seo-robots .seo-robots__field{display:flex;flex-direction:column;gap:6px}golem15-seo-robots .seo-robots__input{box-sizing:border-box;height:44px;padding:0 14px;border:1px solid var(--c-border-strong);border-radius:10px;background:var(--c-surface);color:var(--c-text);font:inherit}golem15-seo-robots .seo-robots__input:focus{outline:none;border-color:var(--c-primary);box-shadow:0 0 0 3px var(--c-ring)}golem15-seo-robots .seo-robots__error{color:var(--c-danger);font-size:13px}golem15-seo-robots .seo-robots__checks{grid-column:1 / -1;display:flex;flex-direction:column;gap:8px}golem15-seo-robots .seo-robots__check{display:inline-flex;align-items:center;gap:8px}golem15-seo-robots .seo-robots__check input{width:20px;height:20px;border-radius:5px;accent-color:var(--c-primary)}golem15-seo-robots .seo-robots__unparsed{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:12px;color:var(--c-danger);font-size:13px}golem15-seo-robots .seo-robots__reset{min-height:32px;padding:0 12px;border:1px solid var(--c-border);border-radius:8px;background:transparent;color:inherit;font:inherit;cursor:pointer}golem15-seo-robots.seo-robots--invalid .seo-robots__track{border-color:var(--c-danger)}golem15-seo-serp .seo-serp__skeleton{max-width:600px;height:132px;border-radius:12px;background:var(--c-skel)}golem15-seo-serp .seo-serp__header{display:flex;justify-content:space-between;align-items:center;gap:16px;flex-wrap:wrap;margin-bottom:12px}golem15-seo-serp .seo-serp__language,golem15-seo-serp .seo-serp__notes{color:var(--c-muted);font-size:13px}golem15-seo-serp .serp-card{--serp-bg: #ffffff;--serp-site: #202124;--serp-title: #1a0dab;--serp-text: #4d5156;box-sizing:border-box;max-width:600px;padding:16px;border:1px solid var(--c-border);border-radius:12px;background:var(--serp-bg);font-family:Arial,sans-serif;overflow-wrap:anywhere}golem15-seo-serp .serp-card--mobile{max-width:360px}golem15-seo-serp .serp-card__site{display:flex;align-items:center;gap:10px}golem15-seo-serp .serp-card__favicon{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border:1px solid var(--c-border);border-radius:50%;background:var(--c-subtle);color:var(--c-muted);font-size:14px;font-weight:600}golem15-seo-serp .serp-card__names{display:flex;flex-direction:column;color:var(--serp-site)}golem15-seo-serp .serp-card__sitename{font-size:14px}golem15-seo-serp .serp-card__crumbs{font-size:12px}golem15-seo-serp .serp-card__title{margin-top:6px;color:var(--serp-title);font-size:20px;line-height:1.3;white-space:nowrap}golem15-seo-serp .serp-card--mobile .serp-card__title{white-space:normal}golem15-seo-serp .serp-card__description{margin-top:4px;color:var(--serp-text);font-size:14px;line-height:1.58}golem15-seo-serp .serp-card__description--missing{font-style:italic}golem15-seo-serp .seo-serp__notes{display:flex;flex-direction:column;gap:8px;margin-top:8px}golem15-seo-serp .seo-serp__error{display:inline-flex;align-items:center;gap:6px;color:var(--c-danger)}golem15-seo-serp .seo-serp__checks,golem15-seo-serp .seo-serp__keyphrase{display:flex;flex-direction:column;gap:6px;margin-top:12px;font-size:13px}golem15-seo-serp .seo-serp__checking{color:var(--c-muted)}golem15-seo-serp .seo-serp__keyphrase-title{font-size:14px;font-weight:600}golem15-seo-serp .seo-serp__check{display:inline-flex;align-items:flex-start;gap:6px}golem15-seo-serp .seo-serp__check--ok{color:var(--c-ok-text, var(--c-text))}golem15-seo-serp .seo-serp__check .seo-icon{flex:none;margin-top:2px}golem15-seo-serp .seo-serp__link{color:var(--c-text);text-decoration:underline}golem15-seo-image .seo-image__preview{margin-bottom:8px}golem15-seo-image .seo-image__img{display:block;width:240px;height:126px;object-fit:cover;border:1px solid var(--c-border);border-radius:10px;background:var(--c-subtle)}golem15-seo-image .seo-image__actions{display:flex;gap:8px}golem15-seo-image .seo-image__button{display:inline-flex;align-items:center;height:36px;padding:0 14px;border:1px solid var(--c-border-strong);border-radius:10px;background:var(--c-surface);color:var(--c-text);font:inherit;font-size:14px;cursor:pointer}golem15-seo-image .seo-image__status{margin-top:6px;color:var(--c-muted);font-size:13px}golem15-seo-image .seo-image__status--error{color:var(--c-danger)}golem15-seo-image.seo-image--invalid .seo-image__img{border-color:var(--c-danger)}golem15-seo-robotstxt .seo-robotstxt__control{box-sizing:border-box;display:block;width:100%;min-height:320px;padding:12px 14px;border:1px solid var(--c-border-strong);border-radius:10px;background:var(--c-surface);color:var(--c-text);font-family:DM Mono,ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;line-height:1.6;white-space:pre;overflow-x:auto;resize:vertical;transition:border-color .15s,box-shadow .15s}golem15-seo-robotstxt .seo-robotstxt__control:focus{outline:none;border-color:var(--c-primary);box-shadow:0 0 0 3px var(--c-ring)}golem15-seo-robotstxt.seo-robotstxt--invalid .seo-robotstxt__control,golem15-seo-robotstxt.seo-robotstxt--over .seo-robotstxt__control{border-color:var(--c-danger)}golem15-seo-robotstxt .seo-robotstxt__lint{margin-top:8px;padding:12px 14px;border:1px solid var(--c-border);border-radius:10px;background:var(--c-subtle);font-size:13px}golem15-seo-robotstxt .seo-robotstxt__finding{display:flex;align-items:flex-start;gap:6px;overflow-wrap:anywhere}golem15-seo-robotstxt .seo-robotstxt__finding+.seo-robotstxt__finding{margin-top:4px}golem15-seo-robotstxt .seo-robotstxt__finding .seo-icon{flex:none;margin-top:3px}golem15-seo-robotstxt .seo-robotstxt__finding--clean{color:var(--c-ok-text, var(--c-text))}golem15-seo-robotstxt .seo-robotstxt__more{margin-top:4px;color:var(--c-muted)}golem15-seo-robotstxt .seo-robotstxt__size{display:flex;justify-content:flex-end;gap:12px;margin-top:6px;color:var(--c-muted);font-size:13px;font-variant-numeric:tabular-nums}golem15-seo-robotstxt .seo-robotstxt__over{color:var(--c-danger)}golem15-seo-robotstester .seo-robotstester__row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:8px}golem15-seo-robotstester .seo-robotstester__field{display:flex;flex-direction:column;gap:4px}golem15-seo-robotstester .seo-robotstester__field--url{flex:1 1 240px}golem15-seo-robotstester .seo-robotstester__field--agent{min-width:200px}golem15-seo-robotstester .seo-robotstester__label{color:var(--c-muted);font-size:13px}golem15-seo-robotstester .seo-robotstester__input,golem15-seo-robotstester .seo-robotstester__select{box-sizing:border-box;height:44px;padding:0 14px;border:1px solid var(--c-border-strong);border-radius:10px;background:var(--c-surface);color:var(--c-text);font:inherit;font-size:14px;transition:border-color .15s,box-shadow .15s}golem15-seo-robotstester .seo-robotstester__input:focus,golem15-seo-robotstester .seo-robotstester__select:focus{outline:none;border-color:var(--c-primary);box-shadow:0 0 0 3px var(--c-ring)}golem15-seo-robotstester .seo-robotstester__button{height:42px;padding:0 16px;border:1px solid var(--c-border-strong);border-radius:10px;background:var(--c-surface);color:var(--c-text);font:inherit;font-size:14px;cursor:pointer}golem15-seo-robotstester .seo-robotstester__button:focus-visible{outline:none;box-shadow:0 0 0 3px var(--c-ring)}golem15-seo-robotstester .seo-robotstester__button:disabled{cursor:default;opacity:.6}golem15-seo-robotstester .seo-robotstester__result{display:flex;align-items:flex-start;gap:6px;margin-top:10px;font-size:14px;overflow-wrap:anywhere}golem15-seo-robotstester .seo-robotstester__result .seo-icon{flex:none;margin-top:3px}golem15-seo-robotstester .seo-robotstester__result--allowed{color:var(--c-ok-text, var(--c-text))}golem15-seo-robotstester .seo-robotstester__result--blocked{color:var(--c-text)}golem15-seo-robotstester .seo-robotstester__result--error{color:var(--c-danger)}golem15-seo-robotstester .seo-robotstester__note{margin:6px 0 0;color:var(--c-muted);font-size:13px}golem15-seo-transfer .seo-transfer__section{display:flex;flex-direction:column;gap:12px}golem15-seo-transfer .seo-transfer__section--import{margin-top:24px;padding-top:24px;border-top:1px solid var(--c-border)}golem15-seo-transfer .seo-transfer__heading{margin:0;color:var(--c-text);font-size:14px;font-weight:600}golem15-seo-transfer .seo-transfer__row{display:flex;flex-wrap:wrap;align-items:flex-end;gap:8px}golem15-seo-transfer .seo-transfer__field{display:flex;flex-direction:column;gap:4px}golem15-seo-transfer .seo-transfer__label{color:var(--c-muted);font-size:13px}golem15-seo-transfer .seo-transfer__select{box-sizing:border-box;min-width:240px;height:44px;padding:0 14px;border:1px solid var(--c-border-strong);border-radius:10px;background:var(--c-surface);color:var(--c-text);font:inherit;font-size:14px;transition:border-color .15s,box-shadow .15s}golem15-seo-transfer .seo-transfer__select:focus{outline:none;border-color:var(--c-primary);box-shadow:0 0 0 3px var(--c-ring)}golem15-seo-transfer .seo-transfer__button{height:42px;padding:0 16px;border:1px solid var(--c-border-strong);border-radius:10px;background:var(--c-surface);color:var(--c-text);font:inherit;font-size:14px;cursor:pointer}golem15-seo-transfer .seo-transfer__button--ghost{border-color:transparent;background:transparent}golem15-seo-transfer .seo-transfer__button--primary{border-color:var(--c-primary);background:var(--c-primary);color:var(--c-on-primary, var(--c-surface))}golem15-seo-transfer .seo-transfer__button:focus-visible{outline:none;box-shadow:0 0 0 3px var(--c-ring)}golem15-seo-transfer .seo-transfer__button:disabled{cursor:default;opacity:.6}golem15-seo-transfer .seo-transfer__filename{align-self:center;color:var(--c-text);font-size:14px;overflow-wrap:anywhere}golem15-seo-transfer .seo-transfer__filename--none{color:var(--c-muted)}golem15-seo-transfer .seo-transfer__error{margin:0;color:var(--c-danger);font-size:14px}golem15-seo-transfer .seo-transfer__note,golem15-seo-transfer .seo-transfer__done{margin:0;color:var(--c-muted);font-size:13px}golem15-seo-transfer .seo-transfer__link{color:var(--c-primary)}golem15-seo-transfer .seo-transfer__preview{display:flex;flex-direction:column;gap:12px}golem15-seo-transfer .seo-transfer__summary{margin:0;font-size:14px;font-weight:600}golem15-seo-transfer .seo-transfer__table-wrap{max-height:480px;overflow:auto}golem15-seo-transfer .seo-transfer__num{font-variant-numeric:tabular-nums;text-align:right}golem15-seo-transfer .seo-transfer__mono{font-family:DM Mono,ui-monospace,monospace;overflow-wrap:anywhere}golem15-seo-transfer .seo-transfer__badge{display:inline-flex;align-items:center;height:24px;margin-right:4px;padding:0 10px;border-radius:999px;font-size:12px;white-space:nowrap}golem15-seo-transfer .seo-transfer__badge--new{background:var(--c-ok-bg);color:var(--c-ok-text)}golem15-seo-transfer .seo-transfer__badge--changed,golem15-seo-transfer .seo-transfer__badge--warning{background:var(--c-sel);color:var(--c-text)}golem15-seo-transfer .seo-transfer__badge--unchanged{background:var(--c-subtle);color:var(--c-muted)}golem15-seo-transfer .seo-transfer__badge--refused{background:var(--c-danger-soft);color:var(--c-danger)}golem15-seo-transfer .seo-transfer__actions{display:flex;justify-content:flex-end;gap:10px}@media(max-width:600px){golem15-seo-robots .seo-robots__panel{grid-template-columns:1fr}golem15-seo-serp .serp-card,golem15-seo-serp .serp-card--mobile{max-width:none}}@media(prefers-reduced-motion:reduce){golem15-seo-robotstxt .seo-robotstxt__control,golem15-seo-robotstester .seo-robotstester__input,golem15-seo-robotstester .seo-robotstester__select,golem15-seo-transfer .seo-transfer__select,golem15-seo-text .seo-text__control,golem15-seo-robots .seo-robots__segment,golem15-seo-serp .seo-serp__segment{transition:none}}golem15-seo-schema{display:flex;flex-direction:column;gap:24px}golem15-seo-schema .seo-schema__section{display:flex;flex-direction:column;gap:6px}golem15-seo-schema .seo-schema__label{font-size:14px;font-weight:600;line-height:1.5;color:var(--c-text)}golem15-seo-schema .seo-schema__select,golem15-seo-schema .seo-schema__input,golem15-seo-parent .seo-parent__select{box-sizing:border-box;height:44px;padding:0 14px;border:1px solid var(--c-border-strong);border-radius:10px;background:var(--c-surface);color:var(--c-text);font:inherit;font-size:14px;transition:border-color .15s}golem15-seo-schema .seo-schema__select,golem15-seo-parent .seo-parent__select{min-width:280px;max-width:100%}golem15-seo-schema .seo-schema__input{width:100%}golem15-seo-schema .seo-schema__fixed{font-size:14px;color:var(--c-text)}golem15-seo-schema .seo-schema__note,golem15-seo-graph .seo-graph__meta{margin:0;font-size:13px;line-height:1.5;color:var(--c-muted)}golem15-seo-schema .seo-schema__section>.seo-schema__note{margin-top:2px}golem15-seo-schema .seo-schema__grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}@media(max-width:600px){golem15-seo-schema .seo-schema__grid{grid-template-columns:1fr}}golem15-seo-schema .seo-schema__cell{display:flex;flex-direction:column;gap:6px}golem15-seo-schema .seo-schema__error{margin:0;font-size:13px;color:var(--c-danger)}golem15-seo-schema .seo-schema__error:empty{display:none}golem15-seo-schema .seo-schema__disclosure-row{display:flex;align-items:center;gap:8px}golem15-seo-schema .seo-schema__disclosure,golem15-seo-graph .seo-graph__disclosure{display:inline-flex;align-items:center;gap:8px;padding:0;border:0;background:none;color:var(--c-text);font:inherit;font-size:14px;font-weight:600;cursor:pointer;align-self:flex-start}golem15-seo-schema .seo-schema__disclosure[aria-expanded=true] .seo-icon,golem15-seo-graph .seo-graph__disclosure[aria-expanded=true] .seo-icon{transform:rotate(180deg)}golem15-seo-schema .seo-schema__panel{display:flex;flex-direction:column;gap:8px}golem15-seo-schema .seo-schema__json{box-sizing:border-box;width:100%;min-height:160px;resize:vertical;padding:12px 14px;border:1px solid var(--c-border-strong);border-radius:10px;background:var(--c-surface);color:var(--c-text);font-family:DM Mono,ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;line-height:1.6}golem15-seo-schema .seo-schema__status,golem15-seo-graph .seo-graph__finding,golem15-seo-graph .seo-graph__ok,golem15-seo-graph .seo-graph__error{display:flex;align-items:center;gap:8px;margin:0;font-size:13px;line-height:1.5}golem15-seo-schema .seo-schema__status--ok,golem15-seo-graph .seo-graph__ok{color:var(--c-ok-text)}golem15-seo-schema .seo-schema__status--error,golem15-seo-graph .seo-graph__error,golem15-seo-graph .seo-graph__finding--required .seo-icon{color:var(--c-danger)}golem15-seo-graph .seo-graph__finding{font-size:14px;color:var(--c-text)}golem15-seo-schema .seo-schema__select:focus-visible,golem15-seo-schema .seo-schema__input:focus-visible,golem15-seo-schema .seo-schema__json:focus-visible,golem15-seo-schema .seo-schema__disclosure:focus-visible,golem15-seo-graph .seo-graph__disclosure:focus-visible,golem15-seo-graph .seo-graph__button:focus-visible,golem15-seo-graph .seo-graph__link:focus-visible,golem15-seo-parent .seo-parent__select:focus-visible{outline:none;box-shadow:0 0 0 3px var(--c-ring)}golem15-seo-schema.seo-schema--invalid .seo-schema__json,golem15-seo-parent.seo-parent--invalid .seo-parent__select{border-color:var(--c-danger)}golem15-seo-graph{display:flex;flex-direction:column;gap:24px}golem15-seo-graph .seo-graph__nodes{display:flex;flex-direction:column;gap:16px}golem15-seo-graph .seo-graph__node{display:flex;flex-direction:column;gap:8px}golem15-seo-graph .seo-graph__node-head{display:flex;align-items:center;gap:8px}golem15-seo-graph .seo-graph__pill{display:inline-flex;align-items:center;height:24px;padding:0 10px;border-radius:999px;background:var(--c-subtle);color:var(--c-muted);font-size:12px;font-weight:600}golem15-seo-graph .seo-graph__id{font-family:DM Mono,ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;color:var(--c-muted)}golem15-seo-graph .seo-graph__json{box-sizing:border-box;max-height:360px;overflow:auto;margin:0;padding:12px 14px;border:1px solid var(--c-border);border-radius:10px;background:var(--c-subtle);color:var(--c-text);font-family:DM Mono,ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;line-height:1.6;white-space:pre}golem15-seo-graph .seo-graph__links{display:flex;flex-direction:column;gap:8px}golem15-seo-graph .seo-graph__link{display:inline-flex;align-items:center;gap:6px;color:var(--c-primary);font-size:14px;align-self:flex-start}golem15-seo-graph .seo-graph__button{align-self:flex-start;height:42px;padding:0 18px;border:1px solid var(--c-border-strong);border-radius:10px;background:var(--c-surface);color:var(--c-text);font:inherit;font-size:14px;font-weight:600;cursor:pointer;transition:background-color .15s}golem15-seo-graph .seo-graph__button:hover{background:var(--c-subtle)} diff --git a/assets/dist/seo.js b/assets/dist/seo.js index 9ef1997..2483a74 100644 --- a/assets/dist/seo.js +++ b/assets/dist/seo.js @@ -1 +1,2 @@ -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.",robotsUnparsed:"The stored value “:value” is not one this editor can show. It stays as it is until you reset it.",robotsReset:"Reset",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"}},se={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ą.",robotsUnparsed:"Zapisanej wartości „:value” ten edytor nie potrafi pokazać. Zostaje bez zmian, dopóki jej nie wyzerujesz.",robotsReset:"Wyzeruj",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"}},ie={en:V,pl:se};function y(n){return(n.getAttribute("ui-locale")||document.documentElement.lang||"en").toLowerCase().split(/[-_]/)[0]||"en"}function L(n){return ie[n]??V}function w(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 w(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",ne={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 T(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 ne[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 I(n,e,t){const s=n.getAttribute(e);if(s===null||s==="")return t;try{return JSON.parse(s)}catch{return t}}function S(n,e,t){n.dispatchEvent(new CustomEvent(e,{detail:t,bubbles:!0,composed:!0}))}let W=0;function E(n){return W+=1,`${n}-${W}`}const oe="golem15-seo-image";function re(n){return/^\/(?![\/\\])/.test(n)||/^https?:\/\//i.test(n)}class ae 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="",S(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&&re(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(),S(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="",S(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 le="golem15-seo-robots";function D(){return{index:"",follow:"",noarchive:!1,nosnippet:!1,noimageindex:!1,maxSnippet:null,maxImagePreview:"",maxVideoPreview:null,unavailableAfter:""}}function X(n){const e=/^(\d{4})-(\d{2})-(\d{2})$/.exec(n);if(!e)return!1;const t=Number(e[1]),s=Number(e[2]),i=Number(e[3]);if(s<1||s>12||i<1)return!1;const a=[31,t%4===0&&t%100!==0||t%400===0?29:28,31,30,31,30,31,31,30,31,30,31][s-1];return i<=a}function ce(n){const e=D(),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||!X(c))return null;e.unavailableAfter=c;break;default:return null}}return e}function de(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 j(n){return[n.noarchive,n.nosnippet,n.noimageindex,n.maxSnippet!==null,n.maxImagePreview!=="",n.maxVideoPreview!==null,n.unavailableAfter!==""].filter(Boolean).length}class ue extends HTMLElement{static observedAttributes=["options","disabled","invalid","ui-locale","locale"];current="";model=D();open=!1;unparsed=!1;get value(){return this.current}set value(e){const t=typeof e=="string"?e:e==null?"":String(e);if(t===this.current&&this.childElementCount>0)return;this.current=t;const s=ce(t);this.unparsed=s===null,this.model=s??D(),this.open=this.open||j(this.model)>0,this.render()}connectedCallback(){this.open=j(this.model)>0,this.render()}attributeChangedCallback(){this.isConnected&&this.render()}labels(){return L(y(this))}commit(){this.unparsed||(this.current=de(this.model),S(this,"summer-change",{value:this.current}),this.render())}render(){const e=this.labels(),t=this.hasAttribute("disabled")||this.unparsed,i=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",[["",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=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),T("chevron-down")),c.addEventListener("click",()=>{this.open=!this.open,this.render()});const d=o("div","seo-robots__advanced-row");d.append(c);const h=j(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 p of["noarchive","nosnippet","noimageindex"]){const f=o("label","seo-robots__check"),m=o("input");m.type="checkbox",m.checked=this.model[p],m.disabled=t,m.dataset.focusKey=p,m.addEventListener("change",()=>{this.model[p]=m.checked,this.commit()}),f.append(m,o("span","",e[p])),g.append(f)}u.append(g);const v=[a,d,u];if(this.unparsed){const p=o("div","seo-robots__unparsed");if(p.setAttribute("role","status"),p.append(T("triangle-alert"),o("span","",w(e.robotsUnparsed,{value:this.current}))),!this.hasAttribute("disabled")){const f=o("button","seo-robots__reset",e.robotsReset);f.type="button",f.dataset.focusKey="reset",f.addEventListener("click",()=>{this.unparsed=!1,this.model=D(),this.commit()}),p.append(f)}v.unshift(p)}this.replaceChildren(...v),r&&this.querySelector(`[data-focus-key="${r}"]`)?.focus()}radioGroup(e,t,s,i){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],v)=>{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+"-"+v,p.addEventListener("click",()=>this.choose(t,h)),p.addEventListener("keydown",m=>{const k=m.key==="ArrowRight"||m.key==="ArrowDown"?1:m.key==="ArrowLeft"||m.key==="ArrowUp"?-1:0;if(k===0||i)return;m.preventDefault();const b=(v+k+s.length)%s.length;this.choose(t,s[b][0]),this.querySelector(`[data-focus-key="${t}-${b}"]`)?.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=X(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,S(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 he="golem15-seo-robotstester",G=["Googlebot","Googlebot-Image","Bingbot","*"];function pe(n,e){const t={agent:e.agent,line:e.line,rule:e.rule};return e.allowed?e.line>0?w(n.allowedRule,t):w(n.allowedNone,t):e.bot?w(n.blockedBot,t):w(n.blocked,t)}class fe 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=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=i,h.addEventListener("keydown",b=>{b.key==="Enter"&&(b.preventDefault(),this.test())}),c.append(d,h);const u=E("seo-robotstester-agent"),g=o("div","seo-robotstester__field seo-robotstester__field--agent"),v=o("label","seo-robotstester__label",s.agent);v.htmlFor=u;const p=o("select","seo-robotstester__select");p.id=u;for(const b of this.context?.agents??G){const x=o("option","",b);x.value=b,p.append(x)}p.value=r,p.value!==r&&(p.value="Googlebot"),g.append(v,p);const f=o("button","seo-robotstester__button");f.type="button",f.addEventListener("click",()=>this.test()),a.append(c,g,f);const m=o("div","seo-robotstester__result");m.setAttribute("role","status");const k=o("div","seo-robotstester__notes");this.input=h,this.select=p,this.button=f,this.line=m,this.notes=k,this.append(a,m,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(T(s?"circle-check":"triangle-alert"),o("span","",pe(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 me="golem15-seo-robotstxt",be=500*1024,F=10,ge=["User-agent","Allow","Disallow","Sitemap","Crawl-delay","Host","Clean-param"];function we(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 ve(n){const e=n.toLowerCase();let t="",s=3;for(const i of ge){const r=we(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=ve(d);e.push(u?{kind:"unknownSuggest",line:r,word:d,suggestion:u}:{kind:"unknown",line:r,word:d})}),e}function ye(n){return new TextEncoder().encode(n).length}function ke(n,e){return w(n[e.kind],{line:e.line,word:e.word??"",suggestion:e.suggestion??""})}class xe 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 i=this.getAttribute("field-name");i&&s.setAttribute("aria-label",i),s.addEventListener("input",()=>{this.hasAttribute("disabled")||(this.current=s.value,this.update(),S(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=_e(this.current),i=[];if(s.length===0){const c=o("div","seo-robotstxt__finding seo-robotstxt__finding--clean");c.append(T("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(T("triangle-alert"),o("span","",ke(e,c))),i.push(d)}s.length>F&&i.push(o("div","seo-robotstxt__more",w(e.moreFindings,{count:A(s.length-F,t)})))}this.panel.replaceChildren(...i);const r=ye(this.current),a=new Intl.NumberFormat(t,{maximumFractionDigits:1}).format(r/1024)+" KB";this.meter.replaceChildren(o("span","",w(e.size,{size:a})));const l=r>be;this.classList.toggle("seo-robotstxt--over",l),l&&this.meter.append(o("span","seo-robotstxt__over",e.tooLarge))}}const Y="20px Arial, sans-serif",J="14px Arial, sans-serif",Q=580,ee=920,Ae=680,ze=30,Te=120;let N;function Ee(){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 Ce(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=Ee();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 Ce(n,s)}const P=" …";function q(n,e,t){if(O(n,e)<=t)return n;const s=u=>O(u+P,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("")+P;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 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(),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,Re)}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||Ie(s?.titleTemplate??"",{title:s?.defaultTitle??"",site:d})||s?.defaultTitle||"",u=l||s?.defaultDescription||"",g=o("div","seo-serp__header"),v=o("div","seo-serp__track");v.setAttribute("role","radiogroup"),v.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()}),v.append(C)}g.append(v),i&&g.append(o("span","seo-serp__language",w(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 m=o("div","serp-card__names");if(m.append(o("span","serp-card__sitename",d||this.context?.host||"")),m.append(o("span","serp-card__crumbs",Me(this.context?.host??"",s?.url??null))),f.append(m),p.append(f),p.append(o("div","serp-card__title",q(h,Y,Q))),u){const _=this.device==="mobile"?Ae:ee;p.append(o("div","serp-card__description",q(u,J,_)))}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(T("circle-alert"),o("span","",e.contextFailed)),k.append(_)}!a&&s?.titleTemplate&&k.append(o("div","",w(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 b=o("div","seo-serp__checks");if(b.setAttribute("role","status"),this.checking)b.append(o("div","seo-serp__checking",e.checking));else{const _=this.duplicates.get(i);if(_)for(const[C,te]of[[_.title,e.dupTitle],[_.description,e.dupDescription]])C.length>0&&b.append(Oe(C,te,e,t))}const x=[g,p,k,b];c&&s&&x.push(Ue(c,{title:h,description:u,h1:s.h1,url:s.url,bodyWords:s.bodyWords},e,t)),this.replaceChildren(...x)}}function Me(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 Oe(n,e,t,s){const i=o("div","seo-serp__check seo-serp__check--warn");i.append(T("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,M).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>M&&r.append(document.createTextNode(" "+w(t.andMore,{count:n.length-M}))),r.append(document.createTextNode(d??"")),i.append(r),i}function Ue(n,e,t,s){const i=o("div","seo-serp__keyphrase");i.append(o("div","seo-serp__keyphrase-title",w(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(T(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>=De,w(t.kpLength,{count:a}),w(t.kpLengthNo,{count:a}))}return i}const Be="golem15-seo-text",He={empty:"info",short:"triangle-alert",long:"triangle-alert",good:"circle-check"};class We 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"),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),S(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=Le(this.current,i),a=Ge(t,r.status,i);this.statusIcon.replaceChildren(T(He[r.status])),this.statusText.textContent=a,this.dataset.status=r.status,this.countText.textContent=w(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 Ge(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 qe="golem15-seo-transfer",Z=60*1024,U=200;function Ke(n){return n.summary.refused>0?"refused":n.count>0?"ready":"nothing"}function Ze(n){const e=n.filter(s=>s.change==="refused"),t=n.filter(s=>s.change!=="refused");return[...e,...t].slice(0,U)}function Ve(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 Xe(){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="",Ve(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,S(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=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)),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=w(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 v=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,v,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"),m=o("a","seo-transfer__link",e.openHistory);m.href=Xe(),f.append(m),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(w(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 b of[e.colLine,e.colChange,e.colAddress,e.colField,e.colBefore,e.colAfter,e.colNote])h.append(o("th","",b));const u=o("thead");u.append(h);const g=o("tbody"),v={new:e.bNew,changed:e.bChanged,unchanged:e.bUnchanged,refused:e.bRefused};for(const b of Ze(r.rows)){const x=o("tr","seo-transfer__tr--"+b.change);x.append(o("td","seo-transfer__num",A(b.line,t)));const _=o("td");_.append(o("span","seo-transfer__badge seo-transfer__badge--"+b.change,v[b.change])),b.warning&&_.append(o("span","seo-transfer__badge seo-transfer__badge--warning",e.bWarning)),x.append(_),x.append(o("td","seo-transfer__mono",b.address)),x.append(o("td","",b.field)),x.append(o("td","",b.before)),x.append(o("td","",b.after)),x.append(o("td","",b.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",w(e.rowCap,{count:A(Math.max(r.total,r.rows.length),t)})));const p=Ke(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"),m=this.button(e.discard,"ghost",()=>{this.preview=null,this.applyError="",this.render()});m.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(m,k),f.hidden=i,a.append(f),a}}const Ye=[[Be,We],[le,ue],[Ne,Pe],[oe,ae],[me,xe],[he,fe],[qe,$e]];for(const[n,e]of Ye)customElements.get(n)||customElements.define(n,e);export{oe as IMAGE_TAG,he as ROBOTSTESTER_TAG,me as ROBOTSTXT_TAG,le as ROBOTS_TAG,Ne as SERP_TAG,ae as SeoImageElement,ue as SeoRobotsElement,fe as SeoRobotsTesterElement,xe as SeoRobotsTxtElement,Pe as SeoSerpElement,We as SeoTextElement,$e as SeoTransferElement,Be as TEXT_TAG,qe as TRANSFER_TAG}; +const ie={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.",robotsUnparsed:"The stored value “:value” is not one this editor can show. It stays as it is until you reset it.",robotsReset:"Reset",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"},schemaType:"Schema type",typeDefault:"Default (:type)",tBlogPosting:"BlogPosting (blog post)",tArticle:"Article (article)",tNewsArticle:"NewsArticle (news)",tWebPage:"WebPage (page)",tAboutPage:"AboutPage (about us)",tContactPage:"ContactPage (contact)",tService:"Service (service or offer)",tCollectionPage:"CollectionPage (listing)",tProfilePage:"ProfilePage (profile)",noteArticle:"Author, dates, image, word count and section come from the content.",notePage:"Name, description and breadcrumbs come from the SEO tab.",noteContact:"Address and opening hours come from the SEO settings (Publisher, Local business).",noteService:"The provider is the publisher from the SEO settings.",noteCollection:"Lists the items shown on the page.",noteProfile:"Name, image and profiles come from the author.",serviceType:"Kind of service",serviceTypePh:"e.g. Mushroom workshops",areaServed:"Area served",areaServedHint:"Cities or regions, separated by commas.",price:"Price",priceHint:"Leave empty when the price is set individually.",currency:"Currency",currencyHint:"ISO code, e.g. PLN.",priceInvalid:"Enter a price such as 150 or 149.99.",currencyInvalid:"Use a three-letter currency code, e.g. PLN.",custom:"Custom JSON-LD (advanced)",customSet:"Set",customHint:"An object or a list of objects. A node with the @id of a generated node is merged into it, and your values win; other nodes are added to the graph. Paste only the JSON, without a script tag.",jsonValid:{one:"Valid JSON: :count node",other:"Valid JSON: :count nodes"},jsonError:"Line :line, column :column: :message",jsonNotObject:"Use an object or a list of objects.",jsonTooLarge:"Over 16 KB: shorten the custom JSON-LD.",forLocale:"JSON-LD for :locale — :url",graphChecking:"Building the graph…",allGood:"No problems: every node has its required and recommended properties.",required:"Missing required property “:property” (:type)",recommended:"Missing recommended property “:property” (:type)",noRules:":type has no rich-result rules at Google; it is checked as valid schema.org only.",showJson:"Show JSON-LD",hideJson:"Hide JSON-LD",richResults:"Check in Google's Rich Results Test",graphNoUrl:"The Rich Results Test link appears after the first save, when the page has an address.",graphUnsaved:"Shows the saved page with your unsaved structured data changes.",layerNodes:"The site may add nodes of its own, such as FAQPage. The site audit checks the rendered result.",graphAction:"Check structured data",graphFailed:"The structured data could not be built. Try again.",primaryDefault:"Default (first category)"},ce={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ą.",robotsUnparsed:"Zapisanej wartości „:value” ten edytor nie potrafi pokazać. Zostaje bez zmian, dopóki jej nie wyzerujesz.",robotsReset:"Wyzeruj",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"},schemaType:"Typ schema",typeDefault:"Domyślny (:type)",tBlogPosting:"BlogPosting (wpis na blogu)",tArticle:"Article (artykuł)",tNewsArticle:"NewsArticle (wiadomość)",tWebPage:"WebPage (strona)",tAboutPage:"AboutPage (o nas)",tContactPage:"ContactPage (kontakt)",tService:"Service (usługa lub oferta)",tCollectionPage:"CollectionPage (lista)",tProfilePage:"ProfilePage (profil)",noteArticle:"Autor, daty, obraz, liczba słów i sekcja pochodzą z treści.",notePage:"Nazwa, opis i ścieżka nawigacji pochodzą z zakładki SEO.",noteContact:"Adres i godziny otwarcia pochodzą z ustawień SEO (Wydawca, Firma lokalna).",noteService:"Usługodawcą jest wydawca z ustawień SEO.",noteCollection:"Wymienia elementy pokazane na stronie.",noteProfile:"Imię i nazwisko, zdjęcie i profile pochodzą od autora.",serviceType:"Rodzaj usługi",serviceTypePh:"np. warsztaty grzybowe",areaServed:"Obszar działania",areaServedHint:"Miasta lub regiony oddzielone przecinkami.",price:"Cena",priceHint:"Zostaw puste, gdy cena jest ustalana indywidualnie.",currency:"Waluta",currencyHint:"Kod ISO, np. PLN.",priceInvalid:"Wpisz cenę, np. 150 lub 149,99.",currencyInvalid:"Użyj trzyliterowego kodu waluty, np. PLN.",custom:"Własny JSON-LD (zaawansowane)",customSet:"Ustawiony",customHint:"Obiekt lub lista obiektów. Węzeł z @id wygenerowanego węzła zostaje z nim scalony, a Twoje wartości mają pierwszeństwo; pozostałe węzły są dodawane do grafu. Wklej sam JSON, bez znacznika script.",jsonValid:{one:"Poprawny JSON: :count węzeł",few:"Poprawny JSON: :count węzły",many:"Poprawny JSON: :count węzłów",other:"Poprawny JSON: :count węzła"},jsonError:"Wiersz :line, kolumna :column: :message",jsonNotObject:"Użyj obiektu lub listy obiektów.",jsonTooLarge:"Ponad 16 KB: skróć własny JSON-LD.",forLocale:"JSON-LD dla :locale — :url",graphChecking:"Budowanie grafu…",allGood:"Brak problemów: każdy węzeł ma wymagane i zalecane właściwości.",required:"Brak wymaganej właściwości „:property” (:type)",recommended:"Brak zalecanej właściwości „:property” (:type)",noRules:":type nie ma reguł wyników z elementami rozszerzonymi w Google; sprawdzana jest tylko poprawność schema.org.",showJson:"Pokaż JSON-LD",hideJson:"Ukryj JSON-LD",richResults:"Sprawdź w Teście wyników z elementami rozszerzonymi Google",graphNoUrl:"Link do testu pojawi się po pierwszym zapisie, gdy strona będzie miała adres.",graphUnsaved:"Pokazuje zapisaną stronę z niezapisanymi zmianami danych strukturalnych.",layerNodes:"Witryna może dodać własne węzły, np. FAQPage. Audyt witryny sprawdza wynik na stronie.",graphAction:"Sprawdź dane strukturalne",graphFailed:"Nie udało się zbudować danych strukturalnych. Spróbuj ponownie.",primaryDefault:"Domyślna (pierwsza kategoria)"},de={en:ie,pl:ce};function _(i){return(i.getAttribute("ui-locale")||document.documentElement.lang||"en").toLowerCase().split(/[-_]/)[0]||"en"}function N(i){return de[i]??ie}function y(i,e={}){const t=Object.keys(e).sort((o,r)=>r.length-o.length);let s=i;for(const o of t)s=s.split(":"+o).join(String(e[o]));return s}function I(i,e,t,s={}){let o="other";try{o=new Intl.PluralRules(t).select(e)}catch{o=e===1?"one":"other"}const r=o==="one"&&i.one||o==="few"&&i.few||o==="many"&&i.many||i.other;return y(r,{...s,count:new Intl.NumberFormat(t).format(e)})}function C(i,e){try{return new Intl.NumberFormat(e).format(i)}catch{return String(i)}}const q="http://www.w3.org/2000/svg",ue={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"],"external-link":["M15 3h6v6","M10 14 21 3","M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"]};function x(i,e=14){const t=document.createElementNS(q,"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 ue[i]??[]){const o=document.createElementNS(q,"path");o.setAttribute("d",s),t.appendChild(o)}return t}function n(i,e="",t){const s=document.createElement(i);return e&&(s.className=e),t!==void 0&&(s.textContent=t),s}function P(i,e,t){const s=i.getAttribute(e);if(s===null||s==="")return t;try{return JSON.parse(s)}catch{return t}}function L(i,e,t){i.dispatchEvent(new CustomEvent(e,{detail:t,bubbles:!0,composed:!0}))}let V=0;function S(i){return V+=1,`${i}-${V}`}class J{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,L(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 bt="golem15-seo-graph";function he(i){return"https://search.google.com/test/rich-results?url="+encodeURIComponent(i)}function pe(i){const e=i.indexOf("#");return e>=0?i.slice(e):i}class me extends HTMLElement{static observedAttributes=["fill-values","busy","state","ui-locale","disabled","label"];data=null;failed=!1;pending=!1;showJSON=!1;shownLocale="";lastFill="";timer;connected=!1;actions=new J(this,()=>this.fail());onResult=e=>{const t=e.detail;this.actions.settle();const s=t?.data;s&&Array.isArray(s.nodes)&&(this.data=s,this.failed=!1),this.pending=this.actions.current!==null,this.render()};onLocale=e=>{const t=e.detail;typeof t=="string"&&t!==""&&t!==this.shownLocale&&(this.shownLocale=t,this.check())};connectedCallback(){this.connected=!0,this.lastFill=this.getAttribute("fill-values")??"",this.addEventListener("summer-result",this.onResult),window.addEventListener("summer:ml-locale",this.onLocale),this.render(),queueMicrotask(()=>{this.connected&&this.check()})}disconnectedCallback(){this.connected=!1,this.removeEventListener("summer-result",this.onResult),window.removeEventListener("summer:ml-locale",this.onLocale),clearTimeout(this.timer),this.actions.reset(),this.pending=!1}attributeChangedCallback(e,t,s){if(this.connected){if(e==="busy"&&s===null){this.actions.flush();return}if(e==="state"&&s==="error"){this.actions.settle()&&this.fail();return}if(e==="fill-values"){const o=s??"";o!==this.lastFill&&(this.lastFill=o,clearTimeout(this.timer),this.timer=setTimeout(()=>this.check(),800));return}this.render()}}labels(){return N(_(this))}check(){clearTimeout(this.timer),this.pending=!0,this.actions.request({op:"graph",locale:this.shownLocale}),this.render()}fail(){this.failed=!0,this.pending=this.actions.current!==null,this.render()}render(){if(!this.connected)return;const e=this.labels(),t=[];this.setAttribute("aria-busy",String(this.pending));const s=this.data,o=n("p","seo-graph__meta");if(s&&(o.textContent=s.url?y(e.forLocale,{locale:(s.locale||"").toUpperCase(),url:s.url}):e.graphNoUrl),this.pending){const r=n("p","seo-graph__meta seo-graph__checking",e.graphChecking);r.setAttribute("role","status"),t.push(r)}if(s){t.push(o);const r=n("div","seo-graph__nodes");let a=0;for(const l of s.nodes){const d=n("div","seo-graph__node"),p=n("div","seo-graph__node-head");p.append(n("span","seo-graph__pill",l.type||"—"),n("code","seo-graph__id",pe(l.id||""))),d.append(p),l.rules||d.append(n("p","seo-graph__meta",y(e.noRules,{type:l.type})));for(const h of l.findings??[]){a++;const m=h.level==="required",b=n("p","seo-graph__finding seo-graph__finding--"+(m?"required":"recommended"));b.append(x(m?"circle-alert":"triangle-alert"),n("span","",y(m?e.required:e.recommended,{property:h.property,type:l.type}))),d.append(b)}r.append(d)}if(t.push(r),a===0&&s.nodes.length>0){const l=n("p","seo-graph__ok");l.append(x("circle-check"),n("span","",e.allGood)),t.push(l)}if(s.graph){const l=n("button","seo-graph__disclosure");l.type="button",l.setAttribute("aria-expanded",String(this.showJSON)),l.append(n("span","",this.showJSON?e.hideJson:e.showJson),x("chevron-down")),l.addEventListener("click",()=>{this.showJSON=!this.showJSON,this.render()}),t.push(l),this.showJSON&&t.push(n("pre","seo-graph__json",JSON.stringify(s.graph,null,2)))}const c=n("div","seo-graph__links");if(s.url){const l=n("a","seo-graph__link");l.href=he(s.url),l.target="_blank",l.rel="noopener",l.append(n("span","",e.richResults),x("external-link")),c.append(l)}c.append(n("p","seo-graph__meta",e.graphUnsaved),n("p","seo-graph__meta",e.layerNodes)),t.push(c)}if(this.failed){const r=n("p","seo-graph__error");r.setAttribute("role","status"),r.append(x("circle-alert"),n("span","",e.graphFailed)),t.push(r)}if(!this.hasAttribute("disabled")){const r=n("button","seo-graph__button",this.getAttribute("label")||e.graphAction);r.type="button",r.addEventListener("click",()=>this.check()),t.push(r)}this.replaceChildren(...t)}fill(){return P(this,"fill-values",{})}}const fe="golem15-seo-image";function ge(i){return/^\/(?![\/\\])/.test(i)||/^https?:\/\//i.test(i)}class be 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="",L(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 N(_(this))}render(){const e=this.labels(),t=this.hasAttribute("disabled"),s=[];if(this.current&&ge(this.current)){const r=n("div","seo-image__preview"),a=n("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=n("div","seo-image__actions"),a=S("seo-image-file"),c=n("input","seo-visually-hidden");c.type="file",c.accept="image/jpeg,image/png,image/gif,image/webp",c.id=a,c.addEventListener("change",()=>{const d=c.files?.[0];d&&(this.pending=S("seo-image-upload"),this.error="",this.render(),L(this,"summer-upload",{id:this.pending,file:d}))});const l=n("label","seo-image__button",this.current?e.imageReplace:e.imageUpload);if(l.htmlFor=a,r.append(c,l),this.current){const d=n("button","seo-image__button seo-image__button--secondary",e.imageRemove);d.type="button",d.addEventListener("click",()=>{this.current="",this.error="",L(this,"summer-change",{value:""}),this.render()}),r.append(d)}s.push(r)}const o=n("div","seo-image__status"+(this.error?" seo-image__status--error":""));o.setAttribute("role","status"),o.textContent=this.pending?e.imageUploading:this.error,s.push(o),this.classList.toggle("seo-image--invalid",this.hasAttribute("invalid")),this.replaceChildren(...s)}}const wt="golem15-seo-parent";function we(i){if(typeof i=="string")return{selected:i,options:[]};const e={selected:"",options:[]};if(i&&typeof i=="object"){const t=i;if(e.selected=typeof t.selected=="string"?t.selected:"",Array.isArray(t.options)){for(const s of t.options)if(s&&typeof s=="object"&&typeof s.value=="string"){const o=s;e.options.push({value:o.value,label:typeof o.label=="string"&&o.label?o.label:o.value})}}}return e}class ye extends HTMLElement{static observedAttributes=["disabled","invalid","ui-locale"];current={selected:"",options:[]};get value(){return this.current.options.some(e=>e.value===this.current.selected)?this.current.selected:""}set value(e){const t=we(e);typeof e=="string"&&this.current.options.length>0&&(t.options=this.current.options),this.current=t,this.render()}connectedCallback(){this.render()}attributeChangedCallback(){this.isConnected&&this.render()}render(){if(!this.isConnected)return;const e=N(_(this)),t=n("select","seo-parent__select");t.id=S("seo-parent"),t.disabled=this.hasAttribute("disabled");const s=n("option","",e.primaryDefault);s.value="",t.append(s);for(const o of this.current.options){const r=n("option","",o.label);r.value=o.value,t.append(r)}t.value=this.value,t.addEventListener("change",()=>{this.current.selected=t.value,L(this,"summer-change",{value:t.value})}),this.classList.toggle("seo-parent--invalid",this.hasAttribute("invalid")),this.replaceChildren(t)}}const ve="golem15-seo-robots";function O(){return{index:"",follow:"",noarchive:!1,nosnippet:!1,noimageindex:!1,maxSnippet:null,maxImagePreview:"",maxVideoPreview:null,unavailableAfter:""}}function ne(i){const e=/^(\d{4})-(\d{2})-(\d{2})$/.exec(i);if(!e)return!1;const t=Number(e[1]),s=Number(e[2]),o=Number(e[3]);if(s<1||s>12||o<1)return!1;const a=[31,t%4===0&&t%100!==0||t%400===0?29:28,31,30,31,30,31,31,30,31,30,31][s-1];return o<=a}function _e(i){const e=O(),t=i.trim();if(t==="")return e;const s=new Set;for(const o of t.split(",")){const r=o.trim().toLowerCase();if(r==="")return null;const a=r.indexOf(":"),c=(a<0?r:r.slice(0,a)).trim(),l=a<0?null:r.slice(a+1).trim(),d=c==="noindex"?"index":c==="nofollow"?"follow":c;if(s.has(d))return null;switch(s.add(d),c){case"index":case"noindex":if(l!==null)return null;e.index=c;break;case"follow":case"nofollow":if(l!==null)return null;e.follow=c;break;case"noarchive":case"nosnippet":case"noimageindex":if(l!==null)return null;e[c]=!0;break;case"max-snippet":case"max-video-preview":{if(l===null||!/^[+-]?\d+$/.test(l))return null;const p=Number.parseInt(l,10);if(p<-1)return null;c==="max-snippet"?e.maxSnippet=p:e.maxVideoPreview=p;break}case"max-image-preview":if(l!=="none"&&l!=="standard"&&l!=="large")return null;e.maxImagePreview=l;break;case"unavailable_after":if(l===null||!ne(l))return null;e.unavailableAfter=l;break;default:return null}}return e}function ke(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 M(i){return[i.noarchive,i.nosnippet,i.noimageindex,i.maxSnippet!==null,i.maxImagePreview!=="",i.maxVideoPreview!==null,i.unavailableAfter!==""].filter(Boolean).length}class xe extends HTMLElement{static observedAttributes=["options","disabled","invalid","ui-locale","locale"];current="";model=O();open=!1;unparsed=!1;get value(){return this.current}set value(e){const t=typeof e=="string"?e:e==null?"":String(e);if(t===this.current&&this.childElementCount>0)return;this.current=t;const s=_e(t);this.unparsed=s===null,this.model=s??O(),this.open=this.open||M(this.model)>0,this.render()}connectedCallback(){this.open=M(this.model)>0,this.render()}attributeChangedCallback(){this.isConnected&&this.render()}labels(){return N(_(this))}commit(){this.unparsed||(this.current=ke(this.model),L(this,"summer-change",{value:this.current}),this.render())}render(){const e=this.labels(),t=this.hasAttribute("disabled")||this.unparsed,o=P(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=n("div","seo-robots__groups");a.append(this.radioGroup(e.indexing,"index",[["",o,"inherit"],["index",e.index,"ok"],["noindex",e.noindex,"danger"]],t),this.radioGroup(e.links,"follow",[["",o,"inherit"],["follow",e.follow,"ok"],["nofollow",e.nofollow,"danger"]],t));const c=S("seo-robots-advanced"),l=n("button","seo-robots__toggle");l.type="button",l.setAttribute("aria-expanded",this.open?"true":"false"),l.setAttribute("aria-controls",c),l.dataset.focusKey="toggle",l.append(n("span","",e.advanced),x("chevron-down")),l.addEventListener("click",()=>{this.open=!this.open,this.render()});const d=n("div","seo-robots__advanced-row");d.append(l);const p=M(this.model);p>0&&d.append(n("span","seo-robots__summary",I(e.advancedSet,p,_(this))));const h=n("div","seo-robots__panel");h.id=c,h.hidden=!this.open,h.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 m=n("div","seo-robots__checks");for(const u of["noarchive","nosnippet","noimageindex"]){const f=n("label","seo-robots__check"),g=n("input");g.type="checkbox",g.checked=this.model[u],g.disabled=t,g.dataset.focusKey=u,g.addEventListener("change",()=>{this.model[u]=g.checked,this.commit()}),f.append(g,n("span","",e[u])),m.append(f)}h.append(m);const b=[a,d,h];if(this.unparsed){const u=n("div","seo-robots__unparsed");if(u.setAttribute("role","status"),u.append(x("triangle-alert"),n("span","",y(e.robotsUnparsed,{value:this.current}))),!this.hasAttribute("disabled")){const f=n("button","seo-robots__reset",e.robotsReset);f.type="button",f.dataset.focusKey="reset",f.addEventListener("click",()=>{this.unparsed=!1,this.model=O(),this.commit()}),u.append(f)}b.unshift(u)}this.replaceChildren(...b),r&&this.querySelector(`[data-focus-key="${r}"]`)?.focus()}radioGroup(e,t,s,o){const r=n("div","seo-robots__group"),a=S("seo-robots-caption"),c=n("span","seo-robots__caption",e);c.id=a;const l=n("div","seo-robots__track");l.setAttribute("role","radiogroup"),l.setAttribute("aria-labelledby",a);const d=this.model[t];return s.forEach(([p,h,m],b)=>{const u=n("button","seo-robots__segment seo-robots__segment--"+m,h);u.type="button",u.setAttribute("role","radio");const f=d===p;u.setAttribute("aria-checked",f?"true":"false"),u.tabIndex=f?0:-1,u.disabled=o,u.dataset.focusKey=t+"-"+b,u.addEventListener("click",()=>this.choose(t,p)),u.addEventListener("keydown",g=>{const A=g.key==="ArrowRight"||g.key==="ArrowDown"?1:g.key==="ArrowLeft"||g.key==="ArrowUp"?-1:0;if(A===0||o)return;g.preventDefault();const w=(b+A+s.length)%s.length;this.choose(t,s[w][0]),this.querySelector(`[data-focus-key="${t}-${w}"]`)?.focus()}),l.append(u)}),r.append(c,l),r}choose(e,t){this.hasAttribute("disabled")||(e==="index"?this.model.index=t:this.model.follow=t,this.commit())}numberField(e,t,s,o,r){const a=n("label","seo-robots__field");a.append(n("span","seo-robots__label",e));const c=n("input","seo-robots__input");c.type="number",c.min="-1",c.step="1",c.disabled=o,c.dataset.focusKey=s;const l=this.model[s];c.value=l===null?"":String(l);const d=n("span","seo-robots__error");return d.setAttribute("role","status"),c.addEventListener("change",()=>{const p=c.value.trim();if(p===""){this.model[s]=null,this.commit();return}if(!/^-?\d+$/.test(p)||Number.parseInt(p,10)<-1){d.textContent=r.invalidNumber;return}this.model[s]=Number.parseInt(p,10),this.commit()}),a.append(c),t&&a.append(n("span","seo-robots__hint",t)),a.append(d),a}selectField(e,t){const s=n("label","seo-robots__field");s.append(n("span","seo-robots__label",e.maxImagePreview));const o=n("select","seo-robots__input");o.disabled=t,o.dataset.focusKey="maxImagePreview";for(const[r,a]of[["",e.notSet],["none",e.none],["standard",e.standard],["large",e.large]]){const c=n("option","",a);c.value=r,c.selected=this.model.maxImagePreview===r,o.append(c)}return o.addEventListener("change",()=>{this.model.maxImagePreview=o.value,this.commit()}),s.append(o),s}dateField(e,t){const s=n("label","seo-robots__field");s.append(n("span","seo-robots__label",e.unavailableAfter));const o=n("input","seo-robots__input");return o.type="date",o.disabled=t,o.dataset.focusKey="unavailableAfter",o.value=this.model.unavailableAfter,o.addEventListener("change",()=>{this.model.unavailableAfter=ne(o.value)?o.value:"",this.commit()}),s.append(o),s}}const Ae="golem15-seo-robotstester",K=["Googlebot","Googlebot-Image","Bingbot","*"];function ze(i,e){const t={agent:e.agent,line:e.line,rule:e.rule};return e.allowed?e.line>0?y(i.allowedRule,t):y(i.allowedNone,t):e.bot?y(i.blockedBot,t):y(i.blocked,t)}class Se extends HTMLElement{static observedAttributes=["busy","state","ui-locale","disabled","label","busy-label"];context=null;result=null;error="";actions=new J(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 N(_(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=_(this)+"|"+(this.context?.agents??K).join(",");if(!e&&this.builtFor===t&&this.input){this.syncControls();return}this.builtFor=t;const s=this.labels(),o=this.input?.value??"",r=this.select?.value??"Googlebot";this.replaceChildren();const a=n("div","seo-robotstester__row"),c=S("seo-robotstester-url"),l=n("div","seo-robotstester__field seo-robotstester__field--url"),d=n("label","seo-robotstester__label",s.url);d.htmlFor=c;const p=n("input","seo-robotstester__input");p.id=c,p.type="text",p.placeholder=s.urlPlaceholder,p.value=o,p.addEventListener("keydown",w=>{w.key==="Enter"&&(w.preventDefault(),this.test())}),l.append(d,p);const h=S("seo-robotstester-agent"),m=n("div","seo-robotstester__field seo-robotstester__field--agent"),b=n("label","seo-robotstester__label",s.agent);b.htmlFor=h;const u=n("select","seo-robotstester__select");u.id=h;for(const w of this.context?.agents??K){const k=n("option","",w);k.value=w,u.append(k)}u.value=r,u.value!==r&&(u.value="Googlebot"),m.append(b,u);const f=n("button","seo-robotstester__button");f.type="button",f.addEventListener("click",()=>this.test()),a.append(l,m,f);const g=n("div","seo-robotstester__result");g.setAttribute("role","status");const A=n("div","seo-robotstester__notes");this.input=p,this.select=u,this.button=f,this.line=g,this.notes=A,this.append(a,g,A),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(n("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(x(s?"circle-check":"triangle-alert"),n("span","",ze(e,this.result)))}else this.line.replaceChildren();const t=[];(this.context?.staging||P(this,"data",{}).staging)&&t.push(n("p","seo-robotstester__note",e.stagingNote)),t.push(n("p","seo-robotstester__note",e.unsaved)),this.notes.replaceChildren(...t)}}const Te="golem15-seo-robotstxt",Ce=500*1024,U=10,Le=["User-agent","Allow","Disallow","Sitemap","Crawl-delay","Host","Clean-param"];function Ne(i,e){const t=Array.from({length:e.length+1},(s,o)=>o);for(let s=1;s<=i.length;s++){let o=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,o+(i[s-1]===e[r-1]?0:1)),o=a}}return t[e.length]}function Ee(i){const e=i.toLowerCase();let t="",s=3;for(const o of Le){const r=Ne(e,o.toLowerCase());r{const r=o+1,a=s.indexOf("#"),c=(a>=0?s.slice(0,a):s).trim();if(c==="")return;const l=c.indexOf(":");if(l<0){e.push({kind:"noColon",line:r});return}const d=c.slice(0,l).trim(),p=c.slice(l+1).trim();switch(d.toLowerCase()){case"user-agent":t=!0;return;case"allow":case"disallow":t?p!==""&&!p.startsWith("/")&&!p.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 h=Ee(d);e.push(h?{kind:"unknownSuggest",line:r,word:d,suggestion:h}:{kind:"unknown",line:r,word:d})}),e}function je(i){return new TextEncoder().encode(i).length}function Re(i,e){return y(i[e.kind],{line:e.line,word:e.word??"",suggestion:e.suggestion??""})}class De 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 N(_(this))}build(){const e=_(this);if(this.builtFor===e&&this.control){this.sync();return}this.builtFor=e,this.replaceChildren();const t=S("seo-robotstxt-lint"),s=n("textarea","seo-robotstxt__control");s.spellcheck=!1,s.setAttribute("wrap","off"),s.value=this.current,s.setAttribute("aria-describedby",t);const o=this.getAttribute("field-name");o&&s.setAttribute("aria-label",o),s.addEventListener("input",()=>{this.hasAttribute("disabled")||(this.current=s.value,this.update(),L(this,"summer-change",{value:this.current}))});const r=n("div","seo-robotstxt__lint");r.id=t,r.setAttribute("role","status");const a=n("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=_(this),s=Pe(this.current),o=[];if(s.length===0){const l=n("div","seo-robotstxt__finding seo-robotstxt__finding--clean");l.append(x("circle-check"),n("span","",e.clean)),o.push(l)}else{for(const l of s.slice(0,U)){const d=n("div","seo-robotstxt__finding");d.append(x("triangle-alert"),n("span","",Re(e,l))),o.push(d)}s.length>U&&o.push(n("div","seo-robotstxt__more",y(e.moreFindings,{count:C(s.length-U,t)})))}this.panel.replaceChildren(...o);const r=je(this.current),a=new Intl.NumberFormat(t,{maximumFractionDigits:1}).format(r/1024)+" KB";this.meter.replaceChildren(n("span","",y(e.size,{size:a})));const c=r>Ce;this.classList.toggle("seo-robotstxt--over",c),c&&this.meter.append(n("span","seo-robotstxt__over",e.tooLarge))}}const yt="golem15-seo-schema",Oe=16*1024;function Z(i){switch(i){case"article":return["BlogPosting","Article","NewsArticle"];case"collection":return["CollectionPage"];case"profile":return["ProfilePage"]}return["WebPage","AboutPage","ContactPage","Service"]}function Ie(i){const e={type:"",fields:{},custom:""};if(typeof i!="string"||i.trim()==="")return e;try{const t=JSON.parse(i);e.type=typeof t.type=="string"?t.type:"",e.fields=t.fields&&typeof t.fields=="object"?{...t.fields}:{},e.custom=typeof t.custom=="string"?t.custom:""}catch{}return e}function Fe(i){const e={};for(const s of["serviceType","areaServed","price","currency"]){const o=(i.fields[s]??"").trim();o!==""&&(e[s]=o)}const t=i.custom.trim();return i.type===""&&t===""&&Object.keys(e).length===0?"":JSON.stringify({type:i.type,fields:e,custom:t})}function Me(i){return i.trim()===""||/^\d+([.,]\d{1,2})?$/.test(i.trim())}function $(i){return i.trim()===""||/^[A-Za-z]{3}$/.test(i.trim())}function X(i,e){const s=i.slice(0,Math.max(0,Math.min(e,i.length))).split(` +`);return{line:s.length,column:s[s.length-1].length+1}}function Y(i){if(i.trim()==="")return{kind:"empty"};if(new TextEncoder().encode(i.trim()).length>Oe)return{kind:"tooLarge"};let e;try{e=JSON.parse(i)}catch(s){const o=s instanceof Error?s.message:String(s),r=/line (\d+) column (\d+)/i.exec(o);if(r)return{kind:"error",line:Number(r[1]),column:Number(r[2]),message:o};const a=/position (\d+)/i.exec(o),c=a?X(i,Number(a[1])):X(i,i.length);return{kind:"error",line:c.line,column:c.column,message:o}}const t=s=>s!==null&&typeof s=="object"&&!Array.isArray(s);return t(e)?{kind:"valid",nodes:1}:Array.isArray(e)&&e.length>0&&e.every(t)?{kind:"valid",nodes:e.length}:{kind:"notObject"}}const Q=(i,e)=>i["t"+e]??e,Ue=(i,e)=>{switch(e){case"BlogPosting":case"Article":case"NewsArticle":return i.noteArticle;case"ContactPage":return i.noteContact;case"Service":return i.noteService;case"CollectionPage":return i.noteCollection;case"ProfilePage":return i.noteProfile}return i.notePage};class He extends HTMLElement{static observedAttributes=["options","disabled","invalid","ui-locale"];current={type:"",fields:{},custom:""};open=!1;status={kind:"empty"};timer;fieldErrors={};connected=!1;get value(){return Fe(this.current)}set value(e){this.current=Ie(e),this.status=Y(this.current.custom),this.open=this.current.custom.trim()!=="",this.render()}connectedCallback(){this.connected=!0,this.render()}disconnectedCallback(){this.connected=!1,clearTimeout(this.timer)}attributeChangedCallback(){this.connected&&this.render()}kind(){const e=P(this,"options",{});return typeof e.kind=="string"?e.kind:""}labels(){return N(_(this))}changed(){L(this,"summer-change",{value:this.value})}effectiveType(){const e=Z(this.kind());return e.includes(this.current.type)?this.current.type:e[0]}render(){if(!this.connected)return;const e=this.labels(),t=this.hasAttribute("disabled"),s=Z(this.kind()),o=[],r=n("div","seo-schema__section"),a=S("seo-schema-type"),c=n("label","seo-schema__label",e.schemaType);if(c.htmlFor=a,s.length>1){const m=n("select","seo-schema__select");m.id=a,m.disabled=t;const b=n("option","",y(e.typeDefault,{type:s[0]}));b.value="",m.append(b);for(const u of s){const f=n("option","",Q(e,u));f.value=u,m.append(f)}m.value=s.includes(this.current.type)?this.current.type:"",m.addEventListener("change",()=>{this.current.type=m.value,this.changed(),this.render()}),r.append(c,m)}else{const m=n("div","seo-schema__fixed",Q(e,s[0]));m.id=a,r.append(c,m)}if(r.append(n("p","seo-schema__note",Ue(e,this.effectiveType()))),o.push(r),this.effectiveType()==="Service"){const m=n("div","seo-schema__grid"),b=(u,f,g,A="",w="")=>{const k=n("div","seo-schema__cell"),v=S("seo-schema-"+u),T=n("label","seo-schema__label",f);T.htmlFor=v;const z=n("input","seo-schema__input");z.id=v,z.type="text",z.disabled=t,z.value=this.current.fields[u]??"",A&&(z.placeholder=A),w&&(z.inputMode=w),u==="currency"&&(z.maxLength=3);const R=n("p","seo-schema__error",this.fieldErrors[u]??"");R.setAttribute("role","status"),z.addEventListener("input",()=>{const F=z.value;(u==="price"?Me(F):u==="currency"?$(F):!0)?(this.current.fields[u]=F,delete this.fieldErrors[u],R.textContent="",this.changed()):(this.fieldErrors[u]=u==="price"?e.priceInvalid:e.currencyInvalid,R.textContent=this.fieldErrors[u])}),u==="currency"&&z.addEventListener("blur",()=>{z.value=z.value.toUpperCase(),$(z.value)&&(this.current.fields.currency=z.value,this.changed())}),k.append(T,z),g&&k.append(n("p","seo-schema__note",g)),k.append(R),m.append(k)};b("serviceType",e.serviceType,"",e.serviceTypePh),b("areaServed",e.areaServed,e.areaServedHint),b("price",e.price,e.priceHint,"","decimal"),b("currency",e.currency,e.currencyHint),o.push(m)}const l=n("div","seo-schema__section"),d=n("button","seo-schema__disclosure");d.type="button",d.setAttribute("aria-expanded",String(this.open));const p=S("seo-schema-custom");d.setAttribute("aria-controls",p),d.append(n("span","",e.custom),x("chevron-down")),d.addEventListener("click",()=>{this.open=!this.open,this.render()});const h=n("div","seo-schema__disclosure-row");if(h.append(d),this.current.custom.trim()!==""&&h.append(n("span","seo-schema__note",e.customSet)),l.append(h),this.open){const m=n("div","seo-schema__panel");m.id=p;const b=S("seo-schema-json"),u=n("p","seo-schema__note",e.customHint);u.id=b+"-hint";const f=n("textarea","seo-schema__json");f.id=b,f.spellcheck=!1,f.disabled=t,f.value=this.current.custom,f.setAttribute("aria-label",e.custom),f.setAttribute("aria-describedby",u.id);const g=n("p","seo-schema__status");g.setAttribute("role","status"),g.setAttribute("aria-live","polite"),this.statusLine(g,e),f.addEventListener("input",()=>{this.current.custom=f.value,this.changed(),clearTimeout(this.timer),this.timer=setTimeout(()=>{this.status=Y(f.value),this.statusLine(g,e)},300)}),m.append(u,f,g),l.append(m)}o.push(l),this.classList.toggle("seo-schema--invalid",this.hasAttribute("invalid")),this.replaceChildren(...o)}statusLine(e,t){const s=this.status;switch(e.className="seo-schema__status",s.kind){case"empty":e.replaceChildren();return;case"valid":e.classList.add("seo-schema__status--ok"),e.replaceChildren(x("circle-check"),n("span","",I(t.jsonValid,s.nodes,_(this))));return;case"error":e.classList.add("seo-schema__status--error"),e.replaceChildren(x("circle-alert"),n("span","",y(t.jsonError,{line:s.line,column:s.column,message:s.message})));return;case"notObject":e.classList.add("seo-schema__status--error"),e.replaceChildren(x("circle-alert"),n("span","",t.jsonNotObject));return;case"tooLarge":e.classList.add("seo-schema__status--error"),e.replaceChildren(x("circle-alert"),n("span","",t.jsonTooLarge))}}}const oe="20px Arial, sans-serif",re="14px Arial, sans-serif",ae=580,le=920,Be=680,We=30,Ge=120;let j;function Je(){if(j!==void 0)return j;try{const e=document.createElement("canvas").getContext("2d");j=e&&typeof e.measureText=="function"?e:null}catch{j=null}return j}function qe(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 W(i,e){const t=Je();if(t){t.font=e;const o=t.measureText(i).width;if(Number.isFinite(o)&&(o>0||i===""))return o}const s=Number.parseFloat(e)||14;return qe(i,s)}const H=" …";function ee(i,e,t){if(W(i,e)<=t)return i;const s=h=>W(h+H,e)<=t,o=Array.from(i),r=[];for(let h=1;h>1;s(o.slice(0,r[h]).join(""))?(a=h,c=h+1):l=h-1}if(a>=0)return o.slice(0,r[a]).join("")+H;let d=0,p=o.length;for(;ds?l="long":a=o}function D(i,e){const t=te(i);if(t.length===0)return!1;const s=te(e);for(let o=0;o+t.length<=s.length;o++){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 o=this.actions.settle();o?this.failed(o):(this.checking=!1,this.render());return}if(e==="fill-values"){this.render(),this.scheduleDuplicates(!1);return}this.render()}}labels(){return N(_(this))}request(e){this.actions.request(e)}failed(e){e.op==="context"?this.contextFailed=!0:this.lastChecked="",this.checking=!1,this.render()}fill(){return P(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(),o=E(s,"seo_title",t.locale)||E(s,"title",t.locale),r=E(s,"seo_description",t.locale)||E(s,"description",t.locale),a=JSON.stringify([t.locale,o,r]);if(a===this.lastChecked)return;clearTimeout(this.timer);const c=()=>{this.lastChecked=a,this.checking=!0,this.render(),this.request({op:"duplicates",locale:t.locale,title:o,description:r})};e?c():this.timer=setTimeout(c,$e)}render(){const e=this.labels(),t=_(this);if(!this.context&&!this.contextFailed){this.setAttribute("aria-busy","true"),this.replaceChildren(n("div","seo-serp__skeleton"));return}this.removeAttribute("aria-busy");const s=this.locale(),o=s?.locale??this.shownLocale,r=this.fill(),a=(E(r,"seo_title",o)||E(r,"title",o)).trim(),c=(E(r,"seo_description",o)||E(r,"description",o)).trim(),l=E(r,"seo_keyphrase",o).trim(),d=s?.siteName??"",p=a||Ye(s?.titleTemplate??"",{title:s?.defaultTitle??"",site:d})||s?.defaultTitle||"",h=c||s?.defaultDescription||"",m=n("div","seo-serp__header"),b=n("div","seo-serp__track");b.setAttribute("role","radiogroup"),b.setAttribute("aria-label",e.device);for(const v of["desktop","mobile"]){const T=n("button","seo-serp__segment",e[v]);T.type="button",T.setAttribute("role","radio"),T.setAttribute("aria-checked",this.device===v?"true":"false"),T.addEventListener("click",()=>{this.device=v,this.render()}),b.append(T)}m.append(b),o&&m.append(n("span","seo-serp__language",y(e.language,{locale:o.toUpperCase()})));const u=n("div","serp-card"+(this.device==="mobile"?" serp-card--mobile":"")),f=n("div","serp-card__site");f.append(n("span","serp-card__favicon",Array.from(d||this.context?.host||"?")[0]?.toUpperCase()??"?"));const g=n("div","serp-card__names");if(g.append(n("span","serp-card__sitename",d||this.context?.host||"")),g.append(n("span","serp-card__crumbs",st(this.context?.host??"",s?.url??null))),f.append(g),u.append(f),u.append(n("div","serp-card__title",ee(p,oe,ae))),h){const v=this.device==="mobile"?Be:le;u.append(n("div","serp-card__description",ee(h,re,v)))}else u.append(n("div","serp-card__description serp-card__description--missing",e.noDescription));const A=n("div","seo-serp__notes");if(this.contextFailed){const v=n("div","seo-serp__error");v.setAttribute("role","status"),v.append(x("circle-alert"),n("span","",e.contextFailed)),A.append(v)}!a&&s?.titleTemplate&&A.append(n("div","",y(e.fromTemplate,{template:s.titleTemplate}))),!c&&s?.defaultDescription&&s.descriptionSource==="content"&&A.append(n("div","",e.fromContent)),s&&s.url===null&&A.append(n("div","",e.noUrl)),(this.getAttribute("record-id")??"")!==""&&s&&A.append(n("div","",e.savedNote));const w=n("div","seo-serp__checks");if(w.setAttribute("role","status"),this.checking)w.append(n("div","seo-serp__checking",e.checking));else{const v=this.duplicates.get(o);if(v)for(const[T,z]of[[v.title,e.dupTitle],[v.description,e.dupDescription]])T.length>0&&w.append(it(T,z,e,t))}const k=[m,u,A,w];l&&s&&k.push(nt(l,{title:p,description:h,h1:s.h1,url:s.url,bodyWords:s.bodyWords},e,t)),this.replaceChildren(...k)}}function st(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 it(i,e,t,s){const o=n("div","seo-serp__check seo-serp__check--warn");o.append(x("triangle-alert"));const r=n("span"),a="\0list\0",c=I(e,i.length,s,{list:a}),[l,d]=c.split(a);return r.append(document.createTextNode(l??"")),i.slice(0,B).forEach((p,h)=>{if(h>0&&r.append(document.createTextNode(", ")),p.url.startsWith("/")&&!p.url.startsWith("//")){const m=n("a","seo-serp__link",p.label);m.href=p.url,m.target="_blank",m.rel="noopener",r.append(m)}else r.append(document.createTextNode(p.label))}),i.length>B&&r.append(document.createTextNode(" "+y(t.andMore,{count:i.length-B}))),r.append(document.createTextNode(d??"")),o.append(r),o}function nt(i,e,t,s){const o=n("div","seo-serp__keyphrase");o.append(n("div","seo-serp__keyphrase-title",y(t.keyphrase,{phrase:i})));const r=(a,c,l)=>{const d=n("div","seo-serp__check "+(a?"seo-serp__check--ok":"seo-serp__check--warn"));d.append(x(a?"circle-check":"triangle-alert"),n("span","",a?c:l)),o.append(d)};if(r(D(i,e.title),t.kpTitle,t.kpTitleNo),e.h1!==null&&r(D(i,e.h1),t.kpH1,t.kpH1No),r(D(i,e.description),t.kpDesc,t.kpDescNo),e.url!==null&&r(D(i,e.url.replace(/[-_/]+/g," ")),t.kpUrl,t.kpUrlNo),e.bodyWords!==null){const a=new Intl.NumberFormat(s).format(e.bodyWords);r(e.bodyWords>=Xe,y(t.kpLength,{count:a}),y(t.kpLengthNo,{count:a}))}return o}const ot="golem15-seo-text",rt={empty:"info",short:"triangle-alert",long:"triangle-alert",good:"circle-check"};class at 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 P(this,"options",{}).measure==="description"?"description":"title"}labels(){return N(_(this))}build(){const e=this.measure()+"|"+_(this);if(this.builtFor===e&&this.control){this.sync();return}this.builtFor=e,this.replaceChildren();const t=S("seo-text-counter"),o=this.measure()==="description"?n("textarea","seo-text__control seo-text__control--area"):n("input","seo-text__control");o instanceof HTMLTextAreaElement?o.rows=3:o.type="text",o.value=this.current,o.setAttribute("aria-describedby",t);const r=this.getAttribute("field-name");r&&o.setAttribute("aria-label",r),o.addEventListener("input",()=>{this.hasAttribute("disabled")||(this.current=o.value.replace(/\s+$/u,""),this.update(!0),L(this,"summer-change",{value:this.current}))});const a=n("div","seo-text__counter");a.id=t;const c=n("span","seo-text__status");this.statusIcon=n("span","seo-text__icon"),this.statusText=n("span","seo-text__status-text"),c.append(this.statusIcon,this.statusText),this.countText=n("span","seo-text__count"),a.append(c,this.countText),this.live=n("span","seo-visually-hidden"),this.live.setAttribute("aria-live","polite"),this.live.setAttribute("role","status"),this.control=o,this.lastStatus=null,this.append(o,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=_(this),o=this.measure(),r=Ke(this.current,o),a=lt(t,r.status,o);this.statusIcon.replaceChildren(x(rt[r.status])),this.statusText.textContent=a,this.dataset.status=r.status,this.countText.textContent=y(t.count,{chars:C(r.chars,s),px:C(r.px,s),max:C(r.maxPx,s)}),e&&this.live&&r.status!==this.lastStatus&&(this.live.textContent=a),this.lastStatus=r.status}}function lt(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 ct="golem15-seo-transfer",se=60*1024,G=200;function dt(i){return i.summary.refused>0?"refused":i.count>0?"ready":"nothing"}function ut(i){const e=i.filter(s=>s.change==="refused"),t=i.filter(s=>s.change!=="refused");return[...e,...t].slice(0,G)}function ht(i,e){const t=new Blob([e],{type:"text/csv;charset=utf-8"}),s=URL.createObjectURL(t),o=document.createElement("a");o.href=s,o.download=i,o.rel="noopener",document.body.append(o),o.click(),o.remove(),setTimeout(()=>URL.revokeObjectURL(s),0)}function pt(){return window.location.pathname.replace(/\/transfer(\/.*)?$/,"/history")}class mt 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="",ht(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 N(_(this))}request(e){this.hasAttribute("disabled")||this.hasAttribute("busy")||this.pending||(this.pending=e.op,L(this,"summer-action",{payload:e}),this.render())}busyLabel(e,t){return this.pending===e&&this.getAttribute("busy-label")||t}datasetSelect(e,t,s){const o=n("div","seo-transfer__field"),r=S("seo-transfer-dataset"),a=n("label","seo-transfer__label",e.dataset);a.htmlFor=r;const c=n("select","seo-transfer__select");c.id=r;const l=[["redirects",e.dsRedirects],["rules",e.dsRules],["records",e.dsRecords]];for(const[d,p]of l){const h=n("option","",p);h.value=d,c.append(h)}return c.value=t,c.disabled=this.hasAttribute("disabled"),c.addEventListener("change",()=>s(c.value)),o.append(a,c),o}button(e,t,s){const o=n("button","seo-transfer__button seo-transfer__button--"+t,e);return o.type="button",o.addEventListener("click",s),o}message(e,t){const s=n("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>se){this.fileError=y(this.labels().fileTooLarge,{limit:C(se/1024,_(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=_(this),s=this.hasAttribute("disabled"),o=this.hasAttribute("busy")||this.pending!==null,r=this.getAttribute("label")||e.chooseFile,a=n("section","seo-transfer__section");a.append(n("h3","seo-transfer__heading",e.exportHeading));const c=n("div","seo-transfer__row");c.append(this.datasetSelect(e,this.exportDataset,f=>this.exportDataset=f));const l=this.button(this.busyLabel("export",e.download),"outline",()=>this.request({op:"export",dataset:this.exportDataset}));l.disabled=o,l.hidden=s,c.append(l),a.append(c),this.exportError&&a.append(this.message(this.exportError,"seo-transfer__error"));const d=n("section","seo-transfer__section seo-transfer__section--import");d.append(n("h3","seo-transfer__heading",e.importHeading));const p=n("div","seo-transfer__row");p.append(this.datasetSelect(e,this.importDataset,f=>{this.importDataset=f,this.preview=null,this.render()}));const h=n("input","seo-transfer__file");h.type="file",h.accept=".csv,text/csv",h.hidden=!0,h.addEventListener("change",()=>{this.chooseFile(h)});const m=this.button(e.chooseFile,"outline",()=>h.click());m.hidden=s,m.disabled=o;const b=n("span",this.fileName?"seo-transfer__filename":"seo-transfer__filename seo-transfer__filename--none",this.fileName||e.noFile),u=this.button(this.busyLabel("preview",r),"outline",()=>this.request({op:"preview",dataset:this.importDataset,csv:this.csv}));if(u.disabled=o||this.csv===""||this.fileError!=="",u.hidden=s,p.append(h,m,b,u),d.append(p),(this.fileError||this.importError)&&d.append(this.message(this.fileError||this.importError,"seo-transfer__error")),this.applied){const f=n("p","seo-transfer__done"),g=n("a","seo-transfer__link",e.openHistory);g.href=pt(),f.append(g),d.append(f)}this.preview&&d.append(this.renderPreview(e,t,o,s)),this.replaceChildren(a,d)}renderPreview(e,t,s,o){const r=this.preview,a=n("div","seo-transfer__preview"),c=r.summary;a.append(this.message(y(e.summary,{new:C(c.new,t),changed:C(c.changed,t),unchanged:C(c.unchanged,t),refused:C(c.refused,t)}),"seo-transfer__summary"));const l=n("div","summer-table-wrap seo-transfer__table-wrap"),d=n("table","summer-table seo-transfer__table"),p=n("tr");for(const w of[e.colLine,e.colChange,e.colAddress,e.colField,e.colBefore,e.colAfter,e.colNote])p.append(n("th","",w));const h=n("thead");h.append(p);const m=n("tbody"),b={new:e.bNew,changed:e.bChanged,unchanged:e.bUnchanged,refused:e.bRefused};for(const w of ut(r.rows)){const k=n("tr","seo-transfer__tr--"+w.change);k.append(n("td","seo-transfer__num",C(w.line,t)));const v=n("td");v.append(n("span","seo-transfer__badge seo-transfer__badge--"+w.change,b[w.change])),w.warning&&v.append(n("span","seo-transfer__badge seo-transfer__badge--warning",e.bWarning)),k.append(v),k.append(n("td","seo-transfer__mono",w.address)),k.append(n("td","",w.field)),k.append(n("td","",w.before)),k.append(n("td","",w.after)),k.append(n("td","",w.note)),m.append(k)}d.append(h,m),l.append(d),a.append(l),(r.total>G||r.rows.length>G)&&a.append(n("p","seo-transfer__note",y(e.rowCap,{count:C(Math.max(r.total,r.rows.length),t)})));const u=dt(r);u==="refused"?a.append(this.message(e.refusedBlocks,"seo-transfer__error")):u==="nothing"&&a.append(this.message(e.nothing,"seo-transfer__note")),this.applyError&&a.append(this.message(this.applyError,"seo-transfer__error"));const f=n("div","seo-transfer__actions"),g=this.button(e.discard,"ghost",()=>{this.preview=null,this.applyError="",this.render()});g.disabled=s;const A=this.button(this.busyLabel("apply",I(e.applyCount,r.count,t)),"primary",()=>this.request({op:"apply",token:r.token}));return A.disabled=s||u!=="ready",f.append(g,A),f.hidden=o,a.append(f),a}}const ft=[[ot,at],[ve,xe],[Ze,tt],[fe,be],[Te,De],[Ae,Se],[ct,mt],["golem15-seo-schema",He],["golem15-seo-graph",me],["golem15-seo-parent",ye]];for(const[i,e]of ft)customElements.get(i)||customElements.define(i,e);export{bt as GRAPH_TAG,fe as IMAGE_TAG,wt as PARENT_TAG,Ae as ROBOTSTESTER_TAG,Te as ROBOTSTXT_TAG,ve as ROBOTS_TAG,yt as SCHEMA_TAG,Ze as SERP_TAG,me as SeoGraphElement,be as SeoImageElement,ye as SeoParentElement,xe as SeoRobotsElement,Se as SeoRobotsTesterElement,De as SeoRobotsTxtElement,He as SeoSchemaElement,tt as SeoSerpElement,at as SeoTextElement,mt as SeoTransferElement,ot as TEXT_TAG,ct as TRANSFER_TAG}; diff --git a/assets/src/elements/graph.ts b/assets/src/elements/graph.ts new file mode 100644 index 0000000..2880014 --- /dev/null +++ b/assets/src/elements/graph.ts @@ -0,0 +1,240 @@ +import { ActionQueue } from './actions' +import { el, fmt, icon, jsonAttr, labelsFor, uiLocaleOf, type Labels } from './labels' + +export const GRAPH_TAG = 'golem15-seo-graph' + +/** One missing property of a node. */ +export interface GraphFinding { + level: 'required' | 'recommended' + property: string +} + +/** A node of the graph with its findings. */ +export interface GraphNode { + id: string + type: string + findings: GraphFinding[] + rules: boolean +} + +/** The seo_graph action's answer (UI-SPEC C18). */ +export interface GraphData { + locale: string + url: string | null + graph: Record | null + nodes: GraphNode[] +} + +interface Payload { + op: 'graph' + locale: string +} + +/** The Rich Results Test address of a page. */ +export function richResultsURL(url: string): string { + return 'https://search.google.com/test/rich-results?url=' + encodeURIComponent(url) +} + +/** The fragment of an @id (#article), the whole id when it has none. */ +function fragment(id: string): string { + const i = id.indexOf('#') + return i >= 0 ? id.slice(i) : id +} + +/** + * E9 golem15-seo-graph (UI-SPEC C18, D-46): the effective @graph of the + * saved page with the unsaved seo_schema value, every node with Google's + * required and recommended findings, the JSON and the Rich Results Test + * link. It asks through summer-action only ({op: graph, locale}), on + * connect, 800 ms after the last fill change and on the ML locale switch. + */ +export class SeoGraphElement extends HTMLElement { + static observedAttributes = ['fill-values', 'busy', 'state', 'ui-locale', 'disabled', 'label'] + + private data: GraphData | null = null + private failed = false + private pending = false + private showJSON = false + private shownLocale = '' + private lastFill = '' + private timer: ReturnType | undefined + private connected = false + private readonly actions = new ActionQueue(this, () => this.fail()) + + private readonly onResult = (ev: Event): void => { + const detail = (ev as CustomEvent).detail as { data?: unknown } | undefined + this.actions.settle() + const data = detail?.data as GraphData | undefined + if (data && Array.isArray(data.nodes)) { + this.data = data + this.failed = false + } + this.pending = this.actions.current !== null + this.render() + } + + private readonly onLocale = (ev: Event): void => { + const locale = (ev as CustomEvent).detail + if (typeof locale === 'string' && locale !== '' && locale !== this.shownLocale) { + this.shownLocale = locale + this.check() + } + } + + connectedCallback(): void { + this.connected = true + this.lastFill = this.getAttribute('fill-values') ?? '' + this.addEventListener('summer-result', this.onResult) + window.addEventListener('summer:ml-locale', this.onLocale) + this.render() + queueMicrotask(() => { + if (this.connected) { + this.check() + } + }) + } + + disconnectedCallback(): void { + this.connected = false + this.removeEventListener('summer-result', this.onResult) + window.removeEventListener('summer:ml-locale', this.onLocale) + clearTimeout(this.timer) + this.actions.reset() + this.pending = false + } + + attributeChangedCallback(name: string, _old: string | null, value: string | null): void { + if (!this.connected) { + return + } + if (name === 'busy' && value === null) { + this.actions.flush() + return + } + if (name === 'state' && value === 'error') { + if (this.actions.settle()) { + this.fail() + } + return + } + if (name === 'fill-values') { + const fill = value ?? '' + if (fill !== this.lastFill) { + this.lastFill = fill + clearTimeout(this.timer) + this.timer = setTimeout(() => this.check(), 800) + } + return + } + this.render() + } + + private labels(): Labels { + return labelsFor(uiLocaleOf(this)) + } + + /** Asks for the graph of the shown locale (one queued request at most). */ + check(): void { + clearTimeout(this.timer) + this.pending = true + this.actions.request({ op: 'graph', locale: this.shownLocale }) + this.render() + } + + private fail(): void { + this.failed = true + this.pending = this.actions.current !== null + this.render() + } + + private render(): void { + if (!this.connected) { + return + } + const labels = this.labels() + const sections: HTMLElement[] = [] + this.setAttribute('aria-busy', String(this.pending)) + const data = this.data + + const header = el('p', 'seo-graph__meta') + if (data) { + header.textContent = data.url ? fmt(labels.forLocale, { locale: (data.locale || '').toUpperCase(), url: data.url }) : labels.graphNoUrl + } + if (this.pending) { + const line = el('p', 'seo-graph__meta seo-graph__checking', labels.graphChecking) + line.setAttribute('role', 'status') + sections.push(line) + } + if (data) { + sections.push(header) + const list = el('div', 'seo-graph__nodes') + let problems = 0 + for (const node of data.nodes) { + const row = el('div', 'seo-graph__node') + const head = el('div', 'seo-graph__node-head') + head.append(el('span', 'seo-graph__pill', node.type || '—'), el('code', 'seo-graph__id', fragment(node.id || ''))) + row.append(head) + if (!node.rules) { + row.append(el('p', 'seo-graph__meta', fmt(labels.noRules, { type: node.type }))) + } + for (const f of node.findings ?? []) { + problems++ + const required = f.level === 'required' + const line = el('p', 'seo-graph__finding seo-graph__finding--' + (required ? 'required' : 'recommended')) + line.append(icon(required ? 'circle-alert' : 'triangle-alert'), el('span', '', fmt(required ? labels.required : labels.recommended, { property: f.property, type: node.type }))) + row.append(line) + } + list.append(row) + } + sections.push(list) + if (problems === 0 && data.nodes.length > 0) { + const ok = el('p', 'seo-graph__ok') + ok.append(icon('circle-check'), el('span', '', labels.allGood)) + sections.push(ok) + } + if (data.graph) { + const toggle = el('button', 'seo-graph__disclosure') + toggle.type = 'button' + toggle.setAttribute('aria-expanded', String(this.showJSON)) + toggle.append(el('span', '', this.showJSON ? labels.hideJson : labels.showJson), icon('chevron-down')) + toggle.addEventListener('click', () => { + this.showJSON = !this.showJSON + this.render() + }) + sections.push(toggle) + if (this.showJSON) { + sections.push(el('pre', 'seo-graph__json', JSON.stringify(data.graph, null, 2))) + } + } + const notes = el('div', 'seo-graph__links') + if (data.url) { + const link = el('a', 'seo-graph__link') + link.href = richResultsURL(data.url) + link.target = '_blank' + link.rel = 'noopener' + link.append(el('span', '', labels.richResults), icon('external-link')) + notes.append(link) + } + notes.append(el('p', 'seo-graph__meta', labels.graphUnsaved), el('p', 'seo-graph__meta', labels.layerNodes)) + sections.push(notes) + } + if (this.failed) { + const line = el('p', 'seo-graph__error') + line.setAttribute('role', 'status') + line.append(icon('circle-alert'), el('span', '', labels.graphFailed)) + sections.push(line) + } + if (!this.hasAttribute('disabled')) { + const button = el('button', 'seo-graph__button', this.getAttribute('label') || labels.graphAction) + button.type = 'button' + button.addEventListener('click', () => this.check()) + sections.push(button) + } + this.replaceChildren(...sections) + } + + /** The fill snapshot (seo_schema), for tests. */ + fill(): Record { + return jsonAttr>(this, 'fill-values', {}) + } +} diff --git a/assets/src/elements/index.ts b/assets/src/elements/index.ts index b565f4e..dc90c12 100644 --- a/assets/src/elements/index.ts +++ b/assets/src/elements/index.ts @@ -1,8 +1,11 @@ import './styles.css' +import { GRAPH_TAG, SeoGraphElement } from './graph' import { IMAGE_TAG, SeoImageElement } from './image' +import { PARENT_TAG, SeoParentElement } from './parent' import { ROBOTS_TAG, SeoRobotsElement } from './robots' import { ROBOTSTESTER_TAG, SeoRobotsTesterElement } from './robotstester' import { ROBOTSTXT_TAG, SeoRobotsTxtElement } from './robotstxt' +import { SCHEMA_TAG, SeoSchemaElement } from './schema' import { SERP_TAG, SeoSerpElement } from './serp' import { SeoTextElement, TEXT_TAG } from './text' import { SeoTransferElement, TRANSFER_TAG } from './transfer' @@ -18,6 +21,9 @@ const elements: Array<[string, CustomElementConstructor]> = [ [ROBOTSTXT_TAG, SeoRobotsTxtElement], [ROBOTSTESTER_TAG, SeoRobotsTesterElement], [TRANSFER_TAG, SeoTransferElement], + ['golem15-seo-schema', SeoSchemaElement], + ['golem15-seo-graph', SeoGraphElement], + ['golem15-seo-parent', SeoParentElement], ] for (const [tag, ctor] of elements) { if (!customElements.get(tag)) { @@ -26,7 +32,13 @@ for (const [tag, ctor] of elements) { } export { + GRAPH_TAG, IMAGE_TAG, + PARENT_TAG, + SCHEMA_TAG, + SeoGraphElement, + SeoParentElement, + SeoSchemaElement, ROBOTS_TAG, ROBOTSTESTER_TAG, ROBOTSTXT_TAG, diff --git a/assets/src/elements/labels.ts b/assets/src/elements/labels.ts index 74ecaf7..3c20237 100644 --- a/assets/src/elements/labels.ts +++ b/assets/src/elements/labels.ts @@ -1,5 +1,5 @@ // Element copy of the SEO field elements (UI-SPEC copywriting E1 to E6, -// plus E7 for the social image), chosen by the ui-locale attribute. Plural +// E7 for the social image, E8 and E9 for structured data), chosen by the ui-locale attribute. Plural // texts are CLDR maps; placeholders use :name. /** A CLDR plural map: one/other in English, one/few/many/other in Polish. */ @@ -134,6 +134,58 @@ export interface Labels { readFailed: string openHistory: string applyCount: Plural + // E8 golem15-seo-schema + schemaType: string + typeDefault: string + tBlogPosting: string + tArticle: string + tNewsArticle: string + tWebPage: string + tAboutPage: string + tContactPage: string + tService: string + tCollectionPage: string + tProfilePage: string + noteArticle: string + notePage: string + noteContact: string + noteService: string + noteCollection: string + noteProfile: string + serviceType: string + serviceTypePh: string + areaServed: string + areaServedHint: string + price: string + priceHint: string + currency: string + currencyHint: string + priceInvalid: string + currencyInvalid: string + custom: string + customSet: string + customHint: string + jsonValid: Plural + jsonError: string + jsonNotObject: string + jsonTooLarge: string + // E9 golem15-seo-graph + forLocale: string + graphChecking: string + allGood: string + required: string + recommended: string + noRules: string + showJson: string + hideJson: string + richResults: string + graphNoUrl: string + graphUnsaved: string + layerNodes: string + graphAction: string + graphFailed: string + // golem15-seo-parent + primaryDefault: string } const en: Labels = { @@ -253,6 +305,55 @@ const en: Labels = { readFailed: 'The file could not be read.', openHistory: 'Open History', applyCount: { one: 'Apply :count change', other: 'Apply :count changes' }, + schemaType: 'Schema type', + typeDefault: 'Default (:type)', + tBlogPosting: 'BlogPosting (blog post)', + tArticle: 'Article (article)', + tNewsArticle: 'NewsArticle (news)', + tWebPage: 'WebPage (page)', + tAboutPage: 'AboutPage (about us)', + tContactPage: 'ContactPage (contact)', + tService: 'Service (service or offer)', + tCollectionPage: 'CollectionPage (listing)', + tProfilePage: 'ProfilePage (profile)', + noteArticle: 'Author, dates, image, word count and section come from the content.', + notePage: 'Name, description and breadcrumbs come from the SEO tab.', + noteContact: 'Address and opening hours come from the SEO settings (Publisher, Local business).', + noteService: 'The provider is the publisher from the SEO settings.', + noteCollection: 'Lists the items shown on the page.', + noteProfile: 'Name, image and profiles come from the author.', + serviceType: 'Kind of service', + serviceTypePh: 'e.g. Mushroom workshops', + areaServed: 'Area served', + areaServedHint: 'Cities or regions, separated by commas.', + price: 'Price', + priceHint: 'Leave empty when the price is set individually.', + currency: 'Currency', + currencyHint: 'ISO code, e.g. PLN.', + priceInvalid: 'Enter a price such as 150 or 149.99.', + currencyInvalid: 'Use a three-letter currency code, e.g. PLN.', + custom: 'Custom JSON-LD (advanced)', + customSet: 'Set', + customHint: 'An object or a list of objects. A node with the @id of a generated node is merged into it, and your values win; other nodes are added to the graph. Paste only the JSON, without a script tag.', + jsonValid: { one: 'Valid JSON: :count node', other: 'Valid JSON: :count nodes' }, + jsonError: 'Line :line, column :column: :message', + jsonNotObject: 'Use an object or a list of objects.', + jsonTooLarge: 'Over 16 KB: shorten the custom JSON-LD.', + forLocale: 'JSON-LD for :locale — :url', + graphChecking: 'Building the graph…', + allGood: 'No problems: every node has its required and recommended properties.', + required: 'Missing required property “:property” (:type)', + recommended: 'Missing recommended property “:property” (:type)', + noRules: ':type has no rich-result rules at Google; it is checked as valid schema.org only.', + showJson: 'Show JSON-LD', + hideJson: 'Hide JSON-LD', + richResults: "Check in Google's Rich Results Test", + graphNoUrl: 'The Rich Results Test link appears after the first save, when the page has an address.', + graphUnsaved: 'Shows the saved page with your unsaved structured data changes.', + layerNodes: 'The site may add nodes of its own, such as FAQPage. The site audit checks the rendered result.', + graphAction: 'Check structured data', + graphFailed: 'The structured data could not be built. Try again.', + primaryDefault: 'Default (first category)', } const pl: Labels = { @@ -382,6 +483,55 @@ const pl: Labels = { 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' }, + schemaType: 'Typ schema', + typeDefault: 'Domyślny (:type)', + tBlogPosting: 'BlogPosting (wpis na blogu)', + tArticle: 'Article (artykuł)', + tNewsArticle: 'NewsArticle (wiadomość)', + tWebPage: 'WebPage (strona)', + tAboutPage: 'AboutPage (o nas)', + tContactPage: 'ContactPage (kontakt)', + tService: 'Service (usługa lub oferta)', + tCollectionPage: 'CollectionPage (lista)', + tProfilePage: 'ProfilePage (profil)', + noteArticle: 'Autor, daty, obraz, liczba słów i sekcja pochodzą z treści.', + notePage: 'Nazwa, opis i ścieżka nawigacji pochodzą z zakładki SEO.', + noteContact: 'Adres i godziny otwarcia pochodzą z ustawień SEO (Wydawca, Firma lokalna).', + noteService: 'Usługodawcą jest wydawca z ustawień SEO.', + noteCollection: 'Wymienia elementy pokazane na stronie.', + noteProfile: 'Imię i nazwisko, zdjęcie i profile pochodzą od autora.', + serviceType: 'Rodzaj usługi', + serviceTypePh: 'np. warsztaty grzybowe', + areaServed: 'Obszar działania', + areaServedHint: 'Miasta lub regiony oddzielone przecinkami.', + price: 'Cena', + priceHint: 'Zostaw puste, gdy cena jest ustalana indywidualnie.', + currency: 'Waluta', + currencyHint: 'Kod ISO, np. PLN.', + priceInvalid: 'Wpisz cenę, np. 150 lub 149,99.', + currencyInvalid: 'Użyj trzyliterowego kodu waluty, np. PLN.', + custom: 'Własny JSON-LD (zaawansowane)', + customSet: 'Ustawiony', + customHint: 'Obiekt lub lista obiektów. Węzeł z @id wygenerowanego węzła zostaje z nim scalony, a Twoje wartości mają pierwszeństwo; pozostałe węzły są dodawane do grafu. Wklej sam JSON, bez znacznika script.', + jsonValid: { one: 'Poprawny JSON: :count węzeł', few: 'Poprawny JSON: :count węzły', many: 'Poprawny JSON: :count węzłów', other: 'Poprawny JSON: :count węzła' }, + jsonError: 'Wiersz :line, kolumna :column: :message', + jsonNotObject: 'Użyj obiektu lub listy obiektów.', + jsonTooLarge: 'Ponad 16 KB: skróć własny JSON-LD.', + forLocale: 'JSON-LD dla :locale — :url', + graphChecking: 'Budowanie grafu…', + allGood: 'Brak problemów: każdy węzeł ma wymagane i zalecane właściwości.', + required: 'Brak wymaganej właściwości „:property” (:type)', + recommended: 'Brak zalecanej właściwości „:property” (:type)', + noRules: ':type nie ma reguł wyników z elementami rozszerzonymi w Google; sprawdzana jest tylko poprawność schema.org.', + showJson: 'Pokaż JSON-LD', + hideJson: 'Ukryj JSON-LD', + richResults: 'Sprawdź w Teście wyników z elementami rozszerzonymi Google', + graphNoUrl: 'Link do testu pojawi się po pierwszym zapisie, gdy strona będzie miała adres.', + graphUnsaved: 'Pokazuje zapisaną stronę z niezapisanymi zmianami danych strukturalnych.', + layerNodes: 'Witryna może dodać własne węzły, np. FAQPage. Audyt witryny sprawdza wynik na stronie.', + graphAction: 'Sprawdź dane strukturalne', + graphFailed: 'Nie udało się zbudować danych strukturalnych. Spróbuj ponownie.', + primaryDefault: 'Domyślna (pierwsza kategoria)', } const dictionaries: Record = { en, pl } @@ -441,6 +591,7 @@ const ICONS: Record = { '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'], + 'external-link': ['M15 3h6v6', 'M10 14 21 3', 'M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6'], } /** An aria-hidden inline SVG icon of size px. */ diff --git a/assets/src/elements/parent.ts b/assets/src/elements/parent.ts new file mode 100644 index 0000000..51b96e0 --- /dev/null +++ b/assets/src/elements/parent.ts @@ -0,0 +1,97 @@ +import { el, emit, labelsFor, uid, uiLocaleOf } from './labels' + +export const PARENT_TAG = 'golem15-seo-parent' + +/** One parent the record can take as its primary category. */ +export interface ParentOption { + value: string + label: string +} + +/** The loaded value: the stored choice and the record's parents. */ +export interface ParentValue { + selected: string + options: ParentOption[] +} + +/** Reads the loaded value (an object) or a plain stored choice. */ +export function parseParent(v: unknown): ParentValue { + if (typeof v === 'string') { + return { selected: v, options: [] } + } + const out: ParentValue = { selected: '', options: [] } + if (v && typeof v === 'object') { + const raw = v as { selected?: unknown; options?: unknown } + out.selected = typeof raw.selected === 'string' ? raw.selected : '' + if (Array.isArray(raw.options)) { + for (const o of raw.options) { + if (o && typeof o === 'object' && typeof (o as ParentOption).value === 'string') { + const opt = o as ParentOption + out.options.push({ value: opt.value, label: typeof opt.label === 'string' && opt.label ? opt.label : opt.value }) + } + } + } + } + return out +} + +/** + * golem15-seo-parent (P33-23, D-86): the primary category of a record, a + * 44px select over the record's parents with the "first category" default. + * The value is the chosen :, "" for the default. A stored choice + * that is no longer one of the parents falls back to the default (P1 says + * so). + */ +export class SeoParentElement extends HTMLElement { + static observedAttributes = ['disabled', 'invalid', 'ui-locale'] + + private current: ParentValue = { selected: '', options: [] } + + get value(): string { + return this.current.options.some((o) => o.value === this.current.selected) ? this.current.selected : '' + } + + set value(v: unknown) { + const next = parseParent(v) + if (typeof v === 'string' && this.current.options.length > 0) { + next.options = this.current.options + } + this.current = next + this.render() + } + + connectedCallback(): void { + this.render() + } + + attributeChangedCallback(): void { + if (this.isConnected) { + this.render() + } + } + + private render(): void { + if (!this.isConnected) { + return + } + const labels = labelsFor(uiLocaleOf(this)) + const select = el('select', 'seo-parent__select') + select.id = uid('seo-parent') + select.disabled = this.hasAttribute('disabled') + const def = el('option', '', labels.primaryDefault) + def.value = '' + select.append(def) + for (const o of this.current.options) { + const opt = el('option', '', o.label) + opt.value = o.value + select.append(opt) + } + select.value = this.value + select.addEventListener('change', () => { + this.current.selected = select.value + emit(this, 'summer-change', { value: select.value }) + }) + this.classList.toggle('seo-parent--invalid', this.hasAttribute('invalid')) + this.replaceChildren(select) + } +} diff --git a/assets/src/elements/schema.ts b/assets/src/elements/schema.ts new file mode 100644 index 0000000..ea2c766 --- /dev/null +++ b/assets/src/elements/schema.ts @@ -0,0 +1,384 @@ +import { el, emit, fmt, icon, jsonAttr, labelsFor, plural, uid, uiLocaleOf, type Labels } from './labels' + +export const SCHEMA_TAG = 'golem15-seo-schema' + +/** The C17 cap of custom JSON-LD, in bytes. */ +export const MAX_CUSTOM_BYTES = 16 * 1024 + +/** The editor's Service fields. */ +export interface ServiceFields { + serviceType?: string + areaServed?: string + price?: string + currency?: string +} + +/** The seoschema value, stored as a JSON string. */ +export interface SchemaValue { + type: string + fields: ServiceFields + custom: string +} + +/** The types an editor may choose per kind (UI-SPEC C17), the default first. */ +export function typesFor(kind: string): string[] { + switch (kind) { + case 'article': + return ['BlogPosting', 'Article', 'NewsArticle'] + case 'collection': + return ['CollectionPage'] + case 'profile': + return ['ProfilePage'] + } + return ['WebPage', 'AboutPage', 'ContactPage', 'Service'] +} + +/** Reads a stored value; anything else is the empty value. */ +export function parseValue(raw: unknown): SchemaValue { + const out: SchemaValue = { type: '', fields: {}, custom: '' } + if (typeof raw !== 'string' || raw.trim() === '') { + return out + } + try { + const v = JSON.parse(raw) as Partial + out.type = typeof v.type === 'string' ? v.type : '' + out.fields = v.fields && typeof v.fields === 'object' ? { ...v.fields } : {} + out.custom = typeof v.custom === 'string' ? v.custom : '' + } catch { + // An unreadable value starts empty. + } + return out +} + +/** Writes a value as its JSON string; the empty value is "". */ +export function serializeValue(v: SchemaValue): string { + const fields: ServiceFields = {} + for (const key of ['serviceType', 'areaServed', 'price', 'currency'] as const) { + const text = (v.fields[key] ?? '').trim() + if (text !== '') { + fields[key] = text + } + } + const custom = v.custom.trim() + if (v.type === '' && custom === '' && Object.keys(fields).length === 0) { + return '' + } + return JSON.stringify({ type: v.type, fields, custom }) +} + +/** A price such as 150, 149.99 or 149,99. */ +export function validPrice(s: string): boolean { + return s.trim() === '' || /^\d+([.,]\d{1,2})?$/.test(s.trim()) +} + +/** A three-letter currency code. */ +export function validCurrency(s: string): boolean { + return s.trim() === '' || /^[A-Za-z]{3}$/.test(s.trim()) +} + +/** The status of custom JSON-LD text. */ +export type JsonStatus = + | { kind: 'empty' } + | { kind: 'valid'; nodes: number } + | { kind: 'error'; line: number; column: number; message: string } + | { kind: 'notObject' } + | { kind: 'tooLarge' } + +/** Line and column of a character offset. */ +function position(text: string, offset: number): { line: number; column: number } { + const before = text.slice(0, Math.max(0, Math.min(offset, text.length))) + const lines = before.split('\n') + return { line: lines.length, column: lines[lines.length - 1].length + 1 } +} + +/** Checks custom JSON-LD the way the server will (UI-SPEC C17). */ +export function checkCustom(text: string): JsonStatus { + if (text.trim() === '') { + return { kind: 'empty' } + } + if (new TextEncoder().encode(text.trim()).length > MAX_CUSTOM_BYTES) { + return { kind: 'tooLarge' } + } + let v: unknown + try { + v = JSON.parse(text) + } catch (err) { + const message = err instanceof Error ? err.message : String(err) + const lc = /line (\d+) column (\d+)/i.exec(message) + if (lc) { + return { kind: 'error', line: Number(lc[1]), column: Number(lc[2]), message } + } + const pos = /position (\d+)/i.exec(message) + const at = pos ? position(text, Number(pos[1])) : position(text, text.length) + return { kind: 'error', line: at.line, column: at.column, message } + } + const isObject = (x: unknown): boolean => x !== null && typeof x === 'object' && !Array.isArray(x) + if (isObject(v)) { + return { kind: 'valid', nodes: 1 } + } + if (Array.isArray(v) && v.length > 0 && v.every(isObject)) { + return { kind: 'valid', nodes: v.length } + } + return { kind: 'notObject' } +} + +const typeLabel = (labels: Labels, type: string): string => (labels as unknown as Record)['t' + type] ?? type + +const noteFor = (labels: Labels, type: string): string => { + switch (type) { + case 'BlogPosting': + case 'Article': + case 'NewsArticle': + return labels.noteArticle + case 'ContactPage': + return labels.noteContact + case 'Service': + return labels.noteService + case 'CollectionPage': + return labels.noteCollection + case 'ProfilePage': + return labels.noteProfile + } + return labels.notePage +} + +/** + * E8 golem15-seo-schema (UI-SPEC C17, D-44): the page's schema type per + * content kind (option kind), the Service fields and custom JSON-LD with a + * live parse status. Its value is a JSON string, so E9 reads it through + * fill; the server checks it again on save. + */ +export class SeoSchemaElement extends HTMLElement { + static observedAttributes = ['options', 'disabled', 'invalid', 'ui-locale'] + + private current: SchemaValue = { type: '', fields: {}, custom: '' } + private open = false + private status: JsonStatus = { kind: 'empty' } + private timer: ReturnType | undefined + private fieldErrors: Record = {} + private connected = false + + get value(): string { + return serializeValue(this.current) + } + + set value(v: unknown) { + this.current = parseValue(v) + this.status = checkCustom(this.current.custom) + this.open = this.current.custom.trim() !== '' + this.render() + } + + connectedCallback(): void { + this.connected = true + this.render() + } + + disconnectedCallback(): void { + this.connected = false + clearTimeout(this.timer) + } + + attributeChangedCallback(): void { + if (this.connected) { + this.render() + } + } + + private kind(): string { + const options = jsonAttr>(this, 'options', {}) + return typeof options.kind === 'string' ? options.kind : '' + } + + private labels(): Labels { + return labelsFor(uiLocaleOf(this)) + } + + private changed(): void { + emit(this, 'summer-change', { value: this.value }) + } + + private effectiveType(): string { + const types = typesFor(this.kind()) + return types.includes(this.current.type) ? this.current.type : types[0] + } + + private render(): void { + if (!this.connected) { + return + } + const labels = this.labels() + const disabled = this.hasAttribute('disabled') + const types = typesFor(this.kind()) + const sections: HTMLElement[] = [] + + // 1. The type row. + const typeRow = el('div', 'seo-schema__section') + const selectId = uid('seo-schema-type') + const label = el('label', 'seo-schema__label', labels.schemaType) + label.htmlFor = selectId + if (types.length > 1) { + const select = el('select', 'seo-schema__select') + select.id = selectId + select.disabled = disabled + const def = el('option', '', fmt(labels.typeDefault, { type: types[0] })) + def.value = '' + select.append(def) + for (const t of types) { + const opt = el('option', '', typeLabel(labels, t)) + opt.value = t + select.append(opt) + } + select.value = types.includes(this.current.type) ? this.current.type : '' + select.addEventListener('change', () => { + this.current.type = select.value + this.changed() + this.render() + }) + typeRow.append(label, select) + } else { + const fixed = el('div', 'seo-schema__fixed', typeLabel(labels, types[0])) + fixed.id = selectId + typeRow.append(label, fixed) + } + typeRow.append(el('p', 'seo-schema__note', noteFor(labels, this.effectiveType()))) + sections.push(typeRow) + + // 2. The Service fields. + if (this.effectiveType() === 'Service') { + const grid = el('div', 'seo-schema__grid') + const field = (key: keyof ServiceFields, text: string, hint: string, placeholder = '', mode = ''): void => { + const wrap = el('div', 'seo-schema__cell') + const id = uid('seo-schema-' + key) + const l = el('label', 'seo-schema__label', text) + l.htmlFor = id + const input = el('input', 'seo-schema__input') + input.id = id + input.type = 'text' + input.disabled = disabled + input.value = this.current.fields[key] ?? '' + if (placeholder) { + input.placeholder = placeholder + } + if (mode) { + input.inputMode = mode + } + if (key === 'currency') { + input.maxLength = 3 + } + const error = el('p', 'seo-schema__error', this.fieldErrors[key] ?? '') + error.setAttribute('role', 'status') + input.addEventListener('input', () => { + const v = input.value + const ok = key === 'price' ? validPrice(v) : key === 'currency' ? validCurrency(v) : true + if (ok) { + this.current.fields[key] = v + delete this.fieldErrors[key] + error.textContent = '' + this.changed() + } else { + this.fieldErrors[key] = key === 'price' ? labels.priceInvalid : labels.currencyInvalid + error.textContent = this.fieldErrors[key] + } + }) + if (key === 'currency') { + input.addEventListener('blur', () => { + input.value = input.value.toUpperCase() + if (validCurrency(input.value)) { + this.current.fields.currency = input.value + this.changed() + } + }) + } + wrap.append(l, input) + if (hint) { + wrap.append(el('p', 'seo-schema__note', hint)) + } + wrap.append(error) + grid.append(wrap) + } + field('serviceType', labels.serviceType, '', labels.serviceTypePh) + field('areaServed', labels.areaServed, labels.areaServedHint) + field('price', labels.price, labels.priceHint, '', 'decimal') + field('currency', labels.currency, labels.currencyHint) + sections.push(grid) + } + + // 3. Custom JSON-LD. + const customSection = el('div', 'seo-schema__section') + const toggle = el('button', 'seo-schema__disclosure') + toggle.type = 'button' + toggle.setAttribute('aria-expanded', String(this.open)) + const panelId = uid('seo-schema-custom') + toggle.setAttribute('aria-controls', panelId) + toggle.append(el('span', '', labels.custom), icon('chevron-down')) + toggle.addEventListener('click', () => { + this.open = !this.open + this.render() + }) + const head = el('div', 'seo-schema__disclosure-row') + head.append(toggle) + if (this.current.custom.trim() !== '') { + head.append(el('span', 'seo-schema__note', labels.customSet)) + } + customSection.append(head) + if (this.open) { + const panel = el('div', 'seo-schema__panel') + panel.id = panelId + const areaId = uid('seo-schema-json') + const hint = el('p', 'seo-schema__note', labels.customHint) + hint.id = areaId + '-hint' + const area = el('textarea', 'seo-schema__json') + area.id = areaId + area.spellcheck = false + area.disabled = disabled + area.value = this.current.custom + area.setAttribute('aria-label', labels.custom) + area.setAttribute('aria-describedby', hint.id) + const line = el('p', 'seo-schema__status') + line.setAttribute('role', 'status') + line.setAttribute('aria-live', 'polite') + this.statusLine(line, labels) + area.addEventListener('input', () => { + this.current.custom = area.value + this.changed() + clearTimeout(this.timer) + this.timer = setTimeout(() => { + this.status = checkCustom(area.value) + this.statusLine(line, labels) + }, 300) + }) + panel.append(hint, area, line) + customSection.append(panel) + } + sections.push(customSection) + + this.classList.toggle('seo-schema--invalid', this.hasAttribute('invalid')) + this.replaceChildren(...sections) + } + + private statusLine(line: HTMLElement, labels: Labels): void { + const s = this.status + line.className = 'seo-schema__status' + switch (s.kind) { + case 'empty': + line.replaceChildren() + return + case 'valid': + line.classList.add('seo-schema__status--ok') + line.replaceChildren(icon('circle-check'), el('span', '', plural(labels.jsonValid, s.nodes, uiLocaleOf(this)))) + return + case 'error': + line.classList.add('seo-schema__status--error') + line.replaceChildren(icon('circle-alert'), el('span', '', fmt(labels.jsonError, { line: s.line, column: s.column, message: s.message }))) + return + case 'notObject': + line.classList.add('seo-schema__status--error') + line.replaceChildren(icon('circle-alert'), el('span', '', labels.jsonNotObject)) + return + case 'tooLarge': + line.classList.add('seo-schema__status--error') + line.replaceChildren(icon('circle-alert'), el('span', '', labels.jsonTooLarge)) + } + } +} diff --git a/assets/src/elements/styles.css b/assets/src/elements/styles.css index f1d08b3..594c6e6 100644 --- a/assets/src/elements/styles.css +++ b/assets/src/elements/styles.css @@ -850,3 +850,279 @@ golem15-seo-transfer .seo-transfer__actions { transition: none; } } + +/* E8 golem15-seo-schema (UI-SPEC C17) */ +golem15-seo-schema { + display: flex; + flex-direction: column; + gap: 24px; +} + +golem15-seo-schema .seo-schema__section { + display: flex; + flex-direction: column; + gap: 6px; +} + +golem15-seo-schema .seo-schema__label { + font-size: 14px; + font-weight: 600; + line-height: 1.5; + color: var(--c-text); +} + +golem15-seo-schema .seo-schema__select, +golem15-seo-schema .seo-schema__input, +golem15-seo-parent .seo-parent__select { + box-sizing: border-box; + height: 44px; + padding: 0 14px; + border: 1px solid var(--c-border-strong); + border-radius: 10px; + background: var(--c-surface); + color: var(--c-text); + font: inherit; + font-size: 14px; + transition: border-color 150ms; +} + +golem15-seo-schema .seo-schema__select, +golem15-seo-parent .seo-parent__select { + min-width: 280px; + max-width: 100%; +} + +golem15-seo-schema .seo-schema__input { + width: 100%; +} + +golem15-seo-schema .seo-schema__fixed { + font-size: 14px; + color: var(--c-text); +} + +golem15-seo-schema .seo-schema__note, +golem15-seo-graph .seo-graph__meta { + margin: 0; + font-size: 13px; + line-height: 1.5; + color: var(--c-muted); +} + +golem15-seo-schema .seo-schema__section > .seo-schema__note { + margin-top: 2px; +} + +golem15-seo-schema .seo-schema__grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +@media (max-width: 600px) { + golem15-seo-schema .seo-schema__grid { + grid-template-columns: 1fr; + } +} + +golem15-seo-schema .seo-schema__cell { + display: flex; + flex-direction: column; + gap: 6px; +} + +golem15-seo-schema .seo-schema__error { + margin: 0; + font-size: 13px; + color: var(--c-danger); +} + +golem15-seo-schema .seo-schema__error:empty { + display: none; +} + +golem15-seo-schema .seo-schema__disclosure-row { + display: flex; + align-items: center; + gap: 8px; +} + +golem15-seo-schema .seo-schema__disclosure, +golem15-seo-graph .seo-graph__disclosure { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 0; + border: 0; + background: none; + color: var(--c-text); + font: inherit; + font-size: 14px; + font-weight: 600; + cursor: pointer; + align-self: flex-start; +} + +golem15-seo-schema .seo-schema__disclosure[aria-expanded='true'] .seo-icon, +golem15-seo-graph .seo-graph__disclosure[aria-expanded='true'] .seo-icon { + transform: rotate(180deg); +} + +golem15-seo-schema .seo-schema__panel { + display: flex; + flex-direction: column; + gap: 8px; +} + +golem15-seo-schema .seo-schema__json { + box-sizing: border-box; + width: 100%; + min-height: 160px; + resize: vertical; + padding: 12px 14px; + border: 1px solid var(--c-border-strong); + border-radius: 10px; + background: var(--c-surface); + color: var(--c-text); + font-family: 'DM Mono', ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 13px; + line-height: 1.6; +} + +golem15-seo-schema .seo-schema__status, +golem15-seo-graph .seo-graph__finding, +golem15-seo-graph .seo-graph__ok, +golem15-seo-graph .seo-graph__error { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + font-size: 13px; + line-height: 1.5; +} + +golem15-seo-schema .seo-schema__status--ok, +golem15-seo-graph .seo-graph__ok { + color: var(--c-ok-text); +} + +golem15-seo-schema .seo-schema__status--error, +golem15-seo-graph .seo-graph__error, +golem15-seo-graph .seo-graph__finding--required .seo-icon { + color: var(--c-danger); +} + +golem15-seo-graph .seo-graph__finding { + font-size: 14px; + color: var(--c-text); +} + +golem15-seo-schema .seo-schema__select:focus-visible, +golem15-seo-schema .seo-schema__input:focus-visible, +golem15-seo-schema .seo-schema__json:focus-visible, +golem15-seo-schema .seo-schema__disclosure:focus-visible, +golem15-seo-graph .seo-graph__disclosure:focus-visible, +golem15-seo-graph .seo-graph__button:focus-visible, +golem15-seo-graph .seo-graph__link:focus-visible, +golem15-seo-parent .seo-parent__select:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--c-ring); +} + +golem15-seo-schema.seo-schema--invalid .seo-schema__json, +golem15-seo-parent.seo-parent--invalid .seo-parent__select { + border-color: var(--c-danger); +} + +/* E9 golem15-seo-graph (UI-SPEC C18) */ +golem15-seo-graph { + display: flex; + flex-direction: column; + gap: 24px; +} + +golem15-seo-graph .seo-graph__nodes { + display: flex; + flex-direction: column; + gap: 16px; +} + +golem15-seo-graph .seo-graph__node { + display: flex; + flex-direction: column; + gap: 8px; +} + +golem15-seo-graph .seo-graph__node-head { + display: flex; + align-items: center; + gap: 8px; +} + +golem15-seo-graph .seo-graph__pill { + display: inline-flex; + align-items: center; + height: 24px; + padding: 0 10px; + border-radius: 999px; + background: var(--c-subtle); + color: var(--c-muted); + font-size: 12px; + font-weight: 600; +} + +golem15-seo-graph .seo-graph__id { + font-family: 'DM Mono', ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 13px; + color: var(--c-muted); +} + +golem15-seo-graph .seo-graph__json { + box-sizing: border-box; + max-height: 360px; + overflow: auto; + margin: 0; + padding: 12px 14px; + border: 1px solid var(--c-border); + border-radius: 10px; + background: var(--c-subtle); + color: var(--c-text); + font-family: 'DM Mono', ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 13px; + line-height: 1.6; + white-space: pre; +} + +golem15-seo-graph .seo-graph__links { + display: flex; + flex-direction: column; + gap: 8px; +} + +golem15-seo-graph .seo-graph__link { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--c-primary); + font-size: 14px; + align-self: flex-start; +} + +golem15-seo-graph .seo-graph__button { + align-self: flex-start; + height: 42px; + padding: 0 18px; + border: 1px solid var(--c-border-strong); + border-radius: 10px; + background: var(--c-surface); + color: var(--c-text); + font: inherit; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: background-color 150ms; +} + +golem15-seo-graph .seo-graph__button:hover { + background: var(--c-subtle); +} diff --git a/classes/field_types.go b/classes/field_types.go index ed7d247..5bc95cc 100644 --- a/classes/field_types.go +++ b/classes/field_types.go @@ -177,7 +177,12 @@ func decodeSchema(app func() *backpack.App) func(context.Context, pact.FormField // parents as :. func decodeParent(app func() *backpack.App) func(context.Context, pact.FormFieldValue) (pact.FormFieldDecoded, error) { return func(ctx context.Context, in pact.FormFieldValue) (pact.FormFieldDecoded, error) { - raw, ok := stringValue(in.Value) + value := in.Value + if m, isMap := value.(map[string]any); isMap { + // The loaded {selected, options} sent back unchanged. + value = m["selected"] + } + raw, ok := stringValue(value) raw = strings.TrimSpace(raw) if !ok { return pact.FormFieldDecoded{}, &pact.FormFieldError{Messages: []string{"golem15.seo::lang.api.value_invalid"}} diff --git a/tests/elements/schema.test.ts b/tests/elements/schema.test.ts new file mode 100644 index 0000000..bd4c0cb --- /dev/null +++ b/tests/elements/schema.test.ts @@ -0,0 +1,180 @@ +import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest' +import { checkCustom, parseValue, serializeValue, validCurrency, validPrice } from '../../assets/src/elements/schema' +import { labelsFor } from '../../assets/src/elements/labels' +import { HostFailure, mountWidget, settle } from './host' + +// E8 golem15-seo-schema, E9 golem15-seo-graph and golem15-seo-parent +// (UI-SPEC C17, C18, P33-23). + +beforeAll(async () => { + await import('../../assets/src/elements/index') +}) + +afterEach(() => { + vi.useRealTimers() + document.body.replaceChildren() +}) + +function mountSchema(kind: string, value: string): HTMLElement & { value: string } { + const node = document.createElement('golem15-seo-schema') as HTMLElement & { value: string } + node.setAttribute('ui-locale', 'en') + node.setAttribute('options', JSON.stringify({ kind })) + node.value = value + document.body.append(node) + return node +} + +describe('E8 value helpers', () => { + it('round-trips the Service fields as the same JSON string', () => { + const raw = JSON.stringify({ type: 'Service', fields: { serviceType: 'Workshops', areaServed: 'Warszawa', price: '150', currency: 'PLN' }, custom: '' }) + expect(serializeValue(parseValue(raw))).toBe(raw) + expect(serializeValue(parseValue(''))).toBe('') + }) + + it('checks prices, currencies and custom JSON-LD', () => { + expect(validPrice('149,99')).toBe(true) + expect(validPrice('abc')).toBe(false) + expect(validCurrency('PLN')).toBe(true) + expect(validCurrency('ZLOTY')).toBe(false) + expect(checkCustom('{"@type": "Thing"}')).toEqual({ kind: 'valid', nodes: 1 }) + expect(checkCustom('[{"a": 1}, {"b": 2}]')).toEqual({ kind: 'valid', nodes: 2 }) + expect(checkCustom('42').kind).toBe('notObject') + expect(checkCustom('{"a": "' + 'x'.repeat(17000) + '"}').kind).toBe('tooLarge') + const bad = checkCustom('{\n "a": 1,\n oops\n}') + expect(bad.kind).toBe('error') + if (bad.kind === 'error') { + expect(bad.line).toBe(3) + expect(bad.column).toBeGreaterThan(0) + } + }) +}) + +describe('E8 golem15-seo-schema', () => { + it('renders the article type options with the default and its note, custom collapsed', () => { + const node = mountSchema('article', '') + const options = [...node.querySelectorAll('option')].map((o) => o.textContent) + expect(options).toEqual(['Default (BlogPosting)', 'BlogPosting (blog post)', 'Article (article)', 'NewsArticle (news)']) + expect(node.textContent).toContain(labelsFor('en').noteArticle) + expect(node.querySelector('textarea')).toBeNull() + expect(node.querySelector('.seo-schema__disclosure')!.getAttribute('aria-expanded')).toBe('false') + }) + + it('shows the Service grid and keeps the last valid price', () => { + const node = mountSchema('page', JSON.stringify({ type: 'Service', fields: { price: '100' }, custom: '' })) + expect(node.querySelectorAll('.seo-schema__grid input')).toHaveLength(4) + const changes: string[] = [] + node.addEventListener('summer-change', (e) => changes.push((e as CustomEvent).detail.value)) + const price = node.querySelectorAll('.seo-schema__grid input')[2] + price.value = 'abc' + price.dispatchEvent(new Event('input')) + expect(node.textContent).toContain(labelsFor('en').priceInvalid) + expect(JSON.parse(node.value).fields.price).toBe('100') + price.value = '149,99' + price.dispatchEvent(new Event('input')) + expect(JSON.parse(changes[changes.length - 1]).fields.price).toBe('149,99') + }) + + it('opens set custom JSON-LD and reports parse errors with line and column', () => { + vi.useFakeTimers() + const node = mountSchema('page', JSON.stringify({ type: '', fields: {}, custom: '{"a": 1}' })) + const area = node.querySelector('textarea')! + expect(area).not.toBeNull() + area.value = '{"a": ' + area.dispatchEvent(new Event('input')) + vi.advanceTimersByTime(300) + expect(node.querySelector('.seo-schema__status')!.textContent).toMatch(/^Line 1, column \d+:/) + area.value = '{"x": "' + 'y'.repeat(17000) + '"}' + area.dispatchEvent(new Event('input')) + vi.advanceTimersByTime(300) + expect(node.querySelector('.seo-schema__status')!.textContent).toBe(labelsFor('en').jsonTooLarge) + }) + + it('shows a single fixed type for collections', () => { + const node = mountSchema('collection', '') + expect(node.querySelector('select')).toBeNull() + expect(node.querySelector('.seo-schema__fixed')!.textContent).toBe('CollectionPage (listing)') + }) +}) + +const answer = { + locale: 'en', + url: 'https://example.test/blog/post', + graph: { '@context': 'https://schema.org', '@graph': [{ '@type': 'BlogPosting' }] }, + nodes: [ + { id: 'https://example.test/blog/post#article', type: 'BlogPosting', rules: true, findings: [{ level: 'recommended', property: 'image' }] }, + { id: 'https://example.test/blog/post#video-1', type: 'VideoObject', rules: true, findings: [{ level: 'required', property: 'uploadDate' }] }, + { id: 'https://example.test/blog/post#webpage', type: 'WebPage', rules: false, findings: [] }, + ], +} + +describe('E9 golem15-seo-graph', () => { + it('asks on connect and renders the nodes, findings, JSON and the Rich Results link', async () => { + const host = mountWidget('golem15-seo-graph', { attrs: { 'ui-locale': 'en' }, answer: () => answer }) + await settle() + expect(host.posted).toEqual([{ op: 'graph', locale: '' }]) + const text = host.node.textContent ?? '' + expect(text).toContain('JSON-LD for EN — https://example.test/blog/post') + expect(text).toContain('Missing recommended property “image” (BlogPosting)') + expect(text).toContain('Missing required property “uploadDate” (VideoObject)') + expect(text).toContain('WebPage has no rich-result rules at Google') + const link = host.node.querySelector('.seo-graph__link')! + expect(link.href).toBe('https://search.google.com/test/rich-results?url=https%3A%2F%2Fexample.test%2Fblog%2Fpost') + expect(link.rel).toBe('noopener') + host.node.querySelector('.seo-graph__disclosure')!.click() + expect(host.node.querySelector('pre')!.textContent).toContain('"@context": "https://schema.org"') + expect(host.node.getAttribute('aria-busy')).toBe('false') + }) + + it('shows allGood without findings, re-checks 800 ms after a fill change and follows the locale', async () => { + const clean = { ...answer, nodes: [{ id: '#webpage', type: 'WebPage', rules: false, findings: [] }] } + const host = mountWidget('golem15-seo-graph', { attrs: { 'ui-locale': 'en' }, answer: () => clean }) + await settle() + expect(host.node.textContent).toContain(labelsFor('en').allGood) + host.node.setAttribute('fill-values', JSON.stringify({ seo_schema: '{"type":"Article"}' })) + await new Promise((r) => setTimeout(r, 850)) + await settle() + expect(host.posted).toHaveLength(2) + window.dispatchEvent(new CustomEvent('summer:ml-locale', { detail: 'pl' })) + await settle() + expect(host.posted[2]).toEqual({ op: 'graph', locale: 'pl' }) + }) + + it('keeps the last graph and shows the danger line when the action fails', async () => { + let fail = false + const host = mountWidget('golem15-seo-graph', { + attrs: { 'ui-locale': 'en' }, + answer: () => { + if (fail) { + throw new HostFailure('nope') + } + return answer + }, + }) + await settle() + fail = true + host.node.querySelector('.seo-graph__button')!.click() + await settle() + expect(host.node.querySelector('.seo-graph__error')!.textContent).toBe(labelsFor('en').graphFailed) + expect(host.node.textContent).toContain('Missing recommended property “image”') + }) +}) + +describe('golem15-seo-parent', () => { + it('lists the options of its value with the default and emits the choice', () => { + const node = document.createElement('golem15-seo-parent') as HTMLElement & { value: unknown } + node.setAttribute('ui-locale', 'en') + node.value = { selected: 'acme-section:2', options: [{ value: 'acme-section:1', label: 'Recipes' }, { value: 'acme-section:2', label: 'Mushrooms' }] } + document.body.append(node) + const select = node.querySelector('select')! + expect([...select.options].map((o) => o.textContent)).toEqual(['Default (first category)', 'Recipes', 'Mushrooms']) + expect(select.value).toBe('acme-section:2') + const changes: string[] = [] + node.addEventListener('summer-change', (e) => changes.push((e as CustomEvent).detail.value)) + select.value = '' + select.dispatchEvent(new Event('change')) + expect(changes).toEqual(['']) + // A stored choice that is no longer a parent falls back to the default. + node.value = { selected: 'acme-section:9', options: [{ value: 'acme-section:1', label: 'Recipes' }] } + expect(node.value).toBe('') + }) +})