@moorage/plugin-seo 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @moorage/plugin-seo
2
2
 
3
+ ## 0.1.6
4
+
5
+ ### Patch Changes
6
+
7
+ - ba5f9ed: The Redirects tab now shows imported redirects as soon as they are added, and drops them as soon as you remove the batch, without reloading the page. The README and listing now say the importer reads CSV exports only.
8
+
3
9
  ## 0.1.5
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -41,7 +41,7 @@ It asks for four permissions: read content, read bylines, read the content schem
41
41
  One page, **SEO**, in the admin sidebar, with three tabs. It works before you touch it: the site is described as an organisation named after the site, each content type is guessed from its name (posts and blog become blog posts, services become services, team becomes people), and your pages are checked by themselves.
42
42
 
43
43
  - **Overview**: how many pages were checked, how many have nothing to fix and how many do; a warning whenever a content type is hidden from Google; one question if you have not said who the site is for; then **Your details**, the few things Google still needs for your kind of site (an address, opening hours and a phone number for a local business; a logo and profiles for a company); then the pages to fix, each with what is missing and a link to the page; and, folded, pages that are fine but could be better.
44
- - **Redirects**: your redirects, with Change and Remove on each, and three buttons above the list. **Add a redirect**: the old address, where to send visitors, and the kind (permanent unless you choose otherwise). **Import from WordPress**: paste a list exported from Redirection, Yoast SEO Premium or Rank Math; it is checked before anything is added and can be removed again as a batch. **Export**: a copyable list. EmDash already redirects a page's old address when you change it.
44
+ - **Redirects**: your redirects, with Change and Remove on each, and three buttons above the list. **Add a redirect**: the old address, where to send visitors, and the kind (permanent unless you choose otherwise). **Import from WordPress**: paste a CSV export from Redirection, Yoast SEO Premium or Rank Math (CSV only: Rank Math's CSV export is in its paid version, and Redirection's JSON, .htaccess and Nginx exports are not read); it is checked before anything is added and can be removed again as a batch. **Export**: a copyable list. EmDash already redirects a page's old address when you change it.
45
45
  - **Settings**: who the site is for (a local business, a company or a person) and what you do, then folded sections ordered by how much they matter for that kind of site. Each title says whether the section is needed, worth adding or optional, and what is already filled in: address and phone, opening hours (two ranges a day for split shifts), logo, profiles elsewhere, more about you, and content types (what each kind of content is, and whether it is hidden from Google, which asks before it saves). One location for now. A local business is described as an organisation until it has an address, because Google requires one.
46
46
 
47
47
  Admins can change settings. Editors see a read-only page with the pages to fix.
@@ -63,7 +63,7 @@ Not yet: Product and Event types (they need price and date fields on the collect
63
63
  - Does it check my page while I type? No. EmDash does not let a general-purpose plugin read unsaved text. The panel and the checks describe the published page, as Google sees it: changes to the page show up once you publish, and changes in the SEO panel show up as soon as they are saved.
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
- - How do I import redirects from WordPress? Export them from Redirection, Yoast SEO Premium or Rank Math, open SEO > Redirects > Import from WordPress, paste the list and press Check list. Nothing is added until you confirm.
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
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.
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. You can leave the page.
69
69
 
package/dist/index.mjs CHANGED
@@ -7,7 +7,7 @@
7
7
  /** @type {import("emdash").PluginDescriptor} */
8
8
  const descriptor = Object.freeze({
9
9
  "id": "seo",
10
- "version": "0.1.5",
10
+ "version": "0.1.6",
11
11
  "format": "standard",
12
12
  "entrypoint": "@moorage/plugin-seo/sandbox",
13
13
  "capabilities": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "seo",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "declaredAccess": {
5
5
  "content": {
6
6
  "read": {}
package/dist/plugin.mjs CHANGED
@@ -3,4 +3,4 @@ function e(e,t){try{return new URL(e,t)}catch{return null}}const t=`config`,n=[`
3
3
  `)}],submit:{label:`Save address and phone`,action_id:`save_contact`}}]},u=a?{type:`accordion`,label:K(`Opening hours`,`needed`,ft(r)),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:g.map(e=>({type:`text_input`,action_id:`hours_${e.toLowerCase()}`,label:e,placeholder:`Closed`,initial_value:b(e,r.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:K(`Logo`,i===`organization`?`needed`:`worth`,[r.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:r.logo??``}],submit:{label:`Save logo`,action_id:`save_logo`}}]},f={type:`accordion`,label:K(`Your profiles elsewhere`,a?`worth`:`needed`,r.sameAs.map(t=>e(t)?.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:[...lt.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
4
  `)}],submit:{label:`Save profiles`,action_id:`save_profiles`}}]},p={type:`accordion`,label:K(`More about you`,`optional`,[r.alternateName,r.legalName,r.foundingDate?`founded ${r.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:r.alternateName??``},{type:`text_input`,action_id:`legal_name`,label:`Registered legal name`,initial_value:r.legalName??``},{type:`text_input`,action_id:`founded`,label:`Founded (year, or YYYY-MM-DD)`,placeholder:`2012`,initial_value:r.foundingDate??``}],submit:{label:`Save`,action_id:`save_company`}}]},m=a?[l,u,d,f,p]:i===`organization`?[d,f,l,p,u]:[f,d,l,p,u];return c.push(...m),c}function Mt(e,t){return e.config.collections[t]?.type??a(t)}function Nt(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: ${S(Mt(e,t.slug))}`,items:x.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 ${S(Mt(e,t.slug))}${r?`, hidden`:``}`}).join(`; `)}`,default_open:r!==void 0,blocks:i}]}function Y(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:kt(e,t.site.name)},{label:`Redirects`,blocks:Se({state:e.redirects,rows:e.redirectRows,more:e.moreRedirects,editing:r.editing,panel:r.panel,problem:r.problem,exported:r.exported,managerUrl:mt(t.site.url,`/_emdash/admin/redirects`)})},{label:`Settings`,blocks:[...jt(e,t.site.name),...Nt(e,r.pendingHide)]}]}],...r.toast?{toast:r.toast}:{}}:{blocks:[...i,...xt(e),...Tt(e,!1),...St(e),{type:`context`,text:`Business details, opening hours and redirects are set by your site admin. Ask them if something there is wrong.`}]}}function Pt(t,n){if(t===``)return{};let r=e(t,`${n.replace(/\/+$/,``)}/`);return!r||r.protocol!==`https:`&&r.protocol!==`http:`?{error:`Couldn't save. The logo link must be a web address starting with https://`}:r.pathname.includes(`/_emdash/api/media/asset/`)?{error:`That link only works when signed in. In Media, use Copy URL on the image instead.`}:{logo:r.href}}function Ft(e,t){let n={tab:W.identity},{site:r}=e.config,i=J(t,`kind`,e.config.steps.identity?q(r):``);if(i!==`local`&&i!==`organization`&&i!==`person`)return{error:`Choose who this site is for.`,state:n};let a=J(t,`business`,q(r)===`local`?r.type:``),o=G.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=J(t,`name`,r.name??``);return{config:{...e.config,site:{...r,type:s,name:c===``?void 0:c,description:J(t,`description`,r.description??``)||void 0,openingHours:i===`local`?r.openingHours:[]},steps:{...e.config.steps,identity:!0}},state:n}}function It(e,t,n){let r={tab:W.identity},i=Pt(J(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 Lt(e,t){let{site:n}=e.config,r={streetAddress:J(t,`street`)||void 0,addressLocality:J(t,`town`)||void 0,addressRegion:J(t,`region`)||void 0,postalCode:J(t,`postcode`)||void 0,addressCountry:J(t,`country`).toUpperCase()||void 0},i=J(t,`price_range`);return{config:{...e.config,site:{...n,email:J(t,`email`)||void 0,telephone:J(t,`telephone`)||void 0,address:Object.values(r).some(e=>e!==void 0)?r:void 0,priceRange:ct.includes(i)&&i!==``?i:void 0,areaServed:_t(J(t,`areas`,n.areaServed.join(`
5
5
  `)))}},state:{tab:W.identity}}}function Rt(e,t){let n={tab:W.identity},r=[];for(let i of g){let a=y(i,J(t,`hours_${i.toLowerCase()}`,b(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 zt(t,n){let r={tab:W.identity},i=ut(t.config.site.sameAs),a=[...lt.map(e=>J(n,`profile_${e.id}`,i.named[e.id]??``)),..._t(J(n,`profiles`,i.other.join(`
6
- `)))].filter(e=>e!==``),o=[];for(let t of a){let n=e(/^https?:\/\//i.test(t)?t:`https://${t}`);if(!n||!n.host.includes(`.`))return{error:`"${t}" is not a web address. Paste the full link to the profile.`,state:r};o.push(n.href)}return{config:{...t.config,site:{...t.config.site,sameAs:o}},state:r}}function Bt(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 Vt(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:gt(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 Ht(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`?x.find(e=>e.value===a)?.value:o.type??Mt(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 Ut(e,t){if(t.scan?.status===`done`)return yt(e,t);let n=t.scan?await ot(e,t.scan,t.config):await nt(e,t.collections.map(e=>e.slug),t.config);return{...t,scan:n,stats:n.status===`done`?n.tally:null}}async function Wt(e,n,r){let i=await vt(r),a=(n??0)>=50,o=ht(e)?e:{},s=typeof o.action_id==`string`?o.action_id:``;if(o.type===`page_load`&&o.page===`widget:seo`)return At(await Ut(r,i));if(!a||o.type===`page_load`||s===``)return Y(await Ut(r,i),r,a,{tab:W.overview});let c=async(e,n=`Saved. Google will pick this up next time it visits.`)=>{if(`error`in e)return Y(i,r,a,{...e.state,toast:{type:`error`,message:e.error}});let o=h(e.config);return await r.kv.set(t,o),Y({...i,config:o},r,a,{...e.state,toast:{type:`success`,message:n}})},l=async(e,t,n)=>{if(e)return Y(i,r,a,{tab:W.redirects,editing:n,...n?{}:{panel:`add`},problem:e});let{rows:o,more:s}=await me(r);return Y({...i,redirectRows:o,moreRedirects:s},r,a,{tab:W.redirects,toast:{type:`success`,message:t}})};if(o.type===`block_action`){if(s===`check_pages`||s===`check_more`){let e=await Ut(r,s===`check_pages`?{...i,scan:null}:i);return Y(e,r,a,{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(s===`set_type`)return c(Ht(i,o.value,`type`));if(s===`set_visible`){let[e,t]=typeof o.value==`string`?o.value.split(`|`):[];return t===`hide`?Y(i,r,a,{tab:W.types,pendingHide:e}):c(Ht(i,o.value,`visible`),`These pages can appear on Google again.`)}if(s===`confirm_hide`&&typeof o.value==`string`)return c(Ht(i,`${o.value}|hide`,`visible`),`These pages are now hidden from Google and other search engines.`);if(s===`cancel_hide`)return Y(i,r,a,{tab:W.types});if(s===`hidden_ok`){let e={...i.config.collections};for(let t of wt(i))e[t.slug]={...e[t.slug],hiddenOk:!0};return c({config:{...i.config,collections:e},state:{tab:W.overview}},`Noted. These pages stay hidden from search.`)}if(s===`edit_redirect`){let e=i.redirectRows.find(e=>e.id===o.value);return Y(i,r,a,{tab:W.redirects,editing:e})}if(s===`open_add`)return Y(i,r,a,{tab:W.redirects,panel:`add`});if(s===`open_import`)return Y(i,r,a,{tab:W.redirects,panel:`import`});if(s===`close_panel`)return Y(i,r,a,{tab:W.redirects});if(s===`remove_redirect`&&typeof o.value==`string`)return l(await ye(r,o.value),`Redirect removed.`);if(s===`add_redirects`||s===`more_redirects`){if(!i.redirects||i.redirects.status===`done`)return Y(i,r,a,{tab:W.redirects});let e=await ce(r,i.redirects);return Y({...i,redirects:e},r,a,{tab:W.redirects,toast:e.status===`done`?{type:`success`,message:`${P(e.added,`redirect`)} added.`}:{type:`info`,message:`${e.added} added so far. The rest follow in the background.`}})}if(s===`stop_redirects`&&i.redirects?.status===`adding`){let e=await Ce(r,i.redirects);return Y({...i,redirects:e},r,a,{tab:W.redirects,toast:{type:`info`,message:`Stopped. ${P(e.added,`redirect`)} added.`}})}if(s===`reset_redirects`)return i.redirects?.status!==`adding`&&await r.kv.delete(M),Y({...i,redirects:i.redirects?.status===`adding`?i.redirects:null},r,a,{tab:W.redirects});if(s===`remove_redirects`&&i.redirects){let e=await le(r,i.redirects);return Y({...i,redirects:e===0?null:i.redirects},r,a,{tab:W.redirects,toast:e===0?{type:`success`,message:`Removed. Your other redirects are untouched.`}:{type:`info`,message:`${e} still to remove. Press Remove again.`}})}if(s===`export_redirects`){let{text:e,count:t}=await ue(r);return Y(i,r,a,{tab:W.redirects,panel:`export`,exported:e,toast:{type:`info`,message:`${P(t,`redirect`)} listed below.`}})}return Y(i,r,a,{tab:W.overview})}if(o.type!==`form_submit`)return Y(i,r,a,{tab:W.overview});let u=ht(o.values)?o.values:{};if(s===`check_redirects`){let e=J(u,`list`);if(e===``)return Y(i,r,a,{tab:W.redirects,toast:{type:`error`,message:`Paste a list first.`}});let t=await oe(r,e);return Y({...i,redirects:t},r,a,{tab:W.redirects,toast:{type:`success`,message:`Checked. Nothing has been added yet.`}})}if(s===`add_redirect`){let e=J(u,`source`),t=J(u,`code`,`301`)===`410`?``:J(u,`destination`);return l(await _e(r,e,t,J(u,`code`,`301`)),t===``?`Done. ${e} now tells visitors the page was removed.`:`Redirect added. ${e} now goes to ${t}.`)}if(s.startsWith(`save_redirect:`)){let e=s.slice(14),t=i.redirectRows.find(t=>t.id===e);return l(await ve(r,e,J(u,`source`,t?.source??``),J(u,`destination`,t?.destination??``),J(u,`code`,String(t?.type??301))),`Redirect changed.`,t)}if(s===`setup_identity`){let e=Ft(i,u);if(`error`in e)return c(e);let t=J(u,`street`),n=J(u,`town`),{site:r}=e.config,a=t!==``||n!==``?{...r.address,...t?{streetAddress:t}:{},...n?{addressLocality:n}:{}}:r.address;return c({config:{...e.config,site:{...r,address:a}},state:{tab:W.overview}})}return s===`save_identity`?c(Ft(i,u)):s===`save_logo`?c(It(i,u,r.site.url)):s===`save_contact`?c(Lt(i,u)):s===`save_hours`?c(Rt(i,u)):s===`save_profiles`?c(zt(i,u)):s===`save_company`?c(Bt(i,u)):s===`save_advanced`?c(Vt(i,u)):Y(i,r,a,{tab:W.overview})}function X(e){return{"@id":e}}function Z(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 Gt(t,n){return e(t,n)?.href??t}function Kt(t,n){return(t.siteUrl||n||(e(t.url)?.origin??``)).replace(/\/+$/,``)}function qt(t,n){if(t.canonical)return Gt(t.canonical,`${n}/`);let r=e(t.url,`${n}/`);return r?(r.search=``,r.hash=``,r.href):t.url}function Jt(e){return e.path===`/`||e.path===``}function Yt(e){return r.includes(e)}function Xt(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?a(e.content.collection):`WebPage`;return i===`WebPage`?e.pageType===`article`?`BlogPosting`:`WebPage`:i}function Zt(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 Z({"@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:X(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 Qt(e){let t=e;try{t=decodeURIComponent(e)}catch{}let n=t.replace(/[-_]+/g,` `).trim();return n.charAt(0).toUpperCase()+n.slice(1)}function $t(e,t,n){let{page:r,config:i,site:a}=e;if(r.breadcrumbs)return r.breadcrumbs.map(e=>({name:e.name,url:Gt(e.url,`${t}/`)}));if(!i.breadcrumbs||Jt(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:Qt(e),url:`${t}${l}${s}`})}),c}function en(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 nn(e){let{page:t,config:n,record:r,site:i}=e,a=Kt(t,i.url),o=qt(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=Xt(t,n,r),g=Yt(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?Gt(t.image,`${a}/`):void 0,w=C??``,T=$t(e,a,l),E=[Zt(n.site,c,a,d),Z({"@type":`WebSite`,"@id":f,url:`${a}/`,name:c,alternateName:n.site.alternateName,description:n.site.description,publisher:X(d),inLanguage:s})];if(e.noindex)return E;let D=Z({"@type":g||_?`WebPage`:h,"@id":p,url:o,name:l,description:t.description,isPartOf:X(f),about:Jt(t)?X(d):void 0,primaryImageOfPage:C?X(w):void 0,image:C?X(w):void 0,breadcrumb:T.length>0?X(m):void 0,mainEntity:_?X(y):v?X(b):void 0,datePublished:x,dateModified:S,inLanguage:s});if(E.push(D),g){let e=(r?.authors??[]).map(e=>Z({"@type":`Person`,name:e.name,url:e.url})),n=t.articleMeta?.author?[{"@type":`Person`,name:t.articleMeta.author}]:[X(d)];E.push(Z({"@type":h,"@id":`${o}#article`,isPartOf:X(p),mainEntityOfPage:X(p),headline:en(l,110),description:t.description,image:C?X(w):void 0,datePublished:x,dateModified:S,author:e.length>0?e:n,publisher:X(d),inLanguage:s}))}_&&E.push(Z({"@type":`Service`,"@id":y,name:l,description:t.description,url:o,image:C?X(w):void 0,provider:X(d),areaServed:n.site.areaServed})),v&&E.push(Z({"@type":`Person`,"@id":b,name:l,description:t.description,url:o,image:C?X(w):void 0,worksFor:u?void 0:X(d)})),C&&E.push({"@type":`ImageObject`,"@id":w,url:C,contentUrl:C,...He(r?.images,C,a)}),T.length>0&&E.push({"@type":`BreadcrumbList`,"@id":m,itemListElement:T.map((e,t)=>Z({"@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=X(k)),E}const rn=`default`;function an(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function on(t){let n=e(t);if(!n)return t;let r=n.pathname.split(`/`).filter(e=>e!==``);return[n.host,...r].join(` › `)}function sn(e,t){return e.length<=t?e:`${e.slice(0,t-1).trimEnd()}…`}async function cn(n,r,i,o){if(!r)return{blocks:[]};let s=(i??0)>=50,c=an(n)?n:{},l=h(await o.kv.get(t)),u=z(o),d=R(r.collection,r.id),f,p=s&&c.type===`block_action`&&c.action_id===`set_type`,[m,g]=await Promise.all([Qe(o,r.collection,r.id,l),o.content?.getPublicUrl?.(r.collection,r.id)]),_=m?.record,v=m?.entry;if(_&&p){let e=typeof c.value==`string`?c.value:rn,t=x.find(t=>t.value===e)?.value,{type:n,...r}=_;_=t?{...r,type:t}:r,await u.put(d,_),f={type:`success`,message:`Saved. Google will pick this up next time it visits.`}}if(!_||!v)return{blocks:[{type:`context`,text:`Save this page to see how it may look on Google.`}]};if(_.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 y=l.site.name??o.site.name,b=o.site.url.replace(/\/+$/,``),C=[`home`,`index`,``].includes(v.slug??``)?`${b}/`:`${b}/${v.slug}`,w=[`home`,`index`].includes(v.slug??``)?`${b}/`:g??C,T=_.title||`(no title)`,E=v.seo?.description?.trim()??``,D=E===``&&L(v.data)!==``,O=E||L(v.data),k=l.collections[r.collection]?.type??a(r.collection),ee=_.type??k,te=_.status===`hidden`,A=_.issues??[],j=[{type:`context`,text:y?`${y} · ${on(w)}`:on(w)},{type:`header`,text:Ee(T)},{type:`section`,text:O===``?`No description yet. Google will pick some text from the page.`:sn(O,160)},...D?[{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:S(ee)},{label:`In search results`,value:_.status===`draft`?`Not published yet`:te?`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(s){let t=nn({page:{url:w,path:e(w)?.pathname??`/`,locale:v.locale,kind:`content`,pageType:`website`,title:T,pageTitle:T,description:O===``?null:O,canonical:v.seo?.canonical??null,image:null,content:{collection:r.collection,id:r.id,slug:v.slug}},config:l,record:_,noindex:te,site:o.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 (${S(k)})`,value:rn},...x.map(({value:e,label:t})=>({value:e,label:t}))]},.../^https?:\/\//.test(w)?[{type:`link`,label:`Test on Google`,target:{kind:`external`,url:`https://search.google.com/test/rich-results?url=${encodeURIComponent(w)}`}}]:[]]},{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":t},null,2)}]})}return{blocks:j,...f?{toast:f}:{}}}const ln=/\s*,\s*/;function un(e,t,n){let r=(e??``).toLowerCase().split(ln).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 dn(e){return e.startsWith(`noindex`)}async function Q(e){return h(await e.kv.get(t))}async function $(e,t,n){let r=n?.id;if(typeof r==`string`)try{await Ze(e,t,r,await Q(e))}catch(n){e.log.warn(`could not refresh entry record`,{collection:t,id:r,error:String(n)})}}const fn={hooks:{"page:metadata":async(e,t)=>{let{page:n}=e,r=await Q(t),i=null;if(n.content){let{collection:e,id:a}=n.content;try{let o=z(t),s=await o.get(R(e,a));i=s??await Ze(t,e,a,r);let c=!!(n.seo?.ogImage||n.image);if(s&&Ye(s,c)){let t=await o.get(R(e,a)),n=t?Ye(t,c):null;n&&await o.put(R(e,a),n),i=n??t??s}}catch(n){t.log.warn(`could not load entry record`,{collection:e,id:a,error:String(n)})}}let a=n.content?r.collections[n.content.collection]:void 0,o=un(n.seo?.robots,a?.noindex===!0,r.robots);return[{kind:`meta`,name:`robots`,content:o},{kind:`jsonld`,id:`primary`,graph:{"@context":`https://schema.org`,"@graph":nn({page:n,config:r,record:i,noindex:dn(o),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{if(await tt(t))return;let e=await nt(t,(await t.schema?.listCollections()??[]).filter(e=>e.routable&&!e.hidden).map(e=>e.slug),await Q(t));e.status===`done`&&e.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 tt(t);e?.status===`running`?await ot(t,e,await Q(t)):await t.cron?.cancel(H);return}if(e.name!==N)return;let n=await ie(t);n?.status===`adding`?await ce(t,n):await t.cron?.cancel(N)},"content:afterDelete":async(e,t)=>{await $e(t,e.collection,e.id)}},routes:{admin:{permission:`plugins:read`,handler:async(e,t)=>Wt(e.input,e.user?.role,t)},"editor/preview":{permission:`content:read`,handler:async(e,t)=>cn(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{fn as default};
6
+ `)))].filter(e=>e!==``),o=[];for(let t of a){let n=e(/^https?:\/\//i.test(t)?t:`https://${t}`);if(!n||!n.host.includes(`.`))return{error:`"${t}" is not a web address. Paste the full link to the profile.`,state:r};o.push(n.href)}return{config:{...t.config,site:{...t.config.site,sameAs:o}},state:r}}function Bt(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 Vt(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:gt(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 Ht(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`?x.find(e=>e.value===a)?.value:o.type??Mt(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 Ut(e,t){if(t.scan?.status===`done`)return yt(e,t);let n=t.scan?await ot(e,t.scan,t.config):await nt(e,t.collections.map(e=>e.slug),t.config);return{...t,scan:n,stats:n.status===`done`?n.tally:null}}async function Wt(e,n,r){let i=await vt(r),a=(n??0)>=50,o=ht(e)?e:{},s=typeof o.action_id==`string`?o.action_id:``;if(o.type===`page_load`&&o.page===`widget:seo`)return At(await Ut(r,i));if(!a||o.type===`page_load`||s===``)return Y(await Ut(r,i),r,a,{tab:W.overview});let c=async(e,n=`Saved. Google will pick this up next time it visits.`)=>{if(`error`in e)return Y(i,r,a,{...e.state,toast:{type:`error`,message:e.error}});let o=h(e.config);return await r.kv.set(t,o),Y({...i,config:o},r,a,{...e.state,toast:{type:`success`,message:n}})},l=async e=>{try{let{rows:t,more:n}=await me(r);return{...e,redirectRows:t,moreRedirects:n}}catch{return e}},u=async(e,t,n)=>e?Y(i,r,a,{tab:W.redirects,editing:n,...n?{}:{panel:`add`},problem:e}):Y(await l(i),r,a,{tab:W.redirects,toast:{type:`success`,message:t}});if(o.type===`block_action`){if(s===`check_pages`||s===`check_more`){let e=await Ut(r,s===`check_pages`?{...i,scan:null}:i);return Y(e,r,a,{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(s===`set_type`)return c(Ht(i,o.value,`type`));if(s===`set_visible`){let[e,t]=typeof o.value==`string`?o.value.split(`|`):[];return t===`hide`?Y(i,r,a,{tab:W.types,pendingHide:e}):c(Ht(i,o.value,`visible`),`These pages can appear on Google again.`)}if(s===`confirm_hide`&&typeof o.value==`string`)return c(Ht(i,`${o.value}|hide`,`visible`),`These pages are now hidden from Google and other search engines.`);if(s===`cancel_hide`)return Y(i,r,a,{tab:W.types});if(s===`hidden_ok`){let e={...i.config.collections};for(let t of wt(i))e[t.slug]={...e[t.slug],hiddenOk:!0};return c({config:{...i.config,collections:e},state:{tab:W.overview}},`Noted. These pages stay hidden from search.`)}if(s===`edit_redirect`){let e=i.redirectRows.find(e=>e.id===o.value);return Y(i,r,a,{tab:W.redirects,editing:e})}if(s===`open_add`)return Y(i,r,a,{tab:W.redirects,panel:`add`});if(s===`open_import`)return Y(i,r,a,{tab:W.redirects,panel:`import`});if(s===`close_panel`)return Y(i,r,a,{tab:W.redirects});if(s===`remove_redirect`&&typeof o.value==`string`)return u(await ye(r,o.value),`Redirect removed.`);if(s===`add_redirects`||s===`more_redirects`){if(!i.redirects||i.redirects.status===`done`)return Y(i,r,a,{tab:W.redirects});let e=await ce(r,i.redirects);return Y(await l({...i,redirects:e}),r,a,{tab:W.redirects,toast:e.status===`done`?{type:`success`,message:`${P(e.added,`redirect`)} added.`}:{type:`info`,message:`${e.added} added so far. The rest follow in the background.`}})}if(s===`stop_redirects`&&i.redirects?.status===`adding`){let e=await Ce(r,i.redirects);return Y({...i,redirects:e},r,a,{tab:W.redirects,toast:{type:`info`,message:`Stopped. ${P(e.added,`redirect`)} added.`}})}if(s===`reset_redirects`)return i.redirects?.status!==`adding`&&await r.kv.delete(M),Y({...i,redirects:i.redirects?.status===`adding`?i.redirects:null},r,a,{tab:W.redirects});if(s===`remove_redirects`&&i.redirects){let e=await le(r,i.redirects);return Y(await l({...i,redirects:e===0?null:i.redirects}),r,a,{tab:W.redirects,toast:e===0?{type:`success`,message:`Removed. Your other redirects are untouched.`}:{type:`info`,message:`${e} still to remove. Press Remove again.`}})}if(s===`export_redirects`){let{text:e,count:t}=await ue(r);return Y(i,r,a,{tab:W.redirects,panel:`export`,exported:e,toast:{type:`info`,message:`${P(t,`redirect`)} listed below.`}})}return Y(i,r,a,{tab:W.overview})}if(o.type!==`form_submit`)return Y(i,r,a,{tab:W.overview});let d=ht(o.values)?o.values:{};if(s===`check_redirects`){let e=J(d,`list`);if(e===``)return Y(i,r,a,{tab:W.redirects,toast:{type:`error`,message:`Paste a list first.`}});let t=await oe(r,e);return Y({...i,redirects:t},r,a,{tab:W.redirects,toast:{type:`success`,message:`Checked. Nothing has been added yet.`}})}if(s===`add_redirect`){let e=J(d,`source`),t=J(d,`code`,`301`)===`410`?``:J(d,`destination`);return u(await _e(r,e,t,J(d,`code`,`301`)),t===``?`Done. ${e} now tells visitors the page was removed.`:`Redirect added. ${e} now goes to ${t}.`)}if(s.startsWith(`save_redirect:`)){let e=s.slice(14),t=i.redirectRows.find(t=>t.id===e);return u(await ve(r,e,J(d,`source`,t?.source??``),J(d,`destination`,t?.destination??``),J(d,`code`,String(t?.type??301))),`Redirect changed.`,t)}if(s===`setup_identity`){let e=Ft(i,d);if(`error`in e)return c(e);let t=J(d,`street`),n=J(d,`town`),{site:r}=e.config,a=t!==``||n!==``?{...r.address,...t?{streetAddress:t}:{},...n?{addressLocality:n}:{}}:r.address;return c({config:{...e.config,site:{...r,address:a}},state:{tab:W.overview}})}return s===`save_identity`?c(Ft(i,d)):s===`save_logo`?c(It(i,d,r.site.url)):s===`save_contact`?c(Lt(i,d)):s===`save_hours`?c(Rt(i,d)):s===`save_profiles`?c(zt(i,d)):s===`save_company`?c(Bt(i,d)):s===`save_advanced`?c(Vt(i,d)):Y(i,r,a,{tab:W.overview})}function X(e){return{"@id":e}}function Z(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 Gt(t,n){return e(t,n)?.href??t}function Kt(t,n){return(t.siteUrl||n||(e(t.url)?.origin??``)).replace(/\/+$/,``)}function qt(t,n){if(t.canonical)return Gt(t.canonical,`${n}/`);let r=e(t.url,`${n}/`);return r?(r.search=``,r.hash=``,r.href):t.url}function Jt(e){return e.path===`/`||e.path===``}function Yt(e){return r.includes(e)}function Xt(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?a(e.content.collection):`WebPage`;return i===`WebPage`?e.pageType===`article`?`BlogPosting`:`WebPage`:i}function Zt(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 Z({"@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:X(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 Qt(e){let t=e;try{t=decodeURIComponent(e)}catch{}let n=t.replace(/[-_]+/g,` `).trim();return n.charAt(0).toUpperCase()+n.slice(1)}function $t(e,t,n){let{page:r,config:i,site:a}=e;if(r.breadcrumbs)return r.breadcrumbs.map(e=>({name:e.name,url:Gt(e.url,`${t}/`)}));if(!i.breadcrumbs||Jt(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:Qt(e),url:`${t}${l}${s}`})}),c}function en(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 nn(e){let{page:t,config:n,record:r,site:i}=e,a=Kt(t,i.url),o=qt(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=Xt(t,n,r),g=Yt(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?Gt(t.image,`${a}/`):void 0,w=C??``,T=$t(e,a,l),E=[Zt(n.site,c,a,d),Z({"@type":`WebSite`,"@id":f,url:`${a}/`,name:c,alternateName:n.site.alternateName,description:n.site.description,publisher:X(d),inLanguage:s})];if(e.noindex)return E;let D=Z({"@type":g||_?`WebPage`:h,"@id":p,url:o,name:l,description:t.description,isPartOf:X(f),about:Jt(t)?X(d):void 0,primaryImageOfPage:C?X(w):void 0,image:C?X(w):void 0,breadcrumb:T.length>0?X(m):void 0,mainEntity:_?X(y):v?X(b):void 0,datePublished:x,dateModified:S,inLanguage:s});if(E.push(D),g){let e=(r?.authors??[]).map(e=>Z({"@type":`Person`,name:e.name,url:e.url})),n=t.articleMeta?.author?[{"@type":`Person`,name:t.articleMeta.author}]:[X(d)];E.push(Z({"@type":h,"@id":`${o}#article`,isPartOf:X(p),mainEntityOfPage:X(p),headline:en(l,110),description:t.description,image:C?X(w):void 0,datePublished:x,dateModified:S,author:e.length>0?e:n,publisher:X(d),inLanguage:s}))}_&&E.push(Z({"@type":`Service`,"@id":y,name:l,description:t.description,url:o,image:C?X(w):void 0,provider:X(d),areaServed:n.site.areaServed})),v&&E.push(Z({"@type":`Person`,"@id":b,name:l,description:t.description,url:o,image:C?X(w):void 0,worksFor:u?void 0:X(d)})),C&&E.push({"@type":`ImageObject`,"@id":w,url:C,contentUrl:C,...He(r?.images,C,a)}),T.length>0&&E.push({"@type":`BreadcrumbList`,"@id":m,itemListElement:T.map((e,t)=>Z({"@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=X(k)),E}const rn=`default`;function an(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function on(t){let n=e(t);if(!n)return t;let r=n.pathname.split(`/`).filter(e=>e!==``);return[n.host,...r].join(` › `)}function sn(e,t){return e.length<=t?e:`${e.slice(0,t-1).trimEnd()}…`}async function cn(n,r,i,o){if(!r)return{blocks:[]};let s=(i??0)>=50,c=an(n)?n:{},l=h(await o.kv.get(t)),u=z(o),d=R(r.collection,r.id),f,p=s&&c.type===`block_action`&&c.action_id===`set_type`,[m,g]=await Promise.all([Qe(o,r.collection,r.id,l),o.content?.getPublicUrl?.(r.collection,r.id)]),_=m?.record,v=m?.entry;if(_&&p){let e=typeof c.value==`string`?c.value:rn,t=x.find(t=>t.value===e)?.value,{type:n,...r}=_;_=t?{...r,type:t}:r,await u.put(d,_),f={type:`success`,message:`Saved. Google will pick this up next time it visits.`}}if(!_||!v)return{blocks:[{type:`context`,text:`Save this page to see how it may look on Google.`}]};if(_.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 y=l.site.name??o.site.name,b=o.site.url.replace(/\/+$/,``),C=[`home`,`index`,``].includes(v.slug??``)?`${b}/`:`${b}/${v.slug}`,w=[`home`,`index`].includes(v.slug??``)?`${b}/`:g??C,T=_.title||`(no title)`,E=v.seo?.description?.trim()??``,D=E===``&&L(v.data)!==``,O=E||L(v.data),k=l.collections[r.collection]?.type??a(r.collection),ee=_.type??k,te=_.status===`hidden`,A=_.issues??[],j=[{type:`context`,text:y?`${y} · ${on(w)}`:on(w)},{type:`header`,text:Ee(T)},{type:`section`,text:O===``?`No description yet. Google will pick some text from the page.`:sn(O,160)},...D?[{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:S(ee)},{label:`In search results`,value:_.status===`draft`?`Not published yet`:te?`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(s){let t=nn({page:{url:w,path:e(w)?.pathname??`/`,locale:v.locale,kind:`content`,pageType:`website`,title:T,pageTitle:T,description:O===``?null:O,canonical:v.seo?.canonical??null,image:null,content:{collection:r.collection,id:r.id,slug:v.slug}},config:l,record:_,noindex:te,site:o.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 (${S(k)})`,value:rn},...x.map(({value:e,label:t})=>({value:e,label:t}))]},.../^https?:\/\//.test(w)?[{type:`link`,label:`Test on Google`,target:{kind:`external`,url:`https://search.google.com/test/rich-results?url=${encodeURIComponent(w)}`}}]:[]]},{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":t},null,2)}]})}return{blocks:j,...f?{toast:f}:{}}}const ln=/\s*,\s*/;function un(e,t,n){let r=(e??``).toLowerCase().split(ln).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 dn(e){return e.startsWith(`noindex`)}async function Q(e){return h(await e.kv.get(t))}async function $(e,t,n){let r=n?.id;if(typeof r==`string`)try{await Ze(e,t,r,await Q(e))}catch(n){e.log.warn(`could not refresh entry record`,{collection:t,id:r,error:String(n)})}}const fn={hooks:{"page:metadata":async(e,t)=>{let{page:n}=e,r=await Q(t),i=null;if(n.content){let{collection:e,id:a}=n.content;try{let o=z(t),s=await o.get(R(e,a));i=s??await Ze(t,e,a,r);let c=!!(n.seo?.ogImage||n.image);if(s&&Ye(s,c)){let t=await o.get(R(e,a)),n=t?Ye(t,c):null;n&&await o.put(R(e,a),n),i=n??t??s}}catch(n){t.log.warn(`could not load entry record`,{collection:e,id:a,error:String(n)})}}let a=n.content?r.collections[n.content.collection]:void 0,o=un(n.seo?.robots,a?.noindex===!0,r.robots);return[{kind:`meta`,name:`robots`,content:o},{kind:`jsonld`,id:`primary`,graph:{"@context":`https://schema.org`,"@graph":nn({page:n,config:r,record:i,noindex:dn(o),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{if(await tt(t))return;let e=await nt(t,(await t.schema?.listCollections()??[]).filter(e=>e.routable&&!e.hidden).map(e=>e.slug),await Q(t));e.status===`done`&&e.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 tt(t);e?.status===`running`?await ot(t,e,await Q(t)):await t.cron?.cancel(H);return}if(e.name!==N)return;let n=await ie(t);n?.status===`adding`?await ce(t,n):await t.cron?.cancel(N)},"content:afterDelete":async(e,t)=>{await $e(t,e.collection,e.id)}},routes:{admin:{permission:`plugins:read`,handler:async(e,t)=>Wt(e.input,e.user?.role,t)},"editor/preview":{permission:`content:read`,handler:async(e,t)=>cn(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{fn as default};
@@ -7,7 +7,7 @@ EmDash already handles titles, descriptions, sitemaps, canonicals and social tag
7
7
  - **Pages to fix.** A list of pages with a missing description (an excerpt counts as one), a shared title or a canonical pointing elsewhere, each with a link to the page. No score and no traffic light: a list that can reach zero.
8
8
  - **Google preview** in the entry editor: the address, title and description as Google may show them, with up to three things to fix.
9
9
  - **Hide from search (noindex)** per collection, with a warning on the SEO page whenever something is hidden.
10
- - **Redirects.** Add, change and remove redirects, or paste a list exported from Redirection, Yoast SEO Premium or Rank Math.
10
+ - **Redirects.** Add, change and remove redirects, or paste a CSV export from Redirection, Yoast SEO Premium or Rank Math.
11
11
 
12
12
  ## What it does not do
13
13
 
package/listing/faq.md CHANGED
@@ -8,7 +8,7 @@ Yes, in two parts. EmDash itself does titles, descriptions, sitemaps and canonic
8
8
 
9
9
  ## How do I import redirects from WordPress?
10
10
 
11
- Export them from Redirection, Yoast SEO Premium or Rank Math, open SEO > Redirects > Import from WordPress, paste the list and press "Check list". Nothing is added until you confirm.
11
+ 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.
12
12
 
13
13
  ## How do I noindex a collection in EmDash?
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moorage/plugin-seo",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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",