ghost 6.18.1 → 6.18.2

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.
Files changed (131) hide show
  1. package/components/tryghost-i18n-6.18.2.tgz +0 -0
  2. package/components/{tryghost-parse-email-address-6.18.1.tgz → tryghost-parse-email-address-6.18.2.tgz} +0 -0
  3. package/content/themes/source/package.json +1 -1
  4. package/core/built/admin/assets/{PolarAngleAxis-Dod0DwfL.js → PolarAngleAxis-DALH8FDm.js} +1 -1
  5. package/core/built/admin/assets/{_baseAssignValue-DnkbkowM.js → _baseAssignValue-D_UsvJRN.js} +1 -1
  6. package/core/built/admin/assets/{a-large-small-C5mgFBRg.js → a-large-small-DVyx4GMu.js} +1 -1
  7. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
  8. package/core/built/admin/assets/admin-x-settings/{code-editor-view-BWi3-ftq.mjs → code-editor-view-DBrulgE8.mjs} +12 -12
  9. package/core/built/admin/assets/admin-x-settings/{index-B3PtvbUw.mjs → index-CsGHbqSU.mjs} +2 -2
  10. package/core/built/admin/assets/admin-x-settings/{index-Did70N9h.mjs → index-Cypgljb3.mjs} +2 -2
  11. package/core/built/admin/assets/admin-x-settings/{index-JaMlaX9G.mjs → index-DgGwb1L-.mjs} +1390 -1381
  12. package/core/built/admin/assets/admin-x-settings/{modals-Bwrf9ptQ.mjs → modals-rdo7i0D3.mjs} +9320 -8655
  13. package/core/built/admin/assets/{at-sign-Bz-SU-S_.js → at-sign-CGNkBrZS.js} +1 -1
  14. package/core/built/admin/assets/{audience-DmVdEmIe.js → audience-CJHVR7kD.js} +1 -1
  15. package/core/built/admin/assets/{avatar-flipboard-C7sxDVEM.js → avatar-flipboard-jqr9Aapd.js} +1 -1
  16. package/core/built/admin/assets/{bluesky-sharing-DJniSF6N.js → bluesky-sharing-C2749SVt.js} +1 -1
  17. package/core/built/admin/assets/{chart-mYz3IJwm.js → chart-BAQCVPCH.js} +1 -1
  18. package/core/built/admin/assets/{chunk.524.9d300778a63b42b0de62.js → chunk.524.428356d01feabbc7b932.js} +7 -7
  19. package/core/built/admin/assets/{chunk.582.2e363cd976d9bba998b9.js → chunk.582.b8b41ba720f49d724992.js} +9 -9
  20. package/core/built/admin/assets/{code-editor-view-BIQWFJ01.js → code-editor-view-YNhJ1w71.js} +1 -1
  21. package/core/built/admin/assets/{comments-B_-hdjc6.js → comments-CPd_iCc3.js} +1 -1
  22. package/core/built/admin/assets/{copy-Bpq3uhnh.js → copy-CNsHZEFR.js} +1 -1
  23. package/core/built/admin/assets/{data-list-BYNMbRIq.js → data-list-Dn5MkmyD.js} +1 -1
  24. package/core/built/admin/assets/{deleted-feed-item-B_AwUKZy.js → deleted-feed-item-Dmm_clMl.js} +1 -1
  25. package/core/built/admin/assets/{edit-profile-DZo2ZcOu.js → edit-profile-BLSdtROQ.js} +1 -1
  26. package/core/built/admin/assets/{empty-indicator-Bn5wG9-T.js → empty-indicator-C2Z0dddU.js} +1 -1
  27. package/core/built/admin/assets/{en-D4zIrMLN.js → en-DuTkaMAI.js} +1 -1
  28. package/core/built/admin/assets/{feed-Bgq4RarU.js → feed-B4Xp4C2J.js} +1 -1
  29. package/core/built/admin/assets/{filters-CpVwu1Gk.js → filters-gnOx5Z45.js} +1 -1
  30. package/core/built/admin/assets/{gh-chart-Br7NXn_b.js → gh-chart-B2aWYkGg.js} +1 -1
  31. package/core/built/admin/assets/{ghost-98a3c5fd6235ebd344594f491cb6d17b.js → ghost-29f8f3c80e41126ba5e3c52ad5727e8a.js} +176 -176
  32. package/core/built/admin/assets/ghost-transistor-BXCFhTPG.png +0 -0
  33. package/core/built/admin/assets/{growth-CiPDnWxJ.js → growth-BysawIWe.js} +1 -1
  34. package/core/built/admin/assets/{hash-Bc1e38oH.js → hash-j3sh-UI3.js} +1 -1
  35. package/core/built/admin/assets/{inbox-BcZNBNhk.js → inbox-1cUdr8Mm.js} +1 -1
  36. package/core/built/admin/assets/index-B2yksBz4.js +13 -0
  37. package/core/built/admin/assets/{index-CMTzNTew.js → index-B8G0f0hb.js} +3 -3
  38. package/core/built/admin/assets/{index-DU3bv9jz.js → index-BVoBTlp_.js} +1 -1
  39. package/core/built/admin/assets/{index-ETsoQLbU.js → index-BgrwCpgw.js} +1 -1
  40. package/core/built/admin/assets/{index-Bm5ZeLtt.js → index-BhY_SbGB.js} +1 -1
  41. package/core/built/admin/assets/index-CkAUXgAi.js +1 -0
  42. package/core/built/admin/assets/index-Cl_EPbQ2.js +1 -0
  43. package/core/built/admin/assets/{index-B_7QsC5T.js → index-D8CXbJm4.js} +1 -1
  44. package/core/built/admin/assets/index-DGBK5k9H.js +1 -0
  45. package/core/built/admin/assets/{index-D212VvOz.js → index-DKAlEWM4.js} +1 -1
  46. package/core/built/admin/assets/index-D_sGUCda.css +1 -0
  47. package/core/built/admin/assets/{index-BFEx_ouC.js → index-DarAAzVH.js} +1 -1
  48. package/core/built/admin/assets/{index-Brg4tecQ.js → index-Dz8eTtOq.js} +1 -1
  49. package/core/built/admin/assets/{index-Cn32t_gv.js → index-I9n711UW.js} +1 -1
  50. package/core/built/admin/assets/{index-CsidX7Si.js → index-WjY3mGep.js} +2 -2
  51. package/core/built/admin/assets/{index-Dv13wKfX.js → index-mhojl7aD.js} +1 -1
  52. package/core/built/admin/assets/{koenig-lexical-DZUWzN0P.js → koenig-lexical-coiS9dIC.js} +1 -1
  53. package/core/built/admin/assets/{kpi-card-DENsK2xK.js → kpi-card-BEFHo2gr.js} +1 -1
  54. package/core/built/admin/assets/{kpis-CDrs2iS1.js → kpis-D-gz-lUk.js} +1 -1
  55. package/core/built/admin/assets/{label-BznQtEEo.js → label-BvlHzzcJ.js} +1 -1
  56. package/core/built/admin/assets/{links-lpAC3T1p.js → links-tdJvBrsT.js} +1 -1
  57. package/core/built/admin/assets/list-filter-DPE4Xj2T.js +6 -0
  58. package/core/built/admin/assets/{lucide-react-CBigk-fq.js → lucide-react-ClQ3Iy7l.js} +1448 -1453
  59. package/core/built/admin/assets/{main-layout-_SYQRjIl.js → main-layout-DY96wK-_.js} +1 -1
  60. package/core/built/admin/assets/{message-square-text-cV8O_qKq.js → message-square-text-Dd1XS5-H.js} +1 -1
  61. package/core/built/admin/assets/{minus-NvnQTlW7.js → minus-CyEc3BE5.js} +1 -1
  62. package/core/built/admin/assets/modals-C06GcVIm.js +77 -0
  63. package/core/built/admin/assets/moderation-B0fdmlig.js +1 -0
  64. package/core/built/admin/assets/newsletter-Byg3ARa5.js +1 -0
  65. package/core/built/admin/assets/{newsletters-BexdXUhn.js → newsletters-DbH39vgA.js} +1 -1
  66. package/core/built/admin/assets/{note-DuaUGOeZ.js → note-Cwy-rIAF.js} +1 -1
  67. package/core/built/admin/assets/overview-CVMLGrVt.js +1 -0
  68. package/core/built/admin/assets/{pagemenu-CZyroidv.js → pagemenu-CkzsZrHC.js} +1 -1
  69. package/core/built/admin/assets/{post-analytics-DLK2SOSQ.js → post-analytics-aA-nG9Fq.js} +1 -1
  70. package/core/built/admin/assets/{post-analytics-context-CF7C67-0.js → post-analytics-context-BLkup0Xh.js} +1 -1
  71. package/core/built/admin/assets/{post-analytics-header-7GtJCx0W.js → post-analytics-header-D9srAPT5.js} +1 -1
  72. package/core/built/admin/assets/{post-share-modal-D8R7DUZP.js → post-share-modal-CkvJtrRw.js} +1 -1
  73. package/core/built/admin/assets/{posts-CL9UDYoW.js → posts-DDkuYoN7.js} +1 -1
  74. package/core/built/admin/assets/{repeat-DgH39UKE.js → repeat-B-SL0yPM.js} +1 -1
  75. package/core/built/admin/assets/{reply-DAaNxiy8.js → reply-BdCPoUQ9.js} +1 -1
  76. package/core/built/admin/assets/{select-Cor2wFXT.js → select-C7aNW8QS.js} +1 -1
  77. package/core/built/admin/assets/{settings-xRx917Gj.js → settings-Bzy1GNfD.js} +1 -1
  78. package/core/built/admin/assets/{settings-BeumESEN.js → settings-D-dhma2e.js} +23 -23
  79. package/core/built/admin/assets/{sort-button-BNW3i4Lb.js → sort-button-Dv8vjh13.js} +1 -1
  80. package/core/built/admin/assets/{source-icon-DvDuzw73.js → source-icon-DTz4isLK.js} +1 -1
  81. package/core/built/admin/assets/{sprout-C3cc0c-K.js → sprout-BwLQTzMf.js} +1 -1
  82. package/core/built/admin/assets/{square-tZp0_n7e.js → square-YF1YE9ex.js} +1 -1
  83. package/core/built/admin/assets/{stats-2Jelnn-Q.js → stats-C5ad0fgQ.js} +1 -1
  84. package/core/built/admin/assets/{stats-view-CESy8ELH.js → stats-view-BvkxPYNX.js} +1 -1
  85. package/core/built/admin/assets/{step-1-DrqdolAh.js → step-1-D7_s-D99.js} +1 -1
  86. package/core/built/admin/assets/{step-2-DmEpKck5.js → step-2-B94Yf7FF.js} +1 -1
  87. package/core/built/admin/assets/{step-3-Bus-0o0n.js → step-3-DswXYYf4.js} +1 -1
  88. package/core/built/admin/assets/{table-BQUcKHfm.js → table-D30IXfUP.js} +1 -1
  89. package/core/built/admin/assets/{tabs-BmdL0X4U.js → tabs-CjNdfW0y.js} +1 -1
  90. package/core/built/admin/assets/{tags-EchqlZUJ.js → tags-B0ux9_dT.js} +1 -1
  91. package/core/built/admin/assets/{tags-CLxXZlOO.js → tags-PGeGAafJ.js} +1 -1
  92. package/core/built/admin/assets/{tiers-nCGyTly9.js → tiers-BaXK0JoI.js} +1 -1
  93. package/core/built/admin/assets/{toggle-group-CM5uf7J1.js → toggle-group-DdY8HF3Y.js} +1 -1
  94. package/core/built/admin/assets/{topic-filter-LTRvZ8aU.js → topic-filter-Boh22uGD.js} +1 -1
  95. package/core/built/admin/assets/{trash-u5BxolyH.js → trash-D7ZWrnDq.js} +1 -1
  96. package/core/built/admin/assets/{url-helpers-D41fEt51.js → url-helpers-mt6MBIi0.js} +1 -1
  97. package/core/built/admin/assets/{use-growth-stats-BJ0O9ewi.js → use-growth-stats-Bg0nE0WG.js} +1 -1
  98. package/core/built/admin/assets/{use-infinite-virtual-scroll-APZWciOk.js → use-infinite-virtual-scroll-DoeI5IY-.js} +1 -1
  99. package/core/built/admin/assets/{use-simple-pagination-DVRHeaAR.js → use-simple-pagination-BJzBULR3.js} +1 -1
  100. package/core/built/admin/assets/{user-round-check-B6j98D6d.js → user-round-check-BLc3L-ei.js} +1 -1
  101. package/core/built/admin/assets/{wallet-cards-KmOh29LP.js → wallet-cards-xX4QZik7.js} +1 -1
  102. package/core/built/admin/assets/web-DQ2qBymm.js +1 -0
  103. package/core/built/admin/index.html +5 -5
  104. package/core/server/lib/image/cached-image-size-from-url.js +37 -25
  105. package/core/server/lib/lexical.js +1 -0
  106. package/core/server/lib/mobiledoc.js +1 -0
  107. package/core/server/services/email-service/email-renderer.js +6 -2
  108. package/core/server/services/email-service/email-service-wrapper.js +2 -2
  109. package/core/server/services/koenig/node-renderers/call-to-action-renderer.js +2 -2
  110. package/core/server/services/koenig/node-renderers/gallery-renderer.js +3 -3
  111. package/core/server/services/koenig/node-renderers/image-renderer.js +3 -3
  112. package/core/server/services/koenig/render-utils/is-content-image.js +18 -0
  113. package/core/server/services/koenig/render-utils/srcset-attribute.js +4 -4
  114. package/core/server/services/members/members-api/controllers/router-controller.js +21 -36
  115. package/core/server/services/members/members-api/repositories/member-repository.js +8 -1
  116. package/core/server/services/offers/offer-bookshelf-repository.js +3 -3
  117. package/core/shared/config/defaults.json +1 -1
  118. package/core/shared/labs.js +2 -1
  119. package/package.json +5 -5
  120. package/components/tryghost-i18n-6.18.1.tgz +0 -0
  121. package/core/built/admin/assets/index-BBrewxpF.css +0 -1
  122. package/core/built/admin/assets/index-BpcL7RmI.js +0 -13
  123. package/core/built/admin/assets/index-CTfJflJ2.js +0 -1
  124. package/core/built/admin/assets/index-D67LJ_H4.js +0 -1
  125. package/core/built/admin/assets/index-ZYDRMgcT.js +0 -1
  126. package/core/built/admin/assets/modals-XRSkribf.js +0 -77
  127. package/core/built/admin/assets/moderation-BQp1GEWG.js +0 -1
  128. package/core/built/admin/assets/newsletter-foM6KNNV.js +0 -1
  129. package/core/built/admin/assets/overview-DgKBNqyc.js +0 -1
  130. package/core/built/admin/assets/web-Cclotbnz.js +0 -1
  131. package/core/server/services/koenig/render-utils/is-local-content-image.js +0 -9
@@ -0,0 +1 @@
1
+ import{j as t,aS as be,aR as xe,by as Se,bz as je,B as oe,bA as _e,bB as Ce,bC as Ne,bD as ve,ch as Le,aj as ye,ai as Te,aU as Ee,r as l,ci as Fe,aT as J,bK as Oe,af as ne,ag as $,cj as ke,bG as De,aP as Ae,u as Ie,aV as Me,aQ as Pe}from"./index-B8G0f0hb.js";import{S as we,a as Ue,b as Ve,c as Re,d as Be,e as ze,f as Ke}from"./select-C7aNW8QS.js";import{u as H,S as ie,a as re,A as Z,b as z,c as q,U as le}from"./post-analytics-context-BLkup0Xh.js";import{a as $e,F as Ge,b as He}from"./message-square-text-Dd1XS5-H.js";import{g as We,a as Qe,S as Ye,b as qe}from"./kpis-D-gz-lUk.js";import{F as Xe,c as W,e as ce}from"./en-DuTkaMAI.js";import{C as ue,a as Je,b as Ze,f as et,c as de,g as tt,u as K,j as ee}from"./pagemenu-CkzsZrHC.js";import{D as st,h as at,i as te,a as ot,b as nt,c as it,d as rt,e as lt,f as ct,g as ut}from"./data-list-Dn5MkmyD.js";import{a as dt,U as mt}from"./wallet-cards-xX4QZik7.js";import{P as pt,a as ht}from"./post-analytics-header-D9srAPT5.js";import{F as gt,c as ft}from"./filters-gnOx5Z45.js";import{b as bt,c as xt,F as St,d as jt,S as _t,T as Ct}from"./lucide-react-ClQ3Iy7l.js";import{E as Nt}from"./empty-indicator-C2Z0dddU.js";import"./posts-DDkuYoN7.js";import"./source-icon-DTz4isLK.js";import"./source-utils-B1S3ZHA2.js";import"./gh-chart-B2aWYkGg.js";import"./chart-BAQCVPCH.js";import"./_baseAssignValue-D_UsvJRN.js";import"./tabs-CjNdfW0y.js";import"./index-DKAlEWM4.js";import"./post-share-modal-CkvJtrRw.js";import"./post-helpers-gInwAwEv.js";import"./trash-D7ZWrnDq.js";import"./sprout-BwLQTzMf.js";import"./a-large-small-DVyx4GMu.js";import"./at-sign-CGNkBrZS.js";import"./copy-CNsHZEFR.js";import"./hash-j3sh-UI3.js";import"./inbox-1cUdr8Mm.js";import"./list-filter-DPE4Xj2T.js";import"./minus-CyEc3BE5.js";import"./tags-PGeGAafJ.js";import"./square-YF1YE9ex.js";import"./user-round-check-BLc3L-ei.js";import"./repeat-B-SL0yPM.js";import"./reply-BdCPoUQ9.js";const se=()=>{const{range:e,setRange:s}=H();return t.jsxs(we,{value:`${e}`,onValueChange:a=>{s(Number(a))},children:[t.jsxs(Ue,{className:"w-auto",children:[t.jsx($e,{className:"mr-2",size:16,strokeWidth:1.5}),t.jsx(Ve,{placeholder:"Select a period"})]}),t.jsx(Re,{align:"end",children:t.jsxs(Be,{children:[t.jsx(ze,{children:"Period"}),Object.values(ie).map(a=>t.jsx(Ke,{value:`${a.value}`,children:a.name},a.value))]})})]})};W.registerLocale(ce);const vt=e=>re[e]||W.getName(e,"en")||"Unknown",Lt=e=>{const s={"UNITED STATES":"US","UNITED STATES OF AMERICA":"US",USA:"US","UNITED KINGDOM":"GB",UK:"GB","GREAT BRITAIN":"GB",NETHERLANDS:"NL"},a=e.toUpperCase();return s[a]||(e.length>2?e.substring(0,2):e)},ae=({tableHeader:e,data:s,onLocationClick:a})=>t.jsxs(st,{children:[e&&t.jsxs(at,{children:[t.jsx(te,{children:"Country"}),t.jsx(te,{children:"Visitors"})]}),t.jsx(ot,{children:s.map(i=>{const o=vt(`${i.location}`),r=a&&i.location!=="Unknown",c=i.location?i.location.toLowerCase():"unknown";return t.jsxs(nt,{className:r?"cursor-pointer":"","data-testid":`location-row-${c}`,onClick:r?()=>a(i.location):void 0,children:[t.jsx(it,{style:{width:`${i.percentage?Math.round(i.percentage*100):0}%`}}),t.jsx(rt,{className:"group-hover/data:max-w-[calc(100%-140px)]",children:t.jsxs("div",{className:"flex items-center space-x-3 overflow-hidden",title:o,children:[t.jsx(Xe,{countryCode:`${Lt(i.location)}`,fallback:t.jsx("span",{className:"flex h-[14px] w-[22px] items-center justify-center rounded-[2px] bg-black text-white",children:t.jsx(Le.SkullAndBones,{className:"size-3"})})}),t.jsx("div",{className:"truncate font-medium",children:o})]})}),t.jsxs(lt,{children:[t.jsx(ct,{children:ye(Number(i.visits))}),t.jsx(ut,{children:Te(i.percentage)})]})]},i.location||"unknown")})})]}),yt=({data:e,isLoading:s,onLocationClick:a})=>{const i=e.slice(0,10);return t.jsx(t.Fragment,{children:s?"":t.jsx(t.Fragment,{children:e&&e.length>0&&t.jsxs(ue,{className:"group/datalist","data-testid":"locations-card",children:[t.jsxs("div",{className:"flex items-center justify-between p-6",children:[t.jsxs(Je,{className:"p-0",children:[t.jsx(Ze,{children:"Locations"}),t.jsx(et,{children:"Where are the readers of this post"})]}),t.jsx(be,{className:"mr-2",children:"Visitors"})]}),t.jsxs(de,{className:"overflow-hidden",children:[t.jsx(xe,{}),t.jsx(ae,{data:i,tableHeader:!1,onLocationClick:a})]}),e.length>10&&t.jsx(tt,{children:t.jsxs(Se,{children:[t.jsx(je,{asChild:!0,children:t.jsxs(oe,{variant:"outline",children:["View all ",t.jsx(dt,{})]})}),t.jsxs(_e,{className:"overflow-y-auto pt-0 sm:max-w-[600px]",children:[t.jsxs(Ce,{className:"sticky top-0 z-40 -mx-6 bg-background/60 p-6 backdrop-blur",children:[t.jsx(Ne,{children:"Top locations"}),t.jsx(ve,{children:"Where are the readers of this post"})]}),t.jsx("div",{className:"group/datalist",children:t.jsx(ae,{data:e,tableHeader:!0,onLocationClick:a})})]})]})})]})})})},me=e=>!e||e.length===0?Z:z.filter(s=>e.includes(s.value)).reduce((s,a)=>s|a.bit,0)||Z,pe=e=>{const s=[];return(e&q.PUBLIC)!==0&&s.push(z[0].value),(e&q.FREE)!==0&&s.push(z[1].value),(e&q.PAID)!==0&&s.push(z[2].value),s.join(",")};W.registerLocale(ce);const Tt=e=>re[e]||W.getName(e,"en")||e,Et=({visits:e})=>t.jsx("span",{className:"order-2 font-mono text-xs text-muted-foreground",children:e.toLocaleString()}),Ft={utm_source:{endpoint:"api_top_utm_sources",valueKey:"utm_source",transformValue:e=>({value:e||"(not set)",label:e||"(not set)"})},utm_medium:{endpoint:"api_top_utm_mediums",valueKey:"utm_medium",transformValue:e=>({value:e||"(not set)",label:e||"(not set)"})},utm_campaign:{endpoint:"api_top_utm_campaigns",valueKey:"utm_campaign",transformValue:e=>({value:e||"(not set)",label:e||"(not set)"})},utm_content:{endpoint:"api_top_utm_contents",valueKey:"utm_content",transformValue:e=>({value:e||"(not set)",label:e||"(not set)"})},utm_term:{endpoint:"api_top_utm_terms",valueKey:"utm_term",transformValue:e=>({value:e||"(not set)",label:e||"(not set)"})},source:{endpoint:"api_top_sources",valueKey:"source",transformValue:e=>({value:e||"",label:e||"Direct"})},location:{endpoint:"api_top_locations",valueKey:"location",filterItem(e){const s=String(e.location||"");return s!==""&&!le.includes(s)},transformValue:e=>({value:e,label:Tt(e)})},device:{endpoint:"api_top_devices",valueKey:"device",transformValue:e=>({value:e,label:e==="mobile-ios"?"iOS":e==="mobile-android"?"Android":e==="desktop"?"Desktop":e==="bot"?"Bot":e})}},Ot=(e,s,a)=>{const i={...a};return e.forEach(o=>{if(o.field===s||o.values.length===0)return;const r=o.values[0];o.field!=="audience"&&(o.field==="source"||o.field==="device"||o.field==="location"||o.field.startsWith("utm_"))&&(i[o.field]=r)}),i},O=(e,s=[],a,i={})=>{const{enabled:o=!0}=i,{statsConfig:r,range:c}=H(),{startDate:b,endDate:m,timezone:x}=ne(c),p=Ft[e],d=l.useMemo(()=>{const j=s.find(h=>h.field==="audience");return me(j?.values)},[s]),S=l.useMemo(()=>{const j={site_uuid:r?.id||"",date_from:$(b),date_to:$(m),timezone:x,member_status:pe(d),limit:"50"};return a&&(j.post_uuid=a),Ot(s,e,j)},[r?.id,b,m,x,d,s,e,a]),{data:v,loading:k}=K({endpoint:p?.endpoint||"",statsConfig:r,params:S,enabled:o&&!!p});return{options:l.useMemo(()=>p?(v||[]).filter(h=>p.filterItem?p.filterItem(h):!0).map(h=>{const E=String(h[p.valueKey]??""),M=Number(h.visits)||0,{value:F,label:y}=p.transformValue?p.transformValue(E):{value:E,label:E};return{label:y,value:F,icon:t.jsx(Et,{visits:M})}}):[],[v,p]),loading:k}};function kt({filters:e,onChange:s,...a}){const{appSettings:i}=Ee(),{post:o}=H(),r=o?.uuid,[c,b]=l.useState(null),[m,x]=l.useState(!1);l.useEffect(()=>{const g=window.matchMedia("(max-width: 1024px)"),C=T=>{x(T.matches)};return C(g),g.addEventListener("change",C),()=>g.removeEventListener("change",C)},[]);const p=l.useMemo(()=>{const g=[{value:"undefined",label:"Public visitors",icon:t.jsx(J,{className:"text-gray-700"})},{value:"free",label:"Free members",icon:t.jsx(Oe,{className:"text-green"})},{value:"paid",label:"Paid members",icon:t.jsx(mt,{className:"text-orange"})}];return i?.paidMembersEnabled?g:g.filter(C=>C.value!=="paid")},[i?.paidMembersEnabled]),d=l.useCallback(g=>{const C=c===g,T=e.some(n=>n.field===g);return C||T},[c,e]),{options:S,loading:v}=O("utm_source",e,r,{enabled:d("utm_source")}),{options:k,loading:I}=O("utm_medium",e,r,{enabled:d("utm_medium")}),{options:j,loading:h}=O("utm_campaign",e,r,{enabled:d("utm_campaign")}),{options:E,loading:M}=O("utm_content",e,r,{enabled:d("utm_content")}),{options:F,loading:y}=O("utm_term",e,r,{enabled:d("utm_term")}),{options:R,loading:D}=O("source",e,r,{enabled:d("source")}),{options:U,loading:A}=O("device",e,r,{enabled:d("device")}),{options:B,loading:P}=O("location",e,r,{enabled:d("location")}),_=l.useMemo(()=>[{value:"is",label:"is"}],[]),Q=l.useMemo(()=>{const g=[{key:"utm_source",label:"UTM Source",type:"select",icon:t.jsx(jt,{className:"size-4"}),placeholder:"Select source",operators:_,defaultOperator:"is",hideOperatorSelect:!0,options:S,isLoading:v,searchable:!0,selectedOptionsClassName:"hidden"},{key:"utm_medium",label:"UTM Medium",type:"select",icon:t.jsx(_t,{className:"size-4"}),placeholder:"Select medium",operators:_,defaultOperator:"is",hideOperatorSelect:!0,options:k,isLoading:I,className:"w-60",popoverContentClassName:"w-60",searchable:!0,selectedOptionsClassName:"hidden"},{key:"utm_campaign",label:"UTM Campaign",type:"select",icon:t.jsx(He,{className:"size-4"}),placeholder:"Select campaign",operators:_,defaultOperator:"is",hideOperatorSelect:!0,options:j,isLoading:h,className:"w-60",popoverContentClassName:"w-60",searchable:!0,selectedOptionsClassName:"hidden"},{key:"utm_content",label:"UTM Content",type:"select",icon:t.jsx(Ct,{className:"size-4"}),placeholder:"Select content",operators:_,defaultOperator:"is",hideOperatorSelect:!0,options:E,isLoading:M,className:"w-60",popoverContentClassName:"w-60",searchable:!0,selectedOptionsClassName:"hidden"},{key:"utm_term",label:"UTM Term",type:"select",icon:t.jsx(ke,{className:"size-4"}),placeholder:"Select term",operators:_,defaultOperator:"is",hideOperatorSelect:!0,options:F,isLoading:y,className:"w-60",popoverContentClassName:"w-60",searchable:!0,selectedOptionsClassName:"hidden"}];return[{group:"Basic",fields:[{key:"audience",label:"Audience",type:"multiselect",icon:t.jsx(Fe,{}),options:p.map(({value:C,label:T,icon:n})=>({value:C,label:T,icon:n})),defaultOperator:"is any of",hideOperatorSelect:!0,autoCloseOnSelect:!0},{key:"source",label:"Source",type:"select",icon:t.jsx(J,{className:"size-4"}),placeholder:"Select source",operators:_,defaultOperator:"is",hideOperatorSelect:!0,options:R,isLoading:D,className:"w-60",popoverContentClassName:"w-60",searchable:!0,selectedOptionsClassName:"hidden"},{key:"device",label:"Device",type:"select",icon:t.jsx(bt,{className:"size-4"}),placeholder:"Select device",operators:_,defaultOperator:"is",hideOperatorSelect:!0,options:U,isLoading:A,selectedOptionsClassName:"hidden"},{key:"location",label:"Location",type:"select",icon:t.jsx(xt,{className:"size-4"}),placeholder:"Select location",operators:_,defaultOperator:"is",hideOperatorSelect:!0,options:B,isLoading:P,searchable:!0,selectedOptionsClassName:"hidden"}]},{group:"UTM parameters",fields:g}]},[S,v,k,I,j,h,E,M,F,y,_,p,R,D,U,A,B,P]),w=e.length>0,Y=l.useCallback(()=>{s&&s([])},[s]);return t.jsxs("div",{className:"mt-3 flex w-full justify-between gap-2 lg:mt-0","data-testid":"stats-filter-container",children:[t.jsx(gt,{addButtonIcon:t.jsx(Ge,{}),addButtonText:w?"Add filter":"Filter",allowMultiple:!1,className:`[&>button]:order-last ${w&&"[&>button]:border-none"}`,fields:Q,filters:e,keyboardShortcut:"f",popoverAlign:m||w?"start":"end",showSearchInput:!1,onActiveFieldChange:b,onChange:s||(()=>{}),...a}),w&&t.jsxs(oe,{className:"hidden font-normal text-muted-foreground lg:flex","data-testid":"stats-filter-clear-button",variant:"ghost",onClick:Y,children:[t.jsx(St,{}),"Clear"]})]})}const G=["audience","source","device","location","utm_source","utm_medium","utm_campaign","utm_content","utm_term"],he="__empty__",ge="%2C";function Dt(e){const s=new URLSearchParams;return e.forEach(a=>{if(G.includes(a.field)&&a.values.length>0){const i=a.values.map(o=>o===""?he:String(o).replace(/,/g,ge)).join(",");s.set(a.field,i)}}),s}const X=new Map;function At(e){return X.has(e)||X.set(e,`url-${e}-${Date.now()}-${Math.random().toString(36).substring(2,11)}`),X.get(e)}function It(e){const s=[],a=new Set(G);return e.forEach((i,o)=>{if(!a.has(o))return;const r=i.split(",").map(c=>c===he?"":c.replace(new RegExp(ge,"g"),","));if(r.length>0){const c=o==="audience"?"is any of":"is";s.push({id:At(o),field:o,operator:c,values:r})}}),s}function Mt(e={}){const[s,a]=De(),{onFiltersChange:i}=e,o=l.useRef(!1),r=l.useMemo(()=>It(s),[s]);l.useEffect(()=>{!o.current&&i&&i(r)},[r,i]);const c=l.useCallback(m=>{o.current=!0;const x=typeof m=="function"?m(r):m,p=Dt(x),d=new URLSearchParams(s);G.forEach(S=>{d.delete(S)}),p.forEach((S,v)=>{d.set(v,S)}),a(d,{replace:!0}),setTimeout(()=>{o.current=!1},0)},[r,s,a]),b=l.useCallback(()=>{o.current=!0;const m=new URLSearchParams(s);G.forEach(x=>{m.delete(x)}),a(m,{replace:!0}),setTimeout(()=>{o.current=!1},0)},[s,a]);return{filters:r,setFilters:c,clearFilters:b}}const Ss=()=>{const e=Ae(),{postId:s}=Ie(),{statsConfig:a,isLoading:i,range:o,data:r,post:c,isPostLoading:b}=H(),{filters:m,setFilters:x}=Mt(),p=l.useMemo(()=>{const n=m.find(f=>f.field==="audience");return me(n?.values)},[m]);l.useEffect(()=>{!b&&c?.email_only&&e(`/posts/analytics/${s}`)},[b,c?.email_only,e,s]);const d=l.useMemo(()=>{if(!c?.published_at)return ie.ALL_TIME.value;const n=Me(c.published_at);return o>n?n:o},[c?.published_at,o]),{startDate:S,endDate:v,timezone:k}=ne(d),I=l.useCallback(()=>{const n=document.querySelector(".overflow-y-scroll");n&&n.scrollTo({top:0,behavior:"smooth"})},[]),j=l.useMemo(()=>{const n={};return m.forEach(f=>{const N=f.field,L=f.values;if(N==="audience")return;const u=L&&L.length>0&&L[0]!==null&&L[0]!==void 0,V=N==="source"&&L?.[0]==="";if(u&&(L[0]!==""||V)){const fe=String(L[0]);n[N]=fe}}),n},[m]),h=l.useCallback((n,f)=>{x(N=>N.find(u=>u.field===n)?N.map(u=>u.field===n?{...u,values:[f]}:u):[...N,ft(n,"is",[f])]),I()},[x,I]),E=l.useCallback(n=>h("location",n),[h]),M=l.useCallback(n=>h("source",n),[h]),F=l.useMemo(()=>{const n={site_uuid:a?.id||"",date_from:$(S),date_to:$(v),timezone:k,member_status:pe(p),post_uuid:"",...j};return!b&&c?.uuid?{...n,post_uuid:c.uuid}:n},[b,c,a?.id,S,v,k,p,j]),{data:y,loading:R}=K({endpoint:"api_kpis",statsConfig:a||{id:""},params:F}),{data:D,loading:U}=K({endpoint:"api_top_locations",statsConfig:a||{id:""},params:F}),{data:A,loading:B}=K({endpoint:"api_top_sources",statsConfig:a||{id:""},params:F}),P=l.useMemo(()=>D?.reduce((n,f)=>n+Number(f.visits),0)||0,[D]),_=l.useMemo(()=>A?A.reduce((n,f)=>n+Number(f.visits||0),0):0,[A]),Q=r?.url,w=r?.icon,Y=l.useMemo(()=>{const n=D?.map(u=>({location:String(u.location),visits:Number(u.visits),percentage:P>0?Number(u.visits)/P:0,isUnknown:le.includes(String(u.location))}))||[],f=n.filter(u=>!u.isUnknown),N=n.filter(u=>u.isUnknown),L=N.length>0?[{location:"Unknown",visits:N.reduce((u,V)=>u+V.visits,0),percentage:N.reduce((u,V)=>u+V.percentage,0)}]:[];return[...f,...L]},[D,P]),g=i||b||R||U||B,C=We(y),T=m.length>0;return t.jsxs(t.Fragment,{children:[t.jsxs(pt,{currentTab:"Web",children:[T&&t.jsx(ee,{children:t.jsx(se,{})}),t.jsxs(ee,{className:`${T?"!mt-0 [grid-area:subactions] lg:!mt-[25px]":"[grid-area:actions]"}`,children:[t.jsx(kt,{filters:m,onChange:x}),!T&&t.jsx(se,{})]})]}),t.jsx(ht,{children:g?t.jsx(ue,{className:"size-full",variant:"plain",children:t.jsx(de,{className:"size-full items-center justify-center",children:t.jsx(Pe,{})})}):y&&y.length!==0&&C.visits!=="0"?t.jsxs(t.Fragment,{children:[t.jsx(Qe,{data:y,range:d}),t.jsxs("div",{className:"flex flex-col gap-6 lg:grid lg:grid-cols-2",children:[t.jsx(yt,{data:Y,isLoading:U,onLocationClick:E}),t.jsx(Ye,{data:A,range:d,siteIcon:w,siteUrl:Q,totalVisitors:_,onSourceClick:M})]})]}):t.jsx("div",{className:"grow",children:t.jsx(Nt,{className:"h-full",description:"Try adjusting filters to see more data.",title:`No visitors ${qe(o)}`,children:t.jsx(J,{strokeWidth:1.5})})})})]})};export{Ss as default};
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <link rel="stylesheet" href="./assets/vendor-0ede59da8efb5e28fa929557f7ff7154.css">
5
5
  <link rel="stylesheet" href="./assets/ghost-3c48d37b32f4fcd7eb15cfd739905a03.css">
6
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%226.18%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%22e3b2a7ff32%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%227891cacd1a%22%2C%22activitypubFilename%22%3A%22activitypub.js%22%2C%22activitypubHash%22%3A%224d76e4db3f%22%2C%22postsFilename%22%3A%22posts.js%22%2C%22postsHash%22%3A%22171bc5b3ae%22%2C%22statsFilename%22%3A%22stats.js%22%2C%22statsHash%22%3A%226f1c278b75%22%2C%22activitypubRemoteConfigUrl%22%3A%22%2F.ghost%2Factivitypub%2Fstable%2Fclient-config%22%7D">
6
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22cdnUrl%22%3A%22%22%2C%22editorUrl%22%3A%22%22%2C%22rootURL%22%3A%22%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%226.18%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%2C%22editorFilename%22%3A%22koenig-lexical.umd.js%22%2C%22editorHash%22%3A%22e3b2a7ff32%22%2C%22adminXSettingsFilename%22%3A%22admin-x-settings.js%22%2C%22adminXSettingsHash%22%3A%22d795c01849%22%2C%22activitypubFilename%22%3A%22activitypub.js%22%2C%22activitypubHash%22%3A%224d76e4db3f%22%2C%22postsFilename%22%3A%22posts.js%22%2C%22postsHash%22%3A%22171bc5b3ae%22%2C%22statsFilename%22%3A%22stats.js%22%2C%22statsHash%22%3A%226f1c278b75%22%2C%22activitypubRemoteConfigUrl%22%3A%22%2F.ghost%2Factivitypub%2Fstable%2Fclient-config%22%7D">
7
7
 
8
8
  <meta charset="UTF-8" />
9
9
 
@@ -17,8 +17,8 @@
17
17
  <meta name="apple-mobile-web-app-title" content="Ghost" />
18
18
  <meta name="apple-mobile-web-app-capable" content="yes" />
19
19
  <meta name="apple-mobile-web-app-status-bar-style" content="black" />
20
- <script type="module" crossorigin src="./assets/index-CMTzNTew.js"></script>
21
- <link rel="stylesheet" crossorigin href="./assets/index-BBrewxpF.css">
20
+ <script type="module" crossorigin src="./assets/index-B8G0f0hb.js"></script>
21
+ <link rel="stylesheet" crossorigin href="./assets/index-D_sGUCda.css">
22
22
  </head>
23
23
  <body class="react-admin">
24
24
  <div id="ember-alerts-wormhole"></div>
@@ -38,7 +38,7 @@
38
38
  <div id="ember-liquid-wormhole"></div>
39
39
  <script src="./assets/vendor-0e6161f8da2ad46cf62cef0a53cfb16a.js"></script>
40
40
  <script src="./assets/chunk.397.fc6a300ea45f7b5ebab7.js"></script>
41
- <script src="./assets/chunk.524.9d300778a63b42b0de62.js"></script>
42
- <script src="./assets/ghost-98a3c5fd6235ebd344594f491cb6d17b.js"></script>
41
+ <script src="./assets/chunk.524.428356d01feabbc7b932.js"></script>
42
+ <script src="./assets/ghost-29f8f3c80e41126ba5e3c52ad5727e8a.js"></script>
43
43
  </body>
44
44
  </html>
@@ -14,6 +14,7 @@ const logging = require('@tryghost/logging');
14
14
  * @property {string} url image url
15
15
  * @property {number} height image height
16
16
  * @property {number} width image width
17
+ * @property {boolean} notFound true if the image is not found
17
18
  */
18
19
 
19
20
  class CachedImageSizeFromUrl {
@@ -30,46 +31,57 @@ class CachedImageSizeFromUrl {
30
31
 
31
32
  /**
32
33
  * Get cached image size from URL
33
- * Always returns {object} imageSizeCache
34
+ * Returns null when dimensions are unavailable (invalid URL, 404, transient
35
+ * errors) so consumers can gracefully skip images with missing dimensions.
36
+ *
37
+ * Caching strategy:
38
+ * - Successful fetches are cached
39
+ * - NotFoundError (404) is cached permanently with a marker
40
+ * - Transient errors (timeouts, 500s) are NOT cached, allowing retry on next call
41
+ * - Stale error entries (cached without dimensions) trigger a retry
42
+ *
34
43
  * @param {string} url
35
44
  * @returns {Promise<ImageSizeCache>}
36
- * @description Takes a url and returns image width and height from cache if available.
37
- * If not in cache, `getImageSizeFromUrl` is called and returns the dimensions in a Promise.
38
45
  */
39
46
  async getCachedImageSizeFromUrl(url) {
40
47
  if (!url || url === undefined || url === null) {
41
- return;
48
+ return null;
42
49
  }
43
50
 
44
51
  const cachedImageSize = await this.cache.get(url);
45
52
 
46
- if (cachedImageSize) {
53
+ // Check for cachedImageSize.width to handle legacy cache entries
54
+ // that were stored as {url} without dimensions or a notFound marker.
55
+ // These stale entries fall through to trigger a re-fetch and self-heal.
56
+ if (cachedImageSize && cachedImageSize.width) {
47
57
  debug('Read image from cache:', url);
58
+ return {...cachedImageSize};
59
+ }
48
60
 
49
- return cachedImageSize;
50
- } else {
51
- try {
52
- const res = await this.getImageSizeFromUrl(url);
53
- await this.cache.set(url, res);
54
-
55
- debug('Cached image:', url);
61
+ // 404s are cached permanently — don't retry
62
+ if (cachedImageSize && cachedImageSize.notFound) {
63
+ debug('Read image from cache (not found):', url);
64
+ return null;
65
+ }
56
66
 
57
- return this.cache.get(url);
58
- } catch (err) {
59
- if (err instanceof errors.NotFoundError) {
60
- debug('Cached image (not found):', url);
61
- } else {
62
- debug('Cached image (error):', url);
63
- logging.error(err);
64
- }
67
+ try {
68
+ const res = await this.getImageSizeFromUrl(url);
69
+ await this.cache.set(url, {...res});
65
70
 
66
- // in case of error we just attach the url
67
- await this.cache.set(url, {
68
- url
69
- });
71
+ debug('Cached image:', url);
70
72
 
71
- return this.cache.get(url);
73
+ return res;
74
+ } catch (err) {
75
+ if (err instanceof errors.NotFoundError) {
76
+ debug('Cached image (not found):', url);
77
+ // Cache 404s with a marker
78
+ await this.cache.set(url, {url, notFound: true});
79
+ } else {
80
+ debug('Image fetch error (not cached):', url);
81
+ logging.error(err);
72
82
  }
83
+
84
+ return null;
73
85
  }
74
86
  }
75
87
  }
@@ -81,6 +81,7 @@ module.exports = {
81
81
 
82
82
  const options = Object.assign({
83
83
  siteUrl: config.get('url'),
84
+ imageBaseUrl: config.get('urls:image') || '',
84
85
  imageOptimization: config.get('imageOptimization'),
85
86
  canTransformImage(storagePath) {
86
87
  const imageTransform = require('@tryghost/image-transform');
@@ -31,6 +31,7 @@ module.exports = {
31
31
 
32
32
  cardFactory = new CardFactory({
33
33
  siteUrl: config.get('url'),
34
+ imageBaseUrl: config.get('urls:image') || '',
34
35
  imageOptimization: config.get('imageOptimization'),
35
36
  canTransformImage(storagePath) {
36
37
  const imageTransform = require('@tryghost/image-transform');
@@ -148,7 +148,7 @@ class EmailRenderer {
148
148
  * @param {object} dependencies.renderers
149
149
  * @param {{render(object, options): string}} dependencies.renderers.lexical
150
150
  * @param {{render(object, options): string}} dependencies.renderers.mobiledoc
151
- * @param {{getImageSizeFromUrl(url: string): Promise<{width: number, height: number}>}} dependencies.imageSize
151
+ * @param {{getCachedImageSizeFromUrl(url: string): Promise<{url: string, width: number, height: number} | null>}} dependencies.imageSize
152
152
  * @param {{urlFor(type: string, optionsOrAbsolute, absolute): string, isSiteUrl(url, context): boolean}} dependencies.urlUtils
153
153
  * @param {{isLocalImage(url: string): boolean}} dependencies.storageUtils
154
154
  * @param {(post: Post) => string} dependencies.getPostUrl
@@ -1402,7 +1402,11 @@ class EmailRenderer {
1402
1402
  };
1403
1403
  } else {
1404
1404
  try {
1405
- const size = await this.#imageSize.getImageSizeFromUrl(href);
1405
+ const size = await this.#imageSize.getCachedImageSizeFromUrl(href);
1406
+
1407
+ if (!size || !size.width) {
1408
+ return {href, width: 0, height: null};
1409
+ }
1406
1410
 
1407
1411
  if (size.width >= visibleWidth) {
1408
1412
  if (!visibleHeight) {
@@ -47,7 +47,7 @@ class EmailServiceWrapper {
47
47
  const audienceFeedback = require('../audience-feedback');
48
48
  const storageUtils = require('../../adapters/storage/utils');
49
49
  const emailAnalyticsJobs = require('../email-analytics/jobs');
50
- const {imageSize} = require('../../lib/image');
50
+ const {cachedImageSizeFromUrl} = require('../../lib/image');
51
51
 
52
52
  // capture errors from mailgun client and log them in sentry
53
53
  const errorHandler = (error) => {
@@ -79,7 +79,7 @@ class EmailServiceWrapper {
79
79
  mobiledoc: mobiledocLib,
80
80
  lexical: lexicalLib
81
81
  },
82
- imageSize,
82
+ imageSize: cachedImageSizeFromUrl,
83
83
  urlUtils,
84
84
  storageUtils,
85
85
  getPostUrl: this.getPostUrl,
@@ -2,7 +2,7 @@ const {renderEmailButton} = require('../render-partials/email-button');
2
2
  const {addCreateDocumentOption} = require('../render-utils/add-create-document-option');
3
3
  const {renderWithVisibility} = require('../render-utils/visibility');
4
4
  const {getResizedImageDimensions} = require('../render-utils/get-resized-image-dimensions');
5
- const {isLocalContentImage} = require('../render-utils/is-local-content-image');
5
+ const {isContentImage} = require('../render-utils/is-content-image');
6
6
  const {buildCleanBasicHtmlForElement} = require('../render-utils/build-clean-basic-html-for-element');
7
7
 
8
8
  const showButton = dataset => dataset.showButton && dataset.buttonUrl && dataset.buttonText;
@@ -74,7 +74,7 @@ function emailCTATemplate(dataset, options = {}) {
74
74
  }
75
75
 
76
76
  if (dataset.layout === 'minimal' && dataset.imageUrl) {
77
- if (isLocalContentImage(dataset.imageUrl, options.siteUrl) && options.canTransformImage?.(dataset.imageUrl)) {
77
+ if (isContentImage(dataset.imageUrl, options.siteUrl, options.imageBaseUrl) && options.canTransformImage?.(dataset.imageUrl)) {
78
78
  const [, imagesPath, filename] = dataset.imageUrl.match(/(.*\/content\/images)\/(.*)/);
79
79
  const iconSize = options?.imageOptimization?.internalImageSizes?.['email-cta-minimal-image'] || {width: 256, height: 256}; // default to 256 since we know the image is a square
80
80
  dataset.imageUrl = `${imagesPath}/size/w${iconSize.width}h${iconSize.height}/${filename}`;
@@ -1,6 +1,6 @@
1
1
  const {addCreateDocumentOption} = require('../render-utils/add-create-document-option');
2
2
  const {getAvailableImageWidths} = require('../render-utils/get-available-image-widths');
3
- const {isLocalContentImage} = require('../render-utils/is-local-content-image');
3
+ const {isContentImage} = require('../render-utils/is-content-image');
4
4
  const {isUnsplashImage} = require('../render-utils/is-unsplash-image');
5
5
  const {getResizedImageDimensions} = require('../render-utils/get-resized-image-dimensions');
6
6
  const {setSrcsetAttribute} = require('../render-utils/srcset-attribute');
@@ -79,7 +79,7 @@ function renderGalleryNode(node, options = {}) {
79
79
  if (
80
80
  defaultMaxWidth &&
81
81
  image.width > defaultMaxWidth &&
82
- isLocalContentImage(image.src, options.siteUrl) &&
82
+ isContentImage(image.src, options.siteUrl, options.imageBaseUrl) &&
83
83
  canTransformImage &&
84
84
  canTransformImage(image.src)
85
85
  ) {
@@ -112,7 +112,7 @@ function renderGalleryNode(node, options = {}) {
112
112
  img.setAttribute('height', newImageDimensions.height);
113
113
  }
114
114
 
115
- if (isLocalContentImage(image.src, options.siteUrl) && options.canTransformImage && options.canTransformImage(image.src)) {
115
+ if (isContentImage(image.src, options.siteUrl, options.imageBaseUrl) && options.canTransformImage && options.canTransformImage(image.src)) {
116
116
  // find available image size next up from 2x600 so we can use it for the "retina" src
117
117
  const availableImageWidths = getAvailableImageWidths(image, options.imageOptimization.contentImageSizes);
118
118
  const srcWidth = availableImageWidths.find(width => width >= 1200);
@@ -1,5 +1,5 @@
1
1
  const {getAvailableImageWidths} = require('../render-utils/get-available-image-widths');
2
- const {isLocalContentImage} = require('../render-utils/is-local-content-image');
2
+ const {isContentImage} = require('../render-utils/is-content-image');
3
3
  const {setSrcsetAttribute} = require('../render-utils/srcset-attribute');
4
4
  const {getResizedImageDimensions} = require('../render-utils/get-resized-image-dimensions');
5
5
  const {addCreateDocumentOption} = require('../render-utils/add-create-document-option');
@@ -49,7 +49,7 @@ function renderImageNode(node, options = {}) {
49
49
  if (
50
50
  defaultMaxWidth &&
51
51
  node.width > defaultMaxWidth &&
52
- isLocalContentImage(node.src, options.siteUrl) &&
52
+ isContentImage(node.src, options.siteUrl, options.imageBaseUrl) &&
53
53
  canTransformImage &&
54
54
  canTransformImage(node.src)
55
55
  ) {
@@ -96,7 +96,7 @@ function renderImageNode(node, options = {}) {
96
96
  img.setAttribute('width', imageDimensions.width);
97
97
  img.setAttribute('height', imageDimensions.height);
98
98
 
99
- if (isLocalContentImage(node.src, options.siteUrl) && options.canTransformImage?.(node.src)) {
99
+ if (isContentImage(node.src, options.siteUrl, options.imageBaseUrl) && options.canTransformImage?.(node.src)) {
100
100
  // find available image size next up from 2x600 so we can use it for the "retina" src
101
101
  const availableImageWidths = getAvailableImageWidths(node, options.imageOptimization.contentImageSizes);
102
102
  const srcWidth = availableImageWidths.find(width => width >= 1200);
@@ -0,0 +1,18 @@
1
+ const matchesContentImagePath = function (url, baseUrl = '', pattern = /^\/?content\/images\//) {
2
+ const normalized = baseUrl.replace(/\/$/, '');
3
+ const path = url.replace(normalized, '');
4
+ return pattern.test(path);
5
+ };
6
+
7
+ const isLocalContentImage = function (url, siteUrl = '') {
8
+ return matchesContentImagePath(url, siteUrl, /^(\/.*|__GHOST_URL__)\/?content\/images\//);
9
+ };
10
+
11
+ const isContentImage = function (url, siteUrl = '', imageBaseUrl = '') {
12
+ return isLocalContentImage(url, siteUrl) || Boolean(imageBaseUrl && matchesContentImagePath(url, imageBaseUrl));
13
+ };
14
+
15
+ module.exports = {
16
+ isLocalContentImage,
17
+ isContentImage
18
+ };
@@ -1,4 +1,4 @@
1
- const {isLocalContentImage} = require('./is-local-content-image');
1
+ const {isContentImage} = require('./is-content-image');
2
2
  const {getAvailableImageWidths} = require('./get-available-image-widths');
3
3
  const {isUnsplashImage} = require('./is-unsplash-image');
4
4
 
@@ -9,14 +9,14 @@ const getSrcsetAttribute = function ({src, width, options}) {
9
9
  return;
10
10
  }
11
11
 
12
- if (isLocalContentImage(src, options.siteUrl) && options.canTransformImage && !options.canTransformImage(src)) {
12
+ if (isContentImage(src, options.siteUrl, options.imageBaseUrl) && options.canTransformImage && !options.canTransformImage(src)) {
13
13
  return;
14
14
  }
15
15
 
16
16
  const srcsetWidths = getAvailableImageWidths({width}, options.imageOptimization.contentImageSizes);
17
17
 
18
- // apply srcset if this is a relative image that matches Ghost's image url structure
19
- if (isLocalContentImage(src, options.siteUrl)) {
18
+ // apply srcset if this is a local or CDN image that matches Ghost's image url structure
19
+ if (isContentImage(src, options.siteUrl, options.imageBaseUrl)) {
20
20
  const [, imagesPath, filename] = src.match(/(.*\/content\/images)\/(.*)/);
21
21
  const srcs = [];
22
22
 
@@ -24,8 +24,8 @@ const messages = {
24
24
  unableToCheckout: 'Unable to initiate checkout session',
25
25
  inviteOnly: 'This site is invite-only, contact the owner for access.',
26
26
  paidOnly: 'This site only accepts paid members.',
27
- memberNotFound: 'No member exists with this email address.',
28
- memberNotFoundSignUp: 'No member exists with this email address. Please sign up first.',
27
+ memberNotFound: 'No member exists with this e-mail address.',
28
+ memberNotFoundSignUp: 'No member exists with this e-mail address. Please sign up first.',
29
29
  invalidType: 'Invalid checkout type.',
30
30
  notConfigured: 'This site is not accepting payments at the moment.',
31
31
  invalidNewsletters: 'Cannot subscribe to invalid newsletters {newsletters}',
@@ -886,33 +886,9 @@ module.exports = class RouterController {
886
886
  const member = await this._memberRepository.get({email: normalizedEmail});
887
887
 
888
888
  if (!member) {
889
- // Member doesn't exist - to prevent enumeration, we don't reveal this
890
- // If self-signup is allowed, send a signup email so they can create an account
891
- // If self-signup is disabled (invite-only), silently return to prevent enumeration
892
- if (this._allowSelfSignup()) {
893
- const blockedEmailDomains = this._settingsCache.get('all_blocked_email_domains');
894
- const emailDomain = normalizedEmail.split('@')[1]?.toLowerCase();
895
- if (emailDomain && blockedEmailDomains.includes(emailDomain)) {
896
- // To prevent enumeration, we don't reveal this
897
- return {};
898
- }
899
-
900
- const tokenData = {
901
- reqIp: req.ip ?? undefined,
902
- attribution: await this._memberAttributionService.getAttribution(req.body.urlHistory)
903
- };
904
- // Send a signup email - this allows them to create an account
905
- return await this._sendEmailWithMagicLink({
906
- email: normalizedEmail,
907
- tokenData,
908
- requestedType: 'signup',
909
- referrer
910
- });
911
- }
912
-
913
- // Self-signup disabled (invite-only): silently return empty response
914
- // to prevent member enumeration
915
- return {};
889
+ throw new errors.BadRequestError({
890
+ message: this._allowSelfSignup() ? tpl(messages.memberNotFoundSignUp) : tpl(messages.memberNotFound)
891
+ });
916
892
  }
917
893
 
918
894
  const tokenData = {};
@@ -980,6 +956,10 @@ module.exports = class RouterController {
980
956
  return res.end(JSON.stringify({offers}));
981
957
  }
982
958
 
959
+ function sendNoOffersAvailable() {
960
+ return sendOffersResponse([]);
961
+ }
962
+
983
963
  if (!identity) {
984
964
  res.writeHead(401);
985
965
  return res.end('Unauthorized');
@@ -1029,45 +1009,50 @@ module.exports = class RouterController {
1029
1009
 
1030
1010
  // No active subscription - return empty offers
1031
1011
  if (activeSubscriptions.length === 0) {
1032
- return sendOffersResponse();
1012
+ return sendNoOffersAvailable();
1033
1013
  }
1034
1014
 
1035
1015
  // Multiple active subscriptions - edge case, return empty offers to avoid ambiguity
1036
1016
  if (activeSubscriptions.length > 1) {
1037
- return sendOffersResponse();
1017
+ return sendNoOffersAvailable();
1038
1018
  }
1039
1019
 
1040
1020
  const activeSubscription = activeSubscriptions[0];
1041
1021
 
1022
+ // If subscription is already set to cancel, don't show retention offers
1023
+ if (activeSubscription.get('cancel_at_period_end')) {
1024
+ return sendNoOffersAvailable();
1025
+ }
1026
+
1042
1027
  // If subscription already has an offer applied (e.g. signup offer), don't show retention offers
1043
1028
  if (activeSubscription.get('offer_id')) {
1044
- return sendOffersResponse();
1029
+ return sendNoOffersAvailable();
1045
1030
  }
1046
1031
 
1047
1032
  // If subscription is in a trial period (either offer-based or tier-based), don't show retention offers
1048
1033
  const trialEndAt = activeSubscription.get('trial_end_at');
1049
1034
  if (trialEndAt && trialEndAt > new Date()) {
1050
- return sendOffersResponse();
1035
+ return sendNoOffersAvailable();
1051
1036
  }
1052
1037
 
1053
1038
  // Get tier and cadence from the subscription
1054
1039
  const stripePrice = activeSubscription.related('stripePrice');
1055
1040
  if (!stripePrice || !stripePrice.id) {
1056
- return sendOffersResponse();
1041
+ return sendNoOffersAvailable();
1057
1042
  }
1058
1043
 
1059
1044
  const stripeProduct = stripePrice.related('stripeProduct');
1060
1045
 
1061
1046
  // If the stripe product is not found, return empty offers
1062
1047
  if (!stripeProduct || !stripeProduct.id) {
1063
- return sendOffersResponse();
1048
+ return sendNoOffersAvailable();
1064
1049
  }
1065
1050
 
1066
1051
  const product = stripeProduct.related('product');
1067
1052
 
1068
1053
  // If the product is not found, return empty offers
1069
1054
  if (!product || !product.id) {
1070
- return sendOffersResponse();
1055
+ return sendNoOffersAvailable();
1071
1056
  }
1072
1057
 
1073
1058
  const tierId = product.id;
@@ -31,7 +31,8 @@ const messages = {
31
31
  offerAlreadyRedeemed: 'This offer has already been redeemed on this subscription',
32
32
  subscriptionNotActive: 'Cannot apply offer to an inactive subscription',
33
33
  subscriptionHasOffer: 'Subscription already has an offer applied',
34
- subscriptionInTrial: 'Cannot apply offer to a subscription in a trial period'
34
+ subscriptionInTrial: 'Cannot apply offer to a subscription in a trial period',
35
+ subscriptionCancelling: 'Cannot apply retention offer to a subscription that is already cancelling'
35
36
  };
36
37
 
37
38
  const SUBSCRIPTION_STATUS_TRIALING = 'trialing';
@@ -1769,6 +1770,12 @@ module.exports = class MemberRepository {
1769
1770
  });
1770
1771
  }
1771
1772
 
1773
+ if (offer.redemption_type === 'retention' && subscriptionModel.get('cancel_at_period_end')) {
1774
+ throw new errors.BadRequestError({
1775
+ message: tpl(messages.subscriptionCancelling)
1776
+ });
1777
+ }
1778
+
1772
1779
  if (offer.tier && offer.tier.id !== tierId) {
1773
1780
  throw new errors.BadRequestError({
1774
1781
  message: tpl(messages.offerTierMismatch)
@@ -19,8 +19,8 @@ const statusTransformer = mapKeyValues({
19
19
  }]
20
20
  });
21
21
 
22
- const rejectNonStatusTransformer = input => mapQuery(input, function (value, key) {
23
- if (key !== 'status') {
22
+ const rejectInvalidTransformer = input => mapQuery(input, function (value, key) {
23
+ if (key !== 'status' && key !== 'redemption_type') {
24
24
  return;
25
25
  }
26
26
 
@@ -29,7 +29,7 @@ const rejectNonStatusTransformer = input => mapQuery(input, function (value, key
29
29
  };
30
30
  });
31
31
 
32
- const mongoTransformer = flowRight(statusTransformer, rejectNonStatusTransformer);
32
+ const mongoTransformer = flowRight(statusTransformer, rejectInvalidTransformer);
33
33
 
34
34
  /**
35
35
  * @typedef {object} BaseOptions
@@ -240,7 +240,7 @@
240
240
  },
241
241
  "portal": {
242
242
  "url": "https://cdn.jsdelivr.net/ghost/portal@~{version}/umd/portal.min.js",
243
- "version": "2.62"
243
+ "version": "2.63"
244
244
  },
245
245
  "sodoSearch": {
246
246
  "url": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/sodo-search.min.js",
@@ -52,7 +52,8 @@ const PRIVATE_FEATURES = [
52
52
  'emailUniqueid',
53
53
  'themeTranslation',
54
54
  'indexnow',
55
- 'transistor'
55
+ 'transistor',
56
+ 'retentionOffers'
56
57
  ];
57
58
 
58
59
  module.exports.GA_KEYS = [...GA_FEATURES];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghost",
3
- "version": "6.18.1",
3
+ "version": "6.18.2",
4
4
  "description": "The professional publishing platform",
5
5
  "author": "Ghost Foundation",
6
6
  "homepage": "https://ghost.org",
@@ -83,7 +83,7 @@
83
83
  "@tryghost/helpers": "1.1.97",
84
84
  "@tryghost/html-to-plaintext": "1.0.4",
85
85
  "@tryghost/http-cache-utils": "0.1.20",
86
- "@tryghost/i18n": "file:components/tryghost-i18n-6.18.1.tgz",
86
+ "@tryghost/i18n": "file:components/tryghost-i18n-6.18.2.tgz",
87
87
  "@tryghost/image-transform": "1.4.6",
88
88
  "@tryghost/job-manager": "1.0.3",
89
89
  "@tryghost/kg-card-factory": "5.1.7",
@@ -105,7 +105,7 @@
105
105
  "@tryghost/mw-vhost": "1.0.1",
106
106
  "@tryghost/nodemailer": "0.3.48",
107
107
  "@tryghost/nql": "0.12.8",
108
- "@tryghost/parse-email-address": "file:components/tryghost-parse-email-address-6.18.1.tgz",
108
+ "@tryghost/parse-email-address": "file:components/tryghost-parse-email-address-6.18.2.tgz",
109
109
  "@tryghost/pretty-cli": "1.2.47",
110
110
  "@tryghost/prometheus-metrics": "1.0.2",
111
111
  "@tryghost/promise": "0.3.15",
@@ -271,8 +271,8 @@
271
271
  "jackspeak": "2.3.6",
272
272
  "moment": "2.24.0",
273
273
  "moment-timezone": "0.5.45",
274
- "@tryghost/i18n": "file:components/tryghost-i18n-6.18.1.tgz",
275
- "@tryghost/parse-email-address": "file:components/tryghost-parse-email-address-6.18.1.tgz"
274
+ "@tryghost/i18n": "file:components/tryghost-i18n-6.18.2.tgz",
275
+ "@tryghost/parse-email-address": "file:components/tryghost-parse-email-address-6.18.2.tgz"
276
276
  },
277
277
  "nx": {
278
278
  "targets": {
Binary file