Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 17 additions & 14 deletions ghost/admin/app/components/editor/email-size-warning.hbs
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
{{#if this.isEnabled}}
{{! Watch for post.updatedAt changes to trigger recalculation after saves }}
<span {{did-update this.checkEmailSize @post.updatedAtUTC}} class="gh-editor-email-size-warning-container">
<span class="gh-editor-email-size-warning gh-editor-email-size-warning--{{this.warningLevel}}" data-warning-active={{if this.warningLevel "true" "false"}}>
{{#if this.warningLevel}}
<span class="{{concat "gh-editor-email-warning-icon-" this.warningLevel}}">
{{#if (has-block)}}
<div>
{{yield this.overLimit this.emailSizeKb}}
</div>
{{else}}
<span {{did-update this.checkEmailSize @post.updatedAtUTC}} class="gh-editor-email-size-warning-container">
<span class="gh-editor-email-size-warning gh-editor-email-size-warning--{{this.overLimit}}" data-warning-active={{if this.overLimit "true" "false"}}>
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class gh-editor-email-size-warning--{{this.overLimit}} will produce gh-editor-email-size-warning--true or gh-editor-email-size-warning--false. This differs from the previous implementation which used specific warning levels like 'yellow' or 'red'. Ensure corresponding CSS exists for these boolean-based class names, or consider using a more semantic class name.

Copilot uses AI. Check for mistakes.
{{#if this.overLimit}}
{{svg-jar "email-warning"}}
</span>
{{/if}}
</span>
{{#if this.overLimit}}
<div class="gh-editor-email-size-popup">
<div class="gh-editor-email-size-popup-title">Looks like this is a long post</div>
<div class="gh-editor-email-size-popup-text">Emails may get clipped in the inbox behind a "View entire message" link when they're over 100kB.</div>
<div class="gh-editor-email-size-popup-used">You've used: <span class="yellow">{{this.emailSizeKb}}kB</span></div>
</div>
{{/if}}
</span>
{{#if this.warningLevel}}
<div class="gh-editor-email-size-popup">
<div class="gh-editor-email-size-popup-title">Looks like this is a long post</div>
<div class="gh-editor-email-size-popup-text">Email newsletters may get cut off in the inbox behind a "View entire message" link when they're over 100kB.</div>
<div class="gh-editor-email-size-popup-used">You've used: <span class={{this.warningLevel}}>{{this.emailSizeKb}}kB</span></div>
</div>
{{/if}}
</span>
{{/if}}
{{/if}}
18 changes: 7 additions & 11 deletions ghost/admin/app/components/editor/email-size-warning.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,15 @@ export default class EmailSizeWarningComponent extends Component {
@service feature;
@service settings;

@tracked warningLevel = null;
@tracked overLimit = false;
@tracked emailSizeKb = null;

get isEnabled() {
return this.feature.emailSizeWarnings
&& this.settings.editorDefaultEmailRecipients !== 'disabled'
&& this.post
&& !this.post.email
&& !this.post.isNew;
}

get post() {
return this.args.post;
&& this.args.post
&& !this.args.post.email
&& !this.args.post.isNew;
}

constructor() {
Expand All @@ -40,8 +36,8 @@ export default class EmailSizeWarningComponent extends Component {

@task({restartable: true})
*checkEmailSizeTask() {
const result = yield this.emailSizeWarning.fetchEmailSize(this.post);
this.warningLevel = result.warningLevel;
this.emailSizeKb = result.emailSizeKb;
const result = yield this.emailSizeWarning.fetchEmailSize(this.args.post);
this.overLimit = result.overLimit;
this.emailSizeKb = result.emailSizeKb || 0;
}
}
11 changes: 11 additions & 0 deletions ghost/admin/app/components/editor/modals/preview/email.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@
/>
</form>
</div>
<Editor::EmailSizeWarning @post={{@post}} as |overLimit emailSizeKb| >
{{#if overLimit}}
<div class="gh-email-preview-clip-container">
{{svg-jar "email-warning"}}
<div>
<div class="gh-email-preview-clip-title">This email is <span class="yellow-d1">{{emailSizeKb}}kB</span></div>
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent styling approach. This line uses a yellow-d1 class directly on the span, while line 45 in options.hbs uses the same pattern. Consider if this should match the popup implementation which uses inline style or CSS variable for consistency across the codebase.

Copilot uses AI. Check for mistakes.
<div class="gh-email-preview-clip-description">Emails may get clipped in the inbox behind a “View entire message” link when they’re over 100kB.</div>
</div>
</div>
{{/if}}
</Editor::EmailSizeWarning>
<iframe
class="gh-pe-iframe"
title="Email preview"
Expand Down
11 changes: 11 additions & 0 deletions ghost/admin/app/components/editor/modals/publish-flow/options.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@
/>
</div>
{{/liquid-if}}

{{#if (and @publishOptions.willEmail (eq this.openSection "publishType"))}}
<Editor::EmailSizeWarning @post={{@publishOptions.post}} as |overLimit emailSizeKb| >
{{#if overLimit}}
<p class="gh-box gh-content-box pa gh-publish-setting-email-warning {{if (eq this.openSection "publishType") "open"}}">
<div class="gh-email-preview-clip-title mb">This email is <span class="yellow-d1">{{emailSizeKb}}kB</span></div>
<div class="gh-email-preview-clip-description">Email newsletters may get clipped in the inbox behind a "View entire message" link when they're over 100kB.</div>
</p>
{{/if}}
</Editor::EmailSizeWarning>
{{/if}}
</div>

{{#unless @publishOptions.emailUnavailable}}
Expand Down
21 changes: 9 additions & 12 deletions ghost/admin/app/services/email-size-warning.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import Service, {inject as service} from '@ember/service';
import {inject} from 'ghost-admin/decorators/inject';
import {task} from 'ember-concurrency';

const YELLOW_THRESHOLD = 90 * 1024; // 90KB
const RED_THRESHOLD = 100 * 1024; // 100KB
const LIMIT_THRESHOLD = 100 * 1024; // 100KB

// Rewritten URLs in real emails have a fixed format:
// {siteUrl}/r/{8-char-hex}?m={36-char-uuid}
Expand Down Expand Up @@ -31,11 +30,11 @@ export default class EmailSizeWarningService extends Service {
* Multiple concurrent calls for the same post version will share a single API request.
*
* @param {Object} post - The post model
* @returns {Promise<{warningLevel: string|null, emailSizeKb: number|null}>}
* @returns {Promise<{overLimit: boolean, emailSizeKb: number|null}>}
*/
fetchEmailSize(post) {
if (!post?.id || post.isNew) {
return Promise.resolve({warningLevel: null, emailSizeKb: null});
return Promise.resolve({overLimit: false, emailSizeKb: null});
}

const postId = post.id;
Expand Down Expand Up @@ -108,7 +107,7 @@ export default class EmailSizeWarningService extends Service {
*_fetchTask(post) {
yield this._loadNewsletter();
if (!this._newsletter) {
return {warningLevel: null, emailSizeKb: null};
return {overLimit: false, emailSizeKb: null};
}

try {
Expand All @@ -127,20 +126,18 @@ export default class EmailSizeWarningService extends Service {
const estimatedSizeBytes = Math.max(0, previewSizeBytes + linkAdjustment);

const emailSizeKb = Math.round(estimatedSizeBytes / 1024);
let warningLevel = null;
let overLimit = false;

if (estimatedSizeBytes >= RED_THRESHOLD) {
warningLevel = 'red';
} else if (estimatedSizeBytes >= YELLOW_THRESHOLD) {
warningLevel = 'yellow';
if (estimatedSizeBytes >= LIMIT_THRESHOLD) {
overLimit = true;
}

return {warningLevel, emailSizeKb};
return {overLimit, emailSizeKb};
}
} catch (error) {
console.error('Email size check failed:', error); // eslint-disable-line no-console
}

return {warningLevel: null, emailSizeKb: null};
return {overLimit: false, emailSizeKb: null};
}
}
9 changes: 9 additions & 0 deletions ghost/admin/app/styles/components/publishmenu.css
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,15 @@
line-height: 1.35em;
}

.gh-publish-setting-email-warning {
transition: all 0.3s ease;
}

.gh-publish-setting-email-warning.open {
transform: translateY(-12px);
margin-bottom: 12px;
}

@media (max-width: 560px) {
.gh-publish-setting-trigger {
font-size: 1.7rem;
Expand Down
2 changes: 1 addition & 1 deletion ghost/admin/app/styles/layouts/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@
}

.gh-editor-email-size-warning svg path {
stroke: currentColor !important;
stroke: var(--yellow) !important;
}

.gh-editor-email-size-popup {
Expand Down
41 changes: 41 additions & 0 deletions ghost/admin/app/styles/layouts/preview-email.css
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,44 @@ p .gh-preview-email-address {
display: none
}
}

.gh-email-preview-clip-container {
display: flex;
background-color: var(--whitegrey-l2);
border-bottom: 1px solid var(--whitegrey);
padding: 20px;
gap: 12px;
align-items: flex-start;
}

.gh-email-preview-clip-container svg {
margin-top: 3px;
width: 16px;
min-width: 16px;
height: auto;
}

.gh-email-preview-clip-container svg path {
stroke: var(--yellow-d1);
}

.gh-email-preview-clip-title {
font-weight: 600;
font-size: 1.4rem;
color: var(--black);
}

.gh-email-preview-clip-title.mb {
margin-bottom: 4px;
}

.gh-email-preview-clip-description {
font-size: 1.3rem;
text-wrap: pretty;
color: var(--middarkgrey)
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing semicolon at end of CSS declaration. While this may work in most browsers, it's best practice to always include the semicolon for consistency and to prevent potential issues.

Copilot uses AI. Check for mistakes.
}

.gh-email-preview-clip-used {
font-size: 1.3rem;
font-weight: 600;
}
Loading