diff --git a/README.md b/README.md index dd54c46..c794d9f 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ The frontend (a Nuxt layer or app) asks `GET /_seo/api/v1/resolve?url=...` for t - **CSV import and export** of redirects, URL rules and the SEO of records (by URL), in the **Import and export** screen (element `golem15-seo-transfer`) and through MCP. Files are UTF-8 with a BOM, comma or semicolon; exported cells that start a formula (`=`, `+`, `-`, `@`, tab, CR), or apostrophes followed by one, are prefixed with an apostrophe, and the import drops exactly that one again, so a re-imported export is unchanged. An import is previewed row by row (new, changed, unchanged, refused, with notes) and applied as one batch with via `import`, or not at all while a row is refused. The screen's apply token is an HMAC-SHA256 under the app key over the file and a fingerprint of the data the preview read; it is refused with `transfer.stale` when the data changed. The screen previews files up to 60 KB and downloads exports up to 250 KB; MCP imports take up to 900 KB per call and exports come in pages of 1000 rows. - **Record events.** A deleted record loses its entries after commit; a created record gets its address snapshot; an updated record whose address changed gets its 301. - **Entry generation.** The `golem15.seo.generate_entries` job (conga) creates missing entries with the current address of every published record and refreshes the effective-value cache; a record whose address changed without a record event gets its address change (with its 301) like an admin save. Editor values are never changed. -- **Field types** `seotext`, `mlseotext`, `seorobots`, `mlseorobots`, `seoimage`, `robotstxt`, `seoschema` and `seoparent`, with the custom elements `golem15-seo-text`, `golem15-seo-robots`, `golem15-seo-serp` (the result preview widget), `golem15-seo-image`, `golem15-seo-robotstxt` (the robots.txt editor), `golem15-seo-robotstester` (the tester widget), `golem15-seo-transfer` (the import and export widget), `golem15-seo-schema` (structured data), `golem15-seo-graph` (the structured data preview widget) and `golem15-seo-parent` (the primary category). +- **Field types** `seotext`, `mlseotext`, `seorobots`, `mlseorobots`, `seoimage`, `robotstxt`, `seoschema` and `seoparent`, with the custom elements `golem15-seo-text`, `golem15-seo-robots`, `golem15-seo-serp` (the result preview widget), `golem15-seo-image`, `golem15-seo-robotstxt` (the robots.txt editor), `golem15-seo-robotstester` (the tester widget), `golem15-seo-transfer` (the import and export widget), `golem15-seo-schema` (structured data), `golem15-seo-graph` (the structured data preview widget), `golem15-seo-parent` (the primary category) and `golem15-seo-links` (the internal links widget). - **Structured data.** Every `ok` resolve answer carries one JSON-LD `@graph` built from the content descriptor and the settings: the page node (WebPage or its subtype), the article with its authors, dates, images, word count and section, the person of a profile page, a Service with its provider and offer, the BreadcrumbList, one VideoObject per YouTube video, the WebSite (with a SearchAction when `search_url` is set) and the publisher. Editors pick the schema type per page, fill the Service fields and add custom JSON-LD; the SEO tab previews the graph with Google's required and recommended properties and links the Rich Results Test. - **Publisher and local business** settings: an organisation or a person with logo, website, contact and sameAs profiles, optionally a local business with address, coordinates, opening hours and price range. - **Breadcrumbs, hreflang and pagination.** The resolve answer carries the breadcrumb trail (through the record's primary category), the hreflang alternates with `x-default`, `html_lang` and `og:locale`, and the page of a paginated address in the site's form (`?page=N` or `//N`). @@ -51,6 +51,7 @@ The frontend (a Nuxt layer or app) asks `GET /_seo/api/v1/resolve?url=...` for t - **Site answer** (`GET /_seo/api/v1/site`) with the facts the frontend layer needs, and **llms.txt** (off by default) listing the indexable pages from the sitemaps. - **Links** settings: links to other sites open in a new tab (on by default), and domains whose links always get `nofollow` or `sponsored`; the resolve answer carries them as `outbound`. - **Auto-links** (`golem15_seo_autolinks`, SEO › **Auto-links**): phrases that texts link to a page, per locale or for all, with an active switch, an origin and a note. Every `ok` resolve answer carries the phrases of its locale (never one linking to the page itself) as `autolinks`, with `autolink_max` from the Links tab (3 by default, 0 turns auto-linking off); the frontend layer links each phrase's first occurrence. **Add focus keyphrases** turns the records' focus keyphrases into phrases. See [Auto-links](#auto-links). +- **Internal links** in the SEO tab (E10): the page's inbound and outbound link counts from the last finished crawl, a warning when no page links to it, and up to 10 pages its text could link to and 10 pages that could link to it (titles and focus keyphrases found in the crawled text), each with **Copy address**; `GET /_seo/api/v1/links` and `seo_get_link_suggestions` answer the same. - **Site cache purge.** Every SEO write (admin, MCP, batch, import, undo, automatic) asks the frontend to drop its cached pages after the commit, once per transaction, through the framework's site-cache purger (`site_cache.purge_url`, `site_cache.purge_token`). ## Usage @@ -198,6 +199,8 @@ The **robots.txt** screen (a singleton) has the editor, the tester and **What cr **SEO › Auto-links** (controller `golem15.seo.autolinks`, after Pages, icon `wand-sparkles`) lists the phrases (50 per page) with the address they link to, the language (**All languages** or a locale), the origin (**Manual**, **Focus keyphrase**, **MCP**) and the update time; an inactive phrase shows **Off**. Filters: language, origin and **Active only**. The form takes the phrase, the address, the language and the **Active** switch, and a note. A phrase needs 3 characters (`autolinks.phrase_short`); a phrase exists once per language, a phrase of every language once at all (`autolinks.phrase_taken` names the address it already links to); the address is a path starting with one `/` or a full `http(s)` address (`autolinks.url_invalid`: `javascript:`, `data:` and protocol-relative addresses are refused). The toolbar's **Add focus keyphrases** adds one phrase per record whose focus keyphrase in a locale is not yet a phrase there, linking to the record's address in that locale (origin **Focus keyphrase**), and toasts the count; a second run adds nothing new. Every change is recorded in History and purges the site's cached pages. +The SEO tab's **Internal links** (widget `golem15-seo-links`, action `seo_links`, after the structured data preview, on the update form of content types whose records have a body: not for collections, profiles or the page registry) reads the last finished crawl: **Links to this page** (pages linking here) and **Links from this page** (pages it links to on the site), a warning when nothing links here, **Pages you could link to from this text** (pages whose H1 or focus keyphrase the crawled text contains and that it does not link to yet, with the public address and **Copy address**) and **Pages that could link here** (pages whose crawled text contains this page's title, H1 or focus keyphrase and that do not link here yet, with their admin form), 10 each. Before the first crawl it says when the counts appear. + The layer links a phrase at its first occurrence in a text, as whole words and ignoring letter case, never inside headings, links, `code`, `pre`, `figcaption` or buttons, and never on the page it links to, at most `autolink_max` phrases per text (the Links tab's **Auto-links per page**, 0 to 20). ### The site answer and llms.txt @@ -253,6 +256,7 @@ The management routes live under `/_seo/api/v1` (bucket `seo-api`, 300 requests | `get_url_seo` | `GET /seo?url=` | Effective SEO of a URL with the source of every value, the redirect decision, the owner and the SEO tab's warnings. | | `set_url_seo` | `PUT /seo` | Values for a URL: the record behind it in the URL's locale, else an exact URL rule; `force_rule` writes a rule. `primary_parent` (`:`) and `schema` (the structured data JSON string) are records only and shared by every locale. Answers `written_to` (`record` or `rule`), `type`, `id`, `locale`, `rule_id`. | | `get_graph` | `GET /graph?url=&schema=` | The JSON-LD graph of a URL and every node's missing required and recommended properties (`rules` false for a type Google has no rules for); `schema` previews an unsaved value. Answers `{locale, url, graph, nodes}`. | +| `get_link_suggestions` | `GET /links?url=` | A page's internal links from the last finished crawl: `{crawledAt, inbound, outbound, orphan, out [{phrase, title, url}], in [{phrase, title, adminUrl}]}`, each list at most 10; `crawledAt`, `inbound` and `outbound` are `null` before the first crawl. | | `list_rules`, `show_rule`, `create_rule`, `update_rule`, `delete_rule` | `GET/POST /rules`, `GET/PUT/DELETE /rules/{id}` | URL rules. | | `list_redirects`, `show_redirect`, `create_redirect`, `update_redirect`, `delete_redirect` | `GET/POST /redirects`, `GET/PUT/DELETE /redirects/{id}` | Redirects; a chain is saved with a warning. | | `list_not_found`, `redirect_not_found`, `gone_not_found`, `delete_not_found` | `GET /notfound`, `POST /notfound/{id}/redirect`, `POST /notfound/{id}/gone`, `DELETE /notfound/{id}` | The 404 log and its actions; an address a rule answers is `409`. | @@ -283,7 +287,7 @@ Lists take `page`, `per_page` (at most 200) and `search`, and answer `{"data": [ - **Crawls** lists the runs newest first (started, status, pages, issues, started by, duration). The preview shows one status callout (running, queued, failed, cancelled, limit reached), the staging and rendered-comparison lines, the stats strip and the issues per kind, each count linking to the filtered Site audit; **Cancel crawl** while queued or running. There is no live progress: reload the page. - The SEO settings' **Site audit** tab (before Verification): **Automatic crawl** (off, daily, weekly), **Pages per crawl** (10–50 000), **Pause between requests** (100–5000 ms), **Crawls kept** (1–50) and **Generic link texts** (seeded with the English and Polish lists). - The SEO settings have a **Pagination** tab (after Addresses), a **Links** tab (**Auto-links per page**, 0 to 20, the new-tab switch and the nofollow and sponsored domain lists, one domain per line; `settings.domain_invalid` names a bad line), a **Publisher** tab with the generated publisher node and its findings, and a **Local business** tab. A person cannot be a local business (`settings.local_person`). -- The SEO tab adds **Primary category** (article types), **Structured data** and its **preview** after the social image; its status warns when a stored primary category is no longer assigned, and the effective values show the schema type and the breadcrumb trail. +- The SEO tab adds **Primary category** (article types), **Structured data** and its **preview** after the social image, then **Internal links** (types with a body); its status warns when a stored primary category is no longer assigned, and the effective values show the schema type and the breadcrumb trail. - Settings tile **SEO settings** (`seo_settings`), a link to the settings singleton controller. - Every SEO screen and the SEO tab require `golem15.seo.manage`. Without it an administrator sees no SEO tab, and a posted SEO value changes nothing. @@ -308,6 +312,7 @@ The element bundle (`assets/dist/seo.js` and `seo.css`) is built from `assets/sr | `schema.Build`, `schema.Input`, `schema.Validate`, `schema.Normalize`, `schema.DecodeCustom`, `schema.Merge` | Package `classes/schema`: the JSON-LD graph of a page, Google's required and recommended properties per type, the structured data value and custom JSON-LD. | | `classes.Alternate`, `classes.Alternates`, `classes.LangTag` | hreflang alternates and language tags; `Service.OGLocale`. | | `classes.Pagination`, `classes.PageConfig` | The resolve answer's pagination block and a site's page form (`Service.PageConfigOf`). | +| `classes.LinkReport`, `classes.LinkOut`, `classes.LinkIn`, `classes.RecordLinksAction`, `classes.LinksActionName`, `classes.MaxLinkSuggestions`, `classes.NormalText` | The internal links of a page (`Service.LinkSuggestions`, `Service.LinksOfURL`, `Service.LinksOfRecord`) and the SEO tab's `seo_links` action. | | `classes.GraphAnswer` | `Service.GraphOfURL` and `Service.GraphOfRecord`, the graph and its findings; `classes.RecordGraphAction` is the SEO tab's preview action. | | `classes.PrimaryParent`, `classes.ParentOption` | A record's primary category and its choices (`Service.ParentOptions`); `Service.Trail` builds the breadcrumb trail, `Service.PublisherOf` the publisher. | | `classes.SitemapExclusion` | `Service.RecordSitemapExclusions`, why the sitemaps leave out a record's addresses; `Service.SitemapListed` reads the listed addresses; `Service.SitemapLines` the robots.txt `Sitemap:` addresses. | @@ -392,4 +397,4 @@ npm ci && npm run typecheck && npm test && bash scripts/check-dist.sh The element tests (`tests/elements`, vitest on happy-dom) drive the widget elements through a fake of the admin's widget host (`tests/elements/host.ts`), including a host that drops an action. -The smoke tests boot the plugin with fake content plugins (`acme.pages`, and `acme.blog` with a category segment for strict URLs) and exercise the SEO tab, the settings tabs, the resolver precedence, history, generation, the URL rule form, redirects (engine, loops and chains, address changes from the admin and from events), the 404 log, normalisation and strict URLs, robots.txt and verification files through the admin and public APIs, and the management API as an MCP client (`TestMCPSetURLSeoTracer`, `TestManagementAPI`, `TestIssues`), batches, undo and CSV (`TestBatch`, `TestUndo`, `TestCSVTransfer`, and the `mcp` and `import` cases of `TestLoopRefused`). The sitemap, the registry and the site-level endpoints are covered by `TestSitemapFilterTracer`, `TestFilterReasons`, `TestNoindexPublishedConflict`, `TestSitemapBlockedIssue`, `TestRegistry`, `TestRouteReport`, `TestSiteEndpoint`, `TestLLMSTxt`, `TestRobotsSitemapLine`, `TestSettingsLinks` and `TestPurgeAfterWrite`; the fixture boots the sitemap plugin next to the SEO plugin. The site audit is covered by `TestCrawlerTracer`, `TestCrawlerChecks` (a planted site with one page per kind and a clean control page, served to the crawler through `fetchguard.WithTransport`), `TestIssueMerge`, `TestStagingCrawl`, `TestRenderSkipped`, `TestCrawlAdmin`, `TestCrawlSchedule` and `TestCrawlTools`, with the fake content plugin `acme.crawl` naming fixing tools. Auto-links are covered by `TestAutolinksTracer` (the phrases of a page's locale in the resolve answer, never the page's own, `autolink_max` 0) and `TestAutolinks` (the screen, its refusals, Add focus keyphrases, the API and MCP). `TestChromeRenderer` renders a page in a real headless Chrome (it runs when `SEO_TEST_CHROME` names a binary or `chromium`, `chromium-browser` or `google-chrome` is on `PATH`, and is skipped otherwise) and `TestRendererGuards` checks the refused paths and private addresses. `TestEveryAdminOperationHasTool`, `TestMCPReadToolsAreGET`, `TestIssueKindLabels` and the static parts of `TestCrawlTools` and `TestCrawlSchedule` run without Docker. +The smoke tests boot the plugin with fake content plugins (`acme.pages`, and `acme.blog` with a category segment for strict URLs) and exercise the SEO tab, the settings tabs, the resolver precedence, history, generation, the URL rule form, redirects (engine, loops and chains, address changes from the admin and from events), the 404 log, normalisation and strict URLs, robots.txt and verification files through the admin and public APIs, and the management API as an MCP client (`TestMCPSetURLSeoTracer`, `TestManagementAPI`, `TestIssues`), batches, undo and CSV (`TestBatch`, `TestUndo`, `TestCSVTransfer`, and the `mcp` and `import` cases of `TestLoopRefused`). The sitemap, the registry and the site-level endpoints are covered by `TestSitemapFilterTracer`, `TestFilterReasons`, `TestNoindexPublishedConflict`, `TestSitemapBlockedIssue`, `TestRegistry`, `TestRouteReport`, `TestSiteEndpoint`, `TestLLMSTxt`, `TestRobotsSitemapLine`, `TestSettingsLinks` and `TestPurgeAfterWrite`; the fixture boots the sitemap plugin next to the SEO plugin. The site audit is covered by `TestCrawlerTracer`, `TestCrawlerChecks` (a planted site with one page per kind and a clean control page, served to the crawler through `fetchguard.WithTransport`), `TestIssueMerge`, `TestStagingCrawl`, `TestRenderSkipped`, `TestCrawlAdmin`, `TestCrawlSchedule` and `TestCrawlTools`, with the fake content plugin `acme.crawl` naming fixing tools. Auto-links are covered by `TestAutolinksTracer` (the phrases of a page's locale in the resolve answer, never the page's own, `autolink_max` 0) and `TestAutolinks` (the screen, its refusals, Add focus keyphrases, the API and MCP); `TestLinkSuggestions` crawls a planted site and checks E10's counts and suggestions through the API and the widget action. `TestChromeRenderer` renders a page in a real headless Chrome (it runs when `SEO_TEST_CHROME` names a binary or `chromium`, `chromium-browser` or `google-chrome` is on `PATH`, and is skipped otherwise) and `TestRendererGuards` checks the refused paths and private addresses. `TestEveryAdminOperationHasTool`, `TestMCPReadToolsAreGET`, `TestIssueKindLabels` and the static parts of `TestCrawlTools` and `TestCrawlSchedule` run without Docker. diff --git a/admin_navigation.go b/admin_navigation.go index 564df40..5251d5d 100644 --- a/admin_navigation.go +++ b/admin_navigation.go @@ -45,6 +45,7 @@ func (*Plugin) Navigation() []pact.NavigationItem { Permissions: []string{controllers.PermissionManage}, Controller: controllers.PagesControllerID, }, + // Auto-links, controller golem15.seo.autolinks (UI-SPEC C15). { Code: "autolinks", Label: "golem15.seo::lang.nav.autolinks", diff --git a/assets/dist/seo.css b/assets/dist/seo.css index e851e10..9e40b77 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-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)} +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)}golem15-seo-links{display:flex;flex-direction:column;gap:24px}golem15-seo-links .seo-links__counts,golem15-seo-links .seo-links__list{display:flex;flex-direction:column;gap:8px}golem15-seo-links .seo-links__counts{gap:16px}golem15-seo-links .seo-links__meta{margin:0;font-size:13px;line-height:1.5;color:var(--c-muted)}golem15-seo-links .seo-links__heading{margin:0;font-size:14px;font-weight:600;line-height:1.5;color:var(--c-text)}golem15-seo-links .seo-links__row{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:14px;line-height:1.5;color:var(--c-text)}@media(max-width:600px){golem15-seo-links .seo-links__row{flex-direction:column;align-items:flex-start}}golem15-seo-links .seo-links__line{min-width:0;overflow-wrap:anywhere}golem15-seo-links .seo-links__title{color:var(--c-text);text-decoration:underline}golem15-seo-links .seo-links__url{font-family:DM Mono,ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;color:var(--c-muted);user-select:all;overflow-wrap:anywhere}golem15-seo-links .seo-links__copy{flex:none;height:34px;padding:0 12px;border:1px solid var(--c-border-strong);border-radius:8px;background:var(--c-surface);color:var(--c-text);font:inherit;font-size:13px;font-weight:600;cursor:pointer;transition:background-color .15s}golem15-seo-links .seo-links__copy:hover{background:var(--c-subtle)}golem15-seo-links .seo-links__copy:focus-visible,golem15-seo-links .seo-links__title:focus-visible{outline:none;box-shadow:0 0 0 3px var(--c-ring)}golem15-seo-links .seo-links__error{display:flex;align-items:center;gap:8px;margin:0;font-size:13px;line-height:1.5;color:var(--c-danger)}golem15-seo-links .seo-links__status{margin:0;font-size:13px;color:var(--c-muted)}golem15-seo-links .seo-links__status:empty{display:none} diff --git a/assets/dist/seo.js b/assets/dist/seo.js index 2483a74..545b94f 100644 --- a/assets/dist/seo.js +++ b/assets/dist/seo.js @@ -1,2 +1,2 @@ -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}; +const ne={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.",linksInbound:"Links to this page",linksOutbound:"Links from this page",linksCrawled:"From the crawl of :time.",linksNotCrawled:"Link counts appear after the first crawl (SEO › Site audit).",linksOrphanTitle:"No page links here",linksOrphanText:"Search engines and visitors may not find this page. Link to it from one of the pages below.",linksOut:"Pages you could link to from this text",linksIn:"Pages that could link here",linksPhrase:"“:phrase” → :title",linksCopy:"Copy address",linksCopied:"Address copied.",linksNone:"No suggestions. They come from titles and focus keyphrases found in the saved text.",linksNoRecord:"Suggestions appear after the first save.",linksChecking:"Loading the internal links…",linksFailed:"The internal links could not be loaded. Try again.",primaryDefault:"Default (first category)"},de={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.",linksInbound:"Linki do tej strony",linksOutbound:"Linki z tej strony",linksCrawled:"Według skanowania z :time.",linksNotCrawled:"Liczby linków pojawią się po pierwszym skanowaniu (SEO › Audyt witryny).",linksOrphanTitle:"Żadna strona tu nie linkuje",linksOrphanText:"Wyszukiwarki i odwiedzający mogą nie znaleźć tej strony. Dodaj do niej link z jednej ze stron poniżej.",linksOut:"Strony, do których możesz dodać link w tym tekście",linksIn:"Strony, które mogą tu linkować",linksPhrase:"„:phrase” → :title",linksCopy:"Kopiuj adres",linksCopied:"Skopiowano adres.",linksNone:"Brak propozycji. Powstają z tytułów i fraz kluczowych znalezionych w zapisanym tekście.",linksNoRecord:"Propozycje pojawią się po pierwszym zapisie.",linksChecking:"Wczytywanie linków wewnętrznych…",linksFailed:"Nie udało się wczytać linków wewnętrznych. Spróbuj ponownie.",primaryDefault:"Domyślna (pierwsza kategoria)"},ue={en:ne,pl:de};function v(n){return(n.getAttribute("ui-locale")||document.documentElement.lang||"en").toLowerCase().split(/[-_]/)[0]||"en"}function L(n){return ue[n]??ne}function y(n,e={}){const t=Object.keys(e).sort((o,r)=>r.length-o.length);let s=n;for(const o of t)s=s.split(":"+o).join(String(e[o]));return s}function I(n,e,t,s={}){let o="other";try{o=new Intl.PluralRules(t).select(e)}catch{o=e===1?"one":"other"}const r=o==="one"&&n.one||o==="few"&&n.few||o==="many"&&n.many||n.other;return y(r,{...s,count:new Intl.NumberFormat(t).format(e)})}function S(n,e){try{return new Intl.NumberFormat(e).format(n)}catch{return String(n)}}const J="http://www.w3.org/2000/svg",he={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 k(n,e=14){const t=document.createElementNS(J,"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 he[n]??[]){const o=document.createElementNS(J,"path");o.setAttribute("d",s),t.appendChild(o)}return t}function i(n,e="",t){const s=document.createElement(n);return e&&(s.className=e),t!==void 0&&(s.textContent=t),s}function P(n,e,t){const s=n.getAttribute(e);if(s===null||s==="")return t;try{return JSON.parse(s)}catch{return t}}function N(n,e,t){n.dispatchEvent(new CustomEvent(e,{detail:t,bubbles:!0,composed:!0}))}let K=0;function T(n){return K+=1,`${n}-${K}`}class F{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,N(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 vt="golem15-seo-graph";function pe(n){return"https://search.google.com/test/rich-results?url="+encodeURIComponent(n)}function me(n){const e=n.indexOf("#");return e>=0?n.slice(e):n}class fe 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 F(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 L(v(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=i("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=i("p","seo-graph__meta seo-graph__checking",e.graphChecking);r.setAttribute("role","status"),t.push(r)}if(s){t.push(o);const r=i("div","seo-graph__nodes");let a=0;for(const l of s.nodes){const d=i("div","seo-graph__node"),u=i("div","seo-graph__node-head");u.append(i("span","seo-graph__pill",l.type||"—"),i("code","seo-graph__id",me(l.id||""))),d.append(u),l.rules||d.append(i("p","seo-graph__meta",y(e.noRules,{type:l.type})));for(const h of l.findings??[]){a++;const m=h.level==="required",b=i("p","seo-graph__finding seo-graph__finding--"+(m?"required":"recommended"));b.append(k(m?"circle-alert":"triangle-alert"),i("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=i("p","seo-graph__ok");l.append(k("circle-check"),i("span","",e.allGood)),t.push(l)}if(s.graph){const l=i("button","seo-graph__disclosure");l.type="button",l.setAttribute("aria-expanded",String(this.showJSON)),l.append(i("span","",this.showJSON?e.hideJson:e.showJson),k("chevron-down")),l.addEventListener("click",()=>{this.showJSON=!this.showJSON,this.render()}),t.push(l),this.showJSON&&t.push(i("pre","seo-graph__json",JSON.stringify(s.graph,null,2)))}const c=i("div","seo-graph__links");if(s.url){const l=i("a","seo-graph__link");l.href=pe(s.url),l.target="_blank",l.rel="noopener",l.append(i("span","",e.richResults),k("external-link")),c.append(l)}c.append(i("p","seo-graph__meta",e.graphUnsaved),i("p","seo-graph__meta",e.layerNodes)),t.push(c)}if(this.failed){const r=i("p","seo-graph__error");r.setAttribute("role","status"),r.append(k("circle-alert"),i("span","",e.graphFailed)),t.push(r)}if(!this.hasAttribute("disabled")){const r=i("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 ge="golem15-seo-image";function be(n){return/^\/(?![\/\\])/.test(n)||/^https?:\/\//i.test(n)}class we 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="",N(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(v(this))}render(){const e=this.labels(),t=this.hasAttribute("disabled"),s=[];if(this.current&&be(this.current)){const r=i("div","seo-image__preview"),a=i("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=i("div","seo-image__actions"),a=T("seo-image-file"),c=i("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=T("seo-image-upload"),this.error="",this.render(),N(this,"summer-upload",{id:this.pending,file:d}))});const l=i("label","seo-image__button",this.current?e.imageReplace:e.imageUpload);if(l.htmlFor=a,r.append(c,l),this.current){const d=i("button","seo-image__button seo-image__button--secondary",e.imageRemove);d.type="button",d.addEventListener("click",()=>{this.current="",this.error="",N(this,"summer-change",{value:""}),this.render()}),r.append(d)}s.push(r)}const o=i("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 _t="golem15-seo-links";function V(){return typeof navigator<"u"&&!!navigator.clipboard&&typeof navigator.clipboard.writeText=="function"}function ye(n,e){const t=new Date(n);if(Number.isNaN(t.getTime()))return n;try{return new Intl.DateTimeFormat(e,{dateStyle:"medium",timeStyle:"short"}).format(t)}catch{return t.toISOString()}}class ve extends HTMLElement{static observedAttributes=["busy","state","ui-locale","disabled"];data=null;failed=!1;pending=!1;shownLocale="";copied="";copiedTimer;connected=!1;actions=new F(this,()=>this.fail());onResult=e=>{const t=e.detail;this.actions.settle();const s=t?.data;s&&Array.isArray(s.out)&&Array.isArray(s.in)&&(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.load())};connectedCallback(){this.connected=!0,this.addEventListener("summer-result",this.onResult),window.addEventListener("summer:ml-locale",this.onLocale),this.render(),queueMicrotask(()=>{this.connected&&this.load()})}disconnectedCallback(){this.connected=!1,this.removeEventListener("summer-result",this.onResult),window.removeEventListener("summer:ml-locale",this.onLocale),clearTimeout(this.copiedTimer),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}this.render()}}labels(){return L(v(this))}load(){this.pending=!0,this.actions.request({op:"links",locale:this.shownLocale}),this.render()}fail(){this.failed=!0,this.pending=this.actions.current!==null,this.render()}copy(e){V()&&navigator.clipboard.writeText(e).then(()=>{this.copied=e,clearTimeout(this.copiedTimer),this.copiedTimer=setTimeout(()=>{this.copied="",this.render()},2e3),this.render()},()=>{})}line(e,t,s,o){const r=i("span","seo-links__line"),[a,c=""]=e.linksPhrase.split(":title");if(r.append(y(a??"",{phrase:t})),o){const l=i("a","seo-links__title",s);l.href=o,l.target="_blank",l.rel="noopener",r.append(l)}else r.append(i("span","seo-links__title",s));return r.append(y(c,{phrase:t})),r}render(){if(!this.connected)return;const e=this.labels(),t=v(this),s=[];if(this.setAttribute("aria-busy",String(this.pending)),this.pending){const a=i("p","seo-links__meta seo-links__checking",e.linksChecking);a.setAttribute("role","status"),s.push(a)}const o=this.data;if(o){const a=i("div","seo-links__counts");if(o.crawledAt!==null&&o.inbound!==null&&o.outbound!==null){const u=i("div","summer-stats");for(const[h,m]of[[o.inbound,e.linksInbound],[o.outbound,e.linksOutbound]]){const b=i("div","summer-stat");b.append(i("span","summer-stat__value",S(h,t)),i("span","summer-stat__label",m)),u.append(b)}a.append(u,i("p","seo-links__meta",y(e.linksCrawled,{time:ye(o.crawledAt,t)})))}else a.append(i("p","seo-links__meta seo-links__not-crawled",e.linksNotCrawled));if(s.push(a),o.orphan){const u=i("div","summer-callout summer-callout--warning seo-links__orphan");u.append(i("p","summer-callout__title",e.linksOrphanTitle),i("p","summer-callout__text",e.linksOrphanText)),s.push(u)}const c=V(),l=i("div","seo-links__list seo-links__list--out");l.append(i("p","seo-links__heading",e.linksOut));for(const u of o.out.slice(0,10)){const h=i("div","seo-links__row");if(h.append(this.line(e,u.phrase,u.title,u.url)),c&&!this.hasAttribute("disabled")){const m=i("button","seo-links__copy",e.linksCopy);m.type="button",m.addEventListener("click",()=>this.copy(u.url)),h.append(m)}else h.append(i("code","seo-links__url",u.url));l.append(h)}o.out.length===0&&l.append(i("p","seo-links__meta seo-links__none",e.linksNone)),s.push(l);const d=i("div","seo-links__list seo-links__list--in");d.append(i("p","seo-links__heading",e.linksIn));for(const u of o.in.slice(0,10)){const h=i("div","seo-links__row");h.append(this.line(e,u.phrase,u.title,u.adminUrl)),d.append(h)}o.in.length===0&&d.append(i("p","seo-links__meta seo-links__none",e.linksNone)),s.push(d)}if(this.failed){const a=i("p","seo-links__error");a.setAttribute("role","status"),a.append(k("circle-alert"),i("span","",e.linksFailed)),s.push(a)}const r=i("p","seo-links__status",this.copied?e.linksCopied:"");r.setAttribute("role","status"),s.push(r),this.replaceChildren(...s)}}const kt="golem15-seo-parent";function _e(n){if(typeof n=="string")return{selected:n,options:[]};const e={selected:"",options:[]};if(n&&typeof n=="object"){const t=n;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 ke 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=_e(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=L(v(this)),t=i("select","seo-parent__select");t.id=T("seo-parent"),t.disabled=this.hasAttribute("disabled");const s=i("option","",e.primaryDefault);s.value="",t.append(s);for(const o of this.current.options){const r=i("option","",o.label);r.value=o.value,t.append(r)}t.value=this.value,t.addEventListener("change",()=>{this.current.selected=t.value,N(this,"summer-change",{value:t.value})}),this.classList.toggle("seo-parent--invalid",this.hasAttribute("invalid")),this.replaceChildren(t)}}const xe="golem15-seo-robots";function D(){return{index:"",follow:"",noarchive:!1,nosnippet:!1,noimageindex:!1,maxSnippet:null,maxImagePreview:"",maxVideoPreview:null,unavailableAfter:""}}function oe(n){const e=/^(\d{4})-(\d{2})-(\d{2})$/.exec(n);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 ze(n){const e=D(),t=n.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 u=Number.parseInt(l,10);if(u<-1)return null;c==="max-snippet"?e.maxSnippet=u:e.maxVideoPreview=u;break}case"max-image-preview":if(l!=="none"&&l!=="standard"&&l!=="large")return null;e.maxImagePreview=l;break;case"unavailable_after":if(l===null||!oe(l))return null;e.unavailableAfter=l;break;default:return null}}return e}function Ae(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 U(n){return[n.noarchive,n.nosnippet,n.noimageindex,n.maxSnippet!==null,n.maxImagePreview!=="",n.maxVideoPreview!==null,n.unavailableAfter!==""].filter(Boolean).length}class Te 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=ze(t);this.unparsed=s===null,this.model=s??D(),this.open=this.open||U(this.model)>0,this.render()}connectedCallback(){this.open=U(this.model)>0,this.render()}attributeChangedCallback(){this.isConnected&&this.render()}labels(){return L(v(this))}commit(){this.unparsed||(this.current=Ae(this.model),N(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=i("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=T("seo-robots-advanced"),l=i("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(i("span","",e.advanced),k("chevron-down")),l.addEventListener("click",()=>{this.open=!this.open,this.render()});const d=i("div","seo-robots__advanced-row");d.append(l);const u=U(this.model);u>0&&d.append(i("span","seo-robots__summary",I(e.advancedSet,u,v(this))));const h=i("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=i("div","seo-robots__checks");for(const p of["noarchive","nosnippet","noimageindex"]){const f=i("label","seo-robots__check"),g=i("input");g.type="checkbox",g.checked=this.model[p],g.disabled=t,g.dataset.focusKey=p,g.addEventListener("change",()=>{this.model[p]=g.checked,this.commit()}),f.append(g,i("span","",e[p])),m.append(f)}h.append(m);const b=[a,d,h];if(this.unparsed){const p=i("div","seo-robots__unparsed");if(p.setAttribute("role","status"),p.append(k("triangle-alert"),i("span","",y(e.robotsUnparsed,{value:this.current}))),!this.hasAttribute("disabled")){const f=i("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)}b.unshift(p)}this.replaceChildren(...b),r&&this.querySelector(`[data-focus-key="${r}"]`)?.focus()}radioGroup(e,t,s,o){const r=i("div","seo-robots__group"),a=T("seo-robots-caption"),c=i("span","seo-robots__caption",e);c.id=a;const l=i("div","seo-robots__track");l.setAttribute("role","radiogroup"),l.setAttribute("aria-labelledby",a);const d=this.model[t];return s.forEach(([u,h,m],b)=>{const p=i("button","seo-robots__segment seo-robots__segment--"+m,h);p.type="button",p.setAttribute("role","radio");const f=d===u;p.setAttribute("aria-checked",f?"true":"false"),p.tabIndex=f?0:-1,p.disabled=o,p.dataset.focusKey=t+"-"+b,p.addEventListener("click",()=>this.choose(t,u)),p.addEventListener("keydown",g=>{const z=g.key==="ArrowRight"||g.key==="ArrowDown"?1:g.key==="ArrowLeft"||g.key==="ArrowUp"?-1:0;if(z===0||o)return;g.preventDefault();const w=(b+z+s.length)%s.length;this.choose(t,s[w][0]),this.querySelector(`[data-focus-key="${t}-${w}"]`)?.focus()}),l.append(p)}),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=i("label","seo-robots__field");a.append(i("span","seo-robots__label",e));const c=i("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=i("span","seo-robots__error");return d.setAttribute("role","status"),c.addEventListener("change",()=>{const u=c.value.trim();if(u===""){this.model[s]=null,this.commit();return}if(!/^-?\d+$/.test(u)||Number.parseInt(u,10)<-1){d.textContent=r.invalidNumber;return}this.model[s]=Number.parseInt(u,10),this.commit()}),a.append(c),t&&a.append(i("span","seo-robots__hint",t)),a.append(d),a}selectField(e,t){const s=i("label","seo-robots__field");s.append(i("span","seo-robots__label",e.maxImagePreview));const o=i("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=i("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=i("label","seo-robots__field");s.append(i("span","seo-robots__label",e.unavailableAfter));const o=i("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=oe(o.value)?o.value:"",this.commit()}),s.append(o),s}}const Se="golem15-seo-robotstester",Z=["Googlebot","Googlebot-Image","Bingbot","*"];function Ce(n,e){const t={agent:e.agent,line:e.line,rule:e.rule};return e.allowed?e.line>0?y(n.allowedRule,t):y(n.allowedNone,t):e.bot?y(n.blockedBot,t):y(n.blocked,t)}class Le extends HTMLElement{static observedAttributes=["busy","state","ui-locale","disabled","label","busy-label"];context=null;result=null;error="";actions=new F(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(v(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=v(this)+"|"+(this.context?.agents??Z).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=i("div","seo-robotstester__row"),c=T("seo-robotstester-url"),l=i("div","seo-robotstester__field seo-robotstester__field--url"),d=i("label","seo-robotstester__label",s.url);d.htmlFor=c;const u=i("input","seo-robotstester__input");u.id=c,u.type="text",u.placeholder=s.urlPlaceholder,u.value=o,u.addEventListener("keydown",w=>{w.key==="Enter"&&(w.preventDefault(),this.test())}),l.append(d,u);const h=T("seo-robotstester-agent"),m=i("div","seo-robotstester__field seo-robotstester__field--agent"),b=i("label","seo-robotstester__label",s.agent);b.htmlFor=h;const p=i("select","seo-robotstester__select");p.id=h;for(const w of this.context?.agents??Z){const x=i("option","",w);x.value=w,p.append(x)}p.value=r,p.value!==r&&(p.value="Googlebot"),m.append(b,p);const f=i("button","seo-robotstester__button");f.type="button",f.addEventListener("click",()=>this.test()),a.append(l,m,f);const g=i("div","seo-robotstester__result");g.setAttribute("role","status");const z=i("div","seo-robotstester__notes");this.input=u,this.select=p,this.button=f,this.line=g,this.notes=z,this.append(a,g,z),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(i("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(k(s?"circle-check":"triangle-alert"),i("span","",Ce(e,this.result)))}else this.line.replaceChildren();const t=[];(this.context?.staging||P(this,"data",{}).staging)&&t.push(i("p","seo-robotstester__note",e.stagingNote)),t.push(i("p","seo-robotstester__note",e.unsaved)),this.notes.replaceChildren(...t)}}const Ne="golem15-seo-robotstxt",Ee=500*1024,H=10,Pe=["User-agent","Allow","Disallow","Sitemap","Crawl-delay","Host","Clean-param"];function je(n,e){const t=Array.from({length:e.length+1},(s,o)=>o);for(let s=1;s<=n.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+(n[s-1]===e[r-1]?0:1)),o=a}}return t[e.length]}function Re(n){const e=n.toLowerCase();let t="",s=3;for(const o of Pe){const r=je(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(),u=c.slice(l+1).trim();switch(d.toLowerCase()){case"user-agent":t=!0;return;case"allow":case"disallow":t?u!==""&&!u.startsWith("/")&&!u.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=Re(d);e.push(h?{kind:"unknownSuggest",line:r,word:d,suggestion:h}:{kind:"unknown",line:r,word:d})}),e}function De(n){return new TextEncoder().encode(n).length}function Ie(n,e){return y(n[e.kind],{line:e.line,word:e.word??"",suggestion:e.suggestion??""})}class Fe 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(v(this))}build(){const e=v(this);if(this.builtFor===e&&this.control){this.sync();return}this.builtFor=e,this.replaceChildren();const t=T("seo-robotstxt-lint"),s=i("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(),N(this,"summer-change",{value:this.current}))});const r=i("div","seo-robotstxt__lint");r.id=t,r.setAttribute("role","status");const a=i("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=v(this),s=Oe(this.current),o=[];if(s.length===0){const l=i("div","seo-robotstxt__finding seo-robotstxt__finding--clean");l.append(k("circle-check"),i("span","",e.clean)),o.push(l)}else{for(const l of s.slice(0,H)){const d=i("div","seo-robotstxt__finding");d.append(k("triangle-alert"),i("span","",Ie(e,l))),o.push(d)}s.length>H&&o.push(i("div","seo-robotstxt__more",y(e.moreFindings,{count:S(s.length-H,t)})))}this.panel.replaceChildren(...o);const r=De(this.current),a=new Intl.NumberFormat(t,{maximumFractionDigits:1}).format(r/1024)+" KB";this.meter.replaceChildren(i("span","",y(e.size,{size:a})));const c=r>Ee;this.classList.toggle("seo-robotstxt--over",c),c&&this.meter.append(i("span","seo-robotstxt__over",e.tooLarge))}}const xt="golem15-seo-schema",Me=16*1024;function $(n){switch(n){case"article":return["BlogPosting","Article","NewsArticle"];case"collection":return["CollectionPage"];case"profile":return["ProfilePage"]}return["WebPage","AboutPage","ContactPage","Service"]}function Ue(n){const e={type:"",fields:{},custom:""};if(typeof n!="string"||n.trim()==="")return e;try{const t=JSON.parse(n);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 He(n){const e={};for(const s of["serviceType","areaServed","price","currency"]){const o=(n.fields[s]??"").trim();o!==""&&(e[s]=o)}const t=n.custom.trim();return n.type===""&&t===""&&Object.keys(e).length===0?"":JSON.stringify({type:n.type,fields:e,custom:t})}function Be(n){return n.trim()===""||/^\d+([.,]\d{1,2})?$/.test(n.trim())}function X(n){return n.trim()===""||/^[A-Za-z]{3}$/.test(n.trim())}function Y(n,e){const s=n.slice(0,Math.max(0,Math.min(e,n.length))).split(` +`);return{line:s.length,column:s[s.length-1].length+1}}function Q(n){if(n.trim()==="")return{kind:"empty"};if(new TextEncoder().encode(n.trim()).length>Me)return{kind:"tooLarge"};let e;try{e=JSON.parse(n)}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?Y(n,Number(a[1])):Y(n,n.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 ee=(n,e)=>n["t"+e]??e,We=(n,e)=>{switch(e){case"BlogPosting":case"Article":case"NewsArticle":return n.noteArticle;case"ContactPage":return n.noteContact;case"Service":return n.noteService;case"CollectionPage":return n.noteCollection;case"ProfilePage":return n.noteProfile}return n.notePage};class Ge 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 He(this.current)}set value(e){this.current=Ue(e),this.status=Q(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 L(v(this))}changed(){N(this,"summer-change",{value:this.value})}effectiveType(){const e=$(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=$(this.kind()),o=[],r=i("div","seo-schema__section"),a=T("seo-schema-type"),c=i("label","seo-schema__label",e.schemaType);if(c.htmlFor=a,s.length>1){const m=i("select","seo-schema__select");m.id=a,m.disabled=t;const b=i("option","",y(e.typeDefault,{type:s[0]}));b.value="",m.append(b);for(const p of s){const f=i("option","",ee(e,p));f.value=p,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=i("div","seo-schema__fixed",ee(e,s[0]));m.id=a,r.append(c,m)}if(r.append(i("p","seo-schema__note",We(e,this.effectiveType()))),o.push(r),this.effectiveType()==="Service"){const m=i("div","seo-schema__grid"),b=(p,f,g,z="",w="")=>{const x=i("div","seo-schema__cell"),_=T("seo-schema-"+p),C=i("label","seo-schema__label",f);C.htmlFor=_;const A=i("input","seo-schema__input");A.id=_,A.type="text",A.disabled=t,A.value=this.current.fields[p]??"",z&&(A.placeholder=z),w&&(A.inputMode=w),p==="currency"&&(A.maxLength=3);const R=i("p","seo-schema__error",this.fieldErrors[p]??"");R.setAttribute("role","status"),A.addEventListener("input",()=>{const M=A.value;(p==="price"?Be(M):p==="currency"?X(M):!0)?(this.current.fields[p]=M,delete this.fieldErrors[p],R.textContent="",this.changed()):(this.fieldErrors[p]=p==="price"?e.priceInvalid:e.currencyInvalid,R.textContent=this.fieldErrors[p])}),p==="currency"&&A.addEventListener("blur",()=>{A.value=A.value.toUpperCase(),X(A.value)&&(this.current.fields.currency=A.value,this.changed())}),x.append(C,A),g&&x.append(i("p","seo-schema__note",g)),x.append(R),m.append(x)};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=i("div","seo-schema__section"),d=i("button","seo-schema__disclosure");d.type="button",d.setAttribute("aria-expanded",String(this.open));const u=T("seo-schema-custom");d.setAttribute("aria-controls",u),d.append(i("span","",e.custom),k("chevron-down")),d.addEventListener("click",()=>{this.open=!this.open,this.render()});const h=i("div","seo-schema__disclosure-row");if(h.append(d),this.current.custom.trim()!==""&&h.append(i("span","seo-schema__note",e.customSet)),l.append(h),this.open){const m=i("div","seo-schema__panel");m.id=u;const b=T("seo-schema-json"),p=i("p","seo-schema__note",e.customHint);p.id=b+"-hint";const f=i("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",p.id);const g=i("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=Q(f.value),this.statusLine(g,e)},300)}),m.append(p,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(k("circle-check"),i("span","",I(t.jsonValid,s.nodes,v(this))));return;case"error":e.classList.add("seo-schema__status--error"),e.replaceChildren(k("circle-alert"),i("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(k("circle-alert"),i("span","",t.jsonNotObject));return;case"tooLarge":e.classList.add("seo-schema__status--error"),e.replaceChildren(k("circle-alert"),i("span","",t.jsonTooLarge))}}}const re="20px Arial, sans-serif",ae="14px Arial, sans-serif",le=580,ce=920,qe=680,Je=30,Ke=120;let j;function Ve(){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 Ze(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 G(n,e){const t=Ve();if(t){t.font=e;const o=t.measureText(n).width;if(Number.isFinite(o)&&(o>0||n===""))return o}const s=Number.parseFloat(e)||14;return Ze(n,s)}const B=" …";function te(n,e,t){if(G(n,e)<=t)return n;const s=h=>G(h+B,e)<=t,o=Array.from(n),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("")+B;let d=0,u=o.length;for(;ds?l="long":a=o}function O(n,e){const t=se(n);if(t.length===0)return!1;const s=se(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 L(v(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,Qe)}render(){const e=this.labels(),t=v(this);if(!this.context&&!this.contextFailed){this.setAttribute("aria-busy","true"),this.replaceChildren(i("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??"",u=a||tt(s?.titleTemplate??"",{title:s?.defaultTitle??"",site:d})||s?.defaultTitle||"",h=c||s?.defaultDescription||"",m=i("div","seo-serp__header"),b=i("div","seo-serp__track");b.setAttribute("role","radiogroup"),b.setAttribute("aria-label",e.device);for(const _ of["desktop","mobile"]){const C=i("button","seo-serp__segment",e[_]);C.type="button",C.setAttribute("role","radio"),C.setAttribute("aria-checked",this.device===_?"true":"false"),C.addEventListener("click",()=>{this.device=_,this.render()}),b.append(C)}m.append(b),o&&m.append(i("span","seo-serp__language",y(e.language,{locale:o.toUpperCase()})));const p=i("div","serp-card"+(this.device==="mobile"?" serp-card--mobile":"")),f=i("div","serp-card__site");f.append(i("span","serp-card__favicon",Array.from(d||this.context?.host||"?")[0]?.toUpperCase()??"?"));const g=i("div","serp-card__names");if(g.append(i("span","serp-card__sitename",d||this.context?.host||"")),g.append(i("span","serp-card__crumbs",ot(this.context?.host??"",s?.url??null))),f.append(g),p.append(f),p.append(i("div","serp-card__title",te(u,re,le))),h){const _=this.device==="mobile"?qe:ce;p.append(i("div","serp-card__description",te(h,ae,_)))}else p.append(i("div","serp-card__description serp-card__description--missing",e.noDescription));const z=i("div","seo-serp__notes");if(this.contextFailed){const _=i("div","seo-serp__error");_.setAttribute("role","status"),_.append(k("circle-alert"),i("span","",e.contextFailed)),z.append(_)}!a&&s?.titleTemplate&&z.append(i("div","",y(e.fromTemplate,{template:s.titleTemplate}))),!c&&s?.defaultDescription&&s.descriptionSource==="content"&&z.append(i("div","",e.fromContent)),s&&s.url===null&&z.append(i("div","",e.noUrl)),(this.getAttribute("record-id")??"")!==""&&s&&z.append(i("div","",e.savedNote));const w=i("div","seo-serp__checks");if(w.setAttribute("role","status"),this.checking)w.append(i("div","seo-serp__checking",e.checking));else{const _=this.duplicates.get(o);if(_)for(const[C,A]of[[_.title,e.dupTitle],[_.description,e.dupDescription]])C.length>0&&w.append(rt(C,A,e,t))}const x=[m,p,z,w];l&&s&&x.push(at(l,{title:u,description:h,h1:s.h1,url:s.url,bodyWords:s.bodyWords},e,t)),this.replaceChildren(...x)}}function ot(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 rt(n,e,t,s){const o=i("div","seo-serp__check seo-serp__check--warn");o.append(k("triangle-alert"));const r=i("span"),a="\0list\0",c=I(e,n.length,s,{list:a}),[l,d]=c.split(a);return r.append(document.createTextNode(l??"")),n.slice(0,W).forEach((u,h)=>{if(h>0&&r.append(document.createTextNode(", ")),u.url.startsWith("/")&&!u.url.startsWith("//")){const m=i("a","seo-serp__link",u.label);m.href=u.url,m.target="_blank",m.rel="noopener",r.append(m)}else r.append(document.createTextNode(u.label))}),n.length>W&&r.append(document.createTextNode(" "+y(t.andMore,{count:n.length-W}))),r.append(document.createTextNode(d??"")),o.append(r),o}function at(n,e,t,s){const o=i("div","seo-serp__keyphrase");o.append(i("div","seo-serp__keyphrase-title",y(t.keyphrase,{phrase:n})));const r=(a,c,l)=>{const d=i("div","seo-serp__check "+(a?"seo-serp__check--ok":"seo-serp__check--warn"));d.append(k(a?"circle-check":"triangle-alert"),i("span","",a?c:l)),o.append(d)};if(r(O(n,e.title),t.kpTitle,t.kpTitleNo),e.h1!==null&&r(O(n,e.h1),t.kpH1,t.kpH1No),r(O(n,e.description),t.kpDesc,t.kpDescNo),e.url!==null&&r(O(n,e.url.replace(/[-_/]+/g," ")),t.kpUrl,t.kpUrlNo),e.bodyWords!==null){const a=new Intl.NumberFormat(s).format(e.bodyWords);r(e.bodyWords>=et,y(t.kpLength,{count:a}),y(t.kpLengthNo,{count:a}))}return o}const lt="golem15-seo-text",ct={empty:"info",short:"triangle-alert",long:"triangle-alert",good:"circle-check"};class dt 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 L(v(this))}build(){const e=this.measure()+"|"+v(this);if(this.builtFor===e&&this.control){this.sync();return}this.builtFor=e,this.replaceChildren();const t=T("seo-text-counter"),o=this.measure()==="description"?i("textarea","seo-text__control seo-text__control--area"):i("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),N(this,"summer-change",{value:this.current}))});const a=i("div","seo-text__counter");a.id=t;const c=i("span","seo-text__status");this.statusIcon=i("span","seo-text__icon"),this.statusText=i("span","seo-text__status-text"),c.append(this.statusIcon,this.statusText),this.countText=i("span","seo-text__count"),a.append(c,this.countText),this.live=i("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=v(this),o=this.measure(),r=Xe(this.current,o),a=ut(t,r.status,o);this.statusIcon.replaceChildren(k(ct[r.status])),this.statusText.textContent=a,this.dataset.status=r.status,this.countText.textContent=y(t.count,{chars:S(r.chars,s),px:S(r.px,s),max:S(r.maxPx,s)}),e&&this.live&&r.status!==this.lastStatus&&(this.live.textContent=a),this.lastStatus=r.status}}function ut(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 ht="golem15-seo-transfer",ie=60*1024,q=200;function pt(n){return n.summary.refused>0?"refused":n.count>0?"ready":"nothing"}function mt(n){const e=n.filter(s=>s.change==="refused"),t=n.filter(s=>s.change!=="refused");return[...e,...t].slice(0,q)}function ft(n,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=n,o.rel="noopener",document.body.append(o),o.click(),o.remove(),setTimeout(()=>URL.revokeObjectURL(s),0)}function gt(){return window.location.pathname.replace(/\/transfer(\/.*)?$/,"/history")}class bt 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="",ft(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(v(this))}request(e){this.hasAttribute("disabled")||this.hasAttribute("busy")||this.pending||(this.pending=e.op,N(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=i("div","seo-transfer__field"),r=T("seo-transfer-dataset"),a=i("label","seo-transfer__label",e.dataset);a.htmlFor=r;const c=i("select","seo-transfer__select");c.id=r;const l=[["redirects",e.dsRedirects],["rules",e.dsRules],["records",e.dsRecords]];for(const[d,u]of l){const h=i("option","",u);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=i("button","seo-transfer__button seo-transfer__button--"+t,e);return o.type="button",o.addEventListener("click",s),o}message(e,t){const s=i("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>ie){this.fileError=y(this.labels().fileTooLarge,{limit:S(ie/1024,v(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=v(this),s=this.hasAttribute("disabled"),o=this.hasAttribute("busy")||this.pending!==null,r=this.getAttribute("label")||e.chooseFile,a=i("section","seo-transfer__section");a.append(i("h3","seo-transfer__heading",e.exportHeading));const c=i("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=i("section","seo-transfer__section seo-transfer__section--import");d.append(i("h3","seo-transfer__heading",e.importHeading));const u=i("div","seo-transfer__row");u.append(this.datasetSelect(e,this.importDataset,f=>{this.importDataset=f,this.preview=null,this.render()}));const h=i("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=i("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=o||this.csv===""||this.fileError!=="",p.hidden=s,u.append(h,m,b,p),d.append(u),(this.fileError||this.importError)&&d.append(this.message(this.fileError||this.importError,"seo-transfer__error")),this.applied){const f=i("p","seo-transfer__done"),g=i("a","seo-transfer__link",e.openHistory);g.href=gt(),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=i("div","seo-transfer__preview"),c=r.summary;a.append(this.message(y(e.summary,{new:S(c.new,t),changed:S(c.changed,t),unchanged:S(c.unchanged,t),refused:S(c.refused,t)}),"seo-transfer__summary"));const l=i("div","summer-table-wrap seo-transfer__table-wrap"),d=i("table","summer-table seo-transfer__table"),u=i("tr");for(const w of[e.colLine,e.colChange,e.colAddress,e.colField,e.colBefore,e.colAfter,e.colNote])u.append(i("th","",w));const h=i("thead");h.append(u);const m=i("tbody"),b={new:e.bNew,changed:e.bChanged,unchanged:e.bUnchanged,refused:e.bRefused};for(const w of mt(r.rows)){const x=i("tr","seo-transfer__tr--"+w.change);x.append(i("td","seo-transfer__num",S(w.line,t)));const _=i("td");_.append(i("span","seo-transfer__badge seo-transfer__badge--"+w.change,b[w.change])),w.warning&&_.append(i("span","seo-transfer__badge seo-transfer__badge--warning",e.bWarning)),x.append(_),x.append(i("td","seo-transfer__mono",w.address)),x.append(i("td","",w.field)),x.append(i("td","",w.before)),x.append(i("td","",w.after)),x.append(i("td","",w.note)),m.append(x)}d.append(h,m),l.append(d),a.append(l),(r.total>q||r.rows.length>q)&&a.append(i("p","seo-transfer__note",y(e.rowCap,{count:S(Math.max(r.total,r.rows.length),t)})));const p=pt(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=i("div","seo-transfer__actions"),g=this.button(e.discard,"ghost",()=>{this.preview=null,this.applyError="",this.render()});g.disabled=s;const z=this.button(this.busyLabel("apply",I(e.applyCount,r.count,t)),"primary",()=>this.request({op:"apply",token:r.token}));return z.disabled=s||p!=="ready",f.append(g,z),f.hidden=o,a.append(f),a}}const wt=[[lt,dt],[xe,Te],[Ye,nt],[ge,we],[Ne,Fe],[Se,Le],[ht,bt],["golem15-seo-schema",Ge],["golem15-seo-graph",fe],["golem15-seo-parent",ke],["golem15-seo-links",ve]];for(const[n,e]of wt)customElements.get(n)||customElements.define(n,e);export{vt as GRAPH_TAG,ge as IMAGE_TAG,_t as LINKS_TAG,kt as PARENT_TAG,Se as ROBOTSTESTER_TAG,Ne as ROBOTSTXT_TAG,xe as ROBOTS_TAG,xt as SCHEMA_TAG,Ye as SERP_TAG,fe as SeoGraphElement,we as SeoImageElement,ve as SeoLinksElement,ke as SeoParentElement,Te as SeoRobotsElement,Le as SeoRobotsTesterElement,Fe as SeoRobotsTxtElement,Ge as SeoSchemaElement,nt as SeoSerpElement,dt as SeoTextElement,bt as SeoTransferElement,lt as TEXT_TAG,ht as TRANSFER_TAG}; diff --git a/assets/src/elements/index.ts b/assets/src/elements/index.ts index dc90c12..7cf15e6 100644 --- a/assets/src/elements/index.ts +++ b/assets/src/elements/index.ts @@ -1,6 +1,7 @@ import './styles.css' import { GRAPH_TAG, SeoGraphElement } from './graph' import { IMAGE_TAG, SeoImageElement } from './image' +import { LINKS_TAG, SeoLinksElement } from './links' import { PARENT_TAG, SeoParentElement } from './parent' import { ROBOTS_TAG, SeoRobotsElement } from './robots' import { ROBOTSTESTER_TAG, SeoRobotsTesterElement } from './robotstester' @@ -24,6 +25,7 @@ const elements: Array<[string, CustomElementConstructor]> = [ ['golem15-seo-schema', SeoSchemaElement], ['golem15-seo-graph', SeoGraphElement], ['golem15-seo-parent', SeoParentElement], + ['golem15-seo-links', SeoLinksElement], ] for (const [tag, ctor] of elements) { if (!customElements.get(tag)) { @@ -34,9 +36,11 @@ for (const [tag, ctor] of elements) { export { GRAPH_TAG, IMAGE_TAG, + LINKS_TAG, PARENT_TAG, SCHEMA_TAG, SeoGraphElement, + SeoLinksElement, SeoParentElement, SeoSchemaElement, ROBOTS_TAG, diff --git a/assets/src/elements/labels.ts b/assets/src/elements/labels.ts index 3c20237..aaefe09 100644 --- a/assets/src/elements/labels.ts +++ b/assets/src/elements/labels.ts @@ -1,5 +1,6 @@ // Element copy of the SEO field elements (UI-SPEC copywriting E1 to E6, -// E7 for the social image, E8 and E9 for structured data), chosen by the ui-locale attribute. Plural +// E7 for the social image, E8 and E9 for structured data, E10 for internal +// links), 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. */ @@ -184,6 +185,22 @@ export interface Labels { layerNodes: string graphAction: string graphFailed: string + // E10 golem15-seo-links + linksInbound: string + linksOutbound: string + linksCrawled: string + linksNotCrawled: string + linksOrphanTitle: string + linksOrphanText: string + linksOut: string + linksIn: string + linksPhrase: string + linksCopy: string + linksCopied: string + linksNone: string + linksNoRecord: string + linksChecking: string + linksFailed: string // golem15-seo-parent primaryDefault: string } @@ -353,6 +370,21 @@ const en: Labels = { 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.', + linksInbound: 'Links to this page', + linksOutbound: 'Links from this page', + linksCrawled: 'From the crawl of :time.', + linksNotCrawled: 'Link counts appear after the first crawl (SEO › Site audit).', + linksOrphanTitle: 'No page links here', + linksOrphanText: 'Search engines and visitors may not find this page. Link to it from one of the pages below.', + linksOut: 'Pages you could link to from this text', + linksIn: 'Pages that could link here', + linksPhrase: '“:phrase” → :title', + linksCopy: 'Copy address', + linksCopied: 'Address copied.', + linksNone: 'No suggestions. They come from titles and focus keyphrases found in the saved text.', + linksNoRecord: 'Suggestions appear after the first save.', + linksChecking: 'Loading the internal links…', + linksFailed: 'The internal links could not be loaded. Try again.', primaryDefault: 'Default (first category)', } @@ -531,6 +563,21 @@ const pl: Labels = { 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.', + linksInbound: 'Linki do tej strony', + linksOutbound: 'Linki z tej strony', + linksCrawled: 'Według skanowania z :time.', + linksNotCrawled: 'Liczby linków pojawią się po pierwszym skanowaniu (SEO › Audyt witryny).', + linksOrphanTitle: 'Żadna strona tu nie linkuje', + linksOrphanText: 'Wyszukiwarki i odwiedzający mogą nie znaleźć tej strony. Dodaj do niej link z jednej ze stron poniżej.', + linksOut: 'Strony, do których możesz dodać link w tym tekście', + linksIn: 'Strony, które mogą tu linkować', + linksPhrase: '„:phrase” → :title', + linksCopy: 'Kopiuj adres', + linksCopied: 'Skopiowano adres.', + linksNone: 'Brak propozycji. Powstają z tytułów i fraz kluczowych znalezionych w zapisanym tekście.', + linksNoRecord: 'Propozycje pojawią się po pierwszym zapisie.', + linksChecking: 'Wczytywanie linków wewnętrznych…', + linksFailed: 'Nie udało się wczytać linków wewnętrznych. Spróbuj ponownie.', primaryDefault: 'Domyślna (pierwsza kategoria)', } diff --git a/assets/src/elements/links.ts b/assets/src/elements/links.ts new file mode 100644 index 0000000..c6f50eb --- /dev/null +++ b/assets/src/elements/links.ts @@ -0,0 +1,261 @@ +import { ActionQueue } from './actions' +import { el, fmt, icon, labelsFor, num, uiLocaleOf, type Labels } from './labels' + +export const LINKS_TAG = 'golem15-seo-links' + +/** A page this text could link to. */ +export interface LinkOut { + phrase: string + title: string + url: string +} + +/** A page that could link here. */ +export interface LinkIn { + phrase: string + title: string + adminUrl: string +} + +/** The seo_links action's answer (UI-SPEC C19). */ +export interface LinksData { + crawledAt: string | null + inbound: number | null + outbound: number | null + orphan: boolean + out: LinkOut[] + in: LinkIn[] +} + +interface Payload { + op: 'links' + locale: string +} + +/** Whether the page can write to the clipboard (a secure context). */ +function canCopy(): boolean { + return typeof navigator !== 'undefined' && !!navigator.clipboard && typeof navigator.clipboard.writeText === 'function' +} + +/** A crawl time in the ui-locale. */ +export function crawlTime(value: string, locale: string): string { + const d = new Date(value) + if (Number.isNaN(d.getTime())) { + return value + } + try { + return new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'short' }).format(d) + } catch { + return d.toISOString() + } +} + +/** + * E10 golem15-seo-links (UI-SPEC C19, D-82): the page's inbound and + * outbound link counts from the last crawl, the orphan warning, up to 10 + * pages this text could link to (with Copy address) and 10 pages that + * could link here. It asks through summer-action only ({op: links, + * locale}), on connect and on the ML locale switch, and changes nothing + * itself. + */ +export class SeoLinksElement extends HTMLElement { + static observedAttributes = ['busy', 'state', 'ui-locale', 'disabled'] + + private data: LinksData | null = null + private failed = false + private pending = false + private shownLocale = '' + private copied = '' + private copiedTimer: 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 LinksData | undefined + if (data && Array.isArray(data.out) && Array.isArray(data.in)) { + 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.load() + } + } + + connectedCallback(): void { + this.connected = true + this.addEventListener('summer-result', this.onResult) + window.addEventListener('summer:ml-locale', this.onLocale) + this.render() + queueMicrotask(() => { + if (this.connected) { + this.load() + } + }) + } + + disconnectedCallback(): void { + this.connected = false + this.removeEventListener('summer-result', this.onResult) + window.removeEventListener('summer:ml-locale', this.onLocale) + clearTimeout(this.copiedTimer) + 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 + } + this.render() + } + + private labels(): Labels { + return labelsFor(uiLocaleOf(this)) + } + + /** Asks for the links of the shown locale (one queued request at most). */ + load(): void { + this.pending = true + this.actions.request({ op: 'links', locale: this.shownLocale }) + this.render() + } + + private fail(): void { + this.failed = true + this.pending = this.actions.current !== null + this.render() + } + + /** Writes url to the clipboard and says so for 2 s. */ + private copy(url: string): void { + if (!canCopy()) { + return + } + void navigator.clipboard.writeText(url).then(() => { + this.copied = url + clearTimeout(this.copiedTimer) + this.copiedTimer = setTimeout(() => { + this.copied = '' + this.render() + }, 2000) + this.render() + }, () => undefined) + } + + /** The suggestion line “:phrase” → :title with the title as a link (or text). */ + private line(labels: Labels, phrase: string, title: string, href: string): HTMLElement { + const line = el('span', 'seo-links__line') + const [before, after = ''] = labels.linksPhrase.split(':title') + line.append(fmt(before ?? '', { phrase })) + if (href) { + const a = el('a', 'seo-links__title', title) + a.href = href + a.target = '_blank' + a.rel = 'noopener' + line.append(a) + } else { + line.append(el('span', 'seo-links__title', title)) + } + line.append(fmt(after, { phrase })) + return line + } + + private render(): void { + if (!this.connected) { + return + } + const labels = this.labels() + const locale = uiLocaleOf(this) + const sections: HTMLElement[] = [] + this.setAttribute('aria-busy', String(this.pending)) + if (this.pending) { + const line = el('p', 'seo-links__meta seo-links__checking', labels.linksChecking) + line.setAttribute('role', 'status') + sections.push(line) + } + const data = this.data + if (data) { + const counts = el('div', 'seo-links__counts') + if (data.crawledAt !== null && data.inbound !== null && data.outbound !== null) { + const stats = el('div', 'summer-stats') + for (const [value, label] of [ + [data.inbound, labels.linksInbound], + [data.outbound, labels.linksOutbound], + ] as Array<[number, string]>) { + const stat = el('div', 'summer-stat') + stat.append(el('span', 'summer-stat__value', num(value, locale)), el('span', 'summer-stat__label', label)) + stats.append(stat) + } + counts.append(stats, el('p', 'seo-links__meta', fmt(labels.linksCrawled, { time: crawlTime(data.crawledAt, locale) }))) + } else { + counts.append(el('p', 'seo-links__meta seo-links__not-crawled', labels.linksNotCrawled)) + } + sections.push(counts) + if (data.orphan) { + const callout = el('div', 'summer-callout summer-callout--warning seo-links__orphan') + callout.append(el('p', 'summer-callout__title', labels.linksOrphanTitle), el('p', 'summer-callout__text', labels.linksOrphanText)) + sections.push(callout) + } + const copyable = canCopy() + const out = el('div', 'seo-links__list seo-links__list--out') + out.append(el('p', 'seo-links__heading', labels.linksOut)) + for (const s of data.out.slice(0, 10)) { + const row = el('div', 'seo-links__row') + row.append(this.line(labels, s.phrase, s.title, s.url)) + if (copyable && !this.hasAttribute('disabled')) { + const button = el('button', 'seo-links__copy', labels.linksCopy) + button.type = 'button' + button.addEventListener('click', () => this.copy(s.url)) + row.append(button) + } else { + row.append(el('code', 'seo-links__url', s.url)) + } + out.append(row) + } + if (data.out.length === 0) { + out.append(el('p', 'seo-links__meta seo-links__none', labels.linksNone)) + } + sections.push(out) + const inbound = el('div', 'seo-links__list seo-links__list--in') + inbound.append(el('p', 'seo-links__heading', labels.linksIn)) + for (const s of data.in.slice(0, 10)) { + const row = el('div', 'seo-links__row') + row.append(this.line(labels, s.phrase, s.title, s.adminUrl)) + inbound.append(row) + } + if (data.in.length === 0) { + inbound.append(el('p', 'seo-links__meta seo-links__none', labels.linksNone)) + } + sections.push(inbound) + } + if (this.failed) { + const line = el('p', 'seo-links__error') + line.setAttribute('role', 'status') + line.append(icon('circle-alert'), el('span', '', labels.linksFailed)) + sections.push(line) + } + const status = el('p', 'seo-links__status', this.copied ? labels.linksCopied : '') + status.setAttribute('role', 'status') + sections.push(status) + this.replaceChildren(...sections) + } +} diff --git a/assets/src/elements/styles.css b/assets/src/elements/styles.css index 594c6e6..64e4217 100644 --- a/assets/src/elements/styles.css +++ b/assets/src/elements/styles.css @@ -1126,3 +1126,116 @@ golem15-seo-graph .seo-graph__button { golem15-seo-graph .seo-graph__button:hover { background: var(--c-subtle); } + +/* E10 golem15-seo-links (UI-SPEC C19) */ +golem15-seo-links { + display: flex; + flex-direction: column; + gap: 24px; +} + +golem15-seo-links .seo-links__counts, +golem15-seo-links .seo-links__list { + display: flex; + flex-direction: column; + gap: 8px; +} + +golem15-seo-links .seo-links__counts { + gap: 16px; +} + +golem15-seo-links .seo-links__meta { + margin: 0; + font-size: 13px; + line-height: 1.5; + color: var(--c-muted); +} + +golem15-seo-links .seo-links__heading { + margin: 0; + font-size: 14px; + font-weight: 600; + line-height: 1.5; + color: var(--c-text); +} + +golem15-seo-links .seo-links__row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + font-size: 14px; + line-height: 1.5; + color: var(--c-text); +} + +@media (max-width: 600px) { + golem15-seo-links .seo-links__row { + flex-direction: column; + align-items: flex-start; + } +} + +golem15-seo-links .seo-links__line { + min-width: 0; + overflow-wrap: anywhere; +} + +golem15-seo-links .seo-links__title { + color: var(--c-text); + text-decoration: underline; +} + +golem15-seo-links .seo-links__url { + font-family: 'DM Mono', ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 13px; + color: var(--c-muted); + user-select: all; + overflow-wrap: anywhere; +} + +golem15-seo-links .seo-links__copy { + flex: none; + height: 34px; + padding: 0 12px; + border: 1px solid var(--c-border-strong); + border-radius: 8px; + background: var(--c-surface); + color: var(--c-text); + font: inherit; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background-color 150ms; +} + +golem15-seo-links .seo-links__copy:hover { + background: var(--c-subtle); +} + +golem15-seo-links .seo-links__copy:focus-visible, +golem15-seo-links .seo-links__title:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--c-ring); +} + +golem15-seo-links .seo-links__error { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + font-size: 13px; + line-height: 1.5; + color: var(--c-danger); +} + +golem15-seo-links .seo-links__status { + margin: 0; + font-size: 13px; + color: var(--c-muted); +} + +golem15-seo-links .seo-links__status:empty { + display: none; +} diff --git a/classes/crawl/fix.go b/classes/crawl/fix.go index 0574c38..17a1dd9 100644 --- a/classes/crawl/fix.go +++ b/classes/crawl/fix.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "strings" + "git.golem15.com/golem15/sm-seo-plugin/classes" "git.golem15.com/golem15/sm-seo-plugin/models" "git.golem15.com/golem15/summercms/modules/pact" ) @@ -142,4 +143,4 @@ func Fingerprint(kind, url, locale, check string) string { } // normal is text compared without case and extra whitespace. -func normal(s string) string { return strings.ToLower(strings.Join(strings.Fields(s), " ")) } +func normal(s string) string { return classes.NormalText(s) } diff --git a/classes/linkgraph.go b/classes/linkgraph.go new file mode 100644 index 0000000..0fd9685 --- /dev/null +++ b/classes/linkgraph.go @@ -0,0 +1,364 @@ +package classes + +import ( + "context" + "encoding/json" + "errors" + "net/url" + "strconv" + "strings" + "time" + + "git.golem15.com/golem15/sm-seo-plugin/models" + "git.golem15.com/golem15/summercms/modules/cabana" + "git.golem15.com/golem15/summercms/modules/pact" + "gorm.io/gorm" +) + +// MaxLinkSuggestions caps each suggestion list of the internal links +// widget (UI-SPEC C19). +const MaxLinkSuggestions = 10 + +// minLinkPhrase is the shortest phrase a link suggestion matches, in +// bytes (the orphan suggestions' limit). +const minLinkPhrase = 3 + +// LinksActionName is the SEO tab's internal links widget action (E10). +const LinksActionName = "seo_links" + +// LinkOut is a page this text could link to: the phrase found in this +// page's crawled text, the target's title and its public address. +type LinkOut struct { + Phrase string `json:"phrase"` + Title string `json:"title"` + URL string `json:"url"` +} + +// LinkIn is a page that could link here: the phrase of this page found in +// its crawled text, its title and its admin form ("" when no record of +// the admin is behind it). +type LinkIn struct { + Phrase string `json:"phrase"` + Title string `json:"title"` + AdminURL string `json:"adminUrl"` +} + +// LinkReport is the internal links of a page from the last finished crawl +// (D-82, UI-SPEC C19): the time of that crawl, the pages linking here and +// the pages linked from here (both nil before the first crawl), whether +// nothing links to the page, and up to 10 pages this text could link to +// and 10 that could link here. +type LinkReport struct { + CrawledAt *time.Time `json:"crawledAt"` + Inbound *int `json:"inbound"` + Outbound *int `json:"outbound"` + Orphan bool `json:"orphan"` + Out []LinkOut `json:"out"` + In []LinkIn `json:"in"` +} + +// NormalText is text in lower case with its whitespace collapsed to one +// space: the form the crawler's suggestions compare phrases in. +func NormalText(s string) string { return strings.ToLower(strings.Join(strings.Fields(s), " ")) } + +// errEnough stops a batched scan once a list is full. +var errEnough = errors.New("golem15.seo: enough suggestions") + +// lastFinishedCrawl is the newest finished crawl, nil before the first. +func lastFinishedCrawl(db *gorm.DB) (*models.Crawl, error) { + var rows []models.Crawl + if err := db.Where("status = ?", models.CrawlFinished).Order("finished_at DESC NULLS LAST, id DESC").Limit(1).Find(&rows).Error; err != nil { + return nil, err + } + if len(rows) == 0 { + return nil, nil + } + return &rows[0], nil +} + +// pageHeading is a crawled page's first H1, else its title. +func pageHeading(p models.CrawlPage) (h1, title string) { + if list := p.H1.Get(); len(list) > 0 { + h1 = strings.TrimSpace(list[0]) + } + title = h1 + if title == "" { + title = strings.TrimSpace(p.Title) + } + return h1, title +} + +// entryKeyphrases maps each address with an SEO entry to its focus +// keyphrase. +func entryKeyphrases(db *gorm.DB) (map[string]string, error) { + var rows []struct { + URL string + Keyphrase string + } + if err := db.Raw(`SELECT url, keyphrase FROM golem15_seo_entries WHERE url <> '' AND keyphrase <> ''`).Scan(&rows).Error; err != nil { + return nil, err + } + out := make(map[string]string, len(rows)) + for _, r := range rows { + out[r.URL] = r.Keyphrase + } + return out, nil +} + +// phraseList keeps the phrases of at least 3 bytes, once each (by their +// normal form), in order. +func phraseList(phrases ...string) []string { + var out []string + seen := map[string]bool{} + for _, p := range phrases { + p = strings.Join(strings.Fields(p), " ") + n := NormalText(p) + if len(n) < minLinkPhrase || seen[n] { + continue + } + seen[n] = true + out = append(out, p) + } + return out +} + +// mentioned returns the first phrase whose normal form normal (a page's +// NormalText) contains, "" when none. +func mentioned(normal string, phrases []string) string { + for _, p := range phrases { + if strings.Contains(normal, NormalText(p)) { + return p + } + } + return "" +} + +// adminURLOf is the admin form of the record behind a site path, "" when +// no content type with an admin controller owns it. +func (s *Service) adminURLOf(ctx context.Context, path string) string { + reg := s.Registry() + if reg == nil { + return "" + } + p, _, _ := strings.Cut(path, "?") + key, match, err := reg.MatchPath(ctx, p, s.UnprefixedLocale()) + if err != nil || match == nil || !match.Exact { + return "" + } + typ, ok := reg.ContentType(key) + if !ok || typ.Content == nil || strings.TrimSpace(typ.Content.Controller) == "" { + return "" + } + return s.adminFormURL(typ.Content.Controller, match.ID) +} + +// LinkSuggestions builds the internal links of the page at path from the +// last finished crawl (D-82, UI-SPEC C19), like the crawl's orphan +// suggestions: the pages whose crawled text contains one of phrases (the +// page's title and focus keyphrase; its crawled H1 is added) and do not +// link here yet, and the pages whose H1 or focus keyphrase this page's +// crawled text contains and that it does not link to yet. Before the +// first crawl the counts are nil and the lists empty. +func (s *Service) LinkSuggestions(ctx context.Context, db *gorm.DB, path string, phrases []string) (LinkReport, error) { + out := LinkReport{Out: []LinkOut{}, In: []LinkIn{}} + db = db.WithContext(ctx) + c, err := lastFinishedCrawl(db) + if err != nil || c == nil { + return out, err + } + out.CrawledAt = c.FinishedAt + var links []models.CrawlLink + if err := db.Select("from_url", "to_url").Where("crawl_id = ? AND internal AND from_url <> to_url AND (from_url = ? OR to_url = ?)", c.ID, path, path).Find(&links).Error; err != nil { + return out, err + } + linkedFrom, linksTo := map[string]bool{}, map[string]bool{} + for _, l := range links { + if l.FromURL == path { + linksTo[l.ToURL] = true + } else { + linkedFrom[l.FromURL] = true + } + } + inbound, outbound := len(linkedFrom), len(linksTo) + out.Inbound, out.Outbound = &inbound, &outbound + var self []models.CrawlPage + if err := db.Where("crawl_id = ? AND url = ?", c.ID, path).Limit(1).Find(&self).Error; err != nil { + return out, err + } + keyphrases, err := entryKeyphrases(db) + if err != nil { + return out, err + } + site := s.SiteURL() + if len(self) > 0 { + out.Orphan = inbound == 0 && path != "/" + h1, _ := pageHeading(self[0]) + phrases = append(phrases, h1) + text := NormalText(self[0].Text) + var others []models.CrawlPage + if err := db.Select("id", "url", "title", "h1").Where("crawl_id = ? AND url <> ? AND status = 200", c.ID, path).Order("url ASC").Find(&others).Error; err != nil { + return out, err + } + for _, o := range others { + if len(out.Out) >= MaxLinkSuggestions { + break + } + if linksTo[o.URL] { + continue + } + oh1, title := pageHeading(o) + if p := mentioned(text, phraseList(oh1, keyphrases[o.URL])); p != "" { + out.Out = append(out.Out, LinkOut{Phrase: p, Title: title, URL: site + o.URL}) + } + } + } + own := phraseList(append(phrases, keyphrases[path])...) + if len(own) == 0 { + return out, nil + } + var batch []models.CrawlPage + err = db.Select("id", "url", "title", "h1", "text").Where("crawl_id = ? AND url <> ? AND status = 200", c.ID, path). + FindInBatches(&batch, 100, func(*gorm.DB, int) error { + for _, p := range batch { + if len(out.In) >= MaxLinkSuggestions { + return errEnough + } + if linkedFrom[p.URL] { + continue + } + if phrase := mentioned(NormalText(p.Text), own); phrase != "" { + _, title := pageHeading(p) + out.In = append(out.In, LinkIn{Phrase: phrase, Title: title, AdminURL: s.adminURLOf(ctx, p.URL)}) + } + } + return nil + }).Error + if err != nil && !errors.Is(err, errEnough) { + return out, err + } + return out, nil +} + +// sitePath is the path of a path or an absolute URL; ErrBadURL otherwise. +func sitePath(raw string) (string, error) { + raw = strings.TrimSpace(raw) + if strings.HasPrefix(raw, "/") && !strings.HasPrefix(raw, "//") { + path, _, _ := strings.Cut(raw, "#") + return path, nil + } + u, err := url.Parse(raw) + if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { + return "", ErrBadURL + } + path := u.EscapedPath() + if path == "" { + path = "/" + } + if u.RawQuery != "" { + path += "?" + u.RawQuery + } + return path, nil +} + +// LinksOfURL is the internal links of a page addressed by URL (a path or +// an absolute URL of the site): the record behind it gives its title in +// the matched locale. +func (s *Service) LinksOfURL(ctx context.Context, db *gorm.DB, raw string) (LinkReport, error) { + path, err := sitePath(raw) + if err != nil { + return LinkReport{}, err + } + var phrases []string + if reg := s.Registry(); reg != nil { + p, _, _ := strings.Cut(path, "?") + if key, match, err := reg.MatchPath(ctx, p, s.UnprefixedLocale()); err == nil && match != nil && match.Exact { + if rec, err := reg.Describe(ctx, key, match.ID, s.UnprefixedLocale()); err == nil && rec != nil { + for _, l := range rec.Locales { + if l.Locale == match.Locale { + phrases = append(phrases, l.Title) + } + } + } + } + } + return s.LinkSuggestions(ctx, db, path, phrases) +} + +// LinksOfRecord is the internal links of record id of content type key in +// locale: its address and title in that locale and its focus keyphrase. A +// record without an address in the locale answers the crawl time only. +func (s *Service) LinksOfRecord(ctx context.Context, db *gorm.DB, key, id, locale string) (LinkReport, error) { + reg := s.Registry() + if reg == nil { + return LinkReport{Out: []LinkOut{}, In: []LinkIn{}}, nil + } + rec, err := reg.Describe(ctx, key, id, s.UnprefixedLocale()) + if err != nil { + return LinkReport{}, err + } + var loc *pact.ContentLocale + if rec != nil { + for i := range rec.Locales { + if rec.Locales[i].Locale == locale { + loc = &rec.Locales[i] + } + } + } + if loc == nil || strings.TrimSpace(loc.URL) == "" { + out := LinkReport{Out: []LinkOut{}, In: []LinkIn{}} + c, err := lastFinishedCrawl(db.WithContext(ctx)) + if err == nil && c != nil { + out.CrawledAt = c.FinishedAt + } + return out, err + } + phrases := []string{loc.Title} + if typ, ok := reg.ContentType(key); ok && typ.Content != nil { + if entry, found, err := LoadEntry(db, typ.Content.Model, id, locale); err == nil && found { + phrases = append(phrases, entry.Keyphrase) + } + } + return s.LinkSuggestions(ctx, db, loc.URL, phrases) +} + +// linksPayload is the seo_links action's payload. +type linksPayload struct { + Op string `json:"op"` + Locale string `json:"locale"` +} + +// RecordLinksAction is the SEO tab's internal links widget action (E10, +// UI-SPEC C19) for content type key: {op: links, locale} answers the +// record's LinkReport in that locale (the default when empty). +func RecordLinksAction(svc func() *Service, key, label, permission string) pact.AdminAction { + return pact.AdminAction{ + Name: LinksActionName, + Label: label, + Permissions: []string{permission}, + Run: func(ctx context.Context, in pact.AdminActionInput) (pact.AdminActionResult, error) { + s := svc() + if s == nil { + return pact.AdminActionResult{}, ErrNotBooted + } + var p linksPayload + if len(in.Payload) > 0 { + if err := json.Unmarshal(in.Payload, &p); err != nil || (p.Op != "" && p.Op != "links") { + return pact.AdminActionResult{}, &cabana.ValidationError{Details: map[string]any{in.Field: []string{"golem15.seo::lang.api.value_invalid"}}} + } + } + db, err := s.DB(ctx) + if err != nil { + return pact.AdminActionResult{}, err + } + if in.RecordID == nil { + return pact.AdminActionResult{Data: LinkReport{Out: []LinkOut{}, In: []LinkIn{}}}, nil + } + locale := p.Locale + if locale == "" { + locale = s.DefaultLocale(ctx, db) + } + report, err := s.LinksOfRecord(ctx, db, key, strconv.FormatUint(*in.RecordID, 10), locale) + return pact.AdminActionResult{Data: report}, err + }, + } +} diff --git a/controllers/api/links.go b/controllers/api/links.go new file mode 100644 index 0000000..b36b80b --- /dev/null +++ b/controllers/api/links.go @@ -0,0 +1,33 @@ +package api + +import ( + "context" + "net/http" + + "git.golem15.com/golem15/sm-seo-plugin/classes" +) + +// GetLinksInput is the query of GET /_seo/api/v1/links. +type GetLinksInput struct { + URL string `json:"url" query:"url" jsonschema:"the page: a path such as /blog/post or an absolute URL of the site"` +} + +// Links serves GET /_seo/api/v1/links?url= (D-82, UI-SPEC C19): the +// page's internal links from the last finished crawl, {crawledAt, +// inbound, outbound, orphan, out [{phrase, title, url}], in [{phrase, +// title, adminUrl}]}; before the first crawl crawledAt, inbound and +// outbound are null and the lists empty. A url that is not a path or an +// http(s) URL is 422. +func Links(service func() *classes.Service) http.HandlerFunc { + return manage(service, func(ctx context.Context, svc *classes.Service, r *http.Request) (int, any, error) { + db, err := read(ctx, svc) + if err != nil { + return 0, nil, err + } + report, err := svc.LinksOfURL(ctx, db, r.URL.Query().Get("url")) + if err != nil { + return 0, nil, err + } + return http.StatusOK, report, nil + }) +} diff --git a/extensions.go b/extensions.go index 1015aa2..887719c 100644 --- a/extensions.go +++ b/extensions.go @@ -235,10 +235,32 @@ fields: tab: golem15.seo::lang.tab.seo ` +// seoLinksYAML is E10, the internal links widget (UI-SPEC C16, C19). +const seoLinksYAML = ` seo_links: + label: golem15.seo::lang.field.links + type: widget + widget: golem15-seo-links + action: seo_links + span: full + context: update + tab: golem15.seo::lang.tab.seo +` + +// hasBody reports whether records of a content type of kind have a body +// to add links to (UI-SPEC C16 row 14): collections and profiles do not, +// nor do the registry's pages, which the site renders itself. +func hasBody(key, kind string) bool { + if key == classes.PageTypeKey { + return false + } + return kind != pact.ContentKindCollection && kind != pact.ContentKindProfile +} + // seoTabFor is the SEO tab's fields document of a content type (UI-SPEC // C16): seoTabYAML with the primary category after seo_robots for article -// types (whose records have parents, P33-23), and the structured data -// field (E8) and its preview (E9) after seo_og_image. +// types (whose records have parents, P33-23), the structured data field +// (E8) and its preview (E9) after seo_og_image, then the internal links +// widget (E10) for types with a body. func seoTabFor(key, kind string) string { doc := seoTabYAML quoted := strconv.Quote(key) @@ -254,6 +276,10 @@ func seoTabFor(key, kind string) string { seo_og_title: `, 1) } + links := "" + if hasBody(key, kind) { + links = seoLinksYAML + } return strings.Replace(doc, " seo_effective:\n", ` seo_schema: label: golem15.seo::lang.field.schema comment: golem15.seo::lang.field.schema_comment @@ -272,7 +298,7 @@ func seoTabFor(key, kind string) string { span: full context: update tab: golem15.seo::lang.tab.seo - seo_effective: +`+links+` seo_effective: `, 1) } @@ -288,6 +314,7 @@ func (p *Plugin) seoTab(key, kind string) *pact.AdminFormExtension { Actions: []pact.AdminAction{ classes.RecordSnippetAction(p.Service, key, "golem15.seo::lang.snippet.action", controllers.PermissionManage), classes.RecordGraphAction(p.Service, key, "golem15.seo::lang.graph.action", controllers.PermissionManage), + classes.RecordLinksAction(p.Service, key, "golem15.seo::lang.field.links", controllers.PermissionManage), }, PartialData: func(ctx context.Context, name string, record any) (any, error) { return p.seoTabPartial(ctx, key, name, record) diff --git a/lang/en/lang.yaml b/lang/en/lang.yaml index bae68a0..f5c5b3a 100644 --- a/lang/en/lang.yaml +++ b/lang/en/lang.yaml @@ -20,6 +20,7 @@ permissions: tab: seo: SEO field: + links: Internal links snippet: Search result preview description_comment: Leave empty to use the description template, then the content's own excerpt. keyphrase: Focus keyphrase diff --git a/lang/pl/lang.yaml b/lang/pl/lang.yaml index 5c51c7b..e67961b 100644 --- a/lang/pl/lang.yaml +++ b/lang/pl/lang.yaml @@ -20,6 +20,7 @@ permissions: tab: seo: SEO field: + links: Linki wewnętrzne snippet: Podgląd w wynikach wyszukiwania description_comment: Zostaw puste, aby użyć szablonu opisu, a potem zajawki treści. keyphrase: Fraza kluczowa diff --git a/linking_test.go b/linking_test.go index 01258ce..22f64c1 100644 --- a/linking_test.go +++ b/linking_test.go @@ -222,3 +222,117 @@ func TestAutolinks(t *testing.T) { t.Fatalf("history vias = %v", vias) } } + +// linkReport is GET /_seo/api/v1/links and the seo_links action's answer. +type linkReport struct { + CrawledAt *string `json:"crawledAt"` + Inbound *int `json:"inbound"` + Outbound *int `json:"outbound"` + Orphan bool `json:"orphan"` + Out []struct { + Phrase string `json:"phrase"` + Title string `json:"title"` + URL string `json:"url"` + } `json:"out"` + In []struct { + Phrase string `json:"phrase"` + Title string `json:"title"` + AdminURL string `json:"adminUrl"` + } `json:"in"` +} + +func (e *testEnv) links(t *testing.T, token, path string) linkReport { + t.Helper() + var out linkReport + if err := json.Unmarshal(e.expect(t, http.StatusOK, http.MethodGet, "/_seo/api/v1/links?url="+urlQueryEscape(path), token, nil).Body.Bytes(), &out); err != nil { + t.Fatal(err) + } + return out +} + +// TestLinkSuggestions covers E10's answer (UI-SPEC C19, D-82): before the +// first crawl the counts are null and the lists empty; after it, the +// inbound and outbound counts, the orphan state, the pages this text +// mentions by H1 or focus keyphrase and does not link to, and the pages +// whose text mentions this page's title or focus keyphrase and do not link +// here, with their admin forms; the SEO tab's seo_links widget answers the +// same, and E10 is in the SEO tab after the graph preview. +func TestLinkSuggestions(t *testing.T) { + env := bootCrawlEnv(t) + token := env.admin(t, true) + hericium := env.createPage(t, token, map[string]any{"title": "Hericium", "slug": "hericium", "published": true}) + env.createPage(t, token, map[string]any{"title": "Shiitake", "slug": "shiitake", "published": true}) + oyster := env.createPage(t, token, map[string]any{"title": "Oyster", "slug": "oyster", "published": true}) + env.expect(t, http.StatusOK, http.MethodPut, "/_seo/api/v1/seo", token, map[string]any{"url": "/pages/hericium", "values": map[string]any{"keyphrase": "lwia grzywa"}}) + env.expect(t, http.StatusOK, http.MethodPut, "/_seo/api/v1/seo", token, map[string]any{"url": "/pages/oyster", "values": map[string]any{"keyphrase": "boczniak"}}) + + before := env.links(t, token, "/pages/hericium") + if before.CrawledAt != nil || before.Inbound != nil || before.Outbound != nil || before.Orphan || len(before.Out) != 0 || len(before.In) != 0 { + t.Fatalf("before the first crawl = %+v", before) + } + + site := newFakeSite() + site.sitemap("/", "/pages/hericium", "/pages/shiitake", "/pages/oyster") + site.plant( + doc{path: "/", title: "Home", links: []string{"/pages/shiitake", "/pages/oyster"}}, + doc{path: "/pages/hericium", title: "Hericium", body: "

Hericium grows near shiitake and boczniak.

"}, + doc{path: "/pages/shiitake", title: "Shiitake", body: "

Shiitake differs from lwia grzywa.

", links: []string{"/pages/oyster"}}, + doc{path: "/pages/oyster", title: "Oyster", body: "

Oyster tastes almost like Hericium.

", links: []string{"/pages/shiitake"}}, + ) + env.crawlOnce(t, token, siteCtx(t, site)) + + got := env.links(t, token, siteURL+"/pages/hericium") + if got.CrawledAt == nil || got.Inbound == nil || *got.Inbound != 0 || got.Outbound == nil || *got.Outbound != 0 || !got.Orphan { + t.Fatalf("hericium counts = %+v", got) + } + outs := map[string]string{} + for _, o := range got.Out { + outs[o.URL] = o.Phrase + "|" + o.Title + } + if len(got.Out) != 2 || outs[siteURL+"/pages/shiitake"] != "Shiitake|Shiitake" || outs[siteURL+"/pages/oyster"] != "boczniak|Oyster" { + t.Fatalf("hericium out = %+v", got.Out) + } + ins := map[string]string{} + for _, i := range got.In { + ins[i.Title] = i.Phrase + "|" + i.AdminURL + } + if len(got.In) != 2 || ins["Shiitake"] == "" || !strings.HasPrefix(ins["Shiitake"], "lwia grzywa|/backend/acme/pages/pages/update/") || ins["Oyster"] != "Hericium|/backend/acme/pages/pages/update/"+oyster { + t.Fatalf("hericium in = %+v", got.In) + } + + // Shiitake: linked from home and oyster, links to oyster (not + // suggested again although its text names no other page). + sh := env.links(t, token, "/pages/shiitake") + if *sh.Inbound != 2 || *sh.Outbound != 1 || sh.Orphan { + t.Fatalf("shiitake counts = %+v", sh) + } + if len(sh.Out) != 1 || sh.Out[0].URL != siteURL+"/pages/hericium" || sh.Out[0].Phrase != "lwia grzywa" { + t.Fatalf("shiitake out = %+v", sh.Out) + } + if len(sh.In) != 1 || sh.In[0].Title != "Hericium" || sh.In[0].AdminURL != "/backend/acme/pages/pages/update/"+hericium { + t.Fatalf("shiitake in = %+v", sh.In) + } + if rec := env.call(t, http.MethodGet, "/_seo/api/v1/links?url=javascript:x", token, nil); rec.Code != http.StatusUnprocessableEntity { + t.Fatalf("a bad url = %d %s", rec.Code, rec.Body.String()) + } + + // The SEO tab: E10 after the graph preview, its action answers the + // same report. + findField(t, env.formSchema(t, token, "acme/pages/pages"), "seo_links") + n, _ := strconv.Atoi(hericium) + rec := env.expect(t, http.StatusOK, http.MethodPost, adminAPI("/acme/pages/pages/widgets/seo_links"), token, map[string]any{"record_id": n, "payload": map[string]any{"op": "links", "locale": "en"}}) + var widget struct { + Data struct { + Data linkReport `json:"data"` + } `json:"data"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &widget); err != nil { + t.Fatal(err) + } + if w := widget.Data.Data; !w.Orphan || len(w.Out) != 2 || len(w.In) != 2 || w.Inbound == nil { + t.Fatalf("widget answer = %s", rec.Body.String()) + } + if rec := env.call(t, http.MethodPost, adminAPI("/acme/pages/pages/widgets/seo_links"), token, map[string]any{"record_id": n, "payload": map[string]any{"op": "graph"}}); rec.Code != http.StatusUnprocessableEntity { + t.Fatalf("unknown op = %d %s", rec.Code, rec.Body.String()) + } +} diff --git a/mcp.go b/mcp.go index 2e237c3..6c5a65f 100644 --- a/mcp.go +++ b/mcp.go @@ -59,6 +59,17 @@ func (p *Plugin) MCP() pact.MCP { Permissions: []string{mcpPermission}, }, + { + Name: "get_link_suggestions", + Title: "Get the internal links of a URL", + Description: "Shows a page's internal links from the last finished crawl: how many pages link to it (inbound) and how many it links to (outbound), whether nothing links to it (orphan), up to 10 pages its text could link to (their title or focus keyphrase appears in it) and up to 10 pages that could link to it (its title or focus keyphrase appears in theirs). Before the first crawl crawledAt, inbound and outbound are null; start one with seo_start_crawl. Add a link by editing the text, or link a phrase on every page with seo_create_autolink.", + Guard: mcpGuard, + Method: http.MethodGet, + Path: "/_seo/api/v1/links", + Input: apicontrollers.GetLinksInput{}, + Permissions: []string{mcpPermission}, + }, + // URL rules (D-02). { Name: "list_rules", diff --git a/operations.go b/operations.go index dac2f72..16e4d68 100644 --- a/operations.go +++ b/operations.go @@ -54,6 +54,7 @@ var AdminOperations = []AdminOperation{ {ContentController, OpExtensionSave, "set_url_seo"}, {ContentController, OpWidgetAction + ":seo_snippet", "get_url_seo"}, {ContentController, OpWidgetAction + ":seo_graph", "get_graph"}, + {ContentController, OpWidgetAction + ":seo_links", "get_link_suggestions"}, // The social image is set by URL through MCP (uploads are no tool). {ContentController, OpUpload, "set_url_seo"}, diff --git a/routes.go b/routes.go index 2c464a9..5cebd43 100644 --- a/routes.go +++ b/routes.go @@ -50,6 +50,7 @@ func (p *Plugin) Routes(r pact.Router) error { g.Get("/seo", apicontrollers.GetSEO(p.Service), api) g.Put("/seo", apicontrollers.SetSEO(p.Service), api) g.Get("/graph", apicontrollers.Graph(p.Service), api) + g.Get("/links", apicontrollers.Links(p.Service), api) g.Get("/rules", apicontrollers.ListRules(p.Service), api) g.Post("/rules", apicontrollers.CreateRule(p.Service), api) g.Get("/rules/{id}", apicontrollers.ShowRule(p.Service), api) diff --git a/seo_tab_test.go b/seo_tab_test.go index 72d4ce8..67b5a1f 100644 --- a/seo_tab_test.go +++ b/seo_tab_test.go @@ -370,7 +370,7 @@ func TestSEOTab(t *testing.T) { env := bootEnv(t) token := env.admin(t, true) - want := "seo_status seo_snippet seo_title seo_description seo_keyphrase seo_canonical seo_robots seo_og_title seo_og_description seo_og_image seo_schema seo_graph seo_effective" + want := "seo_status seo_snippet seo_title seo_description seo_keyphrase seo_canonical seo_robots seo_og_title seo_og_description seo_og_image seo_schema seo_graph seo_links seo_effective" fields := env.formSchema(t, token, "acme/pages/pages") var seoFields []string for _, f := range fields { diff --git a/tests/elements/links.test.ts b/tests/elements/links.test.ts new file mode 100644 index 0000000..62a7506 --- /dev/null +++ b/tests/elements/links.test.ts @@ -0,0 +1,134 @@ +import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest' +import { labelsFor } from '../../assets/src/elements/labels' +import type { LinksData } from '../../assets/src/elements/links' +import { HostFailure, mountWidget, settle } from './host' + +// E10 golem15-seo-links (UI-SPEC C19, D-82). + +beforeAll(async () => { + await import('../../assets/src/elements/index') +}) + +afterEach(() => { + vi.unstubAllGlobals() + vi.useRealTimers() + document.body.replaceChildren() +}) + +const en = labelsFor('en') + +const crawled: LinksData = { + crawledAt: '2026-11-04T10:00:00Z', + inbound: 0, + outbound: 3, + orphan: true, + out: [{ phrase: 'shiitake', title: 'Shiitake', url: 'https://example.com/pages/shiitake' }], + in: [{ phrase: 'lwia grzywa', title: 'Oyster', adminUrl: '/backend/acme/pages/pages/update/3' }], +} + +function stubClipboard(): { writes: string[] } { + const writes: string[] = [] + vi.stubGlobal('navigator', { + ...navigator, + clipboard: { + writeText: (text: string) => { + writes.push(text) + return Promise.resolve() + }, + }, + }) + return { writes } +} + +describe('E10 golem15-seo-links', () => { + it('asks for the links of the shown locale on connect and renders counters, the orphan callout and both lists', async () => { + stubClipboard() + const host = mountWidget('golem15-seo-links', { attrs: { 'ui-locale': 'en' }, answer: () => crawled }) + await settle() + expect(host.posted).toEqual([{ op: 'links', locale: '' }]) + const node = host.node + const values = [...node.querySelectorAll('.summer-stat__value')].map((n) => n.textContent) + expect(values).toEqual(['0', '3']) + expect([...node.querySelectorAll('.summer-stat__label')].map((n) => n.textContent)).toEqual([en.linksInbound, en.linksOutbound]) + expect(node.textContent).toContain('From the crawl of ') + expect(node.querySelector('.summer-callout--warning')!.textContent).toContain(en.linksOrphanTitle) + const headings = [...node.querySelectorAll('.seo-links__heading')].map((n) => n.textContent) + expect(headings).toEqual([en.linksOut, en.linksIn]) + const out = node.querySelector('.seo-links__list--out .seo-links__row')! + expect(out.textContent).toContain('“shiitake” → Shiitake') + const link = out.querySelector('a')! + expect(link.getAttribute('href')).toBe('https://example.com/pages/shiitake') + expect(link.target).toBe('_blank') + expect(link.rel).toBe('noopener') + const inbound = node.querySelector('.seo-links__list--in .seo-links__row a')! + expect(inbound.getAttribute('href')).toBe('/backend/acme/pages/pages/update/3') + expect(node.querySelector('.seo-links__none')).toBeNull() + expect(node.getAttribute('aria-busy')).toBe('false') + }) + + it('shows notCrawled instead of counters before a crawl and none under empty lists', async () => { + const host = mountWidget('golem15-seo-links', { + attrs: { 'ui-locale': 'en' }, + answer: () => ({ crawledAt: null, inbound: null, outbound: null, orphan: false, out: [], in: [] }), + }) + await settle() + const node = host.node + expect(node.querySelector('.summer-stats')).toBeNull() + expect(node.textContent).toContain(en.linksNotCrawled) + expect(node.querySelector('.summer-callout')).toBeNull() + expect(node.querySelectorAll('.seo-links__none')).toHaveLength(2) + expect(node.querySelector('.seo-links__none')!.textContent).toBe(en.linksNone) + }) + + it('shows the checking line with aria-busy while loading and the danger line on failure', async () => { + const host = mountWidget('golem15-seo-links', { + attrs: { 'ui-locale': 'pl' }, + answer: () => { + throw new HostFailure('down') + }, + }) + await Promise.resolve() + expect(host.node.getAttribute('aria-busy')).toBe('true') + expect(host.node.querySelector('.seo-links__checking')!.getAttribute('role')).toBe('status') + await settle() + const error = host.node.querySelector('.seo-links__error')! + expect(error.textContent).toBe(labelsFor('pl').linksFailed) + expect(error.getAttribute('role')).toBe('status') + expect(host.node.getAttribute('aria-busy')).toBe('false') + }) + + it('copies the address and says so for two seconds', async () => { + const clip = stubClipboard() + const host = mountWidget('golem15-seo-links', { attrs: { 'ui-locale': 'en' }, answer: () => crawled }) + await settle() + vi.useFakeTimers() + const button = host.node.querySelector('.seo-links__copy')! + expect(button.textContent).toBe(en.linksCopy) + button.click() + await Promise.resolve() + await Promise.resolve() + expect(clip.writes).toEqual(['https://example.com/pages/shiitake']) + expect(host.node.querySelector('.seo-links__status')!.textContent).toBe(en.linksCopied) + vi.advanceTimersByTime(2000) + expect(host.node.querySelector('.seo-links__status')!.textContent).toBe('') + }) + + it('hides Copy address without a clipboard and keeps the address as text', async () => { + vi.stubGlobal('navigator', { ...navigator, clipboard: undefined }) + const host = mountWidget('golem15-seo-links', { attrs: { 'ui-locale': 'en' }, answer: () => crawled }) + await settle() + expect(host.node.querySelector('.seo-links__copy')).toBeNull() + expect(host.node.querySelector('.seo-links__url')!.textContent).toBe('https://example.com/pages/shiitake') + }) + + it('asks again on the ML locale switch', async () => { + const host = mountWidget('golem15-seo-links', { attrs: { 'ui-locale': 'en' }, answer: () => crawled }) + await settle() + window.dispatchEvent(new CustomEvent('summer:ml-locale', { detail: 'pl' })) + await settle() + expect(host.posted).toEqual([ + { op: 'links', locale: '' }, + { op: 'links', locale: 'pl' }, + ]) + }) +})