@moorage/plugin-seo 0.1.13 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @moorage/plugin-seo
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 672984d: Moved from WordPress with the EmDash Exporter: the Yoast or Rank Math title and description of each post arrive in two fields, SEO Title and SEO Description, that the page does not use. The pages to fix no longer say "No search description" for such a page; they say "A description came from WordPress but the page doesn't use it yet" and how to fix it: copy it into Meta Description in the page's SEO panel. A WordPress search title the page does not use is listed under Worth a look, with a reminder to leave the " | Site name" ending off when you copy it, as your site adds its own name.
8
+
9
+ The Google preview no longer says the excerpt is what the page shows when there is no Meta Description: your theme may use another field (a standfirst or summary), which a plugin cannot see, so the preview shows the excerpt and says so.
10
+
11
+ After the update, the first visit to the SEO page (or the dashboard) checks every page once more, which takes about a minute.
12
+
13
+ ## 0.2.0
14
+
15
+ ### Minor Changes
16
+
17
+ - ada2b2f: Copy settings to another site: at the end of SEO > Settings, the settings are shown as text to copy, with a box to paste another site's. A paste is checked as the forms check what is typed in them (a founding year, full profile links, at most two opening-hour ranges a day, hours only for a local business, the country code in capitals) and shows a table of what would change; nothing is saved until you press Apply these settings. It holds who the site is for, its details, opening hours, profiles and content types, and no passwords or keys. What no form sets (a map position, VAT and tax numbers, the number of employees, the field a page's own markup is read from) stays as each site has it. A content type the paste hides from Google is called out, and still asks this site's admin to confirm it on the Overview. Redirects are not included; copy them with the Redirects tab's Export and Import from WordPress.
18
+
19
+ Only bylines without a role label are published as the article's author. Before, every credited byline was, so "Photos by" or "Reviewed by" credits were published as authors too. When every credit on a post has a role label, the post is described as one with no byline is, and "This article has no author" is listed under Worth a look. After the update, the first visit to the SEO page (or the dashboard) checks every page once more, which takes about a minute, so pages saved before the update are corrected too.
20
+
21
+ The three numbers on the Overview now add up: "Pages checked" no longer includes pages hidden from search, which are listed below the pages to fix instead. On a site with hidden pages, "Pages checked" is lower than before.
22
+
23
+ The Google preview panel has a second link for admins, Check all markup, which opens the schema.org Validator for the page. It reads every type, including the organisation and local business details Google's Rich Results Test skips.
24
+
25
+ README and registry texts: use one schema plugin at a time (EmDash keeps only the first page markup it receives); the Cloudflare install example now shows `sandboxRunner: sandbox()` and the `LOADER` Worker Loader binding; for IndexNow (Bing), search the registry for an IndexNow plugin.
26
+
27
+ ### Patch Changes
28
+
29
+ - 71348af: New screenshots of the screens this release changes and adds.
30
+
3
31
  ## 0.1.13
4
32
 
5
33
  ### Patch Changes
package/README.md CHANGED
@@ -4,7 +4,7 @@ SEO & Schema Markup for [EmDash](https://emdashcms.com): one linked structured d
4
4
 
5
5
  ## What it does today
6
6
 
7
- - Replaces EmDash's single JSON-LD object with one linked graph per page: Organization (or Person, or a LocalBusiness type), WebSite, WebPage, BreadcrumbList, the primary image (with its width and height when it is one of the entry's own images), and BlogPosting with byline authors and dates on article pages.
7
+ - Replaces EmDash's single JSON-LD object with one linked graph per page: Organization (or Person, or a LocalBusiness type), WebSite, WebPage, BreadcrumbList, the primary image (with its width and height when it is one of the entry's own images), and BlogPosting with its authors (the bylines without a role label such as "Photos by") and dates on article pages.
8
8
  - Writes the robots tag: `noindex` per collection, and `max-snippet`, `max-image-preview` and `max-video-preview` limits on indexable pages. A noindex set in an entry's SEO panel is always kept.
9
9
  - Uses the theme's breadcrumb trail when it passes one, otherwise derives Home and one crumb per URL segment.
10
10
  - Adds your own schema from an entry to the same graph: add a JSON field named `json_ld` to a collection and paste any schema.org object (for example an Event or a Product). Give it an `@id` and the page names it as its main entity. An object whose `@id` is already used in the graph is left out.
@@ -21,16 +21,22 @@ pnpm add @moorage/plugin-seo
21
21
  ```
22
22
 
23
23
  ```js
24
- // astro.config.mjs
24
+ // astro.config.mjs (on Cloudflare)
25
+ import { sandbox } from "@emdash-cms/cloudflare";
25
26
  import seo from "@moorage/plugin-seo";
26
27
  import emdash from "emdash/astro";
27
28
 
28
29
  export default defineConfig({
29
- integrations: [emdash({ sandboxed: [seo] })],
30
+ integrations: [emdash({ sandboxed: [seo], sandboxRunner: sandbox() })],
30
31
  });
31
32
  ```
32
33
 
33
- `sandboxed` needs a sandbox runner: on Cloudflare it takes two things: the `LOADER` Worker Loader binding in `wrangler.jsonc` (`"worker_loaders": [{ "binding": "LOADER" }]`, Workers Paid only) and `sandboxRunner: sandbox()` in `astro.config.mjs`, with `sandbox` imported from `@emdash-cms/cloudflare` (answering Yes to sandboxed plugins in `create-emdash` adds only the binding); on Node add `@emdash-cms/sandbox-workerd` and set `sandboxRunner: "@emdash-cms/sandbox-workerd/sandbox"`. Without a runner, list the plugin under `plugins: [seo]` instead, which runs it in-process. Requires EmDash 1.2.0 or newer. Since 0.1.8 your settings survive moving the plugin from one list to the other; when updating from an earlier version, update first, open the SEO page once, then move it.
34
+ ```jsonc
35
+ // wrangler.jsonc: the Worker Loader binding sandbox() runs plugins in (Workers Paid only)
36
+ "worker_loaders": [{ "binding": "LOADER" }]
37
+ ```
38
+
39
+ `sandboxed` needs a sandbox runner: on Cloudflare it takes both lines above, `sandboxRunner: sandbox()` and the `LOADER` Worker Loader binding (answering Yes to sandboxed plugins in `create-emdash` adds only the binding); on Node add `@emdash-cms/sandbox-workerd` and set `sandboxRunner: "@emdash-cms/sandbox-workerd/sandbox"`. Without a runner, list the plugin under `plugins: [seo]` instead, which runs it in-process. Requires EmDash 1.2.0 or newer. Since 0.1.8 your settings survive moving the plugin from one list to the other; when updating from an earlier version, update first, open the SEO page once, then move it.
34
40
 
35
41
  **On Cloudflare's Workers free plan** each request gets about 10 ms of CPU. EmDash's own saving and publishing already comes close to that, and every plugin run in the site's process adds a little, so a save or publish can now and then fail with "Worker exceeded resource limits"; trying again works. Workers Paid (30 seconds per request) does not have this problem.
36
42
 
@@ -40,27 +46,35 @@ It asks for four permissions: read content, read bylines, read the content schem
40
46
 
41
47
  One page, **SEO**, in the admin sidebar, with three tabs. It works before you touch it: the site is described as an organisation named after the site, each content type is guessed from its name (posts and blog become blog posts, services become services, team becomes people), and your pages are checked by themselves.
42
48
 
43
- - **Overview**: how many pages were checked, how many have nothing to fix and how many do; a warning whenever a content type is hidden from Google; one question if you have not said who the site is for; then **Your details**, the few things Google still needs for your kind of site (an address, opening hours and a phone number for a local business; a logo and profiles for a company); then the pages to fix, each with what is missing and a link to the page; and, folded, pages that are fine but could be better.
49
+ - **Overview**: how many pages were checked, how many have nothing to fix and how many do (the last two add up to the first; pages hidden from search are listed below them, not counted); a warning whenever a content type is hidden from Google; one question if you have not said who the site is for; then **Your details**, the few things Google still needs for your kind of site (an address, opening hours and a phone number for a local business; a logo and profiles for a company); then the pages to fix, each with what is missing and a link to the page; and, folded, pages that are fine but could be better.
44
50
  - **Redirects**: your redirects, with Change and Remove on each, and three buttons above the list. **Add a redirect**: the old address, where to send visitors, and the kind (permanent unless you choose otherwise). **Import from WordPress**: paste a CSV export from Redirection, Yoast SEO Premium or Rank Math (CSV only: Rank Math's CSV export is in its paid version, and Redirection's JSON, .htaccess and Nginx exports are not read); it is checked before anything is added and can be removed again as a batch. **Export**: a copyable list. EmDash already redirects a page's old address when you change it.
45
- - **Settings**: who the site is for (a local business, a company or a person) and what you do, then folded sections ordered by how much they matter for that kind of site. Each title says whether the section is needed, worth adding or optional, and what is already filled in: address and phone, opening hours (two ranges a day for split shifts), logo, profiles elsewhere, more about you, and content types (what each kind of content is, and whether it is hidden from Google, which asks before it saves). One location for now. A local business is described as an organisation until it has an address, because Google requires one.
51
+ - **Settings**: who the site is for (a local business, a company or a person) and what you do, then folded sections ordered by how much they matter for that kind of site. Each title says whether the section is needed, worth adding or optional, and what is already filled in: address and phone, opening hours (two ranges a day for split shifts), logo, profiles elsewhere, more about you, and content types (what each kind of content is, and whether it is hidden from Google, which asks before it saves). One location for now. A local business is described as an organisation until it has an address, because Google requires one. At the end, folded, **Copy settings to another site**: the settings as text to copy, and a box to paste another site's. Pasted settings are checked first, with a table of what would change, and saved only when you press Apply these settings. They hold who the site is for, its details, opening hours, profiles and content types; no passwords or keys (the plugin keeps none). Hiding a content type from Google still asks this site's admin to confirm it on the Overview. Redirects are copied separately, with Export and Import from WordPress on the Redirects tab (the import reads the export).
52
+
53
+ | Copy settings to another site | What will change, before Apply | The Google preview panel, with Check all markup |
54
+ | ----------------------------------- | ----------------------------------------- | ----------------------------------------------- |
55
+ | ![](images/settings-copy-0-2-0.png) | ![](images/settings-copy-check-0-2-0.png) | ![](images/google-preview-0-2-0.png) |
46
56
 
47
57
  Admins can change settings. Editors see a read-only page with the pages to fix.
48
58
 
49
- In the entry editor, the **Google preview** panel shows how the published page may look on Google (address, title, description), what kind of page it is, whether it is in search results, and up to three things to fix. Admins can also change one page's type, open Google's Rich Results Test for the page, and view the markup. A dashboard widget shows the number of pages to fix.
59
+ In the entry editor, the **Google preview** panel shows how the published page may look on Google (address, title, description; without a Meta Description it shows the excerpt and says so, since your theme may use another field, such as a standfirst, which a plugin cannot see), what kind of page it is, whether it is in search results, and up to three things to fix. Admins can also change one page's type, open Google's Rich Results Test for the page or the schema.org Validator (**Check all markup**, which also reads the Organization and LocalBusiness details Google's test skips), and view the markup. A dashboard widget shows the number of pages to fix.
50
60
 
51
61
  ### The checks
52
62
 
53
- To fix: no title; a title that says nothing, such as "Home"; no search description (an entry's `excerpt` counts as one, as it does in EmDash); the same title as another page; a canonical address that points at a different page. Worth a look: a title that may be cut off (measured with your site's name after it, as most sites show it); no picture for shared links (dropped once the page has been shown with a picture your theme adds); an article with no author; pictures without alt text; a page with almost no text. Hidden pages are counted separately, and content without a page of its own (testimonials, for example) is left out. There is no score, and no keyword, readability or word-count rule: Google documents none of them as ranking factors.
63
+ To fix: no title; a title that says nothing, such as "Home"; no search description (an entry's `excerpt` counts as one, as it does in EmDash; a description EmDash's WordPress import left in the entry's SEO Description field is named as such, since the page does not use it); the same title as another page; a canonical address that points at a different page. Worth a look: a search title from WordPress, or a description when the excerpt stands in, that the page does not use yet; a title that may be cut off (measured with your site's name after it, as most sites show it); no picture for shared links (dropped once the page has been shown with a picture your theme adds); an article with no author (credits with a role, such as "Photos by", are not authors); pictures without alt text; a page with almost no text. Hidden pages are listed separately and kept out of the counts, and content without a page of its own (testimonials, for example) is left out. There is no score, and no keyword, readability or word-count rule: Google documents none of them as ranking factors.
54
64
 
55
65
  Not yet: Product and Event types (they need price and date fields on the collection), and FAQ (Google stopped showing FAQ results in May 2026). Use the `json_ld` field for those today.
56
66
 
57
67
  ## FAQ
58
68
 
69
+ - I moved from WordPress with the EmDash Exporter. Where are my Yoast titles and descriptions? In each entry's SEO Title and SEO Description fields, which the page does not use; the pages to fix list them. Copy each into the entry's SEO panel (SEO Title and Meta Description), leaving the " | Site name" ending off the title, because your site adds its own name and would otherwise show it twice.
59
70
  - Is it free? Yes, on any number of sites, your own or your clients', with no account and no key. It is open source under the MIT License: you may use, change, copy and share the code, in paid work too, as long as the copyright notice stays with it.
60
71
  - How do I add schema markup to EmDash? Install this plugin; it adds structured data (JSON-LD) to every page.
61
72
  - How do I add my own structured data to one page? Add a JSON field named `json_ld` to the collection and paste the schema.org object into the entry. It joins the page's one graph; the page still has a single JSON-LD script.
62
73
  - Is there a Yoast alternative for EmDash? EmDash core covers titles, descriptions, sitemaps and canonicals; this plugin adds the schema graph, robots rules, previews and checks.
63
74
  - Does it check my page while I type? No. EmDash does not let a general-purpose plugin read unsaved text. The panel and the checks describe the published page, as Google sees it: changes to the page show up once you publish, and changes in the SEO panel show up as soon as they are saved.
75
+ - Can I use it next to another schema plugin? Use one schema plugin at a time. Both send the page's main JSON-LD script, and EmDash keeps only the first one it receives, so the other plugin's markup, or this one's, silently goes missing. Remove the other one, or this one.
76
+ - Does it tell Bing about new pages (IndexNow)? Not yet. Search the EmDash registry for "IndexNow": plugins there notify Bing and the other IndexNow engines when a page is published. Google does not use IndexNow.
77
+ - How do I set up several sites the same way? On the first site, open SEO > Settings > Copy settings to another site and copy the text. On the next site, paste it in the same place, press Check, read what will change, and press Apply these settings. Content types the other site does not have are left out.
64
78
  - Is there an SEO score or a traffic light? No, on purpose. You get a list of things to fix that can reach zero.
65
79
  - Does it add FAQ rich results? No. Google stopped showing FAQ rich results in May 2026.
66
80
  - How do I import redirects from WordPress? Export them as CSV from Redirection, Yoast SEO Premium or Rank Math (Rank Math's CSV export is in its paid version; Redirection's JSON, .htaccess and Nginx exports are not read), open SEO > Redirects > Import from WordPress, paste the list and press Check list. Nothing is added until you confirm.
package/dist/index.mjs CHANGED
@@ -7,7 +7,7 @@
7
7
  /** @type {import("emdash").PluginDescriptor} */
8
8
  const descriptor = Object.freeze({
9
9
  "id": "seo",
10
- "version": "0.1.13",
10
+ "version": "0.2.1",
11
11
  "format": "standard",
12
12
  "entrypoint": "@moorage/plugin-seo/sandbox",
13
13
  "capabilities": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "seo",
3
- "version": "0.1.13",
3
+ "version": "0.2.1",
4
4
  "declaredAccess": {
5
5
  "content": {
6
6
  "read": {}
package/dist/plugin.mjs CHANGED
@@ -1,6 +1,6 @@
1
- function e(e){return e!=null}async function t(e,t,n){try{await e.settings.set(t.name,n)}catch(n){e.log.warn(`could not copy the settings to their new place`,{key:t.name,error:String(n)})}}async function n(n,r,{copy:i=!0}={}){let a=await n.settings.get(r.name);if(e(a))return a;let o=await n.kv.get(r.legacy);return e(o)?(i&&await t(n,r,o),o):null}async function r(n,r,i,{copy:a=!0}={}){let o=r.get(`settings:${i.name}`);if(e(o))return o;let s=r.get(i.legacy);return e(s)?(a&&await t(n,i,s),s):null}function i(t,n){return!e(t.get(`settings:${n.name}`))&&e(t.get(n.legacy))}async function a(e,t,n){await e.settings.set(t.name,n);try{await e.kv.set(t.legacy,n)}catch(n){e.log.warn(`could not keep a copy of the settings for the earlier release`,{key:t.legacy,error:String(n)})}}function o(e,t){try{return new URL(e,t)}catch{return null}}const s={name:`config`,legacy:`config`};async function c(e){return S(await n(e,s,{copy:!1}))}const l=[`WebPage`,`AboutPage`,`ContactPage`,`CollectionPage`,`ProfilePage`],u=[`Article`,`BlogPosting`,`NewsArticle`],d=[`Service`];function f(e){let t=e.toLowerCase();return[`posts`,`post`,`blog`].includes(t)?`BlogPosting`:[`news`,`articles`,`article`].includes(t)?`Article`:[`services`,`service`].includes(t)?`Service`:[`team`,`people`,`staff`,`authors`].includes(t)?`ProfilePage`:`WebPage`}const p={site:{type:`Organization`,sameAs:[],openingHours:[],areaServed:[]},collections:{},robots:{maxSnippet:-1,maxImagePreview:`large`,maxVideoPreview:-1,nosnippet:!1},jsonLdField:`json_ld`,breadcrumbs:!0,steps:{identity:!1,types:!1},hideHelpLine:!1};function m(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function h(e){if(typeof e!=`string`)return;let t=e.trim();return t===``?void 0:t}function g(e){let t=h(e),n=t?o(t)?.protocol:void 0;return n===`https:`||n===`http:`?t:void 0}function _(e,t){return typeof e==`number`&&Number.isInteger(e)&&e>=-1?e:t}function v(e){let t=[...l,...u,...d];return typeof e==`string`&&t.includes(e)?e:void 0}function y(e){if(!m(e))return;let t={};for(let n of[`streetAddress`,`addressLocality`,`addressRegion`,`postalCode`,`addressCountry`]){let r=h(e[n]);r&&(t[n]=r)}return Object.keys(t).length>0?t:void 0}function b(e){if(!Array.isArray(e))return[];let t=[];for(let n of e){if(!m(n)||!Array.isArray(n.days))continue;let e=n.days.filter(e=>typeof e==`string`&&e!==``),r=h(n.opens),i=h(n.closes);e.length>0&&r&&i&&t.push({days:e,opens:r,closes:i})}return t}function x(e){if(!m(e))return{...p.site};let t=m(e.geo)?e.geo:void 0,n=t?.latitude,r=t?.longitude;return{type:h(e.type)??`Organization`,name:h(e.name),alternateName:h(e.alternateName),legalName:h(e.legalName),description:h(e.description),logo:g(e.logo),sameAs:Array.isArray(e.sameAs)?e.sameAs.map(g).filter(e=>e!==void 0):[],email:h(e.email),telephone:h(e.telephone),address:y(e.address),geo:typeof n==`number`&&typeof r==`number`?{latitude:n,longitude:r}:void 0,openingHours:b(e.openingHours),priceRange:h(e.priceRange),areaServed:Array.isArray(e.areaServed)?e.areaServed.map(h).filter(e=>e!==void 0):[],foundingDate:h(e.foundingDate),vatID:h(e.vatID),taxID:h(e.taxID),numberOfEmployees:typeof e.numberOfEmployees==`number`&&e.numberOfEmployees>0?Math.round(e.numberOfEmployees):void 0}}function S(e){if(!m(e))return p;let t={};if(m(e.collections))for(let[n,r]of Object.entries(e.collections))m(r)&&(t[n]={type:v(r.type),noindex:r.noindex===!0,...r.hiddenOk===!0?{hiddenOk:!0}:{}});let n=m(e.robots)?e.robots:{},r=n.maxImagePreview;return{site:x(e.site),collections:t,robots:{maxSnippet:_(n.maxSnippet,p.robots.maxSnippet),maxImagePreview:r===`none`||r===`standard`||r===`large`?r:p.robots.maxImagePreview,maxVideoPreview:_(n.maxVideoPreview,p.robots.maxVideoPreview),nosnippet:n.nosnippet===!0},jsonLdField:h(e.jsonLdField)??p.jsonLdField,breadcrumbs:e.breadcrumbs!==!1,steps:{identity:m(e.steps)&&e.steps.identity===!0,types:m(e.steps)&&e.steps.types===!0},hideHelpLine:e.hideHelpLine===!0}}const C=`help_line`,w=`hide_help_line`,T=`save_help_line`;function E(e){return{type:`toggle`,action_id:w,label:`Hide the help line at the foot of this page`,description:`Only admins see it: links to the guide and to more free plugins.`,initial_value:e}}function D(e){return{type:`form`,block_id:`${C}_switch_${e?`on`:`off`}`,fields:[E(e)],submit:{label:`Save`,action_id:T}}}function O(e,t){let n=e[w];return typeof n==`boolean`?n:t}function k(e){if(!e)return[];let t=(e,t)=>({type:`link`,label:e,target:{kind:`external`,url:t}});return[{type:`divider`,block_id:`${C}_rule`},{type:`actions`,block_id:C,elements:[t(`Help`,`https://moorage.dev/guides/add-schema-markup-to-emdash`),t(`More free plugins`,`https://moorage.dev/plugins`),t(`by Moorage`,`https://moorage.dev`)]}]}const A=[`Monday`,`Tuesday`,`Wednesday`,`Thursday`,`Friday`,`Saturday`,`Sunday`],ee=/^(\d{1,2})(?:[:.](\d{2}))?\s*[-–—]\s*(\d{1,2})(?:[:.](\d{2}))?$/;function te(e,t){let n=Number(e),r=Number(t??`0`);return n>24||r>59||n===24&&r!==0?null:`${String(n).padStart(2,`0`)}:${String(r).padStart(2,`0`)}`}function ne(e,t){let n=t.trim();if(n===``||n.toLowerCase()===`closed`)return[];let r=n.split(/\s*[,;]\s*|\s+and\s+/i).filter(e=>e!==``);if(r.length>2)return null;let i=[];for(let t of r){let n=ee.exec(t);if(!n)return null;let r=te(n[1]??``,n[2]),a=te(n[3]??``,n[4]);if(!r||!a)return null;i.push({days:[e],opens:r,closes:a})}return i}function re(e,t){return t.filter(t=>t.days.includes(e)).map(e=>`${e.opens}-${e.closes}`).join(`, `)}const j=[{value:`WebPage`,label:`Web page`,short:`Web page`},{value:`AboutPage`,label:`About page`,short:`About page`},{value:`ContactPage`,label:`Contact page`,short:`Contact page`},{value:`BlogPosting`,label:`Blog post`,short:`Blog post`},{value:`Article`,label:`Article`,short:`Article`},{value:`Service`,label:`Service`,short:`Service`},{value:`ProfilePage`,label:`Person or team member`,short:`Person`}];function M(e){return j.find(t=>t.value===e)?.short??e}const ie=[301,302,307,308,410,451],ae=2e3,N={empty:`No old address`,query:`Addresses with a ? are not supported`,external:`Points to another website`,noTarget:`No new address`,same:`Old and new address are the same`,code:`Unknown redirect type`,pattern:`Uses a pattern. Add it by hand under Manage > Redirects`,inactive:`Switched off in the export`,repeated:`Listed twice`,exists:`Already exists`,conflict:`Conflicts with an existing redirect`,loop:`Would send visitors in a circle`,tooMany:`Only the first ${ae} rows are imported at once`,file:`Addresses ending in a file type, like .xml or .jpg, are not redirected by EmDash`},oe=/\.\w{1,10}$/;function se(e,t){let n=[],r=``,i=!1;for(let a=0;a<e.length;a+=1){let o=e[a];i?o===`"`&&e[a+1]===`"`?(r+=`"`,a+=1):o===`"`?i=!1:r+=o:o===`"`?i=!0:o===t?(n.push(r.trim()),r=``):r+=o}n.push(r.trim());let a=n[0]??``;return n.length===1&&/\S\s+\S/.test(a)?a.split(/\s+/):n}const ce={source:`source`,origin:`source`,from:`source`,"old url":`source`,old:`source`,sources:`source`,target:`target`,destination:`target`,to:`target`,"new url":`target`,new:`target`,url_to:`target`,regex:`regex`,code:`code`,"http code":`code`,type:`code`,header_code:`code`,format:`format`,matching:`matching`,status:`status`,ignore:`ignore`,id:`id`};function le(e,t){let n=e.map(e=>ce[e.toLowerCase()]),r=n.includes(`source`)&&n.includes(`target`);if(r){let e={};return n.forEach((t,n)=>{t&&e[t]===void 0&&(e[t]=n)}),{format:e.matching===void 0?e.format===void 0?e.regex===void 0?`plain`:`redirection`:`yoast`:`rankmath`,hasHeader:r,index:e}}let i=t.slice(0,20),a=i.map(e=>e[2]??``),o=i.map(e=>(e[3]??``).toLowerCase());return o.length>0&&o.every(e=>e===`plain`||e===`regex`)?{format:`yoast`,hasHeader:r,index:{source:0,target:1,code:2,format:3}}:a.length>0&&a.every(e=>e===`0`||e===`1`)?{format:`redirection`,hasHeader:r,index:{source:0,target:1,regex:2,code:3}}:{format:`plain`,hasHeader:r,index:{source:0,target:1,code:2}}}function ue(e){let t=e.trim();if(t===``)return{reason:N.empty};if(/^https?:\/\//i.test(t)){let e=o(t);if(!e)return{reason:N.empty};t=`${e.pathname}${e.search}`}return t.includes(`?`)?{reason:N.query}:(t.startsWith(`/`)||(t=`/${t}`),t.length>1&&(t=t.replace(/\/+$/,``)),{path:t===``?`/`:t})}const de=/[\\^$.*+?()[\]{}|]/;function fe(e,t){let n=e.trim();if(n.startsWith(`^`)&&(n=n.slice(1)),n.endsWith(`$`)&&(n=n.slice(0,-1)),n.endsWith(`/?`)&&(n=n.slice(0,-2)),!de.test(n))return{source:n,target:t};let r=/^(\/[^\\^$.*+?()[\]{}|]*\/)\(\.\*\)$/.exec(n);return r&&!/\$[2-9]/.test(t)?{source:`${r[1]}[...rest]`,target:t.replace(/\$1/g,`[...rest]`)}:null}function pe(e){if(e===void 0||e.trim()===``)return 301;let t=Number(e.trim());return ie.includes(t)?t:null}function me(e,t){let n=e.split(/\r?\n/),r=n.some(e=>e.includes(` `))?` `:`,`,i=n.map((e,t)=>({line:t+1,cells:se(e,r)})).filter(e=>e.cells.some(e=>e!==``)),a=i[0];if(!a)return{format:`plain`,ready:[],skipped:[]};let s=le(a.cells,i.map(e=>e.cells)),c=s.hasHeader?i.slice(1):i,l=(e,t)=>{let n=s.index[t];return n===void 0?void 0:e[n]},u=[],d=[],f=new Map(t.map(e=>[e.source,e.destination])),p=new Set;for(let{line:e,cells:t}of c){let n=l(t,`source`)??``,r=t=>d.push({line:e,source:n,reason:t});if(u.length>=ae){r(N.tooMany);continue}if([`inactive`,`disabled`].includes((l(t,`status`)??`active`).toLowerCase())){r(N.inactive);continue}let i=pe(l(t,`code`));if(i===null){r(N.code);continue}let a=n,s=l(t,`target`)??``,c=(l(t,`matching`)??`exact`).toLowerCase();if(l(t,`regex`)===`1`||(l(t,`format`)??``).toLowerCase()===`regex`||c===`regex`){let e=fe(a,s);if(!e){r(N.pattern);continue}a=e.source,s=e.target}else if(c===`start`&&a.trim().endsWith(`/`))a=`${a.trim()}[...rest]`;else if(c!==`exact`){r(N.pattern);continue}let m=ue(a);if(`reason`in m){r(m.reason);continue}if(oe.test(m.path)){r(N.file);continue}let h=i===410||i===451,g=``;if(!h){if(s.trim()===``){r(N.noTarget);continue}let e=/^https?:\/\//i.test(s.trim()),t=/^https?:\/\//i.test(n.trim())?o(n.trim())?.host:void 0;if(e&&t!==o(s.trim())?.host){r(N.external);continue}let i=ue(s);if(`reason`in i){r(i.reason===N.empty?N.noTarget:i.reason);continue}if(g=i.path,g===m.path){r(N.same);continue}}if(p.has(m.path)){r(N.repeated);continue}let _=f.get(m.path);if(_!==void 0){r(_===g?N.exists:N.conflict);continue}p.add(m.path),u.push({line:e,source:m.path,destination:g,type:i})}let m=new Map(f);for(let e of u)m.set(e.source,e.destination);let h=[];for(let e of u){let t=e.destination,n=!1;for(let r=0;r<20&&t!==``&&m.has(t);r+=1)if(t=m.get(t)??``,t===e.source){n=!0;break}n?d.push({line:e.line,source:e.source,reason:N.loop}):h.push({source:e.source,destination:e.destination,type:e.type})}return{format:s.format,ready:h,skipped:d}}function he(e){let t=e=>/[",\n]/.test(e)?`"${e.replace(/"/g,`""`)}"`:e;return[`source,target,code`,...e.map(e=>`${t(e.source)},${t(e.destination)},${e.type}`)].join(`
2
- `)}const P=`redirects`,F=`redirects-import`,ge={redirection:`Redirection`,yoast:`Yoast SEO Premium`,rankmath:`Rank Math`,plain:`a plain list`},_e={schedule:`* * * * *`};async function ve(e){let t=await e.kv.get(P);return t&&Array.isArray(t.rows)?t:null}async function ye(e){let t=[],n;for(let r=0;r<5;r+=1){let r=await e.redirects?.list({limit:100,cursor:n});if(!r||(t.push(...r.items),n=r.hasMore?r.cursor:void 0,!n))break}return t}async function be(e,t){let n=me(t,await ye(e)),r={status:`checked`,format:n.format,rows:n.ready,skipped:n.skipped,cursor:0,added:0,group:`Imported ${new Date().toISOString().slice(0,16).replace(`T`,` `)}`};return await e.kv.set(P,r),r}function xe(e){let t=e instanceof Error?e.message:String(e);return/already exists/i.test(t)?{kind:`exists`,message:t}:/invalid source pattern|must be different|destination|path traversal|loop/i.test(t)?{kind:`invalid`,message:t}:{kind:`stop`,message:t}}async function Se(e){try{await e.cron?.schedule(F,_e)}catch{}}async function Ce(e){try{await e.cron?.cancel(F)}catch{}}async function we(e,t,n=8){let r={...t,status:`adding`,skipped:[...t.skipped]},i=t.status===`adding`,a=0,o=async()=>{a+=1;try{return await e.kv.set(P,r),!0}catch{return!1}},s=e=>1+(e===i?1:0),c=0,l=r.cursor;for(;r.cursor<r.rows.length;){let t=r.rows[r.cursor];if(!t)break;let i=r.cursor===r.rows.length-1;if(r.cursor!==l&&a+1+s(i)>n)break;a+=1;try{await e.redirects?.create?.({source:t.source,destination:t.destination,type:t.type,groupName:r.group}),r.added+=1}catch(e){let{kind:n,message:i}=xe(e);if(n===`stop`){if(r.cursor!==l||(r.stalls=(r.stalls??0)+1,r.stalls<3))break;r.skipped.push({line:0,source:t.source,reason:`EmDash could not add it: ${i}`})}else n===`exists`?r.added+=1:r.skipped.push({line:0,source:t.source,reason:`EmDash refused it: ${i}`})}if(r.cursor+=1,r.stalls=0,c+=1,c>=4&&a+1+1+s(!1)<=n){if(!await o())return r;c=0}}return r.cursor>=r.rows.length?(r.status=`done`,i&&await Ce(e)):i||await Se(e),await o(),r}async function Te(e,t,n=8){let r=t.status===`removing`,i={...t,status:`removing`,removed:t.removed??0},a=await e.redirects?.list({limit:100,group:t.group}),o=1,s=a?.items??[],c=e=>1+(e===r?1:0),l=s.length,u=!1;for(let t of s){let r=l===1&&!a?.hasMore;if(o+2+c(r)>n)break;o+=2;try{let n=await e.redirects?.get(t.id);n&&await e.redirects?.delete?.(t.id,{_rev:n._rev}),--l,i.removed=(i.removed??0)+1}catch{u=!0;break}}return l===0&&!a?.hasMore?(await e.kv.delete(P),r&&await Ce(e),null):(i.stalls=u&&i.removed===(t.removed??0)?(t.stalls??0)+1:0,i.stalls>=3?Ee(e,i,r):(r||await Se(e),await e.kv.set(P,i),i))}async function Ee(e,t,n=!0){let{stalls:r,removed:i,...a}=t,o={...a,status:`done`,added:Math.max(0,t.added-(i??0))};return n&&await Ce(e),await e.kv.set(P,o),o}async function De(e){let t=[],n;for(let r=0;r<5;r+=1){let r=await e.redirects?.list({limit:100,cursor:n});if(!r||(t.push(...r.items),n=r.hasMore?r.cursor:void 0,!n))break}return{text:he(t),count:t.length}}function I(e,t){return`${e} ${t}${e===1?``:`s`}`}function Oe(e,t,n){return{type:`meter`,label:e,value:n>0?Math.round(Math.min(t,n)/n*100):0,max:100}}function ke(e){if(e.length===0)return[];let t=[{type:`header`,text:`Skipped (${e.length})`},{type:`context`,text:`Skipped rows are not added. Copy any you still need and add them by hand under Manage > Redirects, which also accepts patterns.`},{type:`table`,page_action_id:`skipped_table`,columns:[{key:`line`,label:`Row`},{key:`source`,label:`Old address`},{key:`reason`,label:`Why`}],rows:e.slice(0,50).map(e=>({line:e.line>0?String(e.line):``,source:e.source,reason:e.reason}))}];return e.length>50&&t.push({type:`context`,text:`And ${e.length-50} more.`}),t}const Ae=[{value:`301`,label:`Permanent`},{value:`302`,label:`Temporary, the old address will come back`},{value:`410`,label:`Page removed on purpose, no new address`}];function je(e){return e===301||e===308?`Permanent`:e===302||e===307?`Temporary`:e===410?`Deleted on purpose`:String(e)}async function Me(e){let t=await e.redirects?.list({limit:20});return{rows:(t?.items??[]).map(({id:e,source:t,destination:n,type:r,hits:i})=>({id:e,source:t,destination:n,type:r,hits:i})),more:t?.hasMore??!1}}function Ne(e){let t=e instanceof Error?e.message:String(e);return/already exists/i.test(t)?`There is already a redirect from that old address. Use Change in the list below.`:/must be different/i.test(t)?`The old and new address are the same.`:/loop/i.test(t)?`That would send visitors in a circle.`:/changed since/i.test(t)?`That redirect was just changed elsewhere. Try again.`:`EmDash could not save that redirect. Check both addresses start with /.`}function Pe(e,t,n){let r=me(he([{source:e,destination:t,type:Number(n)||301}]),[]);return r.ready[0]??r.skipped[0]?.reason??`Type the old address first.`}async function Fe(e,t,n,r){let i=Pe(t,n,r);if(typeof i==`string`)return`${i}.`;try{return await e.redirects?.create?.(i),null}catch(e){return Ne(e)}}async function Ie(e,t,n,r,i){let a=Pe(n,r,i);if(typeof a==`string`)return`${a}.`;try{let n=await e.redirects?.get(t);return n?(await e.redirects?.update?.(t,{...a,_rev:n._rev}),null):`That redirect no longer exists.`}catch(e){return Ne(e)}}async function Le(e,t){try{let n=await e.redirects?.get(t);return n&&await e.redirects?.delete?.(t,{_rev:n._rev}),null}catch(e){return Ne(e)}}function Re(e){if(!e)return[{type:`context`,text:`Paste a list exported from Redirection, Yoast SEO Premium or Rank Math, or any list of old address, new address. Nothing is added until you confirm.`},{type:`form`,block_id:`redirects`,fields:[{type:`text_input`,action_id:`list`,label:`Your list, one redirect per line`,multiline:!0,placeholder:`/old-page,/new-page`,initial_value:``}],submit:{label:`Check list`,action_id:`check_redirects`}}];if(e.status===`checked`){let t=e.rows.length;return[{type:`banner`,variant:t===0?`alert`:`default`,title:`${t} ready, ${e.skipped.length} skipped`,description:t===0?`Nothing in this list can be added. See why below.`:`Read as ${ge[e.format]}. Nothing has been added yet. Existing redirects are never changed.`},{type:`actions`,elements:[...t>0?[{type:`button`,action_id:`add_redirects`,label:`Add ${I(t,`redirect`)}`,style:`primary`,confirm:{title:`Add ${I(t,`redirect`)}?`,text:`Visitors to these old addresses will be sent to the new ones. You can remove them all again afterwards.`,confirm:`Add`,deny:`Cancel`}}]:[],{type:`button`,action_id:`reset_redirects`,label:`Start again`}]},...ke(e.skipped)]}if(e.status===`removing`){let t=e.removed??0;return[Oe(`${t} of ${e.added} removed`,t,Math.max(e.added,t,1)),{type:`context`,text:`The rest are removed in the background, a few each minute. You can leave this page.`},{type:`actions`,elements:[{type:`button`,action_id:`remove_redirects`,label:`Remove more now`,style:`primary`},{type:`button`,action_id:`stop_redirects`,label:`Stop here`}]}]}return e.status===`adding`?[Oe(`${e.added} of ${e.rows.length} added`,e.cursor,e.rows.length),{type:`context`,text:`The rest are added in the background, a few each minute. You can leave this page.`},{type:`actions`,elements:[{type:`button`,action_id:`more_redirects`,label:`Add more now`,style:`primary`},{type:`button`,action_id:`stop_redirects`,label:`Stop here`}]}]:[{type:`banner`,title:`${I(e.added,`redirect`)} added from your list`,description:`They are labelled "${e.group}".`},{type:`actions`,elements:[{type:`button`,action_id:`reset_redirects`,label:`Import another list`},...e.added>0?[{type:`button`,action_id:`remove_redirects`,label:`Remove the ${e.added} just added`,confirm:{title:`Remove these redirects?`,text:`Only the redirects this import added are removed.`,confirm:`Remove`,deny:`Cancel`,style:`danger`}}]:[]]},...ke(e.skipped)]}function ze(e,t){let n=e?String(e.type):`301`;return{type:`form`,block_id:e?`edit_redirect_${e.id}`:`add_one_${t}`,fields:[{type:`text_input`,action_id:`source`,label:`Redirect from`,placeholder:`/old-page`,initial_value:e?.source??``},{type:`text_input`,action_id:`destination`,label:`Redirect to`,placeholder:`/new-page`,initial_value:e?.destination??``,condition:{field:`code`,neq:`410`}},{type:`select`,action_id:`code`,label:`Kind`,options:Ae.some(e=>e.value===n)?Ae:[...Ae,{value:n,label:`${je(Number(n))} (${n})`}],initial_value:n}],submit:e?{label:`Save changes`,action_id:`save_redirect:${e.id}`}:{label:`Add redirect`,action_id:`add_redirect`}}}function Be(e){let{state:t,rows:n,editing:r}=e,i=r?void 0:e.panel??(t?`import`:void 0),a=n[0]?.id??`first`,o=[{type:`context`,text:`Send visitors from an old address to a new one. Pages you rename in EmDash are redirected for you.`},{type:`actions`,elements:[{type:`button`,action_id:`open_add`,label:`Add a redirect`,...i===void 0&&!r?{style:`primary`}:{}},{type:`button`,action_id:`open_import`,label:`Import from WordPress`},{type:`button`,action_id:`export_redirects`,label:`Export`},...e.managerUrl?[{type:`link`,label:`Full Redirects screen`,target:{kind:`external`,url:e.managerUrl}}]:[]]}],s={type:`actions`,elements:[{type:`button`,action_id:`close_panel`,label:`Close`}]},c=e.problem?[{type:`banner`,variant:`error`,title:e.problem}]:[];return r?o.push({type:`header`,text:`Change a redirect`},...c,ze(r,a),s,{type:`divider`}):i===`add`?o.push({type:`header`,text:`Add a redirect`},...c,ze(void 0,a),s,{type:`divider`}):i===`import`?(o.push({type:`header`,text:`Import from WordPress`},...Re(t)),t||o.push(s),o.push({type:`divider`})):i===`export`&&o.push({type:`header`,text:`Export`},{type:`context`,text:`Copy this list to keep a backup or to paste into a spreadsheet. It can be imported again here.`},{type:`code`,code:e.exported??``,language:`bash`},s,{type:`divider`}),n.length===0?o.push({type:`empty`,title:`No redirects yet`,description:`Add one, or import a list from your old site.`,size:`sm`}):(o.push({type:`table`,page_action_id:`redirects_table`,columns:[{key:`source`,label:`Redirect from`},{key:`destination`,label:`Redirect to`},{key:`kind`,label:`Kind`},{key:`hits`,label:`Visits`,format:`number`},{key:`edit`,label:``,format:`element`},{key:`remove`,label:``,format:`element`}],rows:n.map(e=>({source:e.source,destination:e.destination||`(none)`,kind:je(e.type),hits:e.hits,edit:{type:`button`,action_id:`edit_redirect`,label:`Change`,value:e.id},remove:{type:`button`,action_id:`remove_redirect`,label:`Remove`,value:e.id,confirm:{title:`Remove this redirect?`,text:`Visitors to ${e.source} will see a not-found page.`,confirm:`Remove`,deny:`Keep`,style:`danger`}}}))}),e.more&&o.push({type:`context`,text:`Showing the newest ${n.length}. The rest are in the full Redirects screen.`})),o}async function Ve(e,t){let n={...t,status:`done`};try{await e.cron?.cancel(F)}catch{}return await e.kv.set(P,n),n}const He=new Set("iIl.,:;|!'`jtfr ()[]".split(``)),Ue=new Set(`mwMWQO@%`.split(``));function We(e){let t=0;for(let n of e)He.has(n)?t+=5:Ue.has(n)?t+=15:n>=`A`&&n<=`Z`?t+=12:t+=9.5;return Math.round(t)}function Ge(e,t){let n=t?.trim()??``,r=e.trim();return n===``||r===``||r.toLowerCase().includes(n.toLowerCase())?r:`${r}: ${n}`}function Ke(e){if(We(e)<=580)return e;let t=e;for(;t.length>1&&We(`${t}…`)>580;)t=t.slice(0,-1);return`${t.trimEnd()}…`}function qe(e){return e.toLowerCase().replace(/\s+/g,` `).trim()}const Je=new Set([`home`,`homepage`,`home page`,`index`,`untitled`,`welcome`]),Ye=new Set([`_type`,`_key`,`id`,`slug`,`url`,`href`,`src`,`style`,`icon`]);function Xe(e){let t={textLength:0,imagesWithoutAlt:0,hasImage:!1},n=(e,r,i)=>{if(i>12)return;if(typeof e==`string`){!Ye.has(r)&&!/^(https?:)?\//.test(e)&&(t.textLength+=e.trim().length);return}if(Array.isArray(e)){for(let t of e)n(t,r,i+1);return}if(typeof e!=`object`||!e)return;let a=e;(a._type===`image`||typeof a.src==`string`&&`alt`in a)&&(t.hasImage=!0,(typeof a.alt!=`string`||a.alt.trim()===``)&&(t.imagesWithoutAlt+=1));for(let[e,t]of Object.entries(a))n(t,e,i+1)};for(let[t,r]of Object.entries(e))t!==`json_ld`&&n(r,t,0);return t}const L={id:`no-social-image`,level:`look`,message:`This page has no picture for shared links, unless your site has a default sharing image.`,fix:`Add an image in the SEO panel. If your theme adds a picture itself, this clears once the page has been viewed.`};function Ze(e){return typeof e.excerpt==`string`?e.excerpt.trim():``}function Qe(e){let t=[],{seo:n}=e,r=Xe(e.data),i=n?.title?.trim()||e.title.trim();if(i===``&&t.push({id:`no-title`,level:`fix`,message:`This page has no title.`,fix:`Give the page a title.`}),Je.has(i.toLowerCase())&&t.push({id:`generic-title`,level:`fix`,message:`The title is just "${i}", which is what Google shows as the headline.`,fix:`Put your name and what you do in SEO Title, in the page's SEO panel.`}),n&&!n.description?.trim()&&Ze(e.data)===``&&t.push({id:`no-description`,level:`fix`,message:`No search description, so Google writes its own summary of this page.`,fix:`Fill in Meta Description in the page's SEO panel.`}),e.duplicateTitle&&i!==``&&t.push({id:`duplicate-title`,level:`fix`,message:`Another page has the same title, so Google cannot tell them apart.`,fix:`Give this page its own title in the SEO panel.`}),n?.canonical&&e.slug){let r=o(n.canonical,`${e.siteUrl.replace(/\/+$/,``)}/`),i=r?.pathname.replace(/\/+$/,``).split(`/`).pop(),a=[`home`,`index`].includes(e.slug)&&(i??``)===``;r&&i!==e.slug&&!a&&t.push({id:`canonical-elsewhere`,level:`fix`,message:`This page tells Google to show a different page instead (canonical).`,fix:`Clear the canonical address in the SEO panel unless that is intended.`})}if(i!==``&&We(Ge(i,e.siteName))>580&&t.push({id:`title-long`,level:`look`,message:`The end of this title may be cut off in Google.`,fix:`Shorten the title, or put the important words first.`}),n&&!n.image&&!r.hasImage&&t.push(L),e.isArticle&&e.authorCount===0&&t.push({id:`no-author`,level:`look`,message:`This article has no author.`,fix:`Add a byline to the article.`}),r.imagesWithoutAlt>0){let e=r.imagesWithoutAlt;t.push({id:`images-without-alt`,level:`look`,message:`${e} ${e===1?`picture has`:`pictures have`} no description for screen readers.`,fix:`Add alt text to each picture.`})}return r.textLength<80&&t.push({id:`nearly-empty`,level:`look`,message:`This page has almost no text.`,fix:`Add some content, or hide the page from search.`}),t}function $e(e,t){return t?`hidden`:e.some(e=>e.level===`fix`)?`fix`:e.length>0?`look`:`ready`}const et=/^[0-9A-HJKMNP-TV-Z]{26}(\.[A-Za-z0-9]+)?$/i;function tt(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function nt(e){return typeof e==`number`&&Number.isFinite(e)&&e>=1?Math.round(e):void 0}function rt(e){if(!e.startsWith(`/_emdash/api/media/file/`))return;let t=e.slice(24);try{t=decodeURIComponent(t)}catch{return}return t!==``&&!t.includes(`/`)?t:void 0}function it(e,t){let n=o(e,`${t.replace(/\/+$/,``)}/`);if(!n||!/^https?:$/.test(n.protocol)||n.search!==``)return[];let{pathname:r}=n;if(r===`/_image`||r.includes(`/cdn-cgi/image/`))return[];let i=rt(r);if(i)return[`file:${i}`];let a=[`url:${n.origin}${r}`],s=r.slice(1);return et.test(s)&&a.push(`file:${s}`),a}function at(e){if(typeof e!=`string`||e===``)return[];if(e.startsWith(`/`)){let t=e.includes(`?`)?void 0:rt(e);return t?[`file:${t}`]:[]}return/^https?:\/\//i.test(e)?it(e,`http://localhost`).filter(e=>e.startsWith(`url:`)):[]}function ot(e){return typeof e==`string`&&e!==``&&!e.includes(`/`)?[`file:${e}`]:[]}function st(e){let t=nt(e.width),n=nt(e.height);if(t===void 0||n===void 0||typeof e.mimeType==`string`&&!e.mimeType.startsWith(`image/`))return;let r=[];if(e._type===`image`&&tt(e.asset))r.push(...ot(e.asset._ref),...at(e.asset.url));else if(typeof e.id==`string`){if(e.provider===void 0||e.provider===`local`){let t=tt(e.meta)?e.meta:{};r.push(...ot(t.storageKey),...ot(e.id))}r.push(...at(e.src))}let i=[...new Set(r)];return i.length>0?{keys:i,width:t,height:n}:void 0}function ct(e){let t=[],n=new Set,r=(e,i)=>{if(t.length>=10||i>12)return;if(Array.isArray(e)){for(let t of e)r(t,i+1);return}if(!tt(e))return;let a=st(e);if(a&&!a.keys.some(e=>n.has(e))){for(let e of a.keys)n.add(e);t.push(a)}for(let t of Object.values(e))r(t,i+1)};for(let[t,n]of Object.entries(e))t!==`json_ld`&&r(n,0);return t.slice(0,10)}function lt(e,t,n){if(!e||e.length===0)return;let r=it(t,n);if(r.length===0)return;let i=e.find(e=>e.keys.some(e=>r.includes(e)));return i?{width:i.width,height:i.height}:void 0}const ut=16*1024;function R(e,t){return`${e}:${t}`}function z(e){return e.storage.entries}function dt(e){return typeof e==`object`&&!!e&&!Array.isArray(e)&&`@type`in e}function ft(e){let t=e;if(typeof e==`string`){if(e.trim()===``||e.length>ut)return[];try{t=JSON.parse(e)}catch{return[]}}let n=(Array.isArray(t)?t:[t]).filter(dt);return JSON.stringify(n).length>ut?[]:n}function pt(e){let{title:t,name:n}=e.data;return typeof t==`string`?t:typeof n==`string`?n:``}function B(e){let{collection:t,entry:n,authors:r,previous:i,config:a,siteUrl:o}=e,s=a.collections[t],c=i?.type??s?.type??f(t),l=n.seo?.title?.trim()||pt(n),d=n.status===`published`,p=d&&e.hasPages,m=e.hasPages?Qe({slug:n.slug,data:n.data,seo:n.seo,title:pt(n),siteName:e.siteName,siteUrl:o,isArticle:u.includes(c),authorCount:r.length,duplicateTitle:e.duplicateTitle}):[],h=m.some(e=>e.id===L.id),g=h&&i?.imageSeen===!0,_=g?m.filter(e=>e.id!==L.id):m,v=e.hasPages?ct(n.data):[];return{v:1,publishedAt:n.publishedAt,updatedAt:n.updatedAt,authors:r,...i?.type?{type:i.type}:{},custom:ft(n.data[a.jsonLdField]),...v.length>0?{images:v}:{},needsImage:h,...g?{imageSeen:g}:{},collection:t,entryId:n.id,title:l,status:e.hasPages?d?$e(_,n.seo?.noIndex===!0||s?.noindex===!0):`draft`:`skip`,titleKey:p?qe(l):``,issues:_,checkedAt:new Date().toISOString()}}function mt(e,t){return e.status===`hidden`||e.status===`draft`||e.status===`skip`?e.status:$e(t,!1)}function ht(e,t){let n=e.issues??[],r=n.some(e=>e.id===L.id);if(!(e.needsImage??r)||e.imageSeen===!0===t&&r!==t)return null;let i=n.filter(e=>e.id!==L.id),a=t?i:[...i,L],{imageSeen:o,...s}=e;return{...s,needsImage:!0,...t?{imageSeen:!0}:{},issues:a,status:mt(e,a)}}async function gt(e,t){return(await _t(e,[t]))[0]??[]}async function _t(e,t){let n=[...new Set(t.filter(e=>e!==``))];if(n.length===0)return t.map(()=>[]);let r=await z(e).query({where:{titleKey:n.length===1?n[0]??``:{in:n}},limit:10*n.length});return t.map(e=>e===``?[]:r.items.filter(t=>t.data.titleKey===e))}function V(e){return e.issues?.some(e=>e.id===`duplicate-title`)??!1}function vt(e,t){let n=e.length>1;for(let{id:r,data:i}of e)V(i)!==n&&t.set(r,H(i,n))}function H(e,t){let n=(e.issues??[]).filter(e=>e.id!==`duplicate-title`),r=t?[...n,{id:`duplicate-title`,level:`fix`,message:`Another page has the same title, so Google cannot tell them apart.`,fix:`Give this page its own title in the SEO panel.`}]:n;return{...e,issues:r,status:mt(e,r)}}async function yt(e,t,n,r){return(await Ct(e,t,n,r))?.record??null}function bt(e){return(e[0]?.bylines??[]).map(({byline:e})=>({name:e.displayName,...e.websiteUrl?{url:e.websiteUrl}:{}}))}async function xt(e,t,n,r){if(!e.content)return null;let[i,a]=await Promise.all([e.content.get(t,n),e.bylines?.getEntriesBylines(t,[n])??[]]);return i?B({collection:t,entry:i,authors:bt(a),previous:null,config:r,siteUrl:e.site.url,siteName:r.site.name??e.site.name,duplicateTitle:!1,hasPages:!0}):null}async function St(e,t,n,r){let i=await e.content?.get(t,n);if(!i)return null;let a=z(e),o=R(t,n),[s,c,l]=await Promise.all([e.bylines?.getEntriesBylines(t,[n])??[],a.get(o),e.schema?.getCollection(t)]),u=B({collection:t,entry:i,authors:bt(s),previous:c,config:r,siteUrl:e.site.url,siteName:r.site.name??e.site.name,duplicateTitle:!1,hasPages:l?l.routable&&!l.hidden:!0}),d=u.titleKey??``,f=c?.titleKey??``;return{entry:i,previous:c,record:H(u,d!==``&&d===f&&c!==null&&V(c)),newKey:d,oldKey:f}}async function Ct(e,t,n,r){let i=await St(e,t,n,r);if(!i)return null;let{entry:a,record:o,newKey:s,oldKey:c}=i,l=z(e),u=R(t,n);await l.put(u,o);let[d=[],f=[]]=await _t(e,[s,c===s?``:c]),p=new Map,m=d,h=d.find(e=>e.id===u)?.data;if(s!==``&&h?.checkedAt!==o.checkedAt){let e=h??f.find(e=>e.id===u)?.data??null;e&&Tt(e.updatedAt,o.updatedAt)&&(p.set(u,o),m=[...d.filter(e=>e.id!==u),{id:u,data:o}])}return vt(m,p),vt(f.filter(e=>e.id!==u),p),p.size>0&&await l.putMany([...p].map(([e,t])=>({id:e,data:t}))),{record:p.get(u)??o,entry:a}}async function wt(e,t,n,r,i){let a=await St(e,t,n,r);if(!a)return null;let{entry:o,previous:s,newKey:c,oldKey:l}=a,u=i?i(a.record):a.record,d=R(t,n);return s===null||c===l?await z(e).put(d,u):i&&await z(e).put(d,i(s)),{record:u,entry:o}}function Tt(e,t){if(!e||!t)return!1;let n=Date.parse(e),r=Date.parse(t);return Number.isFinite(n)&&Number.isFinite(r)&&n<r}async function Et(e,t,n){let r=z(e),i=R(t,n),a=await r.get(i);await r.delete(i);let o=new Map;vt((await gt(e,a?.titleKey??``)).filter(e=>e.id!==i),o),o.size>0&&await r.putMany([...o].map(([e,t])=>({id:e,data:t})))}const U=`scan`,Dt=`stats`,W=`seo-scan`,Ot=[`ready`,`look`,`fix`,`hidden`];async function kt(e){return e.kv.get(U)}async function At(e,t,n,r=6){let i=await Ft(e,{status:`running`,collections:t,scope:[...t],phase:`entries`,checked:0,tally:{checked:0,fix:0,hidden:0,rows:[],more:!1},startedAt:new Date().toISOString()},n,r,{scheduled:!1});if(i.status===`done`&&!i.sweep)return i;try{await e.cron?.schedule(W,{schedule:`* * * * *`})}catch{}return i}async function jt(e,t,n,r=4){let i=t.collections[0];if(i===void 0)return t.phase=`titles`,0;let a=1,o=new Date().toISOString(),s=e=>(e.checkedAt??``)>=t.startedAt,c=await e.content?.list(i,{limit:20,cursor:t.cursor,where:{status:`published`}}),l=c?.items??[];if(l.length>0&&r<4)return a;if(l.length>0){let r=z(e),u=l.map(e=>e.id),[d,f]=await Promise.all([e.bylines?.getEntriesBylines(i,u)??[],r.getMany(u.map(e=>R(i,e)))]);a+=2;let p=new Map(d.map(e=>[e.entryId,e.bylines])),m=new Set,h=l.map(t=>{let r=R(i,t.id),a=f.get(r)??null;return a&&(Tt(t.updatedAt,a.updatedAt)&&s(a)||(a.checkedAt??``)>=o)?(m.add(r),{id:r,data:a}):{id:r,data:B({hasPages:!0,collection:i,entry:t,authors:(p.get(t.id)??[]).map(({byline:e})=>({name:e.displayName,...e.websiteUrl?{url:e.websiteUrl}:{}})),previous:a,config:n,siteUrl:e.site.url,siteName:n.site.name??e.site.name,duplicateTitle:a?V(a):!1})}});if(t.cursor===void 0&&t.checked===0&&!c?.hasMore&&t.collections.length===1){let e=new Map;for(let{data:t}of h)t.titleKey&&e.set(t.titleKey,(e.get(t.titleKey)??0)+1);for(let t of h){let n=(e.get(t.data.titleKey??``)??0)>1;m.has(t.id)&&V(t.data)!==n&&m.delete(t.id),t.data=H(t.data,n)}t.status=`done`,t.finishedAt=new Date().toISOString(),t.sweep={}}let g=h.filter(e=>!m.has(e.id));g.length>0&&(await r.putMany(g),a+=1);for(let{data:e}of h)Pt(t.tally,e,1);t.checked+=l.length}return c?.hasMore&&c.cursor?t.cursor=c.cursor:(t.cursor=void 0,t.collections=t.collections.slice(1),t.collections.length===0&&t.status!==`done`&&(t.phase=`titles`,t.sweep={})),a}async function Mt(e,t,n){let r=new Set(t.scope??[]),i=z(e),a=t.sweep?.cursor,o=new Set(t.sweep?.kept??[]),s=r.size>0&&e.content?await i.query({where:{status:{in:Ot}},limit:100,cursor:a}):{items:[],hasMore:!1,cursor:void 0},c=s.items.filter(({id:e,data:n})=>!o.has(e)&&r.has(n.collection??``)&&(n.entryId??``)!==``&&(n.checkedAt??``)<t.startedAt).slice(0,1),l=await Promise.all(c.map(({data:t})=>e.content?.get(t.collection??``,t.entryId??``))),u=[],d=[];c.forEach(({id:t,data:r},i)=>{let a=l[i];a?a.status===`published`?o.add(t):d.push({id:t,data:B({hasPages:!0,collection:r.collection??``,entry:a,authors:r.authors,previous:r,config:n,siteUrl:e.site.url,siteName:n.site.name??e.site.name,duplicateTitle:!1})}):u.push(t)}),u.length>0&&await i.deleteMany(u),d.length>0&&await i.putMany(d);let f=o.size>0?{kept:[...o]}:{};c.length>0?t.sweep={...a?{cursor:a}:{},...f}:s.hasMore&&s.cursor?t.sweep={cursor:s.cursor,...f}:delete t.sweep}async function Nt(e,t){let n=z(e),r=await n.query({orderBy:{titleKey:`asc`},limit:100,cursor:t.titleCursor}),i=r.items.filter(e=>(e.data.titleKey??``)!==``),a=new Map(i.map(e=>[e.id,e.data])),o=new Map,s=(e,t,n)=>{V(t)!==n&&o.set(e,n)},c=t.lastTitle;for(let e=0;e<i.length;e+=1){let t=i[e];if(!t)continue;let r=t.data.titleKey??``,o=i[e+1],l=c?.key===r,u=o?.data.titleKey===r;if(l&&c&&!c.flagged&&e===0){let e=await n.get(c.id);e&&(a.set(c.id,e),s(c.id,e,!0))}let d=l||u;s(t.id,t.data,d),c={key:r,id:t.id,flagged:d}}if(o.size>0){let e=new Set([...o.keys()].map(e=>a.get(e)?.titleKey??``)),r=[...a].filter(([,t])=>e.has(t.titleKey??``)).map(([e])=>e),i=await n.getMany(r);for(let t of r){let n=a.get(t),r=i.get(t);(!n||r?.checkedAt!==n.checkedAt||r?.titleKey!==n.titleKey)&&e.delete(n?.titleKey??``)}let s=[];for(let[n,r]of o){let i=a.get(n);if(!i||!e.has(i.titleKey??``))continue;let o=H(i,r);s.push({id:n,data:o}),Pt(t.tally,i,-1),Pt(t.tally,o,1)}s.length>0&&await n.putMany(s)}t.lastTitle=c,r.hasMore&&r.cursor?t.titleCursor=r.cursor:(t.status=`done`,t.finishedAt=new Date().toISOString(),t.titleCursor=void 0,t.lastTitle=void 0)}function G(e,t=`fix`){return{collection:e.collection??``,id:e.entryId??``,title:e.title||`(no title)`,issues:(e.issues??[]).filter(e=>e.level===t).map(e=>e.message)}}function Pt(e,t,n){if(!(!t.status||t.status===`draft`||t.status===`skip`)){if(e.checked+=n,t.status===`hidden`){e.hidden+=n;let r=e.hiddenTitles??[],i=t.title||`(no title)`;e.hiddenTitles=n===1?[...r,i].slice(0,10):r.filter(e=>e!==i)}if(t.status===`look`){let r=e.look??[];e.look=n===1?[...r,G(t,`look`)].slice(0,10):r.filter(e=>e.id!==t.entryId)}t.status===`fix`&&(e.fix+=n,n===1?e.rows.length<10?e.rows.push(G(t)):e.more=!0:e.rows=e.rows.filter(e=>e.id!==t.entryId))}}async function Ft(e,t,n,r=6,{scheduled:i=!0}={}){let a={...t,collections:[...t.collections],tally:{...t.tally,rows:[...t.tally.rows]}};if(a.status===`done`&&!a.sweep)return a;if(a.sweep)await Mt(e,a,n);else if(a.phase===`entries`){let t=0;for(let i=0;i<5&&a.phase===`entries`;i+=1){let o=r-2-t;if(i>0&&o<1)break;let s={checked:a.checked,collections:a.collections.length,cursor:a.cursor};if(t+=await jt(e,a,n,i===0?4:o),a.checked!==s.checked||a.status===`done`||a.collections.length===s.collections&&a.cursor===s.cursor)break}a.phase===`titles`&&a.checked===0&&(a.status=`done`,a.finishedAt=new Date().toISOString())}else await Nt(e,a);if(i&&a.status===`done`&&!a.sweep)try{await e.cron?.cancel(W)}catch{}return await e.kv.set(U,a),a}async function It(e){let t=z(e),[n,r,i,a,o]=await Promise.all([t.count({status:{in:[`ready`,`look`,`fix`,`hidden`]}}),t.count({status:`fix`}),t.query({where:{status:`fix`},limit:10}),t.query({where:{status:`look`},limit:10}),t.query({where:{status:`hidden`},limit:10})]);return{checked:n,fix:r,hidden:o.hasMore?11:o.items.length,rows:i.items.map(({data:e})=>G(e)),more:i.hasMore,look:a.items.map(({data:e})=>G(e,`look`)),hiddenTitles:o.items.map(({data:e})=>e.title||`(no title)`)}}const Lt=new Map;async function Rt(e,t,{fresh:n=!1,now:r=Date.now()}={}){let i=e.site?.url??``,a=Lt.get(e.plugin.id);a||Lt.set(e.plugin.id,a=new Map);let o=a.get(i);if(n||!o||r>=o.until){let e=t(),n={value:e,until:r+3e4};a.set(i,n),e.catch(()=>{a.get(i)===n&&a.delete(i)}),o=n}return structuredClone(await o.value)}function zt(e){Lt.delete(e)}const K={overview:0,redirects:1,identity:2,types:2},q=[{value:`FoodEstablishment`,label:`Restaurant, cafe or bakery`},{value:`Store`,label:`Shop`},{value:`MedicalBusiness`,label:`Health and medical`},{value:`Dentist`,label:`Dentist`},{value:`LegalService`,label:`Legal`},{value:`FinancialService`,label:`Financial`},{value:`HomeAndConstructionBusiness`,label:`Home services (plumber, electrician, builder)`},{value:`HealthAndBeautyBusiness`,label:`Beauty and hair`},{value:`SportsActivityLocation`,label:`Fitness`},{value:`LodgingBusiness`,label:`Hotel or lodging`},{value:`AutomotiveBusiness`,label:`Automotive`},{value:`ProfessionalService`,label:`Professional service`},{value:`LocalBusiness`,label:`Other`}],Bt=[``,`$`,`$$`,`$$$`,`$$$$`],Vt=[{id:`facebook`,label:`Facebook`,hosts:[`www.facebook.com`,`facebook.com`,`fb.com`]},{id:`instagram`,label:`Instagram`,hosts:[`www.instagram.com`,`instagram.com`]},{id:`linkedin`,label:`LinkedIn`,hosts:[`www.linkedin.com`,`linkedin.com`]},{id:`x`,label:`X (Twitter)`,hosts:[`x.com`,`www.x.com`,`twitter.com`,`www.twitter.com`]},{id:`youtube`,label:`YouTube`,hosts:[`www.youtube.com`,`youtube.com`]}];function Ht(e){let t={},n=[];for(let r of e){let e=o(r)?.host??``,i=Vt.find(t=>t.hosts.includes(e));i&&!(i.id in t)?t[i.id]=r:n.push(r)}return{named:t,other:n}}const Ut={needed:`needed`,worth:`worth adding`,optional:`optional`};function J(e,t,n){let r=n.filter(e=>!!e);return r.length>0?`${e}: ${r.join(`, `)}`:`${e} (${Ut[t]}, not added yet)`}function Wt(e){return A.map(t=>{let n=re(t,e.openingHours).replace(`, `,` and `);return n===``?void 0:`${t.slice(0,3)} ${n}`}).filter(e=>e!==void 0)}function Gt(e){let{site:t}=e.config,n=Y(t),r=[];return n===`local`?((!t.address?.streetAddress||!t.address.addressLocality)&&r.push({what:`Address`,why:`Without it Google cannot treat you as a local business.`}),t.openingHours.length===0&&r.push({what:`Opening hours`,why:`Google cannot show when you are open.`}),t.telephone||r.push({what:`Phone number`,why:`Customers cannot call you from Google.`})):(n===`organization`&&!t.logo&&r.push({what:`Logo`,why:`Google shows it beside your name.`}),t.sameAs.length===0&&r.push({what:`Profiles elsewhere`,why:`They help Google connect your other pages to this site.`})),r}function Y(e){return e.type===`Person`?`person`:e.type===`Organization`?`organization`:`local`}function Kt(e,t){let n=o(e);if(!(!n||n.protocol!==`https:`&&n.protocol!==`http:`))return`${n.origin}${t}`}function qt(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function X(e,t,n=``){let r=e[t];return typeof r==`string`?r.trim():n}function Jt(e,t,n){let r=e[t];return typeof r==`boolean`?r:n}function Yt(e){return e.split(/\r?\n/).map(e=>e.trim()).filter(e=>e!==``)}async function Xt(e,{rows:t=!0}={}){let[n,a,o]=await Promise.all([e.kv.list(),e.schema?.listCollections()??[],t?Me(e):{rows:[],more:!1}]),c=new Map(n.map(e=>[e.key,e.value])),l=c.get(P);return{...i(c,s)?{uncopied:{value:c.get(s.legacy)}}:{},config:S(await r(e,c,s,{copy:!1})),collections:a.filter(e=>e.routable&&!e.hidden),redirects:l&&Array.isArray(l.rows)?l:null,redirectRows:o.rows,moreRedirects:o.more,scan:c.get(U)??null,stats:c.get(Dt)??null}}async function Zt(e,n){let r=await It(e);n.uncopied?await t(e,s,n.uncopied.value):JSON.stringify(r)!==JSON.stringify(n.stats)&&await e.kv.set(Dt,r);let{uncopied:i,...a}=n;return{...a,stats:r}}function Qt(e){return e.stats??(e.scan?.status===`done`?e.scan.tally:null)}function $t(e){let{scan:t}=e,n=Qt(e);if(!t||t.status===`running`)return[{type:`banner`,title:`Checking your pages`,description:t?`${t.checked} checked so far. The rest are checked in the background.`:`This starts by itself and takes about a minute.`}];if(!n)return[];let r=n.checked-n.fix-n.hidden;return[{type:`stats`,items:[{label:`Pages checked`,value:n.checked},{label:`Nothing to fix`,value:r},{label:`To fix`,value:n.fix}]}]}function en(e){let{scan:t}=e,n=Qt(e);if(!n||!t||t.status===`running`)return[];let r=[];n.checked===0?r.push({type:`empty`,title:`No published pages yet`,size:`sm`}):n.fix===0?r.push({type:`empty`,title:`No pages to fix. Nice work.`,size:`sm`}):(r.push({type:`header`,text:`To fix`},{type:`table`,page_action_id:`fix_table`,columns:[{key:`page`,label:`Page`},{key:`missing`,label:`What's missing`},{key:`open`,label:``,format:`element`}],rows:n.rows.map(e=>({page:e.title,missing:e.issues.join(` `),open:{type:`link`,label:`Open page`,target:{kind:`content`,collection:e.collection,id:e.id}}}))}),n.more&&r.push({type:`context`,text:`Showing the first 10. Fix these and the next ones appear.`}));let i=n.look??[];if(i.length>0&&r.push({type:`accordion`,label:`Worth a look (${i.length}${i.length===10?`+`:``})`,blocks:[{type:`context`,text:`Nothing is wrong with these pages. These are improvements for when you have time.`},{type:`table`,page_action_id:`look_table`,columns:[{key:`page`,label:`Page`},{key:`advice`,label:`Could be better`},{key:`open`,label:``,format:`element`}],rows:i.map(e=>({page:e.title,advice:e.issues.join(` `),open:{type:`link`,label:`Open page`,target:{kind:`content`,collection:e.collection,id:e.id}}}))}]}),n.hidden>0){let e=(n.hiddenTitles??[]).join(`, `);r.push({type:`context`,text:`${n.hidden>10?`More than 10`:n.hidden} ${n.hidden===1?`page is`:`pages are`} hidden from Google and not counted${e?`: ${e}`:``}. To show one again, open it and switch off "Hide from search engines" in its SEO panel.`})}if(t.finishedAt){let e=new Date(t.finishedAt).toLocaleDateString(`en-GB`,{day:`numeric`,month:`short`});r.push({type:`context`,text:`Each page is checked again when it is saved. Last full check: ${e}.`})}return r}function tn(e){if(!e.scan)return[];let t=e.scan.status===`running`;return[{type:`actions`,elements:[{type:`button`,action_id:t?`check_more`:`check_pages`,label:t?`Update`:`Check all pages again`}]}]}function nn(e){return e.collections.filter(t=>{let n=e.config.collections[t.slug];return n?.noindex===!0&&n.hiddenOk!==!0})}function rn(e,t){let n=nn(e);if(n.length===0)return[];let r=[{type:`banner`,variant:`alert`,title:`Some pages are hidden from Google`,description:`Every page in ${n.map(e=>e.label).join(`, `)} is hidden from search engines. If that is a leftover from building the site, switch it back on in the Settings tab, under Content types.`}];return t&&r.push({type:`actions`,elements:[{type:`button`,action_id:`hidden_ok`,label:`This is intended`}]}),r}function an(e,t){let{steps:n,site:r}=e.config;return n.identity?[]:[{type:`banner`,title:`Tell Google who this site is for`,description:`Google can then show your name and details beside your pages. It takes a minute.`},{type:`accordion`,label:`Set this up (1 minute)`,default_open:!0,blocks:[{type:`form`,block_id:`setup_identity`,fields:[{type:`radio`,action_id:`kind`,label:`This site is for`,options:[{label:`A local business customers visit (shop, cafe, clinic)`,value:`local`},{label:`A company or organisation`,value:`organization`},{label:`A person`,value:`person`}]},{type:`select`,action_id:`business`,label:`Kind of business`,options:[{label:`Choose the closest`,value:``},...q],initial_value:``,condition:{field:`kind`,eq:`local`}},{type:`text_input`,action_id:`name`,label:`Name`,placeholder:`Your name or your business's name`,initial_value:r.name??t},{type:`text_input`,action_id:`street`,label:`Street address`,initial_value:r.address?.streetAddress??``,condition:{field:`kind`,eq:`local`}},{type:`text_input`,action_id:`town`,label:`Town or city`,initial_value:r.address?.addressLocality??``,condition:{field:`kind`,eq:`local`}}],submit:{label:`Save`,action_id:`setup_identity`}}]}]}function on(e,t){let{site:n}=e.config,r=Y(n),i=q.find(e=>e.value===n.type)?.label,a=r===`person`?`a person`:r===`local`?(i??`a local business`).toLowerCase():`a company`,o=[n.address?.streetAddress,n.address?.addressLocality].filter(Boolean).join(`, `),s=Wt(n);return[`Google is told: ${n.name??t}, ${a}`,o?`, ${o}`:``,s.length>0?`, open ${s.join(`, `)}`:``,`. Change this in the Settings tab.`].join(``)}function sn(e,t){if(!e.config.steps.identity)return[];let n=Gt(e);return n.length===0?[{type:`context`,text:on(e,t)}]:[{type:`header`,text:`Your details`},{type:`context`,text:`${n.length} ${n.length===1?`thing`:`things`} Google needs from you. Add ${n.length===1?`it`:`them`} in the Settings tab.`},{type:`table`,page_action_id:`details_table`,columns:[{key:`what`,label:`Missing`},{key:`why`,label:`Why it matters`}],rows:n}]}function cn(e,t){return[...$t(e),...rn(e,!0),...an(e,t),...sn(e,t),...en(e),...tn(e)]}function ln(e){let t=Qt(e),n={type:`actions`,elements:[{type:`link`,label:`Open SEO`,target:{kind:`plugin-page`,path:`/overview`}}]};if(!t||t.checked===0){let{steps:t}=e.config;return{blocks:[{type:`section`,text:t.identity?`Your pages are being checked.`:`One thing left: tell Google who this site is for.`},n]}}return{blocks:[{type:`stats`,items:[t.fix===0?{label:`SEO`,value:`All pages ready`}:{label:t.fix===1?`Page to fix`:`Pages to fix`,value:t.fix}]},n]}}function un(e,t){let{site:n}=e.config,r=Y(n),i=r===`local`,a=e.config.steps.identity,s=Ht(n.sameAs),c=[{type:`context`,text:`Google reads these details from your site and can show them beside your pages. Sections marked needed are followed up on the Overview; the rest are up to you.`},{type:`form`,block_id:`identity`,fields:[{type:`radio`,action_id:`kind`,label:`This site is for`,options:[{label:`A local business customers visit (shop, cafe, clinic)`,value:`local`},{label:`A company or organisation`,value:`organization`},{label:`A person`,value:`person`}],...a?{initial_value:r}:{}},{type:`select`,action_id:`business`,label:`Kind of business`,options:[{label:`Choose the closest`,value:``},...q],initial_value:i?n.type:``,condition:{field:`kind`,eq:`local`}},{type:`text_input`,action_id:`name`,label:`Name`,placeholder:`Your name or your business's name`,initial_value:n.name??t},{type:`text_input`,action_id:`description`,label:`What you do, in one or two sentences`,multiline:!0,initial_value:n.description??``}],submit:{label:`Save`,action_id:`save_identity`}},{type:`context`,text:`Each section below is saved with its own Save button.`}],l={type:`accordion`,label:J(`Address and phone`,i?`needed`:`optional`,[n.address?.streetAddress,n.address?.addressLocality,n.telephone,n.email]),default_open:i&&Gt(e).some(e=>e.what!==`Opening hours`),blocks:[{type:`context`,text:i?`Google needs an address before it treats you as a local business. Use the same details as your Google Business Profile, which is what puts you on the map.`:`Leave the address empty if you have no public one.`},{type:`form`,block_id:`contact`,fields:[{type:`text_input`,action_id:`street`,label:`Street address`,initial_value:n.address?.streetAddress??``},{type:`text_input`,action_id:`town`,label:`Town or city`,initial_value:n.address?.addressLocality??``},{type:`text_input`,action_id:`region`,label:`State or region`,initial_value:n.address?.addressRegion??``},{type:`text_input`,action_id:`postcode`,label:`Postcode`,initial_value:n.address?.postalCode??``},{type:`text_input`,action_id:`country`,label:`Country code (GB, US, IN, AU)`,initial_value:n.address?.addressCountry??``},{type:`text_input`,action_id:`telephone`,label:`Phone`,initial_value:n.telephone??``},{type:`text_input`,action_id:`email`,label:`Email`,initial_value:n.email??``},{type:`select`,action_id:`price_range`,label:`Price range`,options:Bt.map(e=>({label:e===``?`Not shown`:e,value:e})),initial_value:n.priceRange??``},{type:`text_input`,action_id:`areas`,label:`Areas you serve, one per line (if you travel to customers)`,multiline:!0,initial_value:n.areaServed.join(`
3
- `)}],submit:{label:`Save address and phone`,action_id:`save_contact`}}]},u=i?{type:`accordion`,label:J(`Opening hours`,`needed`,Wt(n)),blocks:[{type:`context`,text:`Type the hours for each day, like 9:00-17:00. For a break in the day use two ranges: 12:00-14:30, 18:00-22:00. A day left empty is shown as closed.`},{type:`form`,block_id:`hours`,fields:A.map(e=>({type:`text_input`,action_id:`hours_${e.toLowerCase()}`,label:e,placeholder:`Closed`,initial_value:re(e,n.openingHours)})),submit:{label:`Save opening hours`,action_id:`save_hours`}}]}:{type:`accordion`,label:`Opening hours (only for a local business)`,blocks:[{type:`context`,text:`Opening hours are for a place customers visit. Choose "A local business customers visit" above and save, and this section opens up.`}]},d={type:`accordion`,label:J(`Logo`,r===`organization`?`needed`:`worth`,[n.logo?`added`:void 0]),blocks:[{type:`context`,text:`Google can show your logo beside your name. In Media, open your logo and use Copy URL, then paste it here. Use a square image at least 112 pixels wide.`},{type:`form`,block_id:`logo`,fields:[{type:`text_input`,action_id:`logo_url`,label:`Logo link`,placeholder:`https://example.com/logo.png`,initial_value:n.logo??``}],submit:{label:`Save logo`,action_id:`save_logo`}}]},f={type:`accordion`,label:J(`Your profiles elsewhere`,i?`worth`:`needed`,n.sameAs.map(e=>o(e)?.host.replace(/^www\./,``))),blocks:[{type:`context`,text:`Links to your pages on other sites help Google connect them to you. Paste the full address of each profile you have.`},{type:`form`,block_id:`profiles`,fields:[...Vt.map(e=>({type:`text_input`,action_id:`profile_${e.id}`,label:e.label,placeholder:`https://${e.hosts[0]}/yourname`,initial_value:s.named[e.id]??``})),{type:`text_input`,action_id:`profiles`,label:`Other profiles, one per line`,multiline:!0,initial_value:s.other.join(`
4
- `)}],submit:{label:`Save profiles`,action_id:`save_profiles`}}]},p={type:`accordion`,label:J(`More about you`,`optional`,[n.alternateName,n.legalName,n.foundingDate?`founded ${n.foundingDate}`:void 0]),blocks:[{type:`context`,text:`Nothing here is needed. Skip anything that does not apply.`},{type:`form`,block_id:`company`,fields:[{type:`text_input`,action_id:`alternate_name`,label:`Also known as (a short name or abbreviation)`,initial_value:n.alternateName??``},{type:`text_input`,action_id:`legal_name`,label:`Registered legal name`,initial_value:n.legalName??``},{type:`text_input`,action_id:`founded`,label:`Founded (year, or YYYY-MM-DD)`,placeholder:`2012`,initial_value:n.foundingDate??``}],submit:{label:`Save`,action_id:`save_company`}}]},m=i?[l,u,d,f,p]:r===`organization`?[d,f,l,p,u]:[f,d,l,p,u];return c.push(...m),c}function dn(e,t){return e.config.collections[t]?.type??f(t)}function fn(e,t){let{config:n}=e,r=e.collections.find(e=>e.slug===t),i=[{type:`context`,text:`Google is told what each kind of content is, so it can show it properly: a blog post with its author and date, a service as a service. These were worked out from the names and are usually right. Every kind of content that has its own pages is listed here; content that only appears inside other pages, such as testimonials, is not. To change a single page (an About or Contact page, say), open it and use Change page type in its Google preview panel. Picking from a menu saves straight away.`}];return r&&i.push({type:`banner`,variant:`alert`,title:`Hide every page in ${r.label} from Google?`,description:`People will no longer find these pages through search engines. You can switch this back at any time.`},{type:`actions`,elements:[{type:`button`,action_id:`confirm_hide`,label:`Yes, hide them`,style:`danger`,value:r.slug},{type:`button`,action_id:`cancel_hide`,label:`Cancel`}]}),i.push({type:`table`,page_action_id:`types_table`,empty_text:`This site has no content types with pages yet.`,columns:[{key:`name`,label:`Content type`},{key:`shown`,label:``,format:`element`},{key:`visible`,label:``,format:`element`}],rows:e.collections.map(t=>{let r=n.collections[t.slug]?.noindex===!0;return{name:t.label,shown:{type:`menu`,action_id:`set_type`,label:`Google treats these as: ${M(dn(e,t.slug))}`,items:j.map(({value:e,label:n})=>({label:n,value:`${t.slug}|${e}`}))},visible:{type:`menu`,action_id:`set_visible`,label:r?`Hidden from Google`:`Can appear on Google`,items:[{label:`Can appear on Google`,value:`${t.slug}|show`},{label:`Hide from Google and other search engines`,value:`${t.slug}|hide`}]}}})},{type:`form`,block_id:`breadcrumbs`,fields:[{type:`toggle`,action_id:`breadcrumbs`,label:`Show the path to each page on Google (Home > Services > Cakes)`,description:`Leave on unless your theme already does this.`,initial_value:n.breadcrumbs}],submit:{label:`Save`,action_id:`save_advanced`}}),[{type:`accordion`,label:`Content types (usually right as they are): ${e.collections.map(t=>{let r=n.collections[t.slug]?.noindex===!0;return`${t.label} as ${M(dn(e,t.slug))}${r?`, hidden`:``}`}).join(`; `)}`,default_open:r!==void 0,blocks:i}]}function Z(e,t,n,r){let i=[{type:`header`,text:`SEO`},{type:`context`,text:`How your site appears on Google and other search engines.`}];return n?{blocks:[...i,{type:`tab`,default_tab:r.tab,panels:[{label:`Overview`,blocks:cn(e,t.site.name)},{label:`Redirects`,blocks:Be({state:e.redirects,rows:e.redirectRows,more:e.moreRedirects,editing:r.editing,panel:r.panel,problem:r.problem,exported:r.exported,managerUrl:Kt(t.site.url,`/_emdash/admin/redirects`)})},{label:`Settings`,blocks:[...un(e,t.site.name),...fn(e,r.pendingHide),D(e.config.hideHelpLine)]}]},...k(!e.config.hideHelpLine)],...r.toast?{toast:r.toast}:{}}:{blocks:[...i,...$t(e),...rn(e,!1),...en(e),{type:`context`,text:`Business details, opening hours and redirects are set by your site admin. Ask them if something there is wrong.`}]}}function pn(e,t){if(e===``)return{};let n=o(e,`${t.replace(/\/+$/,``)}/`);return!n||n.protocol!==`https:`&&n.protocol!==`http:`?{error:`Couldn't save. The logo link must be a web address starting with https://`}:n.pathname.includes(`/_emdash/api/media/asset/`)?{error:`That link only works when signed in. In Media, use Copy URL on the image instead.`}:{logo:n.href}}function mn(e,t){let n={tab:K.identity},{site:r}=e.config,i=X(t,`kind`,e.config.steps.identity?Y(r):``);if(i!==`local`&&i!==`organization`&&i!==`person`)return{error:`Choose who this site is for.`,state:n};let a=X(t,`business`,Y(r)===`local`?r.type:``),o=q.find(e=>e.value===a)?.value;if(i===`local`&&!o)return{error:`Choose the kind of business.`,state:n};let s=i===`person`?`Person`:i===`local`?o??`LocalBusiness`:`Organization`,c=X(t,`name`,r.name??``);return{config:{...e.config,site:{...r,type:s,name:c===``?void 0:c,description:X(t,`description`,r.description??``)||void 0,openingHours:i===`local`?r.openingHours:[]},steps:{...e.config.steps,identity:!0}},state:n}}function hn(e,t,n){let r={tab:K.identity},i=pn(X(t,`logo_url`,e.config.site.logo??``),n);return`error`in i?{error:i.error,state:r}:{config:{...e.config,site:{...e.config.site,logo:i.logo}},state:r}}function gn(e,t){let{site:n}=e.config,r={streetAddress:X(t,`street`)||void 0,addressLocality:X(t,`town`)||void 0,addressRegion:X(t,`region`)||void 0,postalCode:X(t,`postcode`)||void 0,addressCountry:X(t,`country`).toUpperCase()||void 0},i=X(t,`price_range`);return{config:{...e.config,site:{...n,email:X(t,`email`)||void 0,telephone:X(t,`telephone`)||void 0,address:Object.values(r).some(e=>e!==void 0)?r:void 0,priceRange:Bt.includes(i)&&i!==``?i:void 0,areaServed:Yt(X(t,`areas`,n.areaServed.join(`
5
- `)))}},state:{tab:K.identity}}}function _n(e,t){let n={tab:K.identity},r=[];for(let i of A){let a=ne(i,X(t,`hours_${i.toLowerCase()}`,re(i,e.config.site.openingHours)));if(!a)return{error:`Couldn't read the hours for ${i}. Write them like 9:00-17:00, with at most two ranges.`,state:n};r.push(...a)}return{config:{...e.config,site:{...e.config.site,openingHours:r}},state:n}}function vn(e,t){let n={tab:K.identity},r=Ht(e.config.site.sameAs),i=[...Vt.map(e=>X(t,`profile_${e.id}`,r.named[e.id]??``)),...Yt(X(t,`profiles`,r.other.join(`
6
- `)))].filter(e=>e!==``),a=[];for(let e of i){let t=o(/^https?:\/\//i.test(e)?e:`https://${e}`);if(!t||!t.host.includes(`.`))return{error:`"${e}" is not a web address. Paste the full link to the profile.`,state:n};a.push(t.href)}return{config:{...e.config,site:{...e.config.site,sameAs:a}},state:n}}function yn(e,t){let n={tab:K.identity},{site:r}=e.config,i=X(t,`founded`,r.foundingDate??``);return i!==``&&!/^\d{4}(-\d{2}-\d{2})?$/.test(i)?{error:`Write the founding date as a year, like 2012, or as 2012-05-01.`,state:n}:{config:{...e.config,site:{...r,alternateName:X(t,`alternate_name`,r.alternateName??``)||void 0,legalName:X(t,`legal_name`,r.legalName??``)||void 0,foundingDate:i||void 0}},state:n}}function bn(e,t){let{robots:n}=e.config,r=X(t,`snippet`,n.nosnippet?`none`:`full`),i=X(t,`image_preview`,n.maxImagePreview);return{config:{...e.config,breadcrumbs:Jt(t,`breadcrumbs`,e.config.breadcrumbs),robots:{...n,nosnippet:r===`none`,maxSnippet:r===`short`?160:-1,maxImagePreview:i===`none`||i===`standard`||i===`large`?i:n.maxImagePreview}},state:{tab:K.types}}}function xn(e,t,n){let r={tab:K.types},[i,a]=typeof t==`string`?t.split(`|`):[];if(!i||!e.collections.some(e=>e.slug===i))return{error:`That content type no longer exists.`,state:r};let o=e.config.collections[i]??{},s=n===`type`?j.find(e=>e.value===a)?.value:o.type??dn(e,i);if(!s)return{error:`Choose one of the listed types.`,state:r};let c=n===`visible`?a===`hide`:o.noindex===!0;return{config:{...e.config,collections:{...e.config.collections,[i]:{type:s,noindex:c}},steps:{...e.config.steps,types:!0}},state:r}}async function Sn(e,t){if(t.scan?.status===`done`)return Zt(e,t);let n=t.scan?await Ft(e,t.scan,t.config,6):await At(e,t.collections.map(e=>e.slug),t.config,6);return{...t,scan:n,stats:n.status===`done`?n.tally:null}}async function Cn(e,t,n){let r=(t??0)>=50,i=qt(e)?e:{},o=typeof i.action_id==`string`?i.action_id:``,c=await Xt(n,{rows:!(r&&i.type===`block_action`&&(o===`add_redirects`||o===`more_redirects`||o===`remove_redirects`))});if(i.type===`page_load`&&i.page===`widget:seo`)return ln(await Sn(n,c));if(!r||i.type===`page_load`||o===``)return Z(await Sn(n,c),n,r,{tab:K.overview});let l=async(e,t=`Saved. Google will pick this up next time it visits.`)=>{if(`error`in e)return Z(c,n,r,{...e.state,toast:{type:`error`,message:e.error}});let i=S(e.config);return await a(n,s,i),zt(n.plugin.id),Z({...c,config:i},n,r,{...e.state,toast:{type:`success`,message:t}})},u=async e=>{try{let{rows:t,more:r}=await Me(n);return{...e,redirectRows:t,moreRedirects:r}}catch{return e}},d=async(e,t,i)=>e?Z(c,n,r,{tab:K.redirects,editing:i,...i?{}:{panel:`add`},problem:e}):Z(await u(c),n,r,{tab:K.redirects,toast:{type:`success`,message:t}});if(i.type===`block_action`){if(o===`check_pages`||o===`check_more`){let e=await Sn(n,o===`check_pages`?{...c,scan:null}:c);return Z(e,n,r,{tab:K.overview,toast:e.scan?.status===`done`?{type:`success`,message:`All pages checked.`}:{type:`info`,message:`${e.scan?.checked??0} pages checked so far. The rest follow in the background.`}})}if(o===`set_type`)return l(xn(c,i.value,`type`));if(o===`set_visible`){let[e,t]=typeof i.value==`string`?i.value.split(`|`):[];return t===`hide`?Z(c,n,r,{tab:K.types,pendingHide:e}):l(xn(c,i.value,`visible`),`These pages can appear on Google again.`)}if(o===`confirm_hide`&&typeof i.value==`string`)return l(xn(c,`${i.value}|hide`,`visible`),`These pages are now hidden from Google and other search engines.`);if(o===`cancel_hide`)return Z(c,n,r,{tab:K.types});if(o===`hidden_ok`){let e={...c.config.collections};for(let t of nn(c))e[t.slug]={...e[t.slug],hiddenOk:!0};return l({config:{...c.config,collections:e},state:{tab:K.overview}},`Noted. These pages stay hidden from search.`)}if(o===`edit_redirect`){let e=c.redirectRows.find(e=>e.id===i.value);return Z(c,n,r,{tab:K.redirects,editing:e})}if(o===`open_add`)return Z(c,n,r,{tab:K.redirects,panel:`add`});if(o===`open_import`)return Z(c,n,r,{tab:K.redirects,panel:`import`});if(o===`close_panel`)return Z(c,n,r,{tab:K.redirects});if(o===`remove_redirect`&&typeof i.value==`string`)return d(await Le(n,i.value),`Redirect removed.`);if(o===`add_redirects`||o===`more_redirects`){let e=c.redirects;if(!e||e.status!==`checked`&&e.status!==`adding`)return Z(await u(c),n,r,{tab:K.redirects});let t=await we(n,e,6);return Z(await u({...c,redirects:t}),n,r,{tab:K.redirects,toast:t.status===`done`?{type:`success`,message:`${I(t.added,`redirect`)} added.`}:{type:`info`,message:`${t.added} added so far. The rest follow in the background, a few each minute.`}})}if(o===`stop_redirects`&&c.redirects?.status===`removing`){let e=c.redirects.removed??0,t=await Ee(n,c.redirects);return Z(await u({...c,redirects:t}),n,r,{tab:K.redirects,toast:{type:`info`,message:`Stopped. ${I(e,`redirect`)} removed.`}})}if(o===`stop_redirects`&&c.redirects?.status===`adding`){let e=await Ve(n,c.redirects);return Z({...c,redirects:e},n,r,{tab:K.redirects,toast:{type:`info`,message:`Stopped. ${I(e.added,`redirect`)} added.`}})}if(o===`reset_redirects`){let e=c.redirects?.status===`adding`||c.redirects?.status===`removing`;return e||await n.kv.delete(P),Z({...c,redirects:e?c.redirects:null},n,r,{tab:K.redirects})}if(o===`remove_redirects`&&(c.redirects?.status===`done`||c.redirects?.status===`removing`)){let e=await Te(n,c.redirects,6);return Z(await u({...c,redirects:e}),n,r,{tab:K.redirects,toast:e?e.status===`done`?{type:`error`,message:`EmDash would not remove some of them. Press Remove to try again, or remove them in the full Redirects screen.`}:{type:`info`,message:`${e.removed??0} removed so far. The rest are removed in the background, a few each minute.`}:{type:`success`,message:`Removed. Your other redirects are untouched.`}})}if(o===`remove_redirects`)return Z(await u(c),n,r,{tab:K.redirects});if(o===`export_redirects`){let{text:e,count:t}=await De(n);return Z(c,n,r,{tab:K.redirects,panel:`export`,exported:e,toast:{type:`info`,message:`${I(t,`redirect`)} listed below.`}})}return Z(c,n,r,{tab:K.overview})}if(i.type!==`form_submit`)return Z(c,n,r,{tab:K.overview});let f=qt(i.values)?i.values:{};if(o===`check_redirects`){let e=X(f,`list`);if(e===``)return Z(c,n,r,{tab:K.redirects,toast:{type:`error`,message:`Paste a list first.`}});let t=await be(n,e);return Z({...c,redirects:t},n,r,{tab:K.redirects,toast:{type:`success`,message:`Checked. Nothing has been added yet.`}})}if(o===`add_redirect`){let e=X(f,`source`),t=X(f,`code`,`301`)===`410`?``:X(f,`destination`);return d(await Fe(n,e,t,X(f,`code`,`301`)),t===``?`Done. ${e} now tells visitors the page was removed.`:`Redirect added. ${e} now goes to ${t}.`)}if(o.startsWith(`save_redirect:`)){let e=o.slice(14),t=c.redirectRows.find(t=>t.id===e);return d(await Ie(n,e,X(f,`source`,t?.source??``),X(f,`destination`,t?.destination??``),X(f,`code`,String(t?.type??301))),`Redirect changed.`,t)}if(o===`setup_identity`){let e=mn(c,f);if(`error`in e)return l(e);let t=X(f,`street`),n=X(f,`town`),{site:r}=e.config,i=t!==``||n!==``?{...r.address,...t?{streetAddress:t}:{},...n?{addressLocality:n}:{}}:r.address;return l({config:{...e.config,site:{...r,address:i}},state:{tab:K.overview}})}if(o===`save_identity`)return l(mn(c,f));if(o===`save_logo`)return l(hn(c,f,n.site.url));if(o===`save_contact`)return l(gn(c,f));if(o===`save_hours`)return l(_n(c,f));if(o===`save_profiles`)return l(vn(c,f));if(o===`save_company`)return l(yn(c,f));if(o===`save_advanced`)return l(bn(c,f));if(o===T){let e=O(f,c.config.hideHelpLine);return l({config:{...c.config,hideHelpLine:e},state:{tab:K.types}},e?`Saved. The help line is hidden.`:`Saved. The help line shows again.`)}return Z(c,n,r,{tab:K.overview})}function Q(e){return{"@id":e}}function $(e){let t={};for(let[n,r]of Object.entries(e))r==null||r===``||Array.isArray(r)&&r.length===0||(t[n]=r);return t}function wn(e,t){return o(e,t)?.href??e}function Tn(e,t){return(e.siteUrl||t||(o(e.url)?.origin??``)).replace(/\/+$/,``)}function En(e,t){if(e.canonical)return wn(e.canonical,`${t}/`);let n=o(e.url,`${t}/`);return n?(n.search=``,n.hash=``,n.href):e.url}function Dn(e){return e.path===`/`||e.path===``}function On(e){return u.includes(e)}function kn(e,t,n){let r=e.content?t.collections[e.content.collection]:void 0;if(n?.type)return n.type;if(r?.type)return r.type;let i=e.content?f(e.content.collection):`WebPage`;return i===`WebPage`?e.pageType===`article`?`BlogPosting`:`WebPage`:i}function An(e,t,n,r){let i=`${n}/#logo`,a=e.type===`Person`,o=!a&&e.type!==`Organization`&&e.address===void 0?`Organization`:e.type,s=!a&&(e.address!==void 0||e.geo!==void 0||e.openingHours.length>0);return $({"@type":o,"@id":r,name:t,alternateName:e.alternateName,legalName:a?void 0:e.legalName,description:e.description,url:`${n}/`,logo:e.logo&&!a?{"@type":`ImageObject`,"@id":i,url:e.logo,contentUrl:e.logo,caption:t}:void 0,image:e.logo?a?e.logo:Q(i):void 0,sameAs:e.sameAs,email:e.email,telephone:e.telephone,address:e.address?{"@type":`PostalAddress`,...e.address}:void 0,geo:e.geo&&!a?{"@type":`GeoCoordinates`,...e.geo}:void 0,openingHoursSpecification:s?e.openingHours.map(e=>({"@type":`OpeningHoursSpecification`,dayOfWeek:e.days,opens:e.opens,closes:e.closes})):void 0,priceRange:a?void 0:e.priceRange,areaServed:a?void 0:e.areaServed,foundingDate:a?void 0:e.foundingDate,vatID:a?void 0:e.vatID,taxID:a?void 0:e.taxID,numberOfEmployees:!a&&e.numberOfEmployees?{"@type":`QuantitativeValue`,value:e.numberOfEmployees}:void 0})}function jn(e){let t=e;try{t=decodeURIComponent(e)}catch{}let n=t.replace(/[-_]+/g,` `).trim();return n.charAt(0).toUpperCase()+n.slice(1)}function Mn(e,t,n){let{page:r,config:i,site:a}=e;if(r.breadcrumbs)return r.breadcrumbs.map(e=>({name:e.name,url:wn(e.url,`${t}/`)}));if(!i.breadcrumbs||Dn(r))return[];let o=r.path.split(`/`).filter(e=>e!==``);if(o.length===0)return[];let s=a.trailingSlash===`always`?`/`:``,c=[{name:`Home`,url:`${t}/`}],l=``;return o.forEach((e,r)=>{l+=`/${e}`;let i=r===o.length-1;c.push({name:i?n:jn(e),url:`${t}${l}${s}`})}),c}function Nn(e,t){return e.length<=t?e:`${e.slice(0,t-1).trimEnd()}…`}function Pn(e,t,n){let r;for(let{"@context":i,...a}of t){let t=typeof a[`@id`]==`string`?a[`@id`]:void 0;if(t!==void 0){if(n.has(t))continue;n.add(t),r??=t}e.push(a)}return r}function Fn(e){let{page:t,config:n,record:r,site:i}=e,a=Tn(t,i.url),o=En(t,a),s=t.locale??i.locale,c=n.site.name??t.siteName??i.name,l=t.pageTitle??t.title??c,u=n.site.type===`Person`,d=`${a}/#${u?`person`:`organization`}`,f=`${a}/#website`,p=o,m=`${o}#breadcrumb`,h=kn(t,n,r),g=On(h),_=h===`Service`,v=h===`ProfilePage`,y=`${o}#service`,b=`${o}#person`,x=r?.publishedAt??t.articleMeta?.publishedTime??void 0,S=r?.updatedAt??t.articleMeta?.modifiedTime??void 0,C=t.image?wn(t.image,`${a}/`):void 0,w=C??``,T=Mn(e,a,l),E=[An(n.site,c,a,d),$({"@type":`WebSite`,"@id":f,url:`${a}/`,name:c,alternateName:n.site.alternateName,description:n.site.description,publisher:Q(d),inLanguage:s})];if(e.noindex)return E;let D=$({"@type":g||_?`WebPage`:h,"@id":p,url:o,name:l,description:t.description,isPartOf:Q(f),about:Dn(t)?Q(d):void 0,primaryImageOfPage:C?Q(w):void 0,image:C?Q(w):void 0,breadcrumb:T.length>0?Q(m):void 0,mainEntity:_?Q(y):v?Q(b):void 0,datePublished:x,dateModified:S,inLanguage:s});if(E.push(D),g){let e=(r?.authors??[]).map(e=>$({"@type":`Person`,name:e.name,url:e.url})),n=t.articleMeta?.author?[{"@type":`Person`,name:t.articleMeta.author}]:[Q(d)];E.push($({"@type":h,"@id":`${o}#article`,isPartOf:Q(p),mainEntityOfPage:Q(p),headline:Nn(l,110),description:t.description,image:C?Q(w):void 0,datePublished:x,dateModified:S,author:e.length>0?e:n,publisher:Q(d),inLanguage:s}))}_&&E.push($({"@type":`Service`,"@id":y,name:l,description:t.description,url:o,image:C?Q(w):void 0,provider:Q(d),areaServed:n.site.areaServed})),v&&E.push($({"@type":`Person`,"@id":b,name:l,description:t.description,url:o,image:C?Q(w):void 0,worksFor:u?void 0:Q(d)})),C&&E.push({"@type":`ImageObject`,"@id":w,url:C,contentUrl:C,...lt(r?.images,C,a)}),T.length>0&&E.push({"@type":`BreadcrumbList`,"@id":m,itemListElement:T.map((e,t)=>$({"@type":`ListItem`,position:t+1,name:e.name,item:t===T.length-1?void 0:e.url}))});let O=new Set([`${a}/#logo`]);for(let e of E)typeof e[`@id`]==`string`&&O.add(e[`@id`]);let k=Pn(E,r?.custom??[],O);return k!==void 0&&!g&&D.mainEntity===void 0&&(D.mainEntity=Q(k)),E}const In=`default`;function Ln(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Rn(e){let t=o(e);if(!t)return e;let n=t.pathname.split(`/`).filter(e=>e!==``);return[t.host,...n].join(` › `)}function zn(e,t){return e.length<=t?e:`${e.slice(0,t-1).trimEnd()}…`}async function Bn(e,t,n,r){if(!t)return{blocks:[]};let i=(n??0)>=50,a=Ln(e)?e:{},s=await c(r),l=i&&a.type===`block_action`&&a.action_id===`set_type`,u=typeof a.value==`string`?a.value:In,d=j.find(e=>e.value===u)?.value,[p,m]=await Promise.all([wt(r,t.collection,t.id,s,l?e=>{let{type:t,...n}=e;return d?{...n,type:d}:n}:void 0),r.content?.getPublicUrl?.(t.collection,t.id)]),h=p?.record,g=p?.entry,_=h&&l?{type:`success`,message:`Saved. Google will pick this up next time it visits.`}:void 0;if(!h||!g)return{blocks:[{type:`context`,text:`Save this page to see how it may look on Google.`}]};if(h.status===`skip`)return{blocks:[{type:`context`,text:`This kind of content has no page of its own, so Google only sees it inside other pages. There is nothing to set here.`}]};let v=s.site.name??r.site.name,y=r.site.url.replace(/\/+$/,``),b=[`home`,`index`,``].includes(g.slug??``)?`${y}/`:`${y}/${g.slug}`,x=[`home`,`index`].includes(g.slug??``)?`${y}/`:m??b,S=h.title||`(no title)`,C=g.seo?.description?.trim()??``,w=C===``&&Ze(g.data)!==``,T=C||Ze(g.data),E=s.collections[t.collection]?.type??f(t.collection),D=h.type??E,O=h.status===`hidden`,k=h.issues??[],A=[{type:`context`,text:v?`${v} · ${Rn(x)}`:Rn(x)},{type:`header`,text:Ke(h.title?Ge(S,v):S)},{type:`section`,text:T===``?`No description yet. Google will pick some text from the page.`:zn(T,160)},...w?[{type:`context`,text:`This description is the page's excerpt. To write a different one for search, fill in Meta Description in the SEO panel.`}]:[],{type:`context`,text:`What Google sees: the published page, with the SEO title and description as last saved, and your site's name after the title as most sites show it. Google may rewrite them.`},{type:`fields`,fields:[{label:`Shown to Google as`,value:M(D)},{label:`In search results`,value:h.status===`draft`?`Not published yet`:O?`Hidden from search`:`Yes`}]}];if(k.length===0)A.push({type:`context`,text:`Nothing to fix on this page.`});else{let e=[...k].sort((e,t)=>Number(t.level===`fix`)-Number(e.level===`fix`));for(let t of e.slice(0,3))A.push({type:`section`,text:`${t.level===`fix`?`To fix`:`Worth a look`}: ${t.message}`},{type:`context`,text:t.fix});e.length>3&&A.push({type:`context`,text:`And ${e.length-3} more.`})}if(i){let e=Fn({page:{url:x,path:o(x)?.pathname??`/`,locale:g.locale,kind:`content`,pageType:`website`,title:S,pageTitle:S,description:T===``?null:T,canonical:g.seo?.canonical??null,image:null,content:{collection:t.collection,id:t.id,slug:g.slug}},config:s,record:h,noindex:O,site:r.site});A.push({type:`divider`},{type:`actions`,elements:[{type:`menu`,action_id:`set_type`,label:`Change page type`,items:[{label:`Same as other pages here (${M(E)})`,value:In},...j.map(({value:e,label:t})=>({value:e,label:t}))]},.../^https?:\/\//.test(x)?[{type:`link`,label:`Test on Google`,target:{kind:`external`,url:`https://search.google.com/test/rich-results?url=${encodeURIComponent(x)}`}}]:[]]},{type:`accordion`,label:`View the markup`,blocks:[{type:`context`,text:`The schema markup (JSON-LD) this page sends to search engines.`},{type:`code`,language:`jsonc`,code:JSON.stringify({"@context":`https://schema.org`,"@graph":e},null,2)}]})}return{blocks:A,..._?{toast:_}:{}}}const Vn=/\s*,\s*/;function Hn(e,t,n){let r=(e??``).toLowerCase().split(Vn).map(e=>e.trim()).filter(e=>e!==``),i=t||r.includes(`noindex`)||r.includes(`none`),a=r.includes(`nofollow`)||r.includes(`none`),o=[i?`noindex`:`index`,a?`nofollow`:`follow`],s=new Set([`index`,`noindex`,`follow`,`nofollow`,`none`,`all`]),c=r.filter(e=>!s.has(e));if(!i){let e=e=>c.some(t=>t.startsWith(e));n.nosnippet||c.includes(`nosnippet`)?c.includes(`nosnippet`)||c.push(`nosnippet`):e(`max-snippet:`)||c.push(`max-snippet:${n.maxSnippet}`),e(`max-image-preview:`)||c.push(`max-image-preview:${n.maxImagePreview}`),e(`max-video-preview:`)||c.push(`max-video-preview:${n.maxVideoPreview}`)}return[...o,...new Set(c)].join(`, `)}function Un(e){return e.startsWith(`noindex`)}function Wn(e){return/[?&]_preview=/.test(e)}async function Gn(e,t,n){let r=n?.id;if(typeof r==`string`)try{await yt(e,t,r,await c(e))}catch(n){e.log.warn(`could not refresh entry record`,{collection:t,id:r,error:String(n)})}}const Kn={hooks:{"page:metadata":async(e,t)=>{let{page:n}=e,r=n.content,[i,a]=await Promise.all([Rt(t,()=>c(t),{fresh:Wn(n.url)}),(async()=>{if(!r)return null;try{return{stored:await z(t).get(R(r.collection,r.id))}}catch(e){return{error:e}}})()]),o=null;if(r&&a){let{collection:e,id:s}=r;try{if(`error`in a)throw a.error;let{stored:r}=a,c=z(t);o=r??await xt(t,e,s,i);let l=!!(n.seo?.ogImage||n.image);if(r&&ht(r,l)){let t=await c.get(R(e,s)),n=t?ht(t,l):null;n&&await c.put(R(e,s),n),o=n??t??r}}catch(n){t.log.warn(`could not load entry record`,{collection:e,id:s,error:String(n)})}}let s=n.content?i.collections[n.content.collection]:void 0,l=Hn(n.seo?.robots,s?.noindex===!0,i.robots);return[{kind:`meta`,name:`robots`,content:l},{kind:`jsonld`,id:`primary`,graph:{"@context":`https://schema.org`,"@graph":Fn({page:n,config:i,record:o,noindex:Un(l),site:t.site})}}]},"content:afterSave":async(e,t)=>{await Gn(t,e.collection,e.content)},"content:afterPublish":async(e,t)=>{await Gn(t,e.collection,e.content)},"content:afterUnpublish":async(e,t)=>{await Gn(t,e.collection,e.content)},"content:afterRestore":async(e,t)=>{await Gn(t,e.collection,e.content)},"plugin:activate":async(e,t)=>{try{let[e,n]=await Promise.all([t.kv.list(),t.schema?.listCollections()??[]]),i=new Map(e.map(e=>[e.key,e.value]));if(i.get(U))return;let a=S(await r(t,i,s,{copy:!1})),o=await At(t,n.filter(e=>e.routable&&!e.hidden).map(e=>e.slug),a,7);o.status===`done`&&o.checked===0&&await t.kv.delete(U)}catch(e){t.log.warn(`could not start the first page check`,{error:String(e)})}},cron:async(e,t)=>{if(e.name===W){let e=await kt(t);e?.status===`running`||e?.sweep?await Ft(t,e,await c(t)):await t.cron?.cancel(W);return}if(e.name!==F)return;let n=await ve(t);n?.status===`adding`?await we(t,n):n?.status===`removing`?await Te(t,n):await t.cron?.cancel(F)},"content:afterDelete":async(e,t)=>{await Et(t,e.collection,e.id)}},routes:{admin:{permission:`plugins:read`,handler:async(e,t)=>Cn(e.input,e.user?.role,t)},"editor/preview":{permission:`content:read`,handler:async(e,t)=>Bn(e.input,e.ui?.entry,e.user?.role,t)},hello:{handler:async(e,t)=>(t.log.info(`hello route called`,{pluginId:t.plugin.id}),{greeting:`hello`,pluginId:t.plugin.id})}}};export{Kn as default};
1
+ function e(e){return e!=null}async function t(e,t,n){try{await e.settings.set(t.name,n)}catch(n){e.log.warn(`could not copy the settings to their new place`,{key:t.name,error:String(n)})}}async function n(n,r,{copy:i=!0}={}){let a=await n.settings.get(r.name);if(e(a))return a;let o=await n.kv.get(r.legacy);return e(o)?(i&&await t(n,r,o),o):null}async function r(n,r,i,{copy:a=!0}={}){let o=r.get(`settings:${i.name}`);if(e(o))return o;let s=r.get(i.legacy);return e(s)?(a&&await t(n,i,s),s):null}function i(t,n){return!e(t.get(`settings:${n.name}`))&&e(t.get(n.legacy))}async function a(e,t,n){await e.settings.set(t.name,n);try{await e.kv.set(t.legacy,n)}catch(n){e.log.warn(`could not keep a copy of the settings for the earlier release`,{key:t.legacy,error:String(n)})}}function o(e,t){try{return new URL(e,t)}catch{return null}}function s(e,t){if(e===``)return{};let n=o(e,`${t.replace(/\/+$/,``)}/`);return!n||n.protocol!==`https:`&&n.protocol!==`http:`?{error:`Couldn't save. The logo link must be a web address starting with https://`}:n.pathname.includes(`/_emdash/api/media/asset/`)?{error:`That link only works when signed in. In Media, use Copy URL on the image instead.`}:{logo:n.href}}function c(e){let t=o(/^https?:\/\//i.test(e)?e:`https://${e}`);return t&&t.host.includes(`.`)?t.href:null}const l={name:`config`,legacy:`config`};async function u(e){return T(await n(e,l,{copy:!1}))}const d=[`WebPage`,`AboutPage`,`ContactPage`,`CollectionPage`,`ProfilePage`],f=[`Article`,`BlogPosting`,`NewsArticle`],p=[`Service`];function m(e){let t=e.toLowerCase();return[`posts`,`post`,`blog`].includes(t)?`BlogPosting`:[`news`,`articles`,`article`].includes(t)?`Article`:[`services`,`service`].includes(t)?`Service`:[`team`,`people`,`staff`,`authors`].includes(t)?`ProfilePage`:`WebPage`}const h={site:{type:`Organization`,sameAs:[],openingHours:[],areaServed:[]},collections:{},robots:{maxSnippet:-1,maxImagePreview:`large`,maxVideoPreview:-1,nosnippet:!1},jsonLdField:`json_ld`,breadcrumbs:!0,steps:{identity:!1,types:!1},hideHelpLine:!1};function g(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function _(e){if(typeof e!=`string`)return;let t=e.trim();return t===``?void 0:t}function v(e){let t=_(e),n=t?o(t)?.protocol:void 0;return n===`https:`||n===`http:`?t:void 0}function y(e,t){return typeof e==`number`&&Number.isInteger(e)&&e>=-1?e:t}function b(e){let t=[...d,...f,...p];return typeof e==`string`&&t.includes(e)?e:void 0}function x(e){if(!g(e))return;let t={};for(let n of[`streetAddress`,`addressLocality`,`addressRegion`,`postalCode`,`addressCountry`]){let r=_(e[n]);r&&(t[n]=r)}return Object.keys(t).length>0?t:void 0}function S(e){if(!Array.isArray(e))return[];let t=[];for(let n of e){if(!g(n)||!Array.isArray(n.days))continue;let e=n.days.filter(e=>typeof e==`string`&&e!==``),r=_(n.opens),i=_(n.closes);e.length>0&&r&&i&&t.push({days:e,opens:r,closes:i})}return t}function C(e){if(!g(e))return{...h.site};let t=g(e.geo)?e.geo:void 0,n=t?.latitude,r=t?.longitude;return{type:_(e.type)??`Organization`,name:_(e.name),alternateName:_(e.alternateName),legalName:_(e.legalName),description:_(e.description),logo:v(e.logo),sameAs:Array.isArray(e.sameAs)?e.sameAs.map(v).filter(e=>e!==void 0):[],email:_(e.email),telephone:_(e.telephone),address:x(e.address),geo:typeof n==`number`&&typeof r==`number`?{latitude:n,longitude:r}:void 0,openingHours:S(e.openingHours),priceRange:_(e.priceRange),areaServed:Array.isArray(e.areaServed)?e.areaServed.map(_).filter(e=>e!==void 0):[],foundingDate:_(e.foundingDate),vatID:_(e.vatID),taxID:_(e.taxID),numberOfEmployees:typeof e.numberOfEmployees==`number`&&e.numberOfEmployees>0?Math.round(e.numberOfEmployees):void 0}}function w(e){return/^\d{4}(-\d{2}-\d{2})?$/.test(e)}function T(e){if(!g(e))return h;let t={};if(g(e.collections))for(let[n,r]of Object.entries(e.collections))g(r)&&(t[n]={type:b(r.type),noindex:r.noindex===!0,...r.hiddenOk===!0?{hiddenOk:!0}:{}});let n=g(e.robots)?e.robots:{},r=n.maxImagePreview;return{site:C(e.site),collections:t,robots:{maxSnippet:y(n.maxSnippet,h.robots.maxSnippet),maxImagePreview:r===`none`||r===`standard`||r===`large`?r:h.robots.maxImagePreview,maxVideoPreview:y(n.maxVideoPreview,h.robots.maxVideoPreview),nosnippet:n.nosnippet===!0},jsonLdField:_(e.jsonLdField)??h.jsonLdField,breadcrumbs:e.breadcrumbs!==!1,steps:{identity:g(e.steps)&&e.steps.identity===!0,types:g(e.steps)&&e.steps.types===!0},hideHelpLine:e.hideHelpLine===!0}}const E=`help_line`,D=`hide_help_line`,O=`save_help_line`;function k(e){return{type:`toggle`,action_id:D,label:`Hide the help line at the foot of this page`,description:`Only admins see it: links to the guide and to more free plugins.`,initial_value:e}}function A(e){return{type:`form`,block_id:`${E}_switch_${e?`on`:`off`}`,fields:[k(e)],submit:{label:`Save`,action_id:O}}}function ee(e,t){let n=e[D];return typeof n==`boolean`?n:t}function te(e){if(!e)return[];let t=(e,t)=>({type:`link`,label:e,target:{kind:`external`,url:t}});return[{type:`divider`,block_id:`${E}_rule`},{type:`actions`,block_id:E,elements:[t(`Help`,`https://moorage.dev/guides/add-schema-markup-to-emdash`),t(`More free plugins`,`https://moorage.dev/plugins`),t(`by Moorage`,`https://moorage.dev`)]}]}const j=[`Monday`,`Tuesday`,`Wednesday`,`Thursday`,`Friday`,`Saturday`,`Sunday`],ne=/^(\d{1,2})(?:[:.](\d{2}))?\s*[-–—]\s*(\d{1,2})(?:[:.](\d{2}))?$/;function re(e,t){let n=Number(e),r=Number(t??`0`);return n>24||r>59||n===24&&r!==0?null:`${String(n).padStart(2,`0`)}:${String(r).padStart(2,`0`)}`}function ie(e,t){let n=t.trim();if(n===``||n.toLowerCase()===`closed`)return[];let r=n.split(/\s*[,;]\s*|\s+and\s+/i).filter(e=>e!==``);if(r.length>2)return null;let i=[];for(let t of r){let n=ne.exec(t);if(!n)return null;let r=re(n[1]??``,n[2]),a=re(n[3]??``,n[4]);if(!r||!a)return null;i.push({days:[e],opens:r,closes:a})}return i}function M(e,t){return t.filter(t=>t.days.includes(e)).map(e=>`${e.opens}-${e.closes}`).join(`, `)}function ae(e){let t=[];for(let n of j){let r=ie(n,e(n));if(!r)return{day:n};t.push(...r)}return{hours:t}}const N=[{value:`WebPage`,label:`Web page`,short:`Web page`},{value:`AboutPage`,label:`About page`,short:`About page`},{value:`ContactPage`,label:`Contact page`,short:`Contact page`},{value:`BlogPosting`,label:`Blog post`,short:`Blog post`},{value:`Article`,label:`Article`,short:`Article`},{value:`Service`,label:`Service`,short:`Service`},{value:`ProfilePage`,label:`Person or team member`,short:`Person`}],P=[{value:`FoodEstablishment`,label:`Restaurant, cafe or bakery`},{value:`Store`,label:`Shop`},{value:`MedicalBusiness`,label:`Health and medical`},{value:`Dentist`,label:`Dentist`},{value:`LegalService`,label:`Legal`},{value:`FinancialService`,label:`Financial`},{value:`HomeAndConstructionBusiness`,label:`Home services (plumber, electrician, builder)`},{value:`HealthAndBeautyBusiness`,label:`Beauty and hair`},{value:`SportsActivityLocation`,label:`Fitness`},{value:`LodgingBusiness`,label:`Hotel or lodging`},{value:`AutomotiveBusiness`,label:`Automotive`},{value:`ProfessionalService`,label:`Professional service`},{value:`LocalBusiness`,label:`Other`}],oe=[``,`$`,`$$`,`$$$`,`$$$$`];function F(e){return N.find(t=>t.value===e)?.short??e}const se=[301,302,307,308,410,451],ce=2e3,I={empty:`No old address`,query:`Addresses with a ? are not supported`,external:`Points to another website`,noTarget:`No new address`,same:`Old and new address are the same`,code:`Unknown redirect type`,pattern:`Uses a pattern. Add it by hand under Manage > Redirects`,inactive:`Switched off in the export`,repeated:`Listed twice`,exists:`Already exists`,conflict:`Conflicts with an existing redirect`,loop:`Would send visitors in a circle`,tooMany:`Only the first ${ce} rows are imported at once`,file:`Addresses ending in a file type, like .xml or .jpg, are not redirected by EmDash`},le=/\.\w{1,10}$/;function ue(e,t){let n=[],r=``,i=!1;for(let a=0;a<e.length;a+=1){let o=e[a];i?o===`"`&&e[a+1]===`"`?(r+=`"`,a+=1):o===`"`?i=!1:r+=o:o===`"`?i=!0:o===t?(n.push(r.trim()),r=``):r+=o}n.push(r.trim());let a=n[0]??``;return n.length===1&&/\S\s+\S/.test(a)?a.split(/\s+/):n}const de={source:`source`,origin:`source`,from:`source`,"old url":`source`,old:`source`,sources:`source`,target:`target`,destination:`target`,to:`target`,"new url":`target`,new:`target`,url_to:`target`,regex:`regex`,code:`code`,"http code":`code`,type:`code`,header_code:`code`,format:`format`,matching:`matching`,status:`status`,ignore:`ignore`,id:`id`};function fe(e,t){let n=e.map(e=>de[e.toLowerCase()]),r=n.includes(`source`)&&n.includes(`target`);if(r){let e={};return n.forEach((t,n)=>{t&&e[t]===void 0&&(e[t]=n)}),{format:e.matching===void 0?e.format===void 0?e.regex===void 0?`plain`:`redirection`:`yoast`:`rankmath`,hasHeader:r,index:e}}let i=t.slice(0,20),a=i.map(e=>e[2]??``),o=i.map(e=>(e[3]??``).toLowerCase());return o.length>0&&o.every(e=>e===`plain`||e===`regex`)?{format:`yoast`,hasHeader:r,index:{source:0,target:1,code:2,format:3}}:a.length>0&&a.every(e=>e===`0`||e===`1`)?{format:`redirection`,hasHeader:r,index:{source:0,target:1,regex:2,code:3}}:{format:`plain`,hasHeader:r,index:{source:0,target:1,code:2}}}function pe(e){let t=e.trim();if(t===``)return{reason:I.empty};if(/^https?:\/\//i.test(t)){let e=o(t);if(!e)return{reason:I.empty};t=`${e.pathname}${e.search}`}return t.includes(`?`)?{reason:I.query}:(t.startsWith(`/`)||(t=`/${t}`),t.length>1&&(t=t.replace(/\/+$/,``)),{path:t===``?`/`:t})}const me=/[\\^$.*+?()[\]{}|]/;function he(e,t){let n=e.trim();if(n.startsWith(`^`)&&(n=n.slice(1)),n.endsWith(`$`)&&(n=n.slice(0,-1)),n.endsWith(`/?`)&&(n=n.slice(0,-2)),!me.test(n))return{source:n,target:t};let r=/^(\/[^\\^$.*+?()[\]{}|]*\/)\(\.\*\)$/.exec(n);return r&&!/\$[2-9]/.test(t)?{source:`${r[1]}[...rest]`,target:t.replace(/\$1/g,`[...rest]`)}:null}function ge(e){if(e===void 0||e.trim()===``)return 301;let t=Number(e.trim());return se.includes(t)?t:null}function _e(e,t){let n=e.split(/\r?\n/),r=n.some(e=>e.includes(` `))?` `:`,`,i=n.map((e,t)=>({line:t+1,cells:ue(e,r)})).filter(e=>e.cells.some(e=>e!==``)),a=i[0];if(!a)return{format:`plain`,ready:[],skipped:[]};let s=fe(a.cells,i.map(e=>e.cells)),c=s.hasHeader?i.slice(1):i,l=(e,t)=>{let n=s.index[t];return n===void 0?void 0:e[n]},u=[],d=[],f=new Map(t.map(e=>[e.source,e.destination])),p=new Set;for(let{line:e,cells:t}of c){let n=l(t,`source`)??``,r=t=>d.push({line:e,source:n,reason:t});if(u.length>=ce){r(I.tooMany);continue}if([`inactive`,`disabled`].includes((l(t,`status`)??`active`).toLowerCase())){r(I.inactive);continue}let i=ge(l(t,`code`));if(i===null){r(I.code);continue}let a=n,s=l(t,`target`)??``,c=(l(t,`matching`)??`exact`).toLowerCase();if(l(t,`regex`)===`1`||(l(t,`format`)??``).toLowerCase()===`regex`||c===`regex`){let e=he(a,s);if(!e){r(I.pattern);continue}a=e.source,s=e.target}else if(c===`start`&&a.trim().endsWith(`/`))a=`${a.trim()}[...rest]`;else if(c!==`exact`){r(I.pattern);continue}let m=pe(a);if(`reason`in m){r(m.reason);continue}if(le.test(m.path)){r(I.file);continue}let h=i===410||i===451,g=``;if(!h){if(s.trim()===``){r(I.noTarget);continue}let e=/^https?:\/\//i.test(s.trim()),t=/^https?:\/\//i.test(n.trim())?o(n.trim())?.host:void 0;if(e&&t!==o(s.trim())?.host){r(I.external);continue}let i=pe(s);if(`reason`in i){r(i.reason===I.empty?I.noTarget:i.reason);continue}if(g=i.path,g===m.path){r(I.same);continue}}if(p.has(m.path)){r(I.repeated);continue}let _=f.get(m.path);if(_!==void 0){r(_===g?I.exists:I.conflict);continue}p.add(m.path),u.push({line:e,source:m.path,destination:g,type:i})}let m=new Map(f);for(let e of u)m.set(e.source,e.destination);let h=[];for(let e of u){let t=e.destination,n=!1;for(let r=0;r<20&&t!==``&&m.has(t);r+=1)if(t=m.get(t)??``,t===e.source){n=!0;break}n?d.push({line:e.line,source:e.source,reason:I.loop}):h.push({source:e.source,destination:e.destination,type:e.type})}return{format:s.format,ready:h,skipped:d}}function ve(e){let t=e=>/[",\n]/.test(e)?`"${e.replace(/"/g,`""`)}"`:e;return[`source,target,code`,...e.map(e=>`${t(e.source)},${t(e.destination)},${e.type}`)].join(`
2
+ `)}const L=`redirects`,R=`redirects-import`,ye={redirection:`Redirection`,yoast:`Yoast SEO Premium`,rankmath:`Rank Math`,plain:`a plain list`},be={schedule:`* * * * *`};async function xe(e){let t=await e.kv.get(L);return t&&Array.isArray(t.rows)?t:null}async function Se(e){let t=[],n;for(let r=0;r<5;r+=1){let r=await e.redirects?.list({limit:100,cursor:n});if(!r||(t.push(...r.items),n=r.hasMore?r.cursor:void 0,!n))break}return t}async function Ce(e,t){let n=_e(t,await Se(e)),r={status:`checked`,format:n.format,rows:n.ready,skipped:n.skipped,cursor:0,added:0,group:`Imported ${new Date().toISOString().slice(0,16).replace(`T`,` `)}`};return await e.kv.set(L,r),r}function we(e){let t=e instanceof Error?e.message:String(e);return/already exists/i.test(t)?{kind:`exists`,message:t}:/invalid source pattern|must be different|destination|path traversal|loop/i.test(t)?{kind:`invalid`,message:t}:{kind:`stop`,message:t}}async function Te(e){try{await e.cron?.schedule(R,be)}catch{}}async function Ee(e){try{await e.cron?.cancel(R)}catch{}}async function De(e,t,n=8){let r={...t,status:`adding`,skipped:[...t.skipped]},i=t.status===`adding`,a=0,o=async()=>{a+=1;try{return await e.kv.set(L,r),!0}catch{return!1}},s=e=>1+(e===i?1:0),c=0,l=r.cursor;for(;r.cursor<r.rows.length;){let t=r.rows[r.cursor];if(!t)break;let i=r.cursor===r.rows.length-1;if(r.cursor!==l&&a+1+s(i)>n)break;a+=1;try{await e.redirects?.create?.({source:t.source,destination:t.destination,type:t.type,groupName:r.group}),r.added+=1}catch(e){let{kind:n,message:i}=we(e);if(n===`stop`){if(r.cursor!==l||(r.stalls=(r.stalls??0)+1,r.stalls<3))break;r.skipped.push({line:0,source:t.source,reason:`EmDash could not add it: ${i}`})}else n===`exists`?r.added+=1:r.skipped.push({line:0,source:t.source,reason:`EmDash refused it: ${i}`})}if(r.cursor+=1,r.stalls=0,c+=1,c>=4&&a+1+1+s(!1)<=n){if(!await o())return r;c=0}}return r.cursor>=r.rows.length?(r.status=`done`,i&&await Ee(e)):i||await Te(e),await o(),r}async function Oe(e,t,n=8){let r=t.status===`removing`,i={...t,status:`removing`,removed:t.removed??0},a=await e.redirects?.list({limit:100,group:t.group}),o=1,s=a?.items??[],c=e=>1+(e===r?1:0),l=s.length,u=!1;for(let t of s){let r=l===1&&!a?.hasMore;if(o+2+c(r)>n)break;o+=2;try{let n=await e.redirects?.get(t.id);n&&await e.redirects?.delete?.(t.id,{_rev:n._rev}),--l,i.removed=(i.removed??0)+1}catch{u=!0;break}}return l===0&&!a?.hasMore?(await e.kv.delete(L),r&&await Ee(e),null):(i.stalls=u&&i.removed===(t.removed??0)?(t.stalls??0)+1:0,i.stalls>=3?ke(e,i,r):(r||await Te(e),await e.kv.set(L,i),i))}async function ke(e,t,n=!0){let{stalls:r,removed:i,...a}=t,o={...a,status:`done`,added:Math.max(0,t.added-(i??0))};return n&&await Ee(e),await e.kv.set(L,o),o}async function Ae(e){let t=[],n;for(let r=0;r<5;r+=1){let r=await e.redirects?.list({limit:100,cursor:n});if(!r||(t.push(...r.items),n=r.hasMore?r.cursor:void 0,!n))break}return{text:ve(t),count:t.length}}function z(e,t){return`${e} ${t}${e===1?``:`s`}`}function je(e,t,n){return{type:`meter`,label:e,value:n>0?Math.round(Math.min(t,n)/n*100):0,max:100}}function Me(e){if(e.length===0)return[];let t=[{type:`header`,text:`Skipped (${e.length})`},{type:`context`,text:`Skipped rows are not added. Copy any you still need and add them by hand under Manage > Redirects, which also accepts patterns.`},{type:`table`,page_action_id:`skipped_table`,columns:[{key:`line`,label:`Row`},{key:`source`,label:`Old address`},{key:`reason`,label:`Why`}],rows:e.slice(0,50).map(e=>({line:e.line>0?String(e.line):``,source:e.source,reason:e.reason}))}];return e.length>50&&t.push({type:`context`,text:`And ${e.length-50} more.`}),t}const Ne=[{value:`301`,label:`Permanent`},{value:`302`,label:`Temporary, the old address will come back`},{value:`410`,label:`Page removed on purpose, no new address`}];function Pe(e){return e===301||e===308?`Permanent`:e===302||e===307?`Temporary`:e===410?`Deleted on purpose`:String(e)}async function Fe(e){let t=await e.redirects?.list({limit:20});return{rows:(t?.items??[]).map(({id:e,source:t,destination:n,type:r,hits:i})=>({id:e,source:t,destination:n,type:r,hits:i})),more:t?.hasMore??!1}}function Ie(e){let t=e instanceof Error?e.message:String(e);return/already exists/i.test(t)?`There is already a redirect from that old address. Use Change in the list below.`:/must be different/i.test(t)?`The old and new address are the same.`:/loop/i.test(t)?`That would send visitors in a circle.`:/changed since/i.test(t)?`That redirect was just changed elsewhere. Try again.`:`EmDash could not save that redirect. Check both addresses start with /.`}function Le(e,t,n){let r=_e(ve([{source:e,destination:t,type:Number(n)||301}]),[]);return r.ready[0]??r.skipped[0]?.reason??`Type the old address first.`}async function Re(e,t,n,r){let i=Le(t,n,r);if(typeof i==`string`)return`${i}.`;try{return await e.redirects?.create?.(i),null}catch(e){return Ie(e)}}async function ze(e,t,n,r,i){let a=Le(n,r,i);if(typeof a==`string`)return`${a}.`;try{let n=await e.redirects?.get(t);return n?(await e.redirects?.update?.(t,{...a,_rev:n._rev}),null):`That redirect no longer exists.`}catch(e){return Ie(e)}}async function Be(e,t){try{let n=await e.redirects?.get(t);return n&&await e.redirects?.delete?.(t,{_rev:n._rev}),null}catch(e){return Ie(e)}}function Ve(e){if(!e)return[{type:`context`,text:`Paste a list exported from Redirection, Yoast SEO Premium or Rank Math, or any list of old address, new address. Nothing is added until you confirm.`},{type:`form`,block_id:`redirects`,fields:[{type:`text_input`,action_id:`list`,label:`Your list, one redirect per line`,multiline:!0,placeholder:`/old-page,/new-page`,initial_value:``}],submit:{label:`Check list`,action_id:`check_redirects`}}];if(e.status===`checked`){let t=e.rows.length;return[{type:`banner`,variant:t===0?`alert`:`default`,title:`${t} ready, ${e.skipped.length} skipped`,description:t===0?`Nothing in this list can be added. See why below.`:`Read as ${ye[e.format]}. Nothing has been added yet. Existing redirects are never changed.`},{type:`actions`,elements:[...t>0?[{type:`button`,action_id:`add_redirects`,label:`Add ${z(t,`redirect`)}`,style:`primary`,confirm:{title:`Add ${z(t,`redirect`)}?`,text:`Visitors to these old addresses will be sent to the new ones. You can remove them all again afterwards.`,confirm:`Add`,deny:`Cancel`}}]:[],{type:`button`,action_id:`reset_redirects`,label:`Start again`}]},...Me(e.skipped)]}if(e.status===`removing`){let t=e.removed??0;return[je(`${t} of ${e.added} removed`,t,Math.max(e.added,t,1)),{type:`context`,text:`The rest are removed in the background, a few each minute. You can leave this page.`},{type:`actions`,elements:[{type:`button`,action_id:`remove_redirects`,label:`Remove more now`,style:`primary`},{type:`button`,action_id:`stop_redirects`,label:`Stop here`}]}]}return e.status===`adding`?[je(`${e.added} of ${e.rows.length} added`,e.cursor,e.rows.length),{type:`context`,text:`The rest are added in the background, a few each minute. You can leave this page.`},{type:`actions`,elements:[{type:`button`,action_id:`more_redirects`,label:`Add more now`,style:`primary`},{type:`button`,action_id:`stop_redirects`,label:`Stop here`}]}]:[{type:`banner`,title:`${z(e.added,`redirect`)} added from your list`,description:`They are labelled "${e.group}".`},{type:`actions`,elements:[{type:`button`,action_id:`reset_redirects`,label:`Import another list`},...e.added>0?[{type:`button`,action_id:`remove_redirects`,label:`Remove the ${e.added} just added`,confirm:{title:`Remove these redirects?`,text:`Only the redirects this import added are removed.`,confirm:`Remove`,deny:`Cancel`,style:`danger`}}]:[]]},...Me(e.skipped)]}function He(e,t){let n=e?String(e.type):`301`;return{type:`form`,block_id:e?`edit_redirect_${e.id}`:`add_one_${t}`,fields:[{type:`text_input`,action_id:`source`,label:`Redirect from`,placeholder:`/old-page`,initial_value:e?.source??``},{type:`text_input`,action_id:`destination`,label:`Redirect to`,placeholder:`/new-page`,initial_value:e?.destination??``,condition:{field:`code`,neq:`410`}},{type:`select`,action_id:`code`,label:`Kind`,options:Ne.some(e=>e.value===n)?Ne:[...Ne,{value:n,label:`${Pe(Number(n))} (${n})`}],initial_value:n}],submit:e?{label:`Save changes`,action_id:`save_redirect:${e.id}`}:{label:`Add redirect`,action_id:`add_redirect`}}}function Ue(e){let{state:t,rows:n,editing:r}=e,i=r?void 0:e.panel??(t?`import`:void 0),a=n[0]?.id??`first`,o=[{type:`context`,text:`Send visitors from an old address to a new one. Pages you rename in EmDash are redirected for you.`},{type:`actions`,elements:[{type:`button`,action_id:`open_add`,label:`Add a redirect`,...i===void 0&&!r?{style:`primary`}:{}},{type:`button`,action_id:`open_import`,label:`Import from WordPress`},{type:`button`,action_id:`export_redirects`,label:`Export`},...e.managerUrl?[{type:`link`,label:`Full Redirects screen`,target:{kind:`external`,url:e.managerUrl}}]:[]]}],s={type:`actions`,elements:[{type:`button`,action_id:`close_panel`,label:`Close`}]},c=e.problem?[{type:`banner`,variant:`error`,title:e.problem}]:[];return r?o.push({type:`header`,text:`Change a redirect`},...c,He(r,a),s,{type:`divider`}):i===`add`?o.push({type:`header`,text:`Add a redirect`},...c,He(void 0,a),s,{type:`divider`}):i===`import`?(o.push({type:`header`,text:`Import from WordPress`},...Ve(t)),t||o.push(s),o.push({type:`divider`})):i===`export`&&o.push({type:`header`,text:`Export`},{type:`context`,text:`Copy this list to keep a backup or to paste into a spreadsheet. It can be imported again here.`},{type:`code`,code:e.exported??``,language:`bash`},s,{type:`divider`}),n.length===0?o.push({type:`empty`,title:`No redirects yet`,description:`Add one, or import a list from your old site.`,size:`sm`}):(o.push({type:`table`,page_action_id:`redirects_table`,columns:[{key:`source`,label:`Redirect from`},{key:`destination`,label:`Redirect to`},{key:`kind`,label:`Kind`},{key:`hits`,label:`Visits`,format:`number`},{key:`edit`,label:``,format:`element`},{key:`remove`,label:``,format:`element`}],rows:n.map(e=>({source:e.source,destination:e.destination||`(none)`,kind:Pe(e.type),hits:e.hits,edit:{type:`button`,action_id:`edit_redirect`,label:`Change`,value:e.id},remove:{type:`button`,action_id:`remove_redirect`,label:`Remove`,value:e.id,confirm:{title:`Remove this redirect?`,text:`Visitors to ${e.source} will see a not-found page.`,confirm:`Remove`,deny:`Keep`,style:`danger`}}}))}),e.more&&o.push({type:`context`,text:`Showing the newest ${n.length}. The rest are in the full Redirects screen.`})),o}async function We(e,t){let n={...t,status:`done`};try{await e.cron?.cancel(R)}catch{}return await e.kv.set(L,n),n}const Ge=/%%[a-z0-9_]+%%|%[a-z][a-z0-9_]*%/i;function Ke(e){if(typeof e!=`string`)return``;let t=e.replace(/\s+/g,` `).trim();return Ge.test(t)?``:t}function qe(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}function Je(e,t){let n=(Array.isArray(t)?t:[t??``]).map(e=>e.trim()).filter(e=>e!==``),r=`-–—:·•*⋆|~«»<>/`.split(``).map(qe).join(``);for(let t of n){let n=RegExp(`\\s*[${r}]\\s*${qe(t)}\\s*$`,`i`),i=e.replace(n,``).trim();if(i!==``&&i!==e.trim())return i}return e.trim()}const Ye=(e,t)=>e.replace(/\s+/g,` `).trim().toLowerCase()===t.replace(/\s+/g,` `).trim().toLowerCase();function Xe(e){if(!e.seo)return null;let t=Ke(e.data.seo_description),n=Ke(e.data.seo_title),r=n===``?``:Je(n,e.siteName),i=r!==``&&!Ye(r,e.title);if(t===``&&!i)return null;let a=e.seo.title?.trim()??``,o=e.seo.description?.trim()??``,s={own:!1};return i&&(a===``?s.title=r:Ye(a,r)||(s.own=!0)),t!==``&&(o===``?s.description=t:Ye(o,t)||(s.own=!0)),s}const Ze=new Set("iIl.,:;|!'`jtfr ()[]".split(``)),Qe=new Set(`mwMWQO@%`.split(``));function $e(e){let t=0;for(let n of e)Ze.has(n)?t+=5:Qe.has(n)?t+=15:n>=`A`&&n<=`Z`?t+=12:t+=9.5;return Math.round(t)}function et(e,t){let n=t?.trim()??``,r=e.trim();return n===``||r===``||r.toLowerCase().includes(n.toLowerCase())?r:`${r}: ${n}`}function tt(e){if($e(e)<=580)return e;let t=e;for(;t.length>1&&$e(`${t}…`)>580;)t=t.slice(0,-1);return`${t.trimEnd()}…`}function nt(e){return e.toLowerCase().replace(/\s+/g,` `).trim()}const rt=new Set([`home`,`homepage`,`home page`,`index`,`untitled`,`welcome`]),it=new Set([`_type`,`_key`,`id`,`slug`,`url`,`href`,`src`,`style`,`icon`]);function at(e){let t={textLength:0,imagesWithoutAlt:0,hasImage:!1},n=(e,r,i)=>{if(i>12)return;if(typeof e==`string`){!it.has(r)&&!/^(https?:)?\//.test(e)&&(t.textLength+=e.trim().length);return}if(Array.isArray(e)){for(let t of e)n(t,r,i+1);return}if(typeof e!=`object`||!e)return;let a=e;(a._type===`image`||typeof a.src==`string`&&`alt`in a)&&(t.hasImage=!0,(typeof a.alt!=`string`||a.alt.trim()===``)&&(t.imagesWithoutAlt+=1));for(let[e,t]of Object.entries(a))n(t,e,i+1)};for(let[t,r]of Object.entries(e))t!==`json_ld`&&n(r,t,0);return t}const B={id:`no-social-image`,level:`look`,message:`This page has no picture for shared links, unless your site has a default sharing image.`,fix:`Add an image in the SEO panel. If your theme adds a picture itself, this clears once the page has been viewed.`};function ot(e){return typeof e.excerpt==`string`?e.excerpt.trim():``}const st=`Copy it from the SEO Title field into SEO Title in the page's SEO panel, without the " | Site name" ending (your site adds its name).`;function ct(e){let t=[],{seo:n}=e,r=at(e.data),i=n?.title?.trim()||e.title.trim(),a=Xe({data:e.data,seo:n,title:e.title,siteName:e.siteNames??e.siteName}),s=a?.title!==void 0,c=`came from WordPress but the page doesn't use it yet`;i===``&&t.push({id:`no-title`,level:`fix`,message:s?`This page has no title. One ${c}.`:`This page has no title.`,fix:s?st:`Give the page a title.`});let l=rt.has(i.toLowerCase());if(l&&t.push({id:`generic-title`,level:`fix`,message:`The title is just "${i}", which is what Google shows as the headline.${s?` A better one ${c}.`:``}`,fix:s?st:`Put your name and what you do in SEO Title, in the page's SEO panel.`}),s&&i!==``&&!l&&t.push({id:`imported-title`,level:`look`,message:`A search title ${c}.`,fix:st}),n&&!n.description?.trim()){let n=ot(e.data)!==``;a?.description===void 0?n||t.push({id:`no-description`,level:`fix`,message:`No search description, so Google writes its own summary of this page.`,fix:`Fill in Meta Description in the page's SEO panel.`}):t.push({id:`imported-description`,level:n?`look`:`fix`,message:n?`A description ${c}.`:`A description ${c}, so Google writes its own summary of this page.`,fix:`Copy it from the SEO Description field into Meta Description in the page's SEO panel.`})}if(e.duplicateTitle&&i!==``&&t.push({id:`duplicate-title`,level:`fix`,message:`Another page has the same title, so Google cannot tell them apart.`,fix:`Give this page its own title in the SEO panel.`}),n?.canonical&&e.slug){let r=o(n.canonical,`${e.siteUrl.replace(/\/+$/,``)}/`),i=r?.pathname.replace(/\/+$/,``).split(`/`).pop(),a=[`home`,`index`].includes(e.slug)&&(i??``)===``;r&&i!==e.slug&&!a&&t.push({id:`canonical-elsewhere`,level:`fix`,message:`This page tells Google to show a different page instead (canonical).`,fix:`Clear the canonical address in the SEO panel unless that is intended.`})}if(i!==``&&$e(et(i,e.siteName))>580&&t.push({id:`title-long`,level:`look`,message:`The end of this title may be cut off in Google.`,fix:`Shorten the title, or put the important words first.`}),n&&!n.image&&!r.hasImage&&t.push(B),e.isArticle&&e.authorCount===0&&t.push({id:`no-author`,level:`look`,message:`This article has no author.`,fix:`Add a byline to the article.`}),r.imagesWithoutAlt>0){let e=r.imagesWithoutAlt;t.push({id:`images-without-alt`,level:`look`,message:`${e} ${e===1?`picture has`:`pictures have`} no description for screen readers.`,fix:`Add alt text to each picture.`})}return r.textLength<80&&t.push({id:`nearly-empty`,level:`look`,message:`This page has almost no text.`,fix:`Add some content, or hide the page from search.`}),t}function lt(e,t){return t?`hidden`:e.some(e=>e.level===`fix`)?`fix`:e.length>0?`look`:`ready`}const ut=/^[0-9A-HJKMNP-TV-Z]{26}(\.[A-Za-z0-9]+)?$/i;function dt(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function ft(e){return typeof e==`number`&&Number.isFinite(e)&&e>=1?Math.round(e):void 0}function pt(e){if(!e.startsWith(`/_emdash/api/media/file/`))return;let t=e.slice(24);try{t=decodeURIComponent(t)}catch{return}return t!==``&&!t.includes(`/`)?t:void 0}function mt(e,t){let n=o(e,`${t.replace(/\/+$/,``)}/`);if(!n||!/^https?:$/.test(n.protocol)||n.search!==``)return[];let{pathname:r}=n;if(r===`/_image`||r.includes(`/cdn-cgi/image/`))return[];let i=pt(r);if(i)return[`file:${i}`];let a=[`url:${n.origin}${r}`],s=r.slice(1);return ut.test(s)&&a.push(`file:${s}`),a}function ht(e){if(typeof e!=`string`||e===``)return[];if(e.startsWith(`/`)){let t=e.includes(`?`)?void 0:pt(e);return t?[`file:${t}`]:[]}return/^https?:\/\//i.test(e)?mt(e,`http://localhost`).filter(e=>e.startsWith(`url:`)):[]}function gt(e){return typeof e==`string`&&e!==``&&!e.includes(`/`)?[`file:${e}`]:[]}function _t(e){let t=ft(e.width),n=ft(e.height);if(t===void 0||n===void 0||typeof e.mimeType==`string`&&!e.mimeType.startsWith(`image/`))return;let r=[];if(e._type===`image`&&dt(e.asset))r.push(...gt(e.asset._ref),...ht(e.asset.url));else if(typeof e.id==`string`){if(e.provider===void 0||e.provider===`local`){let t=dt(e.meta)?e.meta:{};r.push(...gt(t.storageKey),...gt(e.id))}r.push(...ht(e.src))}let i=[...new Set(r)];return i.length>0?{keys:i,width:t,height:n}:void 0}function vt(e){let t=[],n=new Set,r=(e,i)=>{if(t.length>=10||i>12)return;if(Array.isArray(e)){for(let t of e)r(t,i+1);return}if(!dt(e))return;let a=_t(e);if(a&&!a.keys.some(e=>n.has(e))){for(let e of a.keys)n.add(e);t.push(a)}for(let t of Object.values(e))r(t,i+1)};for(let[t,n]of Object.entries(e))t!==`json_ld`&&r(n,0);return t.slice(0,10)}function yt(e,t,n){if(!e||e.length===0)return;let r=mt(t,n);if(r.length===0)return;let i=e.find(e=>e.keys.some(e=>r.includes(e)));return i?{width:i.width,height:i.height}:void 0}const bt=16*1024;function V(e,t){return`${e}:${t}`}function H(e){return e.storage.entries}function xt(e){return typeof e==`object`&&!!e&&!Array.isArray(e)&&`@type`in e}function St(e){let t=e;if(typeof e==`string`){if(e.trim()===``||e.length>bt)return[];try{t=JSON.parse(e)}catch{return[]}}let n=(Array.isArray(t)?t:[t]).filter(xt);return JSON.stringify(n).length>bt?[]:n}function Ct(e){let{title:t,name:n}=e.data;return typeof t==`string`?t:typeof n==`string`?n:``}function wt(e){let{collection:t,entry:n,authors:r,previous:i,config:a,siteUrl:o}=e,s=a.collections[t],c=i?.type??s?.type??m(t),l=n.seo?.title?.trim()||Ct(n),u=n.status===`published`,d=u&&e.hasPages,p=e.hasPages?ct({slug:n.slug,data:n.data,seo:n.seo,title:Ct(n),siteName:e.siteName,siteNames:[e.siteName,e.emdashSiteName??``],siteUrl:o,isArticle:f.includes(c),authorCount:r.length,duplicateTitle:e.duplicateTitle}):[],h=p.some(e=>e.id===B.id),g=h&&i?.imageSeen===!0,_=g?p.filter(e=>e.id!==B.id):p,v=e.hasPages?vt(n.data):[];return{v:1,publishedAt:n.publishedAt,updatedAt:n.updatedAt,authors:r,...i?.type?{type:i.type}:{},custom:St(n.data[a.jsonLdField]),...v.length>0?{images:v}:{},needsImage:h,...g?{imageSeen:g}:{},collection:t,entryId:n.id,title:l,status:e.hasPages?u?lt(_,n.seo?.noIndex===!0||s?.noindex===!0):`draft`:`skip`,titleKey:d?nt(l):``,issues:_,checkedAt:new Date().toISOString()}}function Tt(e,t){return e.status===`hidden`||e.status===`draft`||e.status===`skip`?e.status:lt(t,!1)}function Et(e,t){let n=e.issues??[],r=n.some(e=>e.id===B.id);if(!(e.needsImage??r)||e.imageSeen===!0===t&&r!==t)return null;let i=n.filter(e=>e.id!==B.id),a=t?i:[...i,B],{imageSeen:o,...s}=e;return{...s,needsImage:!0,...t?{imageSeen:!0}:{},issues:a,status:Tt(e,a)}}async function Dt(e,t){return(await Ot(e,[t]))[0]??[]}async function Ot(e,t){let n=[...new Set(t.filter(e=>e!==``))];if(n.length===0)return t.map(()=>[]);let r=await H(e).query({where:{titleKey:n.length===1?n[0]??``:{in:n}},limit:10*n.length});return t.map(e=>e===``?[]:r.items.filter(t=>t.data.titleKey===e))}function U(e){return e.issues?.some(e=>e.id===`duplicate-title`)??!1}function kt(e,t){let n=e.length>1;for(let{id:r,data:i}of e)U(i)!==n&&t.set(r,W(i,n))}function W(e,t){let n=(e.issues??[]).filter(e=>e.id!==`duplicate-title`),r=t?[...n,{id:`duplicate-title`,level:`fix`,message:`Another page has the same title, so Google cannot tell them apart.`,fix:`Give this page its own title in the SEO panel.`}]:n;return{...e,issues:r,status:Tt(e,r)}}async function At(e,t,n,r){return(await Ft(e,t,n,r))?.record??null}function jt(e){return(e??[]).filter(({roleLabel:e})=>typeof e!=`string`||e.trim()===``).map(({byline:e})=>({name:e.displayName,...e.websiteUrl?{url:e.websiteUrl}:{}}))}function Mt(e){return jt(e[0]?.bylines)}async function Nt(e,t,n,r){if(!e.content)return null;let[i,a]=await Promise.all([e.content.get(t,n),e.bylines?.getEntriesBylines(t,[n])??[]]);return i?wt({collection:t,entry:i,authors:Mt(a),previous:null,config:r,siteUrl:e.site.url,siteName:r.site.name??e.site.name,emdashSiteName:e.site.name,duplicateTitle:!1,hasPages:!0}):null}async function Pt(e,t,n,r){let i=await e.content?.get(t,n);if(!i)return null;let a=H(e),o=V(t,n),[s,c,l]=await Promise.all([e.bylines?.getEntriesBylines(t,[n])??[],a.get(o),e.schema?.getCollection(t)]),u=wt({collection:t,entry:i,authors:Mt(s),previous:c,config:r,siteUrl:e.site.url,siteName:r.site.name??e.site.name,emdashSiteName:e.site.name,duplicateTitle:!1,hasPages:l?l.routable&&!l.hidden:!0}),d=u.titleKey??``,f=c?.titleKey??``;return{entry:i,previous:c,record:W(u,d!==``&&d===f&&c!==null&&U(c)),newKey:d,oldKey:f}}async function Ft(e,t,n,r){let i=await Pt(e,t,n,r);if(!i)return null;let{entry:a,record:o,newKey:s,oldKey:c}=i,l=H(e),u=V(t,n);await l.put(u,o);let[d=[],f=[]]=await Ot(e,[s,c===s?``:c]),p=new Map,m=d,h=d.find(e=>e.id===u)?.data;if(s!==``&&h?.checkedAt!==o.checkedAt){let e=h??f.find(e=>e.id===u)?.data??null;e&&Lt(e.updatedAt,o.updatedAt)&&(p.set(u,o),m=[...d.filter(e=>e.id!==u),{id:u,data:o}])}return kt(m,p),kt(f.filter(e=>e.id!==u),p),p.size>0&&await l.putMany([...p].map(([e,t])=>({id:e,data:t}))),{record:p.get(u)??o,entry:a}}async function It(e,t,n,r,i){let a=await Pt(e,t,n,r);if(!a)return null;let{entry:o,previous:s,newKey:c,oldKey:l}=a,u=i?i(a.record):a.record,d=V(t,n);return s===null||c===l?await H(e).put(d,u):i&&await H(e).put(d,i(s)),{record:u,entry:o}}function Lt(e,t){if(!e||!t)return!1;let n=Date.parse(e),r=Date.parse(t);return Number.isFinite(n)&&Number.isFinite(r)&&n<r}async function Rt(e,t,n){let r=H(e),i=V(t,n),a=await r.get(i);await r.delete(i);let o=new Map;kt((await Dt(e,a?.titleKey??``)).filter(e=>e.id!==i),o),o.size>0&&await r.putMany([...o].map(([e,t])=>({id:e,data:t})))}const G=`scan`,zt=`stats`,Bt=`seo-scan`,Vt=[`ready`,`look`,`fix`,`hidden`];async function Ht(e){return e.kv.get(G)}async function Ut(e,t,n,r=6){let i=await Yt(e,{status:`running`,collections:t,scope:[...t],phase:`entries`,checked:0,rev:3,tally:{checked:0,fix:0,hidden:0,rows:[],more:!1},startedAt:new Date().toISOString()},n,r,{scheduled:!1});if(i.status===`done`&&!i.sweep)return i;try{await e.cron?.schedule(Bt,{schedule:`* * * * *`})}catch{}return i}async function Wt(e,t,n,r=4){let i=t.collections[0];if(i===void 0)return t.phase=`titles`,0;let a=1,o=new Date().toISOString(),s=e=>(e.checkedAt??``)>=t.startedAt,c=await e.content?.list(i,{limit:20,cursor:t.cursor,where:{status:`published`}}),l=c?.items??[];if(l.length>0&&r<4)return a;if(l.length>0){let r=H(e),u=l.map(e=>e.id),[d,f]=await Promise.all([e.bylines?.getEntriesBylines(i,u)??[],r.getMany(u.map(e=>V(i,e)))]);a+=2;let p=new Map(d.map(e=>[e.entryId,e.bylines])),m=new Set,h=l.map(t=>{let r=V(i,t.id),a=f.get(r)??null;return a&&(Lt(t.updatedAt,a.updatedAt)&&s(a)||(a.checkedAt??``)>=o)?(m.add(r),{id:r,data:a}):{id:r,data:wt({hasPages:!0,collection:i,entry:t,authors:jt(p.get(t.id)),previous:a,config:n,siteUrl:e.site.url,siteName:n.site.name??e.site.name,emdashSiteName:e.site.name,duplicateTitle:a?U(a):!1})}});if(t.cursor===void 0&&t.checked===0&&!c?.hasMore&&t.collections.length===1){let e=new Map;for(let{data:t}of h)t.titleKey&&e.set(t.titleKey,(e.get(t.titleKey)??0)+1);for(let t of h){let n=(e.get(t.data.titleKey??``)??0)>1;m.has(t.id)&&U(t.data)!==n&&m.delete(t.id),t.data=W(t.data,n)}t.status=`done`,t.finishedAt=new Date().toISOString(),t.sweep={}}let g=h.filter(e=>!m.has(e.id));g.length>0&&(await r.putMany(g),a+=1);for(let{data:e}of h)Jt(t.tally,e,1);t.checked+=l.length}return c?.hasMore&&c.cursor?t.cursor=c.cursor:(t.cursor=void 0,t.collections=t.collections.slice(1),t.collections.length===0&&t.status!==`done`&&(t.phase=`titles`,t.sweep={})),a}async function Gt(e,t,n){let r=new Set(t.scope??[]),i=H(e),a=t.sweep?.cursor,o=new Set(t.sweep?.kept??[]),s=r.size>0&&e.content?await i.query({where:{status:{in:Vt}},limit:100,cursor:a}):{items:[],hasMore:!1,cursor:void 0},c=s.items.filter(({id:e,data:n})=>!o.has(e)&&r.has(n.collection??``)&&(n.entryId??``)!==``&&(n.checkedAt??``)<t.startedAt).slice(0,1),l=await Promise.all(c.map(({data:t})=>e.content?.get(t.collection??``,t.entryId??``))),u=[],d=[];c.forEach(({id:t,data:r},i)=>{let a=l[i];a?a.status===`published`?o.add(t):d.push({id:t,data:wt({hasPages:!0,collection:r.collection??``,entry:a,authors:r.authors,previous:r,config:n,siteUrl:e.site.url,siteName:n.site.name??e.site.name,emdashSiteName:e.site.name,duplicateTitle:!1})}):u.push(t)}),u.length>0&&await i.deleteMany(u),d.length>0&&await i.putMany(d);let f=o.size>0?{kept:[...o]}:{};c.length>0?t.sweep={...a?{cursor:a}:{},...f}:s.hasMore&&s.cursor?t.sweep={cursor:s.cursor,...f}:delete t.sweep}async function Kt(e,t){let n=H(e),r=await n.query({orderBy:{titleKey:`asc`},limit:100,cursor:t.titleCursor}),i=r.items.filter(e=>(e.data.titleKey??``)!==``),a=new Map(i.map(e=>[e.id,e.data])),o=new Map,s=(e,t,n)=>{U(t)!==n&&o.set(e,n)},c=t.lastTitle;for(let e=0;e<i.length;e+=1){let t=i[e];if(!t)continue;let r=t.data.titleKey??``,o=i[e+1],l=c?.key===r,u=o?.data.titleKey===r;if(l&&c&&!c.flagged&&e===0){let e=await n.get(c.id);e&&(a.set(c.id,e),s(c.id,e,!0))}let d=l||u;s(t.id,t.data,d),c={key:r,id:t.id,flagged:d}}if(o.size>0){let e=new Set([...o.keys()].map(e=>a.get(e)?.titleKey??``)),r=[...a].filter(([,t])=>e.has(t.titleKey??``)).map(([e])=>e),i=await n.getMany(r);for(let t of r){let n=a.get(t),r=i.get(t);(!n||r?.checkedAt!==n.checkedAt||r?.titleKey!==n.titleKey)&&e.delete(n?.titleKey??``)}let s=[];for(let[n,r]of o){let i=a.get(n);if(!i||!e.has(i.titleKey??``))continue;let o=W(i,r);s.push({id:n,data:o}),Jt(t.tally,i,-1),Jt(t.tally,o,1)}s.length>0&&await n.putMany(s)}t.lastTitle=c,r.hasMore&&r.cursor?t.titleCursor=r.cursor:(t.status=`done`,t.finishedAt=new Date().toISOString(),t.titleCursor=void 0,t.lastTitle=void 0)}function qt(e,t=`fix`){return{collection:e.collection??``,id:e.entryId??``,title:e.title||`(no title)`,issues:(e.issues??[]).filter(e=>e.level===t).map(e=>e.message)}}function Jt(e,t,n){if(!(!t.status||t.status===`draft`||t.status===`skip`)){if(t.status!==`hidden`&&(e.checked+=n),t.status===`hidden`){e.hidden+=n;let r=e.hiddenTitles??[],i=t.title||`(no title)`;e.hiddenTitles=n===1?[...r,i].slice(0,10):r.filter(e=>e!==i)}if(t.status===`look`){let r=e.look??[];e.look=n===1?[...r,qt(t,`look`)].slice(0,10):r.filter(e=>e.id!==t.entryId)}t.status===`fix`&&(e.fix+=n,n===1?e.rows.length<10?e.rows.push(qt(t)):e.more=!0:e.rows=e.rows.filter(e=>e.id!==t.entryId))}}async function Yt(e,t,n,r=6,{scheduled:i=!0}={}){let a={...t,collections:[...t.collections],tally:{...t.tally,rows:[...t.tally.rows]}};if(a.status===`done`&&!a.sweep)return a;if(a.sweep)await Gt(e,a,n);else if(a.phase===`entries`){let t=0;for(let i=0;i<5&&a.phase===`entries`;i+=1){let o=r-2-t;if(i>0&&o<1)break;let s={checked:a.checked,collections:a.collections.length,cursor:a.cursor};if(t+=await Wt(e,a,n,i===0?4:o),a.checked!==s.checked||a.status===`done`||a.collections.length===s.collections&&a.cursor===s.cursor)break}a.phase===`titles`&&a.checked===0&&(a.status=`done`,a.finishedAt=new Date().toISOString())}else await Kt(e,a);if(i&&a.status===`done`&&!a.sweep)try{await e.cron?.cancel(Bt)}catch{}return await e.kv.set(G,a),a}async function Xt(e){let t=H(e),[n,r,i,a,o]=await Promise.all([t.count({status:{in:[`ready`,`look`,`fix`]}}),t.count({status:`fix`}),t.query({where:{status:`fix`},limit:10}),t.query({where:{status:`look`},limit:10}),t.query({where:{status:`hidden`},limit:10})]);return{checked:n,fix:r,hidden:o.hasMore?11:o.items.length,rows:i.items.map(({data:e})=>qt(e)),more:i.hasMore,look:a.items.map(({data:e})=>qt(e,`look`)),hiddenTitles:o.items.map(({data:e})=>e.title||`(no title)`)}}const Zt=new Map;async function Qt(e,t,{fresh:n=!1,now:r=Date.now()}={}){let i=e.site?.url??``,a=Zt.get(e.plugin.id);a||Zt.set(e.plugin.id,a=new Map);let o=a.get(i);if(n||!o||r>=o.until){let e=t(),n={value:e,until:r+3e4};a.set(i,n),e.catch(()=>{a.get(i)===n&&a.delete(i)}),o=n}return structuredClone(await o.value)}function $t(e){Zt.delete(e)}const en=`emdash-seo-settings`,tn=[`geo`,`vatID`,`taxID`,`numberOfEmployees`];function nn(e){let{geo:t,vatID:n,taxID:r,numberOfEmployees:i,...a}=e;return a}function rn(e,t=Object.keys(e.collections)){let n={};for(let r of t){let t=e.collections[r];n[r]={type:t?.type??m(r),noindex:t?.noindex===!0}}return{format:en,version:1,site:nn(e.site),collections:n,robots:e.robots,breadcrumbs:e.breadcrumbs}}function an(e,t){return JSON.stringify(rn(e,t.map(e=>e.slug)),null,2)}function on(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}const sn=`This is not a copy of SEO settings. On the other site, open SEO > Settings > Copy settings to another site and copy the whole text.`;function cn(e){return e===`Organization`?`A company or organisation`:e===`Person`?`A person`:`A local business customers visit (shop, cafe, clinic)`}function ln(e,t){let n=e.type??`Organization`,r=P.some(e=>e.value===n);if(n!==`Organization`&&n!==`Person`&&!r)return{error:`Couldn't read the kind of site in these settings.`};let i={...e,type:n};if(i.logo=void 0,e.logo!==void 0&&e.logo!==``){let n=typeof e.logo==`string`?s(e.logo.trim(),t):void 0;if(!n||`error`in n)return{error:`Couldn't read the logo link in these settings: it must be a web address starting with https://`};i.logo=n.logo}if(e.sameAs!==void 0){let t=Array.isArray(e.sameAs)?e.sameAs:[null],n=[];for(let e of t){if(typeof e==`string`&&e.trim()===``)continue;let t=typeof e==`string`?c(e.trim()):null;if(!t)return{error:`Couldn't read a profile link in these settings: each must be a web address.`};n.push(t)}i.sameAs=n}if(!r)i.openingHours=[];else if(e.openingHours!==void 0){let t=Array.isArray(e.openingHours)?e.openingHours:[null],n=t.every(e=>on(e)&&Array.isArray(e.days)&&e.days.length>0&&e.days.every(e=>j.includes(e))&&typeof e.opens==`string`&&typeof e.closes==`string`)?ae(e=>M(e,t)):void 0;if(!n||`day`in n)return{error:`Couldn't read the opening hours in these settings.`};i.openingHours=n.hours}return e.priceRange!==void 0&&!oe.includes(String(e.priceRange))?{error:`Couldn't read the price range in these settings.`}:(on(e.address)&&typeof e.address.addressCountry==`string`&&(i.address={...e.address,addressCountry:e.address.addressCountry.toUpperCase()}),e.foundingDate!==void 0&&e.foundingDate!==``&&(typeof e.foundingDate!=`string`||!w(e.foundingDate.trim()))?{error:`Couldn't read the founding date in these settings: write it as a year, like 2012, or as 2012-05-01.`}:{site:i})}function un(e){let{robots:t}=T({robots:e});return{...t,maxSnippet:!t.nosnippet&&t.maxSnippet>=0?160:-1,maxVideoPreview:t.maxVideoPreview>=0?160:-1}}const dn={large:`Large`,standard:`Small`,none:`No image`};function fn(e,t,n){let{site:r}=e,i=`(none)`,a=r.address?[r.address.streetAddress,r.address.addressLocality,r.address.addressRegion,r.address.postalCode,r.address.addressCountry].filter(Boolean).join(`, `):``,o=j.map(e=>{let t=M(e,r.openingHours);return t===``?``:`${e.slice(0,3)} ${t}`}).filter(Boolean).join(`; `),s=P.find(e=>e.value===r.type)?.label,{robots:c}=e,l=[[`This site is for`,cn(r.type)],[`Kind of business`,s??i],[`Name`,r.name??n],[`Also known as`,r.alternateName??i],[`Registered legal name`,r.legalName??i],[`What you do`,r.description??i],[`Logo`,r.logo??i],[`Profiles elsewhere`,r.sameAs.join(`, `)||i],[`Email`,r.email??i],[`Phone`,r.telephone??i],[`Address`,a||i],[`Opening hours`,o||i],[`Price range`,r.priceRange??`Not shown`],[`Areas served`,r.areaServed.join(`, `)||i],[`Founded`,r.foundingDate??i]];for(let n of t){let t=e.collections[n.slug];l.push([`${n.label}: Google treats these as`,F(t?.type??m(n.slug))],[`${n.label}: on Google`,t?.noindex?`Hidden from Google`:`Can appear on Google`])}return l.push([`Path to each page on Google`,e.breadcrumbs?`Shown`:`Not shown`],[`Text in search results`,c.nosnippet?`No text`:c.maxSnippet>=0?`A short line`:`As much as Google likes`],[`Picture size in search results`,dn[c.maxImagePreview]],[`Video previews in search results`,c.maxVideoPreview>=0?`Up to 160 seconds`:`As long as Google likes`]),l}function pn(e,t,n,r,i=``){let a=e;if(typeof e==`string`){if(e.trim()===``)return{error:`Paste the settings first.`};try{a=JSON.parse(e)}catch{return{error:sn}}}if(!on(a)||a.format!==en||!on(a.site))return{error:sn};if(typeof a.version!=`number`||a.version>1)return{error:`These settings come from a newer version of SEO. Update it on this site first.`};let o=ln(a.site,r);if(`error`in o)return o;let s=T({site:o.site,collections:a.collections,breadcrumbs:a.breadcrumbs}),c=un(a.robots),l=new Map(n.map(e=>[e.slug,e.label])),u={...t.collections},d=[],f=[];for(let[e,n]of Object.entries(s.collections)){let r=l.get(e);if(r===void 0){d.push(e);continue}let i=t.collections[e],a=n.noindex===!0;a&&!i?.noindex&&f.push(r),u[e]={type:n.type,noindex:a,...a&&i?.noindex&&i.hiddenOk?{hiddenOk:!0}:{}}}let p={...t,site:{...nn(s.site),...Object.fromEntries(tn.map(e=>[e,t.site[e]]))},collections:u,robots:c,breadcrumbs:s.breadcrumbs,steps:{...t.steps,identity:!0}},m=new Map(fn(t,n,i)),h=fn(p,n,i).filter(([e,t])=>m.get(e)!==t).map(([e,t])=>({what:e,now:m.get(e)??``,after:t}));return{config:p,transfer:rn({...s,robots:c}),changes:h,leftOut:d,hides:f}}const K={overview:0,redirects:1,identity:2,types:2},mn=[{id:`facebook`,label:`Facebook`,hosts:[`www.facebook.com`,`facebook.com`,`fb.com`]},{id:`instagram`,label:`Instagram`,hosts:[`www.instagram.com`,`instagram.com`]},{id:`linkedin`,label:`LinkedIn`,hosts:[`www.linkedin.com`,`linkedin.com`]},{id:`x`,label:`X (Twitter)`,hosts:[`x.com`,`www.x.com`,`twitter.com`,`www.twitter.com`]},{id:`youtube`,label:`YouTube`,hosts:[`www.youtube.com`,`youtube.com`]}];function hn(e){let t={},n=[];for(let r of e){let e=o(r)?.host??``,i=mn.find(t=>t.hosts.includes(e));i&&!(i.id in t)?t[i.id]=r:n.push(r)}return{named:t,other:n}}const gn={needed:`needed`,worth:`worth adding`,optional:`optional`};function q(e,t,n){let r=n.filter(e=>!!e);return r.length>0?`${e}: ${r.join(`, `)}`:`${e} (${gn[t]}, not added yet)`}function _n(e){return j.map(t=>{let n=M(t,e.openingHours).replace(`, `,` and `);return n===``?void 0:`${t.slice(0,3)} ${n}`}).filter(e=>e!==void 0)}function vn(e){let{site:t}=e.config,n=J(t),r=[];return n===`local`?((!t.address?.streetAddress||!t.address.addressLocality)&&r.push({what:`Address`,why:`Without it Google cannot treat you as a local business.`}),t.openingHours.length===0&&r.push({what:`Opening hours`,why:`Google cannot show when you are open.`}),t.telephone||r.push({what:`Phone number`,why:`Customers cannot call you from Google.`})):(n===`organization`&&!t.logo&&r.push({what:`Logo`,why:`Google shows it beside your name.`}),t.sameAs.length===0&&r.push({what:`Profiles elsewhere`,why:`They help Google connect your other pages to this site.`})),r}function J(e){return e.type===`Person`?`person`:e.type===`Organization`?`organization`:`local`}function yn(e,t){let n=o(e);if(!(!n||n.protocol!==`https:`&&n.protocol!==`http:`))return`${n.origin}${t}`}function bn(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Y(e,t,n=``){let r=e[t];return typeof r==`string`?r.trim():n}function xn(e,t,n){let r=e[t];return typeof r==`boolean`?r:n}function Sn(e){return e.split(/\r?\n/).map(e=>e.trim()).filter(e=>e!==``)}async function Cn(e,{rows:t=!0}={}){let[n,a,o]=await Promise.all([e.kv.list(),e.schema?.listCollections()??[],t?Fe(e):{rows:[],more:!1}]),s=new Map(n.map(e=>[e.key,e.value])),c=s.get(L);return{...i(s,l)?{uncopied:{value:s.get(l.legacy)}}:{},config:T(await r(e,s,l,{copy:!1})),collections:a.filter(e=>e.routable&&!e.hidden),redirects:c&&Array.isArray(c.rows)?c:null,redirectRows:o.rows,moreRedirects:o.more,scan:s.get(G)??null,stats:s.get(zt)??null}}async function wn(e,n){let r=await Xt(e);n.uncopied?await t(e,l,n.uncopied.value):JSON.stringify(r)!==JSON.stringify(n.stats)&&await e.kv.set(zt,r);let{uncopied:i,...a}=n;return{...a,stats:r}}function Tn(e){return e.stats??(e.scan?.status===`done`?e.scan.tally:null)}function En(e){let{scan:t}=e,n=Tn(e);if(!t||t.status===`running`)return[{type:`banner`,title:`Checking your pages`,description:t?`${t.checked} checked so far. The rest are checked in the background.`:`This starts by itself and takes about a minute.`}];if(!n)return[];let r=n.checked-n.fix;return[{type:`stats`,items:[{label:`Pages checked`,value:n.checked},{label:`Nothing to fix`,value:r},{label:`To fix`,value:n.fix}]}]}function Dn(e){let{scan:t}=e,n=Tn(e);if(!n||!t||t.status===`running`)return[];let r=[];n.checked===0&&n.hidden===0?r.push({type:`empty`,title:`No published pages yet`,size:`sm`}):n.checked===0?r.push({type:`empty`,title:`No pages that can appear on Google`,size:`sm`}):n.fix===0?r.push({type:`empty`,title:`No pages to fix. Nice work.`,size:`sm`}):(r.push({type:`header`,text:`To fix`},{type:`table`,page_action_id:`fix_table`,columns:[{key:`page`,label:`Page`},{key:`missing`,label:`What's missing`},{key:`open`,label:``,format:`element`}],rows:n.rows.map(e=>({page:e.title,missing:e.issues.join(` `),open:{type:`link`,label:`Open page`,target:{kind:`content`,collection:e.collection,id:e.id}}}))}),n.more&&r.push({type:`context`,text:`Showing the first 10. Fix these and the next ones appear.`}));let i=n.look??[];if(i.length>0&&r.push({type:`accordion`,label:`Worth a look (${i.length}${i.length===10?`+`:``})`,blocks:[{type:`context`,text:`Nothing is wrong with these pages. These are improvements for when you have time.`},{type:`table`,page_action_id:`look_table`,columns:[{key:`page`,label:`Page`},{key:`advice`,label:`Could be better`},{key:`open`,label:``,format:`element`}],rows:i.map(e=>({page:e.title,advice:e.issues.join(` `),open:{type:`link`,label:`Open page`,target:{kind:`content`,collection:e.collection,id:e.id}}}))}]}),n.hidden>0){let e=(n.hiddenTitles??[]).join(`, `);r.push({type:`context`,text:`${n.hidden>10?`More than 10`:n.hidden} ${n.hidden===1?`page is`:`pages are`} hidden from Google and not counted above${e?`: ${e}`:``}. To show one again, open it and switch off "Hide from search engines" in its SEO panel.`})}if(t.finishedAt){let e=new Date(t.finishedAt).toLocaleDateString(`en-GB`,{day:`numeric`,month:`short`});r.push({type:`context`,text:`Each page is checked again when it is saved. Last full check: ${e}.`})}return r}function On(e){if(!e.scan)return[];let t=e.scan.status===`running`;return[{type:`actions`,elements:[{type:`button`,action_id:t?`check_more`:`check_pages`,label:t?`Update`:`Check all pages again`}]}]}function kn(e){return e.collections.filter(t=>{let n=e.config.collections[t.slug];return n?.noindex===!0&&n.hiddenOk!==!0})}function An(e,t){let n=kn(e);if(n.length===0)return[];let r=[{type:`banner`,variant:`alert`,title:`Some pages are hidden from Google`,description:`Every page in ${n.map(e=>e.label).join(`, `)} is hidden from search engines. If that is a leftover from building the site, switch it back on in the Settings tab, under Content types.`}];return t&&r.push({type:`actions`,elements:[{type:`button`,action_id:`hidden_ok`,label:`This is intended`}]}),r}function jn(e,t){let{steps:n,site:r}=e.config;return n.identity?[]:[{type:`banner`,title:`Tell Google who this site is for`,description:`Google can then show your name and details beside your pages. It takes a minute.`},{type:`accordion`,label:`Set this up (1 minute)`,default_open:!0,blocks:[{type:`form`,block_id:`setup_identity`,fields:[{type:`radio`,action_id:`kind`,label:`This site is for`,options:[{label:`A local business customers visit (shop, cafe, clinic)`,value:`local`},{label:`A company or organisation`,value:`organization`},{label:`A person`,value:`person`}]},{type:`select`,action_id:`business`,label:`Kind of business`,options:[{label:`Choose the closest`,value:``},...P],initial_value:``,condition:{field:`kind`,eq:`local`}},{type:`text_input`,action_id:`name`,label:`Name`,placeholder:`Your name or your business's name`,initial_value:r.name??t},{type:`text_input`,action_id:`street`,label:`Street address`,initial_value:r.address?.streetAddress??``,condition:{field:`kind`,eq:`local`}},{type:`text_input`,action_id:`town`,label:`Town or city`,initial_value:r.address?.addressLocality??``,condition:{field:`kind`,eq:`local`}}],submit:{label:`Save`,action_id:`setup_identity`}}]}]}function Mn(e,t){let{site:n}=e.config,r=J(n),i=P.find(e=>e.value===n.type)?.label,a=r===`person`?`a person`:r===`local`?(i??`a local business`).toLowerCase():`a company`,o=[n.address?.streetAddress,n.address?.addressLocality].filter(Boolean).join(`, `),s=_n(n);return[`Google is told: ${n.name??t}, ${a}`,o?`, ${o}`:``,s.length>0?`, open ${s.join(`, `)}`:``,`. Change this in the Settings tab.`].join(``)}function Nn(e,t){if(!e.config.steps.identity)return[];let n=vn(e);return n.length===0?[{type:`context`,text:Mn(e,t)}]:[{type:`header`,text:`Your details`},{type:`context`,text:`${n.length} ${n.length===1?`thing`:`things`} Google needs from you. Add ${n.length===1?`it`:`them`} in the Settings tab.`},{type:`table`,page_action_id:`details_table`,columns:[{key:`what`,label:`Missing`},{key:`why`,label:`Why it matters`}],rows:n}]}function Pn(e,t){return[...En(e),...An(e,!0),...jn(e,t),...Nn(e,t),...Dn(e),...On(e)]}function Fn(e){let t=Tn(e),n={type:`actions`,elements:[{type:`link`,label:`Open SEO`,target:{kind:`plugin-page`,path:`/overview`}}]};if(!t||t.checked===0&&t.hidden===0){let{steps:t}=e.config;return{blocks:[{type:`section`,text:t.identity?`Your pages are being checked.`:`One thing left: tell Google who this site is for.`},n]}}return{blocks:[{type:`stats`,items:[t.checked===0?{label:`SEO`,value:`Every page is hidden from Google`}:t.fix===0?{label:`SEO`,value:`All pages ready`}:{label:t.fix===1?`Page to fix`:`Pages to fix`,value:t.fix}]},n]}}function In(e,t){let{site:n}=e.config,r=J(n),i=r===`local`,a=e.config.steps.identity,s=hn(n.sameAs),c=[{type:`context`,text:`Google reads these details from your site and can show them beside your pages. Sections marked needed are followed up on the Overview; the rest are up to you.`},{type:`form`,block_id:`identity`,fields:[{type:`radio`,action_id:`kind`,label:`This site is for`,options:[{label:`A local business customers visit (shop, cafe, clinic)`,value:`local`},{label:`A company or organisation`,value:`organization`},{label:`A person`,value:`person`}],...a?{initial_value:r}:{}},{type:`select`,action_id:`business`,label:`Kind of business`,options:[{label:`Choose the closest`,value:``},...P],initial_value:i?n.type:``,condition:{field:`kind`,eq:`local`}},{type:`text_input`,action_id:`name`,label:`Name`,placeholder:`Your name or your business's name`,initial_value:n.name??t},{type:`text_input`,action_id:`description`,label:`What you do, in one or two sentences`,multiline:!0,initial_value:n.description??``}],submit:{label:`Save`,action_id:`save_identity`}},{type:`context`,text:`Each section below is saved with its own Save button.`}],l={type:`accordion`,label:q(`Address and phone`,i?`needed`:`optional`,[n.address?.streetAddress,n.address?.addressLocality,n.telephone,n.email]),default_open:i&&vn(e).some(e=>e.what!==`Opening hours`),blocks:[{type:`context`,text:i?`Google needs an address before it treats you as a local business. Use the same details as your Google Business Profile, which is what puts you on the map.`:`Leave the address empty if you have no public one.`},{type:`form`,block_id:`contact`,fields:[{type:`text_input`,action_id:`street`,label:`Street address`,initial_value:n.address?.streetAddress??``},{type:`text_input`,action_id:`town`,label:`Town or city`,initial_value:n.address?.addressLocality??``},{type:`text_input`,action_id:`region`,label:`State or region`,initial_value:n.address?.addressRegion??``},{type:`text_input`,action_id:`postcode`,label:`Postcode`,initial_value:n.address?.postalCode??``},{type:`text_input`,action_id:`country`,label:`Country code (GB, US, IN, AU)`,initial_value:n.address?.addressCountry??``},{type:`text_input`,action_id:`telephone`,label:`Phone`,initial_value:n.telephone??``},{type:`text_input`,action_id:`email`,label:`Email`,initial_value:n.email??``},{type:`select`,action_id:`price_range`,label:`Price range`,options:oe.map(e=>({label:e===``?`Not shown`:e,value:e})),initial_value:n.priceRange??``},{type:`text_input`,action_id:`areas`,label:`Areas you serve, one per line (if you travel to customers)`,multiline:!0,initial_value:n.areaServed.join(`
3
+ `)}],submit:{label:`Save address and phone`,action_id:`save_contact`}}]},u=i?{type:`accordion`,label:q(`Opening hours`,`needed`,_n(n)),blocks:[{type:`context`,text:`Type the hours for each day, like 9:00-17:00. For a break in the day use two ranges: 12:00-14:30, 18:00-22:00. A day left empty is shown as closed.`},{type:`form`,block_id:`hours`,fields:j.map(e=>({type:`text_input`,action_id:`hours_${e.toLowerCase()}`,label:e,placeholder:`Closed`,initial_value:M(e,n.openingHours)})),submit:{label:`Save opening hours`,action_id:`save_hours`}}]}:{type:`accordion`,label:`Opening hours (only for a local business)`,blocks:[{type:`context`,text:`Opening hours are for a place customers visit. Choose "A local business customers visit" above and save, and this section opens up.`}]},d={type:`accordion`,label:q(`Logo`,r===`organization`?`needed`:`worth`,[n.logo?`added`:void 0]),blocks:[{type:`context`,text:`Google can show your logo beside your name. In Media, open your logo and use Copy URL, then paste it here. Use a square image at least 112 pixels wide.`},{type:`form`,block_id:`logo`,fields:[{type:`text_input`,action_id:`logo_url`,label:`Logo link`,placeholder:`https://example.com/logo.png`,initial_value:n.logo??``}],submit:{label:`Save logo`,action_id:`save_logo`}}]},f={type:`accordion`,label:q(`Your profiles elsewhere`,i?`worth`:`needed`,n.sameAs.map(e=>o(e)?.host.replace(/^www\./,``))),blocks:[{type:`context`,text:`Links to your pages on other sites help Google connect them to you. Paste the full address of each profile you have.`},{type:`form`,block_id:`profiles`,fields:[...mn.map(e=>({type:`text_input`,action_id:`profile_${e.id}`,label:e.label,placeholder:`https://${e.hosts[0]}/yourname`,initial_value:s.named[e.id]??``})),{type:`text_input`,action_id:`profiles`,label:`Other profiles, one per line`,multiline:!0,initial_value:s.other.join(`
4
+ `)}],submit:{label:`Save profiles`,action_id:`save_profiles`}}]},p={type:`accordion`,label:q(`More about you`,`optional`,[n.alternateName,n.legalName,n.foundingDate?`founded ${n.foundingDate}`:void 0]),blocks:[{type:`context`,text:`Nothing here is needed. Skip anything that does not apply.`},{type:`form`,block_id:`company`,fields:[{type:`text_input`,action_id:`alternate_name`,label:`Also known as (a short name or abbreviation)`,initial_value:n.alternateName??``},{type:`text_input`,action_id:`legal_name`,label:`Registered legal name`,initial_value:n.legalName??``},{type:`text_input`,action_id:`founded`,label:`Founded (year, or YYYY-MM-DD)`,placeholder:`2012`,initial_value:n.foundingDate??``}],submit:{label:`Save`,action_id:`save_company`}}]},m=i?[l,u,d,f,p]:r===`organization`?[d,f,l,p,u]:[f,d,l,p,u];return c.push(...m),c}function Ln(e,t){return e.config.collections[t]?.type??m(t)}function Rn(e,t){let{config:n}=e,r=e.collections.find(e=>e.slug===t),i=[{type:`context`,text:`Google is told what each kind of content is, so it can show it properly: a blog post with its author and date, a service as a service. These were worked out from the names and are usually right. Every kind of content that has its own pages is listed here; content that only appears inside other pages, such as testimonials, is not. To change a single page (an About or Contact page, say), open it and use Change page type in its Google preview panel. Picking from a menu saves straight away.`}];return r&&i.push({type:`banner`,variant:`alert`,title:`Hide every page in ${r.label} from Google?`,description:`People will no longer find these pages through search engines. You can switch this back at any time.`},{type:`actions`,elements:[{type:`button`,action_id:`confirm_hide`,label:`Yes, hide them`,style:`danger`,value:r.slug},{type:`button`,action_id:`cancel_hide`,label:`Cancel`}]}),i.push({type:`table`,page_action_id:`types_table`,empty_text:`This site has no content types with pages yet.`,columns:[{key:`name`,label:`Content type`},{key:`shown`,label:``,format:`element`},{key:`visible`,label:``,format:`element`}],rows:e.collections.map(t=>{let r=n.collections[t.slug]?.noindex===!0;return{name:t.label,shown:{type:`menu`,action_id:`set_type`,label:`Google treats these as: ${F(Ln(e,t.slug))}`,items:N.map(({value:e,label:n})=>({label:n,value:`${t.slug}|${e}`}))},visible:{type:`menu`,action_id:`set_visible`,label:r?`Hidden from Google`:`Can appear on Google`,items:[{label:`Can appear on Google`,value:`${t.slug}|show`},{label:`Hide from Google and other search engines`,value:`${t.slug}|hide`}]}}})},{type:`form`,block_id:`breadcrumbs`,fields:[{type:`toggle`,action_id:`breadcrumbs`,label:`Show the path to each page on Google (Home > Services > Cakes)`,description:`Leave on unless your theme already does this.`,initial_value:n.breadcrumbs}],submit:{label:`Save`,action_id:`save_advanced`}}),[{type:`accordion`,label:`Content types (usually right as they are): ${e.collections.map(t=>{let r=n.collections[t.slug]?.noindex===!0;return`${t.label} as ${F(Ln(e,t.slug))}${r?`, hidden`:``}`}).join(`; `)}`,default_open:r!==void 0,blocks:i}]}function zn(e,t){let n=[{type:`context`,text:`To set up another site the same way, copy this text, then on that site open SEO > Settings > Copy settings to another site, paste it and press Check. It holds who the site is for, its details, opening hours, profiles and content types; no passwords or keys. Redirects are copied on their own, with Export and Import from WordPress on the Redirects tab.`},{type:`code`,block_id:`settings_export`,language:`jsonc`,code:an(e.config,e.collections)},{type:`form`,block_id:`settings_import_${t?.read?`checked`:t?.pasted?`retry`:`paste`}`,fields:[{type:`text_input`,action_id:`settings`,label:`Paste settings copied from another site`,multiline:!0,initial_value:t?.pasted??``}],submit:{label:`Check`,action_id:`check_settings`}}],r=t?.read;return r&&(r.hides.length>0&&n.push({type:`banner`,variant:`alert`,title:`These settings hide every page in ${r.hides.join(`, `)} from Google`,description:`People will no longer find these pages through search engines. If that is not what you want, change it on the Content types menu after applying.`}),r.leftOut.length>0&&n.push({type:`context`,text:`Left out, as this site has no such content: ${r.leftOut.join(`, `)}.`}),r.changes.length===0?n.push({type:`empty`,title:`Nothing would change: this site already has these settings.`,size:`sm`}):n.push({type:`header`,text:`What will change`},{type:`table`,page_action_id:`transfer_table`,columns:[{key:`what`,label:`Setting`},{key:`now`,label:`Now`},{key:`after`,label:`After`}],rows:r.changes.map(e=>({...e}))},{type:`actions`,block_id:`settings_import_confirm`,elements:[{type:`button`,action_id:`apply_settings`,label:`Apply these settings`,style:`primary`,value:r.transfer},{type:`button`,action_id:`cancel_settings`,label:`Cancel`}]})),[{type:`accordion`,label:`Copy settings to another site (optional)`,default_open:t!==void 0,blocks:n}]}function X(e,t,n,r){let i=[{type:`header`,text:`SEO`},{type:`context`,text:`How your site appears on Google and other search engines.`}];return n?{blocks:[...i,{type:`tab`,default_tab:r.tab,panels:[{label:`Overview`,blocks:Pn(e,t.site.name)},{label:`Redirects`,blocks:Ue({state:e.redirects,rows:e.redirectRows,more:e.moreRedirects,editing:r.editing,panel:r.panel,problem:r.problem,exported:r.exported,managerUrl:yn(t.site.url,`/_emdash/admin/redirects`)})},{label:`Settings`,blocks:[...In(e,t.site.name),...Rn(e,r.pendingHide),...zn(e,r.transfer),A(e.config.hideHelpLine)]}]},...te(!e.config.hideHelpLine)],...r.toast?{toast:r.toast}:{}}:{blocks:[...i,...En(e),...An(e,!1),...Dn(e),{type:`context`,text:`Business details, opening hours and redirects are set by your site admin. Ask them if something there is wrong.`}]}}function Bn(e,t){let n={tab:K.identity},{site:r}=e.config,i=Y(t,`kind`,e.config.steps.identity?J(r):``);if(i!==`local`&&i!==`organization`&&i!==`person`)return{error:`Choose who this site is for.`,state:n};let a=Y(t,`business`,J(r)===`local`?r.type:``),o=P.find(e=>e.value===a)?.value;if(i===`local`&&!o)return{error:`Choose the kind of business.`,state:n};let s=i===`person`?`Person`:i===`local`?o??`LocalBusiness`:`Organization`,c=Y(t,`name`,r.name??``);return{config:{...e.config,site:{...r,type:s,name:c===``?void 0:c,description:Y(t,`description`,r.description??``)||void 0,openingHours:i===`local`?r.openingHours:[]},steps:{...e.config.steps,identity:!0}},state:n}}function Vn(e,t,n){let r={tab:K.identity},i=s(Y(t,`logo_url`,e.config.site.logo??``),n);return`error`in i?{error:i.error,state:r}:{config:{...e.config,site:{...e.config.site,logo:i.logo}},state:r}}function Hn(e,t){let{site:n}=e.config,r={streetAddress:Y(t,`street`)||void 0,addressLocality:Y(t,`town`)||void 0,addressRegion:Y(t,`region`)||void 0,postalCode:Y(t,`postcode`)||void 0,addressCountry:Y(t,`country`).toUpperCase()||void 0},i=Y(t,`price_range`);return{config:{...e.config,site:{...n,email:Y(t,`email`)||void 0,telephone:Y(t,`telephone`)||void 0,address:Object.values(r).some(e=>e!==void 0)?r:void 0,priceRange:oe.includes(i)&&i!==``?i:void 0,areaServed:Sn(Y(t,`areas`,n.areaServed.join(`
5
+ `)))}},state:{tab:K.identity}}}function Un(e,t){let n={tab:K.identity},r=ae(n=>Y(t,`hours_${n.toLowerCase()}`,M(n,e.config.site.openingHours)));return`day`in r?{error:`Couldn't read the hours for ${r.day}. Write them like 9:00-17:00, with at most two ranges.`,state:n}:{config:{...e.config,site:{...e.config.site,openingHours:r.hours}},state:n}}function Wn(e,t){let n={tab:K.identity},r=hn(e.config.site.sameAs),i=[...mn.map(e=>Y(t,`profile_${e.id}`,r.named[e.id]??``)),...Sn(Y(t,`profiles`,r.other.join(`
6
+ `)))].filter(e=>e!==``),a=[];for(let e of i){let t=c(e);if(!t)return{error:`"${e}" is not a web address. Paste the full link to the profile.`,state:n};a.push(t)}return{config:{...e.config,site:{...e.config.site,sameAs:a}},state:n}}function Gn(e,t){let n={tab:K.identity},{site:r}=e.config,i=Y(t,`founded`,r.foundingDate??``);return i!==``&&!w(i)?{error:`Write the founding date as a year, like 2012, or as 2012-05-01.`,state:n}:{config:{...e.config,site:{...r,alternateName:Y(t,`alternate_name`,r.alternateName??``)||void 0,legalName:Y(t,`legal_name`,r.legalName??``)||void 0,foundingDate:i||void 0}},state:n}}function Kn(e,t){let{robots:n}=e.config,r=Y(t,`snippet`,n.nosnippet?`none`:`full`),i=Y(t,`image_preview`,n.maxImagePreview);return{config:{...e.config,breadcrumbs:xn(t,`breadcrumbs`,e.config.breadcrumbs),robots:{...n,nosnippet:r===`none`,maxSnippet:r===`short`?160:-1,maxImagePreview:i===`none`||i===`standard`||i===`large`?i:n.maxImagePreview}},state:{tab:K.types}}}function qn(e,t,n){let r={tab:K.types},[i,a]=typeof t==`string`?t.split(`|`):[];if(!i||!e.collections.some(e=>e.slug===i))return{error:`That content type no longer exists.`,state:r};let o=e.config.collections[i]??{},s=n===`type`?N.find(e=>e.value===a)?.value:o.type??Ln(e,i);if(!s)return{error:`Choose one of the listed types.`,state:r};let c=n===`visible`?a===`hide`:o.noindex===!0;return{config:{...e.config,collections:{...e.config.collections,[i]:{type:s,noindex:c}},steps:{...e.config.steps,types:!0}},state:r}}async function Jn(e,t){let n=t.scan&&(t.scan.rev??1)>=3?t.scan:null;if(n?.status===`done`)return wn(e,t);let r=n?await Yt(e,n,t.config,6):await Ut(e,t.collections.map(e=>e.slug),t.config,6);return{...t,scan:r,stats:r.status===`done`?r.tally:null}}async function Yn(e,t,n){let r=(t??0)>=50,i=bn(e)?e:{},o=typeof i.action_id==`string`?i.action_id:``,s=await Cn(n,{rows:!(r&&i.type===`block_action`&&(o===`add_redirects`||o===`more_redirects`||o===`remove_redirects`))});if(i.type===`page_load`&&i.page===`widget:seo`)return Fn(await Jn(n,s));if(!r||i.type===`page_load`||o===``)return X(await Jn(n,s),n,r,{tab:K.overview});let c=async(e,t=`Saved. Google will pick this up next time it visits.`)=>{if(`error`in e)return X(s,n,r,{...e.state,toast:{type:`error`,message:e.error}});let i=T(e.config);return await a(n,l,i),$t(n.plugin.id),X({...s,config:i},n,r,{...e.state,toast:{type:`success`,message:t}})},u=async e=>{try{let{rows:t,more:r}=await Fe(n);return{...e,redirectRows:t,moreRedirects:r}}catch{return e}},d=async(e,t,i)=>e?X(s,n,r,{tab:K.redirects,editing:i,...i?{}:{panel:`add`},problem:e}):X(await u(s),n,r,{tab:K.redirects,toast:{type:`success`,message:t}});if(i.type===`block_action`){if(o===`check_pages`||o===`check_more`){let e=await Jn(n,o===`check_pages`?{...s,scan:null}:s);return X(e,n,r,{tab:K.overview,toast:e.scan?.status===`done`?{type:`success`,message:`All pages checked.`}:{type:`info`,message:`${e.scan?.checked??0} pages checked so far. The rest follow in the background.`}})}if(o===`set_type`)return c(qn(s,i.value,`type`));if(o===`set_visible`){let[e,t]=typeof i.value==`string`?i.value.split(`|`):[];return t===`hide`?X(s,n,r,{tab:K.types,pendingHide:e}):c(qn(s,i.value,`visible`),`These pages can appear on Google again.`)}if(o===`confirm_hide`&&typeof i.value==`string`)return c(qn(s,`${i.value}|hide`,`visible`),`These pages are now hidden from Google and other search engines.`);if(o===`cancel_hide`)return X(s,n,r,{tab:K.types});if(o===`hidden_ok`){let e={...s.config.collections};for(let t of kn(s))e[t.slug]={...e[t.slug],hiddenOk:!0};return c({config:{...s.config,collections:e},state:{tab:K.overview}},`Noted. These pages stay hidden from search.`)}if(o===`apply_settings`){let e=pn(i.value,s.config,s.collections,n.site.url,n.site.name);return`error`in e?X(s,n,r,{tab:K.identity,transfer:{},toast:{type:`error`,message:e.error}}):c({config:e.config,state:{tab:K.identity}},`Settings copied. To see what they change in the pages to fix, press Check all pages again on the Overview.`)}if(o===`cancel_settings`)return X(s,n,r,{tab:K.identity});if(o===`edit_redirect`){let e=s.redirectRows.find(e=>e.id===i.value);return X(s,n,r,{tab:K.redirects,editing:e})}if(o===`open_add`)return X(s,n,r,{tab:K.redirects,panel:`add`});if(o===`open_import`)return X(s,n,r,{tab:K.redirects,panel:`import`});if(o===`close_panel`)return X(s,n,r,{tab:K.redirects});if(o===`remove_redirect`&&typeof i.value==`string`)return d(await Be(n,i.value),`Redirect removed.`);if(o===`add_redirects`||o===`more_redirects`){let e=s.redirects;if(!e||e.status!==`checked`&&e.status!==`adding`)return X(await u(s),n,r,{tab:K.redirects});let t=await De(n,e,6);return X(await u({...s,redirects:t}),n,r,{tab:K.redirects,toast:t.status===`done`?{type:`success`,message:`${z(t.added,`redirect`)} added.`}:{type:`info`,message:`${t.added} added so far. The rest follow in the background, a few each minute.`}})}if(o===`stop_redirects`&&s.redirects?.status===`removing`){let e=s.redirects.removed??0,t=await ke(n,s.redirects);return X(await u({...s,redirects:t}),n,r,{tab:K.redirects,toast:{type:`info`,message:`Stopped. ${z(e,`redirect`)} removed.`}})}if(o===`stop_redirects`&&s.redirects?.status===`adding`){let e=await We(n,s.redirects);return X({...s,redirects:e},n,r,{tab:K.redirects,toast:{type:`info`,message:`Stopped. ${z(e.added,`redirect`)} added.`}})}if(o===`reset_redirects`){let e=s.redirects?.status===`adding`||s.redirects?.status===`removing`;return e||await n.kv.delete(L),X({...s,redirects:e?s.redirects:null},n,r,{tab:K.redirects})}if(o===`remove_redirects`&&(s.redirects?.status===`done`||s.redirects?.status===`removing`)){let e=await Oe(n,s.redirects,6);return X(await u({...s,redirects:e}),n,r,{tab:K.redirects,toast:e?e.status===`done`?{type:`error`,message:`EmDash would not remove some of them. Press Remove to try again, or remove them in the full Redirects screen.`}:{type:`info`,message:`${e.removed??0} removed so far. The rest are removed in the background, a few each minute.`}:{type:`success`,message:`Removed. Your other redirects are untouched.`}})}if(o===`remove_redirects`)return X(await u(s),n,r,{tab:K.redirects});if(o===`export_redirects`){let{text:e,count:t}=await Ae(n);return X(s,n,r,{tab:K.redirects,panel:`export`,exported:e,toast:{type:`info`,message:`${z(t,`redirect`)} listed below.`}})}return X(s,n,r,{tab:K.overview})}if(i.type!==`form_submit`)return X(s,n,r,{tab:K.overview});let f=bn(i.values)?i.values:{};if(o===`check_redirects`){let e=Y(f,`list`);if(e===``)return X(s,n,r,{tab:K.redirects,toast:{type:`error`,message:`Paste a list first.`}});let t=await Ce(n,e);return X({...s,redirects:t},n,r,{tab:K.redirects,toast:{type:`success`,message:`Checked. Nothing has been added yet.`}})}if(o===`check_settings`){let e=Y(f,`settings`),t=pn(e,s.config,s.collections,n.site.url,n.site.name);return`error`in t?X(s,n,r,{tab:K.identity,transfer:{pasted:e},toast:{type:`error`,message:t.error}}):X(s,n,r,{tab:K.identity,transfer:{pasted:e,read:t},toast:{type:`success`,message:`Checked. Nothing has been saved yet.`}})}if(o===`add_redirect`){let e=Y(f,`source`),t=Y(f,`code`,`301`)===`410`?``:Y(f,`destination`);return d(await Re(n,e,t,Y(f,`code`,`301`)),t===``?`Done. ${e} now tells visitors the page was removed.`:`Redirect added. ${e} now goes to ${t}.`)}if(o.startsWith(`save_redirect:`)){let e=o.slice(14),t=s.redirectRows.find(t=>t.id===e);return d(await ze(n,e,Y(f,`source`,t?.source??``),Y(f,`destination`,t?.destination??``),Y(f,`code`,String(t?.type??301))),`Redirect changed.`,t)}if(o===`setup_identity`){let e=Bn(s,f);if(`error`in e)return c(e);let t=Y(f,`street`),n=Y(f,`town`),{site:r}=e.config,i=t!==``||n!==``?{...r.address,...t?{streetAddress:t}:{},...n?{addressLocality:n}:{}}:r.address;return c({config:{...e.config,site:{...r,address:i}},state:{tab:K.overview}})}if(o===`save_identity`)return c(Bn(s,f));if(o===`save_logo`)return c(Vn(s,f,n.site.url));if(o===`save_contact`)return c(Hn(s,f));if(o===`save_hours`)return c(Un(s,f));if(o===`save_profiles`)return c(Wn(s,f));if(o===`save_company`)return c(Gn(s,f));if(o===`save_advanced`)return c(Kn(s,f));if(o===O){let e=ee(f,s.config.hideHelpLine);return c({config:{...s.config,hideHelpLine:e},state:{tab:K.types}},e?`Saved. The help line is hidden.`:`Saved. The help line shows again.`)}return X(s,n,r,{tab:K.overview})}function Z(e){return{"@id":e}}function Q(e){let t={};for(let[n,r]of Object.entries(e))r==null||r===``||Array.isArray(r)&&r.length===0||(t[n]=r);return t}function Xn(e,t){return o(e,t)?.href??e}function Zn(e,t){return(e.siteUrl||t||(o(e.url)?.origin??``)).replace(/\/+$/,``)}function Qn(e,t){if(e.canonical)return Xn(e.canonical,`${t}/`);let n=o(e.url,`${t}/`);return n?(n.search=``,n.hash=``,n.href):e.url}function $n(e){return e.path===`/`||e.path===``}function er(e){return f.includes(e)}function tr(e,t,n){let r=e.content?t.collections[e.content.collection]:void 0;if(n?.type)return n.type;if(r?.type)return r.type;let i=e.content?m(e.content.collection):`WebPage`;return i===`WebPage`?e.pageType===`article`?`BlogPosting`:`WebPage`:i}function nr(e,t,n,r){let i=`${n}/#logo`,a=e.type===`Person`,o=!a&&e.type!==`Organization`&&e.address===void 0?`Organization`:e.type,s=!a&&(e.address!==void 0||e.geo!==void 0||e.openingHours.length>0);return Q({"@type":o,"@id":r,name:t,alternateName:e.alternateName,legalName:a?void 0:e.legalName,description:e.description,url:`${n}/`,logo:e.logo&&!a?{"@type":`ImageObject`,"@id":i,url:e.logo,contentUrl:e.logo,caption:t}:void 0,image:e.logo?a?e.logo:Z(i):void 0,sameAs:e.sameAs,email:e.email,telephone:e.telephone,address:e.address?{"@type":`PostalAddress`,...e.address}:void 0,geo:e.geo&&!a?{"@type":`GeoCoordinates`,...e.geo}:void 0,openingHoursSpecification:s?e.openingHours.map(e=>({"@type":`OpeningHoursSpecification`,dayOfWeek:e.days,opens:e.opens,closes:e.closes})):void 0,priceRange:a?void 0:e.priceRange,areaServed:a?void 0:e.areaServed,foundingDate:a?void 0:e.foundingDate,vatID:a?void 0:e.vatID,taxID:a?void 0:e.taxID,numberOfEmployees:!a&&e.numberOfEmployees?{"@type":`QuantitativeValue`,value:e.numberOfEmployees}:void 0})}function rr(e){let t=e;try{t=decodeURIComponent(e)}catch{}let n=t.replace(/[-_]+/g,` `).trim();return n.charAt(0).toUpperCase()+n.slice(1)}function ir(e,t,n){let{page:r,config:i,site:a}=e;if(r.breadcrumbs)return r.breadcrumbs.map(e=>({name:e.name,url:Xn(e.url,`${t}/`)}));if(!i.breadcrumbs||$n(r))return[];let o=r.path.split(`/`).filter(e=>e!==``);if(o.length===0)return[];let s=a.trailingSlash===`always`?`/`:``,c=[{name:`Home`,url:`${t}/`}],l=``;return o.forEach((e,r)=>{l+=`/${e}`;let i=r===o.length-1;c.push({name:i?n:rr(e),url:`${t}${l}${s}`})}),c}function ar(e,t){return e.length<=t?e:`${e.slice(0,t-1).trimEnd()}…`}function or(e,t,n){let r;for(let{"@context":i,...a}of t){let t=typeof a[`@id`]==`string`?a[`@id`]:void 0;if(t!==void 0){if(n.has(t))continue;n.add(t),r??=t}e.push(a)}return r}function sr(e){let{page:t,config:n,record:r,site:i}=e,a=Zn(t,i.url),o=Qn(t,a),s=t.locale??i.locale,c=n.site.name??t.siteName??i.name,l=t.pageTitle??t.title??c,u=n.site.type===`Person`,d=`${a}/#${u?`person`:`organization`}`,f=`${a}/#website`,p=o,m=`${o}#breadcrumb`,h=tr(t,n,r),g=er(h),_=h===`Service`,v=h===`ProfilePage`,y=`${o}#service`,b=`${o}#person`,x=r?.publishedAt??t.articleMeta?.publishedTime??void 0,S=r?.updatedAt??t.articleMeta?.modifiedTime??void 0,C=t.image?Xn(t.image,`${a}/`):void 0,w=C??``,T=ir(e,a,l),E=[nr(n.site,c,a,d),Q({"@type":`WebSite`,"@id":f,url:`${a}/`,name:c,alternateName:n.site.alternateName,description:n.site.description,publisher:Z(d),inLanguage:s})];if(e.noindex)return E;let D=Q({"@type":g||_?`WebPage`:h,"@id":p,url:o,name:l,description:t.description,isPartOf:Z(f),about:$n(t)?Z(d):void 0,primaryImageOfPage:C?Z(w):void 0,image:C?Z(w):void 0,breadcrumb:T.length>0?Z(m):void 0,mainEntity:_?Z(y):v?Z(b):void 0,datePublished:x,dateModified:S,inLanguage:s});if(E.push(D),g){let e=(r?.authors??[]).map(e=>Q({"@type":`Person`,name:e.name,url:e.url})),n=t.articleMeta?.author?[{"@type":`Person`,name:t.articleMeta.author}]:[Z(d)];E.push(Q({"@type":h,"@id":`${o}#article`,isPartOf:Z(p),mainEntityOfPage:Z(p),headline:ar(l,110),description:t.description,image:C?Z(w):void 0,datePublished:x,dateModified:S,author:e.length>0?e:n,publisher:Z(d),inLanguage:s}))}_&&E.push(Q({"@type":`Service`,"@id":y,name:l,description:t.description,url:o,image:C?Z(w):void 0,provider:Z(d),areaServed:n.site.areaServed})),v&&E.push(Q({"@type":`Person`,"@id":b,name:l,description:t.description,url:o,image:C?Z(w):void 0,worksFor:u?void 0:Z(d)})),C&&E.push({"@type":`ImageObject`,"@id":w,url:C,contentUrl:C,...yt(r?.images,C,a)}),T.length>0&&E.push({"@type":`BreadcrumbList`,"@id":m,itemListElement:T.map((e,t)=>Q({"@type":`ListItem`,position:t+1,name:e.name,item:t===T.length-1?void 0:e.url}))});let O=new Set([`${a}/#logo`]);for(let e of E)typeof e[`@id`]==`string`&&O.add(e[`@id`]);let k=or(E,r?.custom??[],O);return k!==void 0&&!g&&D.mainEntity===void 0&&(D.mainEntity=Z(k)),E}const cr=`default`;function lr(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function ur(e){let t=o(e);if(!t)return e;let n=t.pathname.split(`/`).filter(e=>e!==``);return[t.host,...n].join(` › `)}function dr(e,t){return e.length<=t?e:`${e.slice(0,t-1).trimEnd()}…`}async function fr(e,t,n,r){if(!t)return{blocks:[]};let i=(n??0)>=50,a=lr(e)?e:{},s=await u(r),c=i&&a.type===`block_action`&&a.action_id===`set_type`,l=typeof a.value==`string`?a.value:cr,d=N.find(e=>e.value===l)?.value,[f,p]=await Promise.all([It(r,t.collection,t.id,s,c?e=>{let{type:t,...n}=e;return d?{...n,type:d}:n}:void 0),r.content?.getPublicUrl?.(t.collection,t.id)]),h=f?.record,g=f?.entry,_=h&&c?{type:`success`,message:`Saved. Google will pick this up next time it visits.`}:void 0;if(!h||!g)return{blocks:[{type:`context`,text:`Save this page to see how it may look on Google.`}]};if(h.status===`skip`)return{blocks:[{type:`context`,text:`This kind of content has no page of its own, so Google only sees it inside other pages. There is nothing to set here.`}]};let v=s.site.name??r.site.name,y=r.site.url.replace(/\/+$/,``),b=[`home`,`index`,``].includes(g.slug??``)?`${y}/`:`${y}/${g.slug}`,x=[`home`,`index`].includes(g.slug??``)?`${y}/`:p??b,S=h.title||`(no title)`,C=g.seo?.description?.trim()??``,w=C===``&&ot(g.data)!==``,T=C||ot(g.data),E=s.collections[t.collection]?.type??m(t.collection),D=h.type??E,O=h.status===`hidden`,k=h.issues??[],A=[{type:`context`,text:v?`${v} · ${ur(x)}`:ur(x)},{type:`header`,text:tt(h.title?et(S,v):S)},{type:`section`,text:T===``?`No Meta Description yet. Unless your theme fills one in from another field, Google picks some text from the page.`:dr(T,160)},...w?[{type:`context`,text:`No Meta Description, so this is the page's excerpt, which EmDash uses in its place. Your theme may show another field instead, such as a standfirst or summary. To choose what search shows, fill in Meta Description in the SEO panel.`}]:[],{type:`context`,text:`What Google sees: the published page, with the SEO title and description as last saved, and your site's name after the title as most sites show it. Google may rewrite them.`},{type:`fields`,fields:[{label:`Shown to Google as`,value:F(D)},{label:`In search results`,value:h.status===`draft`?`Not published yet`:O?`Hidden from search`:`Yes`}]}];if(k.length===0)A.push({type:`context`,text:`Nothing to fix on this page.`});else{let e=[...k].sort((e,t)=>Number(t.level===`fix`)-Number(e.level===`fix`));for(let t of e.slice(0,3))A.push({type:`section`,text:`${t.level===`fix`?`To fix`:`Worth a look`}: ${t.message}`},{type:`context`,text:t.fix});e.length>3&&A.push({type:`context`,text:`And ${e.length-3} more.`})}if(i){let e=sr({page:{url:x,path:o(x)?.pathname??`/`,locale:g.locale,kind:`content`,pageType:`website`,title:S,pageTitle:S,description:T===``?null:T,canonical:g.seo?.canonical??null,image:null,content:{collection:t.collection,id:t.id,slug:g.slug}},config:s,record:h,noindex:O,site:r.site});A.push({type:`divider`},{type:`actions`,elements:[{type:`menu`,action_id:`set_type`,label:`Change page type`,items:[{label:`Same as other pages here (${F(E)})`,value:cr},...N.map(({value:e,label:t})=>({value:e,label:t}))]},.../^https?:\/\//.test(x)?[{type:`link`,label:`Test on Google`,target:{kind:`external`,url:`https://search.google.com/test/rich-results?url=${encodeURIComponent(x)}`}},{type:`link`,label:`Check all markup`,target:{kind:`external`,url:`https://validator.schema.org/#url=${encodeURIComponent(x)}`}}]:[]]},{type:`accordion`,label:`View the markup`,blocks:[{type:`context`,text:`The schema markup (JSON-LD) this page sends to search engines.`},{type:`code`,language:`jsonc`,code:JSON.stringify({"@context":`https://schema.org`,"@graph":e},null,2)}]})}return{blocks:A,..._?{toast:_}:{}}}const pr=/\s*,\s*/;function mr(e,t,n){let r=(e??``).toLowerCase().split(pr).map(e=>e.trim()).filter(e=>e!==``),i=t||r.includes(`noindex`)||r.includes(`none`),a=r.includes(`nofollow`)||r.includes(`none`),o=[i?`noindex`:`index`,a?`nofollow`:`follow`],s=new Set([`index`,`noindex`,`follow`,`nofollow`,`none`,`all`]),c=r.filter(e=>!s.has(e));if(!i){let e=e=>c.some(t=>t.startsWith(e));n.nosnippet||c.includes(`nosnippet`)?c.includes(`nosnippet`)||c.push(`nosnippet`):e(`max-snippet:`)||c.push(`max-snippet:${n.maxSnippet}`),e(`max-image-preview:`)||c.push(`max-image-preview:${n.maxImagePreview}`),e(`max-video-preview:`)||c.push(`max-video-preview:${n.maxVideoPreview}`)}return[...o,...new Set(c)].join(`, `)}function hr(e){return e.startsWith(`noindex`)}function gr(e){return/[?&]_preview=/.test(e)}async function $(e,t,n){let r=n?.id;if(typeof r==`string`)try{await At(e,t,r,await u(e))}catch(n){e.log.warn(`could not refresh entry record`,{collection:t,id:r,error:String(n)})}}const _r={hooks:{"page:metadata":async(e,t)=>{let{page:n}=e,r=n.content,[i,a]=await Promise.all([Qt(t,()=>u(t),{fresh:gr(n.url)}),(async()=>{if(!r)return null;try{return{stored:await H(t).get(V(r.collection,r.id))}}catch(e){return{error:e}}})()]),o=null;if(r&&a){let{collection:e,id:s}=r;try{if(`error`in a)throw a.error;let{stored:r}=a,c=H(t);o=r??await Nt(t,e,s,i);let l=!!(n.seo?.ogImage||n.image);if(r&&Et(r,l)){let t=await c.get(V(e,s)),n=t?Et(t,l):null;n&&await c.put(V(e,s),n),o=n??t??r}}catch(n){t.log.warn(`could not load entry record`,{collection:e,id:s,error:String(n)})}}let s=n.content?i.collections[n.content.collection]:void 0,c=mr(n.seo?.robots,s?.noindex===!0,i.robots);return[{kind:`meta`,name:`robots`,content:c},{kind:`jsonld`,id:`primary`,graph:{"@context":`https://schema.org`,"@graph":sr({page:n,config:i,record:o,noindex:hr(c),site:t.site})}}]},"content:afterSave":async(e,t)=>{await $(t,e.collection,e.content)},"content:afterPublish":async(e,t)=>{await $(t,e.collection,e.content)},"content:afterUnpublish":async(e,t)=>{await $(t,e.collection,e.content)},"content:afterRestore":async(e,t)=>{await $(t,e.collection,e.content)},"plugin:activate":async(e,t)=>{try{let[e,n]=await Promise.all([t.kv.list(),t.schema?.listCollections()??[]]),i=new Map(e.map(e=>[e.key,e.value]));if(i.get(G))return;let a=T(await r(t,i,l,{copy:!1})),o=await Ut(t,n.filter(e=>e.routable&&!e.hidden).map(e=>e.slug),a,7);o.status===`done`&&o.checked===0&&await t.kv.delete(G)}catch(e){t.log.warn(`could not start the first page check`,{error:String(e)})}},cron:async(e,t)=>{if(e.name===Bt){let e=await Ht(t);e?.status===`running`||e?.sweep?await Yt(t,e,await u(t)):await t.cron?.cancel(Bt);return}if(e.name!==R)return;let n=await xe(t);n?.status===`adding`?await De(t,n):n?.status===`removing`?await Oe(t,n):await t.cron?.cancel(R)},"content:afterDelete":async(e,t)=>{await Rt(t,e.collection,e.id)}},routes:{admin:{permission:`plugins:read`,handler:async(e,t)=>Yn(e.input,e.user?.role,t)},"editor/preview":{permission:`content:read`,handler:async(e,t)=>fr(e.input,e.ui?.entry,e.user?.role,t)},hello:{handler:async(e,t)=>(t.log.info(`hello route called`,{pluginId:t.plugin.id}),{greeting:`hello`,pluginId:t.plugin.id})}}};export{_r as default};
@@ -30,9 +30,10 @@
30
30
  "banner": { "file": "./images/banner.png" },
31
31
  "screenshots": [
32
32
  { "file": "./images/overview-0-1-12.png", "lang": "en" },
33
- { "file": "./images/google-preview-0-1-12.png", "lang": "en" },
33
+ { "file": "./images/google-preview-0-2-0.png", "lang": "en" },
34
34
  { "file": "./images/redirects-0-1-12.png", "lang": "en" },
35
35
  { "file": "./images/settings-0-1-12.png", "lang": "en" },
36
+ { "file": "./images/settings-copy-check-0-2-0.png", "lang": "en" },
36
37
  ],
37
38
  },
38
39
  },
package/listing/faq.md CHANGED
@@ -28,7 +28,7 @@ Yes. Choose "A local business customers visit" on the SEO page and fill in the a
28
28
 
29
29
  ## Will my pages get rich results on Google?
30
30
 
31
- The markup makes pages eligible; Google decides what it shows. Admins can open Google's Rich Results Test from the Google preview panel.
31
+ The markup makes pages eligible; Google decides what it shows. Admins can open Google's Rich Results Test and the schema.org Validator from the Google preview panel.
32
32
 
33
33
  ## Where do I write the meta description?
34
34
 
@@ -5,10 +5,19 @@ npm install @moorage/plugin-seo
5
5
  ```
6
6
 
7
7
  ```js
8
- // astro.config.mjs
8
+ // astro.config.mjs (on Cloudflare)
9
+ import { sandbox } from "@emdash-cms/cloudflare";
9
10
  import seo from "@moorage/plugin-seo";
10
11
 
11
- emdash({ sandboxed: [seo] });
12
+ emdash({ sandboxed: [seo], sandboxRunner: sandbox() });
12
13
  ```
13
14
 
15
+ On Cloudflare, `sandbox()` also needs the Worker Loader binding in `wrangler.jsonc`: `"worker_loaders": [{ "binding": "LOADER" }]` (Workers Paid). Without a sandbox runner, list it under `plugins: [seo]` instead.
16
+
17
+ Use one schema plugin at a time: EmDash keeps only the first page markup it receives, so a second schema plugin's markup, or this one's, silently goes missing.
18
+
14
19
  Then open **SEO** in the admin sidebar. It works before you change anything; answering one question about who the site is for makes the result more accurate.
20
+
21
+ Setting up several sites the same way: SEO > Settings > Copy settings to another site shows the settings as text; paste it on the next site, press Check to see what will change, then apply.
22
+
23
+ For IndexNow (Bing and others), search this registry for an IndexNow plugin.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moorage/plugin-seo",
3
- "version": "0.1.13",
3
+ "version": "0.2.1",
4
4
  "description": "SEO for EmDash: schema markup, JSON-LD structured data, Google preview, SEO checks, noindex, WordPress redirects import. Yoast alternative.",
5
5
  "keywords": [
6
6
  "emdash",