@moorage/plugin-seo 0.1.9 → 0.1.12
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 +23 -0
- package/README.md +6 -4
- package/dist/index.mjs +1 -1
- package/dist/manifest.json +1 -1
- package/dist/plugin.mjs +6 -6
- package/emdash-plugin.jsonc +10 -5
- package/listing/installation.md +1 -1
- package/listing/security.md +7 -0
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @moorage/plugin-seo
|
|
2
2
|
|
|
3
|
+
## 0.1.12
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b497b9a: Install texts corrected: the admin's entry for installing plugins is **Registry**, not Plugins; running sandboxed on Cloudflare needs both the `LOADER` Worker Loader binding in `wrangler.jsonc` and `sandboxRunner: sandbox()` in `astro.config.mjs` (answering Yes in `create-emdash` adds only the binding). Content Approval: its package homepage now points to https://moorage.dev/plugins/content-approval, and the FAQ says Contributors still see "Plugin action failed" in the editor on EmDash 1.4.0.
|
|
8
|
+
- b497b9a: Importing redirects (from Redirection, Yoast, Rank Math or a plain list) now skips rows whose old address ends in a file type, such as `/sitemap_index.xml` or `/wp-content/uploads/photo.jpg`, with the reason "Addresses ending in a file type, like .xml or .jpg, are not redirected by EmDash". EmDash treats such addresses as file requests and never runs a redirect for them, so these rows were added but never worked. Adding one redirect by hand gives the same reason.
|
|
9
|
+
- b497b9a: The progress bar while adding or removing imported redirects now shows the right percentage: removing 3 read "1%" and "2%" instead of 33% and 67%, because the admin's meter prints its value as a share of 100. The bar now counts out of 100, which screen readers also announce correctly.
|
|
10
|
+
- b497b9a: "The end of this title may be cut off in Google" now measures the title as most pages show it: the page's title followed by the site's name. Before, a title that fitted on its own but not with ": Your site" after it was not flagged. The Google preview's headline shows the same, so the two agree. EmDash leaves the title tag to the theme and a plugin cannot read the separator a site uses, so the check assumes ": " (" | " or " — " are about as wide); a title that already contains the site's name is measured as it is.
|
|
11
|
+
|
|
12
|
+
## 0.1.11
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- 8370eca: New screenshots of the current admin page, with the help line at its foot.
|
|
17
|
+
- 6516427: Admins see a small help line at the foot of the page, with a link to the guide; it can be hidden in Settings.
|
|
18
|
+
- ee58b7f: The README now says to restart the site after changing its address.
|
|
19
|
+
|
|
20
|
+
## 0.1.10
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- 8e315f1: The EmDash registry listing gets an icon and a banner; no change to how the plugin works.
|
|
25
|
+
|
|
3
26
|
## 0.1.9
|
|
4
27
|
|
|
5
28
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ It does not write the page title, the sitemap or robots.txt; EmDash core and you
|
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
|
-
From **
|
|
17
|
+
From **Registry** in your EmDash admin (it is listed in the [EmDash registry](https://plugins.emdashcms.com/plugins/@moorage.dev/seo)), or from npm, registered in your site's code:
|
|
18
18
|
|
|
19
19
|
```sh
|
|
20
20
|
pnpm add @moorage/plugin-seo
|
|
@@ -30,7 +30,7 @@ export default defineConfig({
|
|
|
30
30
|
});
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
`sandboxed` needs a sandbox runner: on Cloudflare it
|
|
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
34
|
|
|
35
35
|
**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
36
|
|
|
@@ -50,7 +50,7 @@ In the entry editor, the **Google preview** panel shows how the published page m
|
|
|
50
50
|
|
|
51
51
|
### The checks
|
|
52
52
|
|
|
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; 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.
|
|
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.
|
|
54
54
|
|
|
55
55
|
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
56
|
|
|
@@ -64,8 +64,10 @@ Not yet: Product and Event types (they need price and date fields on the collect
|
|
|
64
64
|
- 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
65
|
- Does it add FAQ rich results? No. Google stopped showing FAQ rich results in May 2026.
|
|
66
66
|
- 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.
|
|
67
|
-
- Why were some redirects skipped? EmDash has no regular-expression redirects. Exact addresses and "everything under a folder" are converted; other patterns are listed for you to rewrite by hand. Redirects to another website and addresses with a `?` are not supported by EmDash.
|
|
67
|
+
- Why were some redirects skipped? EmDash has no regular-expression redirects. Exact addresses and "everything under a folder" are converted; other patterns are listed for you to rewrite by hand. Redirects to another website and addresses with a `?` are not supported by EmDash. Nor are old addresses that end in a file type, such as `/sitemap_index.xml` or `/wp-content/uploads/photo.jpg`: EmDash treats them as file requests and never redirects them.
|
|
68
68
|
- Why does a large import take a while on Cloudflare? Sandboxed plugins may only do a few operations per request, so a long list is added a few rows at a time in the background, and removing an import works the same way. The page shows how far it got. You can leave the page.
|
|
69
|
+
- What is the line at the foot of the page? Only admins see it: "Help · More free plugins · by Moorage", links to the guide on moorage.dev and to more free plugins. Editors never see it. To hide it, switch on "Hide the help line at the foot of this page" at the bottom of the Settings tab and press Save.
|
|
70
|
+
- Why do the Google preview and the checks show the site's old address? Plugins keep the site address EmDash had when the server started; SEO uses it for the preview, for checking canonicals and logo links, and for the structured data when a page has no address of its own. After changing the site's address, restart the site.
|
|
69
71
|
|
|
70
72
|
## Changelog
|
|
71
73
|
|
package/dist/index.mjs
CHANGED
package/dist/manifest.json
CHANGED
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}};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}}}const C=[`Monday`,`Tuesday`,`Wednesday`,`Thursday`,`Friday`,`Saturday`,`Sunday`],w=/^(\d{1,2})(?:[:.](\d{2}))?\s*[-–—]\s*(\d{1,2})(?:[:.](\d{2}))?$/;function T(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 E(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=w.exec(t);if(!n)return null;let r=T(n[1]??``,n[2]),a=T(n[3]??``,n[4]);if(!r||!a)return null;i.push({days:[e],opens:r,closes:a})}return i}function D(e,t){return t.filter(t=>t.days.includes(e)).map(e=>`${e.opens}-${e.closes}`).join(`, `)}const O=[{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 k(e){return O.find(t=>t.value===e)?.short??e}const ee=[301,302,307,308,410,451],A=2e3,j={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 ${A} rows are imported at once`};function te(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 ne={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 re(e,t){let n=e.map(e=>ne[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 ie(e){let t=e.trim();if(t===``)return{reason:j.empty};if(/^https?:\/\//i.test(t)){let e=o(t);if(!e)return{reason:j.empty};t=`${e.pathname}${e.search}`}return t.includes(`?`)?{reason:j.query}:(t.startsWith(`/`)||(t=`/${t}`),t.length>1&&(t=t.replace(/\/+$/,``)),{path:t===``?`/`:t})}const ae=/[\\^$.*+?()[\]{}|]/;function oe(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)),!ae.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 se(e){if(e===void 0||e.trim()===``)return 301;let t=Number(e.trim());return ee.includes(t)?t:null}function ce(e,t){let n=e.split(/\r?\n/),r=n.some(e=>e.includes(` `))?` `:`,`,i=n.map((e,t)=>({line:t+1,cells:te(e,r)})).filter(e=>e.cells.some(e=>e!==``)),a=i[0];if(!a)return{format:`plain`,ready:[],skipped:[]};let s=re(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>=A){r(j.tooMany);continue}if([`inactive`,`disabled`].includes((l(t,`status`)??`active`).toLowerCase())){r(j.inactive);continue}let i=se(l(t,`code`));if(i===null){r(j.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=oe(a,s);if(!e){r(j.pattern);continue}a=e.source,s=e.target}else if(c===`start`&&a.trim().endsWith(`/`))a=`${a.trim()}[...rest]`;else if(c!==`exact`){r(j.pattern);continue}let m=ie(a);if(`reason`in m){r(m.reason);continue}let h=i===410||i===451,g=``;if(!h){if(s.trim()===``){r(j.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(j.external);continue}let i=ie(s);if(`reason`in i){r(i.reason===j.empty?j.noTarget:i.reason);continue}if(g=i.path,g===m.path){r(j.same);continue}}if(p.has(m.path)){r(j.repeated);continue}let _=f.get(m.path);if(_!==void 0){r(_===g?j.exists:j.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:j.loop}):h.push({source:e.source,destination:e.destination,type:e.type})}return{format:s.format,ready:h,skipped:d}}function le(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 M=`redirects`,N=`redirects-import`,ue={redirection:`Redirection`,yoast:`Yoast SEO Premium`,rankmath:`Rank Math`,plain:`a plain list`},de={schedule:`* * * * *`};async function fe(e){let t=await e.kv.get(M);return t&&Array.isArray(t.rows)?t:null}async function pe(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 me(e,t){let n=ce(t,await pe(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(M,r),r}function he(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 ge(e){try{await e.cron?.schedule(N,de)}catch{}}async function _e(e){try{await e.cron?.cancel(N)}catch{}}async function ve(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(M,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}=he(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 _e(e)):i||await ge(e),await o(),r}async function ye(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(M),r&&await _e(e),null):(i.stalls=u&&i.removed===(t.removed??0)?(t.stalls??0)+1:0,i.stalls>=3?be(e,i,r):(r||await ge(e),await e.kv.set(M,i),i))}async function be(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 _e(e),await e.kv.set(M,o),o}async function xe(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:le(t),count:t.length}}function P(e,t){return`${e} ${t}${e===1?``:`s`}`}function Se(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 Ce=[{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 we(e){return e===301||e===308?`Permanent`:e===302||e===307?`Temporary`:e===410?`Deleted on purpose`:String(e)}async function Te(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 Ee(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 De(e,t,n){let r=ce(le([{source:e,destination:t,type:Number(n)||301}]),[]);return r.ready[0]??r.skipped[0]?.reason??`Type the old address first.`}async function Oe(e,t,n,r){let i=De(t,n,r);if(typeof i==`string`)return`${i}.`;try{return await e.redirects?.create?.(i),null}catch(e){return Ee(e)}}async function ke(e,t,n,r,i){let a=De(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 Ee(e)}}async function Ae(e,t){try{let n=await e.redirects?.get(t);return n&&await e.redirects?.delete?.(t,{_rev:n._rev}),null}catch(e){return Ee(e)}}function je(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 ${ue[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 ${P(t,`redirect`)}`,style:`primary`,confirm:{title:`Add ${P(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`}]},...Se(e.skipped)]}if(e.status===`removing`){let t=e.removed??0;return[{type:`meter`,label:`${t} of ${e.added} removed`,value:t,max: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`?[{type:`meter`,label:`${e.added} of ${e.rows.length} added`,value:e.cursor,max: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:`${P(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`}}]:[]]},...Se(e.skipped)]}function Me(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:Ce.some(e=>e.value===n)?Ce:[...Ce,{value:n,label:`${we(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 Ne(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,Me(r,a),s,{type:`divider`}):i===`add`?o.push({type:`header`,text:`Add a redirect`},...c,Me(void 0,a),s,{type:`divider`}):i===`import`?(o.push({type:`header`,text:`Import from WordPress`},...je(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:we(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 Pe(e,t){let n={...t,status:`done`};try{await e.cron?.cancel(N)}catch{}return await e.kv.set(M,n),n}const Fe=new Set("iIl.,:;|!'`jtfr ()[]".split(``)),Ie=new Set(`mwMWQO@%`.split(``));function Le(e){let t=0;for(let n of e)Fe.has(n)?t+=5:Ie.has(n)?t+=15:n>=`A`&&n<=`Z`?t+=12:t+=9.5;return Math.round(t)}function Re(e){if(Le(e)<=580)return e;let t=e;for(;t.length>1&&Le(`${t}…`)>580;)t=t.slice(0,-1);return`${t.trimEnd()}…`}function ze(e){return e.toLowerCase().replace(/\s+/g,` `).trim()}const Be=new Set([`home`,`homepage`,`home page`,`index`,`untitled`,`welcome`]),Ve=new Set([`_type`,`_key`,`id`,`slug`,`url`,`href`,`src`,`style`,`icon`]);function He(e){let t={textLength:0,imagesWithoutAlt:0,hasImage:!1},n=(e,r,i)=>{if(i>12)return;if(typeof e==`string`){!Ve.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 F={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 Ue(e){return typeof e.excerpt==`string`?e.excerpt.trim():``}function We(e){let t=[],{seo:n}=e,r=He(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.`}),Be.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()&&Ue(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!==``&&Le(i)>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(F),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 Ge(e,t){return t?`hidden`:e.some(e=>e.level===`fix`)?`fix`:e.length>0?`look`:`ready`}const Ke=/^[0-9A-HJKMNP-TV-Z]{26}(\.[A-Za-z0-9]+)?$/i;function qe(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function Je(e){return typeof e==`number`&&Number.isFinite(e)&&e>=1?Math.round(e):void 0}function Ye(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 Xe(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=Ye(r);if(i)return[`file:${i}`];let a=[`url:${n.origin}${r}`],s=r.slice(1);return Ke.test(s)&&a.push(`file:${s}`),a}function Ze(e){if(typeof e!=`string`||e===``)return[];if(e.startsWith(`/`)){let t=e.includes(`?`)?void 0:Ye(e);return t?[`file:${t}`]:[]}return/^https?:\/\//i.test(e)?Xe(e,`http://localhost`).filter(e=>e.startsWith(`url:`)):[]}function Qe(e){return typeof e==`string`&&e!==``&&!e.includes(`/`)?[`file:${e}`]:[]}function $e(e){let t=Je(e.width),n=Je(e.height);if(t===void 0||n===void 0||typeof e.mimeType==`string`&&!e.mimeType.startsWith(`image/`))return;let r=[];if(e._type===`image`&&qe(e.asset))r.push(...Qe(e.asset._ref),...Ze(e.asset.url));else if(typeof e.id==`string`){if(e.provider===void 0||e.provider===`local`){let t=qe(e.meta)?e.meta:{};r.push(...Qe(t.storageKey),...Qe(e.id))}r.push(...Ze(e.src))}let i=[...new Set(r)];return i.length>0?{keys:i,width:t,height:n}:void 0}function et(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(!qe(e))return;let a=$e(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 tt(e,t,n){if(!e||e.length===0)return;let r=Xe(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 nt=16*1024;function I(e,t){return`${e}:${t}`}function L(e){return e.storage.entries}function rt(e){return typeof e==`object`&&!!e&&!Array.isArray(e)&&`@type`in e}function it(e){let t=e;if(typeof e==`string`){if(e.trim()===``||e.length>nt)return[];try{t=JSON.parse(e)}catch{return[]}}let n=(Array.isArray(t)?t:[t]).filter(rt);return JSON.stringify(n).length>nt?[]:n}function at(e){let{title:t,name:n}=e.data;return typeof t==`string`?t:typeof n==`string`?n:``}function R(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()||at(n),d=n.status===`published`,p=d&&e.hasPages,m=e.hasPages?We({slug:n.slug,data:n.data,seo:n.seo,title:at(n),siteUrl:o,isArticle:u.includes(c),authorCount:r.length,duplicateTitle:e.duplicateTitle}):[],h=m.some(e=>e.id===F.id),g=h&&i?.imageSeen===!0,_=g?m.filter(e=>e.id!==F.id):m,v=e.hasPages?et(n.data):[];return{v:1,publishedAt:n.publishedAt,updatedAt:n.updatedAt,authors:r,...i?.type?{type:i.type}:{},custom:it(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?Ge(_,n.seo?.noIndex===!0||s?.noindex===!0):`draft`:`skip`,titleKey:p?ze(l):``,issues:_,checkedAt:new Date().toISOString()}}function ot(e,t){return e.status===`hidden`||e.status===`draft`||e.status===`skip`?e.status:Ge(t,!1)}function st(e,t){let n=e.issues??[],r=n.some(e=>e.id===F.id);if(!(e.needsImage??r)||e.imageSeen===!0===t&&r!==t)return null;let i=n.filter(e=>e.id!==F.id),a=t?i:[...i,F],{imageSeen:o,...s}=e;return{...s,needsImage:!0,...t?{imageSeen:!0}:{},issues:a,status:ot(e,a)}}async function ct(e,t){return(await lt(e,[t]))[0]??[]}async function lt(e,t){let n=[...new Set(t.filter(e=>e!==``))];if(n.length===0)return t.map(()=>[]);let r=await L(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 z(e){return e.issues?.some(e=>e.id===`duplicate-title`)??!1}function ut(e,t){let n=e.length>1;for(let{id:r,data:i}of e)z(i)!==n&&t.set(r,B(i,n))}function B(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:ot(e,r)}}async function dt(e,t,n,r){return(await ht(e,t,n,r))?.record??null}function ft(e){return(e[0]?.bylines??[]).map(({byline:e})=>({name:e.displayName,...e.websiteUrl?{url:e.websiteUrl}:{}}))}async function pt(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?R({collection:t,entry:i,authors:ft(a),previous:null,config:r,siteUrl:e.site.url,duplicateTitle:!1,hasPages:!0}):null}async function mt(e,t,n,r){let i=await e.content?.get(t,n);if(!i)return null;let a=L(e),o=I(t,n),[s,c,l]=await Promise.all([e.bylines?.getEntriesBylines(t,[n])??[],a.get(o),e.schema?.getCollection(t)]),u=R({collection:t,entry:i,authors:ft(s),previous:c,config:r,siteUrl:e.site.url,duplicateTitle:!1,hasPages:l?l.routable&&!l.hidden:!0}),d=u.titleKey??``,f=c?.titleKey??``;return{entry:i,previous:c,record:B(u,d!==``&&d===f&&c!==null&&z(c)),newKey:d,oldKey:f}}async function ht(e,t,n,r){let i=await mt(e,t,n,r);if(!i)return null;let{entry:a,record:o,newKey:s,oldKey:c}=i,l=L(e),u=I(t,n);await l.put(u,o);let[d=[],f=[]]=await lt(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&&_t(e.updatedAt,o.updatedAt)&&(p.set(u,o),m=[...d.filter(e=>e.id!==u),{id:u,data:o}])}return ut(m,p),ut(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 gt(e,t,n,r,i){let a=await mt(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=I(t,n);return s===null||c===l?await L(e).put(d,u):i&&await L(e).put(d,i(s)),{record:u,entry:o}}function _t(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 vt(e,t,n){let r=L(e),i=I(t,n),a=await r.get(i);await r.delete(i);let o=new Map;ut((await ct(e,a?.titleKey??``)).filter(e=>e.id!==i),o),o.size>0&&await r.putMany([...o].map(([e,t])=>({id:e,data:t})))}const V=`scan`,yt=`stats`,H=`seo-scan`,bt=[`ready`,`look`,`fix`,`hidden`];async function xt(e){return e.kv.get(V)}async function St(e,t,n,r=6){let i=await Dt(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(H,{schedule:`* * * * *`})}catch{}return i}async function Ct(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=L(e),u=l.map(e=>e.id),[d,f]=await Promise.all([e.bylines?.getEntriesBylines(i,u)??[],r.getMany(u.map(e=>I(i,e)))]);a+=2;let p=new Map(d.map(e=>[e.entryId,e.bylines])),m=new Set,h=l.map(t=>{let r=I(i,t.id),a=f.get(r)??null;return a&&(_t(t.updatedAt,a.updatedAt)&&s(a)||(a.checkedAt??``)>=o)?(m.add(r),{id:r,data:a}):{id:r,data:R({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,duplicateTitle:a?z(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)&&z(t.data)!==n&&m.delete(t.id),t.data=B(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)Et(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 wt(e,t,n){let r=new Set(t.scope??[]),i=L(e),a=t.sweep?.cursor,o=new Set(t.sweep?.kept??[]),s=r.size>0&&e.content?await i.query({where:{status:{in:bt}},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:R({hasPages:!0,collection:r.collection??``,entry:a,authors:r.authors,previous:r,config:n,siteUrl:e.site.url,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 Tt(e,t){let n=L(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)=>{z(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=B(i,r);s.push({id:n,data:o}),Et(t.tally,i,-1),Et(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 U(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 Et(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,U(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(U(t)):e.more=!0:e.rows=e.rows.filter(e=>e.id!==t.entryId))}}async function Dt(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 wt(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 Ct(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 Tt(e,a);if(i&&a.status===`done`&&!a.sweep)try{await e.cron?.cancel(H)}catch{}return await e.kv.set(V,a),a}async function Ot(e){let t=L(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})=>U(e)),more:i.hasMore,look:a.items.map(({data:e})=>U(e,`look`)),hiddenTitles:o.items.map(({data:e})=>e.title||`(no title)`)}}const kt=new Map;async function At(e,t,{fresh:n=!1,now:r=Date.now()}={}){let i=e.site?.url??``,a=kt.get(e.plugin.id);a||kt.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 jt(e){kt.delete(e)}const W={overview:0,redirects:1,identity:2,types:2},G=[{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`}],Mt=[``,`$`,`$$`,`$$$`,`$$$$`],Nt=[{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 Pt(e){let t={},n=[];for(let r of e){let e=o(r)?.host??``,i=Nt.find(t=>t.hosts.includes(e));i&&!(i.id in t)?t[i.id]=r:n.push(r)}return{named:t,other:n}}const Ft={needed:`needed`,worth:`worth adding`,optional:`optional`};function K(e,t,n){let r=n.filter(e=>!!e);return r.length>0?`${e}: ${r.join(`, `)}`:`${e} (${Ft[t]}, not added yet)`}function It(e){return C.map(t=>{let n=D(t,e.openingHours).replace(`, `,` and `);return n===``?void 0:`${t.slice(0,3)} ${n}`}).filter(e=>e!==void 0)}function Lt(e){let{site:t}=e.config,n=q(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 q(e){return e.type===`Person`?`person`:e.type===`Organization`?`organization`:`local`}function Rt(e,t){let n=o(e);if(!(!n||n.protocol!==`https:`&&n.protocol!==`http:`))return`${n.origin}${t}`}function zt(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function J(e,t,n=``){let r=e[t];return typeof r==`string`?r.trim():n}function Bt(e,t,n){let r=e[t];return typeof r==`boolean`?r:n}function Vt(e){return e.split(/\r?\n/).map(e=>e.trim()).filter(e=>e!==``)}async function Ht(e,{rows:t=!0}={}){let[n,a,o]=await Promise.all([e.kv.list(),e.schema?.listCollections()??[],t?Te(e):{rows:[],more:!1}]),c=new Map(n.map(e=>[e.key,e.value])),l=c.get(M);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(V)??null,stats:c.get(yt)??null}}async function Ut(e,n){let r=await Ot(e);n.uncopied?await t(e,s,n.uncopied.value):JSON.stringify(r)!==JSON.stringify(n.stats)&&await e.kv.set(yt,r);let{uncopied:i,...a}=n;return{...a,stats:r}}function Wt(e){return e.stats??(e.scan?.status===`done`?e.scan.tally:null)}function Gt(e){let{scan:t}=e,n=Wt(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 Kt(e){let{scan:t}=e,n=Wt(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 qt(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 Jt(e){return e.collections.filter(t=>{let n=e.config.collections[t.slug];return n?.noindex===!0&&n.hiddenOk!==!0})}function Yt(e,t){let n=Jt(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 Xt(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:``},...G],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 Zt(e,t){let{site:n}=e.config,r=q(n),i=G.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=It(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 Qt(e,t){if(!e.config.steps.identity)return[];let n=Lt(e);return n.length===0?[{type:`context`,text:Zt(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 $t(e,t){return[...Gt(e),...Yt(e,!0),...Xt(e,t),...Qt(e,t),...Kt(e),...qt(e)]}function en(e){let t=Wt(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 tn(e,t){let{site:n}=e.config,r=q(n),i=r===`local`,a=e.config.steps.identity,s=Pt(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:``},...G],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:K(`Address and phone`,i?`needed`:`optional`,[n.address?.streetAddress,n.address?.addressLocality,n.telephone,n.email]),default_open:i&&Lt(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:Mt.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:
|
|
4
|
-
`)}],submit:{label:`Save profiles`,action_id:`save_profiles`}}]},p={type:`accordion`,label:
|
|
5
|
-
`)))}},state:{tab:
|
|
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 dn(e,t){let n={tab:W.identity},{site:r}=e.config,i=J(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:J(t,`alternate_name`,r.alternateName??``)||void 0,legalName:J(t,`legal_name`,r.legalName??``)||void 0,foundingDate:i||void 0}},state:n}}function fn(e,t){let{robots:n}=e.config,r=J(t,`snippet`,n.nosnippet?`none`:`full`),i=J(t,`image_preview`,n.maxImagePreview);return{config:{...e.config,breadcrumbs:Bt(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:W.types}}}function X(e,t,n){let r={tab:W.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`?O.find(e=>e.value===a)?.value:o.type??nn(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 pn(e,t){if(t.scan?.status===`done`)return Ut(e,t);let n=t.scan?await Dt(e,t.scan,t.config,6):await St(e,t.collections.map(e=>e.slug),t.config,6);return{...t,scan:n,stats:n.status===`done`?n.tally:null}}async function mn(e,t,n){let r=(t??0)>=50,i=zt(e)?e:{},o=typeof i.action_id==`string`?i.action_id:``,c=await Ht(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 en(await pn(n,c));if(!r||i.type===`page_load`||o===``)return Y(await pn(n,c),n,r,{tab:W.overview});let l=async(e,t=`Saved. Google will pick this up next time it visits.`)=>{if(`error`in e)return Y(c,n,r,{...e.state,toast:{type:`error`,message:e.error}});let i=S(e.config);return await a(n,s,i),jt(n.plugin.id),Y({...c,config:i},n,r,{...e.state,toast:{type:`success`,message:t}})},u=async e=>{try{let{rows:t,more:r}=await Te(n);return{...e,redirectRows:t,moreRedirects:r}}catch{return e}},d=async(e,t,i)=>e?Y(c,n,r,{tab:W.redirects,editing:i,...i?{}:{panel:`add`},problem:e}):Y(await u(c),n,r,{tab:W.redirects,toast:{type:`success`,message:t}});if(i.type===`block_action`){if(o===`check_pages`||o===`check_more`){let e=await pn(n,o===`check_pages`?{...c,scan:null}:c);return Y(e,n,r,{tab:W.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(X(c,i.value,`type`));if(o===`set_visible`){let[e,t]=typeof i.value==`string`?i.value.split(`|`):[];return t===`hide`?Y(c,n,r,{tab:W.types,pendingHide:e}):l(X(c,i.value,`visible`),`These pages can appear on Google again.`)}if(o===`confirm_hide`&&typeof i.value==`string`)return l(X(c,`${i.value}|hide`,`visible`),`These pages are now hidden from Google and other search engines.`);if(o===`cancel_hide`)return Y(c,n,r,{tab:W.types});if(o===`hidden_ok`){let e={...c.config.collections};for(let t of Jt(c))e[t.slug]={...e[t.slug],hiddenOk:!0};return l({config:{...c.config,collections:e},state:{tab:W.overview}},`Noted. These pages stay hidden from search.`)}if(o===`edit_redirect`){let e=c.redirectRows.find(e=>e.id===i.value);return Y(c,n,r,{tab:W.redirects,editing:e})}if(o===`open_add`)return Y(c,n,r,{tab:W.redirects,panel:`add`});if(o===`open_import`)return Y(c,n,r,{tab:W.redirects,panel:`import`});if(o===`close_panel`)return Y(c,n,r,{tab:W.redirects});if(o===`remove_redirect`&&typeof i.value==`string`)return d(await Ae(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 Y(await u(c),n,r,{tab:W.redirects});let t=await ve(n,e,6);return Y(await u({...c,redirects:t}),n,r,{tab:W.redirects,toast:t.status===`done`?{type:`success`,message:`${P(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 be(n,c.redirects);return Y(await u({...c,redirects:t}),n,r,{tab:W.redirects,toast:{type:`info`,message:`Stopped. ${P(e,`redirect`)} removed.`}})}if(o===`stop_redirects`&&c.redirects?.status===`adding`){let e=await Pe(n,c.redirects);return Y({...c,redirects:e},n,r,{tab:W.redirects,toast:{type:`info`,message:`Stopped. ${P(e.added,`redirect`)} added.`}})}if(o===`reset_redirects`){let e=c.redirects?.status===`adding`||c.redirects?.status===`removing`;return e||await n.kv.delete(M),Y({...c,redirects:e?c.redirects:null},n,r,{tab:W.redirects})}if(o===`remove_redirects`&&(c.redirects?.status===`done`||c.redirects?.status===`removing`)){let e=await ye(n,c.redirects,6);return Y(await u({...c,redirects:e}),n,r,{tab:W.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 Y(await u(c),n,r,{tab:W.redirects});if(o===`export_redirects`){let{text:e,count:t}=await xe(n);return Y(c,n,r,{tab:W.redirects,panel:`export`,exported:e,toast:{type:`info`,message:`${P(t,`redirect`)} listed below.`}})}return Y(c,n,r,{tab:W.overview})}if(i.type!==`form_submit`)return Y(c,n,r,{tab:W.overview});let f=zt(i.values)?i.values:{};if(o===`check_redirects`){let e=J(f,`list`);if(e===``)return Y(c,n,r,{tab:W.redirects,toast:{type:`error`,message:`Paste a list first.`}});let t=await me(n,e);return Y({...c,redirects:t},n,r,{tab:W.redirects,toast:{type:`success`,message:`Checked. Nothing has been added yet.`}})}if(o===`add_redirect`){let e=J(f,`source`),t=J(f,`code`,`301`)===`410`?``:J(f,`destination`);return d(await Oe(n,e,t,J(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 ke(n,e,J(f,`source`,t?.source??``),J(f,`destination`,t?.destination??``),J(f,`code`,String(t?.type??301))),`Redirect changed.`,t)}if(o===`setup_identity`){let e=on(c,f);if(`error`in e)return l(e);let t=J(f,`street`),n=J(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:W.overview}})}return o===`save_identity`?l(on(c,f)):o===`save_logo`?l(sn(c,f,n.site.url)):o===`save_contact`?l(cn(c,f)):o===`save_hours`?l(ln(c,f)):o===`save_profiles`?l(un(c,f)):o===`save_company`?l(dn(c,f)):o===`save_advanced`?l(fn(c,f)):Y(c,n,r,{tab:W.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 hn(e,t){return o(e,t)?.href??e}function gn(e,t){return(e.siteUrl||t||(o(e.url)?.origin??``)).replace(/\/+$/,``)}function _n(e,t){if(e.canonical)return hn(e.canonical,`${t}/`);let n=o(e.url,`${t}/`);return n?(n.search=``,n.hash=``,n.href):e.url}function vn(e){return e.path===`/`||e.path===``}function yn(e){return u.includes(e)}function bn(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 xn(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 Sn(e){let t=e;try{t=decodeURIComponent(e)}catch{}let n=t.replace(/[-_]+/g,` `).trim();return n.charAt(0).toUpperCase()+n.slice(1)}function Cn(e,t,n){let{page:r,config:i,site:a}=e;if(r.breadcrumbs)return r.breadcrumbs.map(e=>({name:e.name,url:hn(e.url,`${t}/`)}));if(!i.breadcrumbs||vn(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:Sn(e),url:`${t}${l}${s}`})}),c}function wn(e,t){return e.length<=t?e:`${e.slice(0,t-1).trimEnd()}…`}function Tn(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 En(e){let{page:t,config:n,record:r,site:i}=e,a=gn(t,i.url),o=_n(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=bn(t,n,r),g=yn(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?hn(t.image,`${a}/`):void 0,w=C??``,T=Cn(e,a,l),E=[xn(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:vn(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:wn(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,...tt(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=Tn(E,r?.custom??[],O);return k!==void 0&&!g&&D.mainEntity===void 0&&(D.mainEntity=Z(k)),E}const Dn=`default`;function On(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function kn(e){let t=o(e);if(!t)return e;let n=t.pathname.split(`/`).filter(e=>e!==``);return[t.host,...n].join(` › `)}function An(e,t){return e.length<=t?e:`${e.slice(0,t-1).trimEnd()}…`}async function jn(e,t,n,r){if(!t)return{blocks:[]};let i=(n??0)>=50,a=On(e)?e:{},s=await c(r),l=i&&a.type===`block_action`&&a.action_id===`set_type`,u=typeof a.value==`string`?a.value:Dn,d=O.find(e=>e.value===u)?.value,[p,m]=await Promise.all([gt(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===``&&Ue(g.data)!==``,T=C||Ue(g.data),E=s.collections[t.collection]?.type??f(t.collection),D=h.type??E,ee=h.status===`hidden`,A=h.issues??[],j=[{type:`context`,text:v?`${v} · ${kn(x)}`:kn(x)},{type:`header`,text:Re(S)},{type:`section`,text:T===``?`No description yet. Google will pick some text from the page.`:An(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. Google may rewrite them, and your site may add its name to the title.`},{type:`fields`,fields:[{label:`Shown to Google as`,value:k(D)},{label:`In search results`,value:h.status===`draft`?`Not published yet`:ee?`Hidden from search`:`Yes`}]}];if(A.length===0)j.push({type:`context`,text:`Nothing to fix on this page.`});else{let e=[...A].sort((e,t)=>Number(t.level===`fix`)-Number(e.level===`fix`));for(let t of e.slice(0,3))j.push({type:`section`,text:`${t.level===`fix`?`To fix`:`Worth a look`}: ${t.message}`},{type:`context`,text:t.fix});e.length>3&&j.push({type:`context`,text:`And ${e.length-3} more.`})}if(i){let e=En({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:ee,site:r.site});j.push({type:`divider`},{type:`actions`,elements:[{type:`menu`,action_id:`set_type`,label:`Change page type`,items:[{label:`Same as other pages here (${k(E)})`,value:Dn},...O.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:j,..._?{toast:_}:{}}}const Mn=/\s*,\s*/;function Nn(e,t,n){let r=(e??``).toLowerCase().split(Mn).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 Pn(e){return e.startsWith(`noindex`)}function Fn(e){return/[?&]_preview=/.test(e)}async function $(e,t,n){let r=n?.id;if(typeof r==`string`)try{await dt(e,t,r,await c(e))}catch(n){e.log.warn(`could not refresh entry record`,{collection:t,id:r,error:String(n)})}}const In={hooks:{"page:metadata":async(e,t)=>{let{page:n}=e,r=n.content,[i,a]=await Promise.all([At(t,()=>c(t),{fresh:Fn(n.url)}),(async()=>{if(!r)return null;try{return{stored:await L(t).get(I(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=L(t);o=r??await pt(t,e,s,i);let l=!!(n.seo?.ogImage||n.image);if(r&&st(r,l)){let t=await c.get(I(e,s)),n=t?st(t,l):null;n&&await c.put(I(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=Nn(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":En({page:n,config:i,record:o,noindex:Pn(l),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(V))return;let a=S(await r(t,i,s,{copy:!1})),o=await St(t,n.filter(e=>e.routable&&!e.hidden).map(e=>e.slug),a,7);o.status===`done`&&o.checked===0&&await t.kv.delete(V)}catch(e){t.log.warn(`could not start the first page check`,{error:String(e)})}},cron:async(e,t)=>{if(e.name===H){let e=await xt(t);e?.status===`running`||e?.sweep?await Dt(t,e,await c(t)):await t.cron?.cancel(H);return}if(e.name!==N)return;let n=await fe(t);n?.status===`adding`?await ve(t,n):n?.status===`removing`?await ye(t,n):await t.cron?.cancel(N)},"content:afterDelete":async(e,t)=>{await vt(t,e.collection,e.id)}},routes:{admin:{permission:`plugins:read`,handler:async(e,t)=>mn(e.input,e.user?.role,t)},"editor/preview":{permission:`content:read`,handler:async(e,t)=>jn(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{In 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}}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};
|
package/emdash-plugin.jsonc
CHANGED
|
@@ -18,23 +18,28 @@
|
|
|
18
18
|
"description": { "file": "listing/description.md" },
|
|
19
19
|
"installation": { "file": "listing/installation.md" },
|
|
20
20
|
"faq": { "file": "listing/faq.md" },
|
|
21
|
+
"security": { "file": "listing/security.md" },
|
|
21
22
|
},
|
|
22
23
|
// Registry listing only: the EmDash version a site needs, and the gallery.
|
|
23
24
|
"release": {
|
|
24
25
|
"requires": { "env:emdash": ">=1.2.0" },
|
|
25
26
|
"artifacts": {
|
|
27
|
+
// This plugin's own glyph on the Moorage land tile, from icon.svg (scripts/render-banners.mjs in apps/demo-site).
|
|
28
|
+
"icon": { "file": "./images/icon.png" },
|
|
29
|
+
// Cover at the top of the listing and its share image: 3:1, from banner.svg (scripts/render-banners.mjs in apps/demo-site).
|
|
30
|
+
"banner": { "file": "./images/banner.png" },
|
|
26
31
|
"screenshots": [
|
|
27
|
-
{ "file": "./images/overview.png", "lang": "en" },
|
|
28
|
-
{ "file": "./images/google-preview.png", "lang": "en" },
|
|
29
|
-
{ "file": "./images/redirects.png", "lang": "en" },
|
|
30
|
-
{ "file": "./images/settings.png", "lang": "en" },
|
|
32
|
+
{ "file": "./images/overview-0-1-11.png", "lang": "en" },
|
|
33
|
+
{ "file": "./images/google-preview-0-1-11.png", "lang": "en" },
|
|
34
|
+
{ "file": "./images/redirects-0-1-11.png", "lang": "en" },
|
|
35
|
+
{ "file": "./images/settings-0-1-11.png", "lang": "en" },
|
|
31
36
|
],
|
|
32
37
|
},
|
|
33
38
|
},
|
|
34
39
|
"publisher": "did:plc:cus3srdqglppyowqrw3hvjfj",
|
|
35
40
|
|
|
36
41
|
"license": "MIT",
|
|
37
|
-
"author": { "name": "Moorage" },
|
|
42
|
+
"author": { "name": "Moorage", "url": "https://moorage.dev" },
|
|
38
43
|
"security": { "email": "security@moorage.dev" },
|
|
39
44
|
|
|
40
45
|
// Trust contract — what runtime APIs the plugin asks for.
|
package/listing/installation.md
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
## Reporting a problem
|
|
2
|
+
|
|
3
|
+
Email **security@moorage.dev**, not a public issue. Say which version you use, what you found and how to reproduce it. Fixes go into the latest release; update to it to receive them. The source is public on GitHub.
|
|
4
|
+
|
|
5
|
+
## What it keeps and sends
|
|
6
|
+
|
|
7
|
+
It keeps one small record per entry in the site's own database: dates, authors, the page type and any extra structured data you add. It sends nothing to any outside service and loads no outside scripts. Redirects change only when you add, edit or remove one, or confirm an import.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moorage/plugin-seo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
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",
|
|
@@ -51,11 +51,12 @@
|
|
|
51
51
|
"emdash": ">=1.2.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@emdash-cms/blocks": "1.
|
|
55
|
-
"@emdash-cms/
|
|
56
|
-
"@emdash-cms/plugin-
|
|
54
|
+
"@emdash-cms/blocks": "1.4.0",
|
|
55
|
+
"@emdash-cms/cloudflare": "1.4.0",
|
|
56
|
+
"@emdash-cms/plugin-cli": "0.13.5",
|
|
57
|
+
"@emdash-cms/plugin-test": "0.2.11",
|
|
57
58
|
"@moorage/typescript-config": "0.0.0",
|
|
58
|
-
"emdash": "1.
|
|
59
|
+
"emdash": "1.4.0",
|
|
59
60
|
"typescript": "~6.0.3",
|
|
60
61
|
"vitest": "4.1.10"
|
|
61
62
|
},
|