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
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const logging = require('@tryghost/logging');
const {default: ObjectID} = require('bson-objectid');
const {createTransactionalMigration} = require('../../utils');

const DEFAULT_SLUG = 'default-automated-email';

module.exports = createTransactionalMigration(
async function up(knex) {
logging.info('Inserting default email_design_settings row');

const existing = await knex('email_design_settings').where({slug: DEFAULT_SLUG}).first();

if (existing) {
logging.warn('Default email_design_settings row already exists, skipping');
return;
}
Comment on lines +13 to +16
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

question: When would this ever happen? If it did, should we throw an error instead?

Copy link
Copy Markdown
Collaborator Author

@cmraible cmraible Mar 26, 2026

Choose a reason for hiding this comment

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

When would this ever happen?

In the normal course of a Ghost site, it's highly unlikely that it would, so honest answer is probably never.

If it did, should we throw an error instead?

Good question - I think not, but I'm happy to be challenged on that. Rationale for not throwing an error: it doesn't hurt anything to no-op here: if the row already exists, nothing really breaks in Ghost.

If we instead throw an error, it would block any subsequent migrations from running, and prevent Ghost from booting. I do think there's a valid fail-fast argument to be made that preventing Ghost from booting if the schema/data is in an unexpected state is the right thing to do, which would be an interesting discussion. Practically speaking though, I'm mostly just following the existing conventions in previous migrations:

rg -U 'if \(existing\w*\).*\n.*\n.*return;' ghost/core/core/server/data/migrations/versions/

ghost/core/core/server/data/migrations/versions/5.40/2023-03-21-19-02-add-self-serve-integration-api-key.js
39:        if (existingKey) {
40:            logging.warn('Admin API key for "Self-Serve Migration Integration" already exists');
41:            return;

ghost/core/core/server/data/migrations/versions/6.14/2026-01-15-12-01-00-add-transistor-integration-api-key.js
37:        if (existingKey) {
38:            logging.warn('Admin API key already exists for Transistor integration');
39:            return;

ghost/core/core/server/data/migrations/versions/5.40/2023-03-21-18-42-add-self-serve-integration-role.js
14:        if (existingRole) {
15:            logging.warn('"Self-Serve Migration Integration" role already exists, skipping');
16:            return;

ghost/core/core/server/data/migrations/versions/6.14/2026-01-15-12-00-00-add-transistor-integration.js
14:        if (existing) {
15:            logging.warn('Found existing Transistor integration');
16:            return;

ghost/core/core/server/data/migrations/versions/5.113/2025-03-07-12-24-00-add-super-editor.js
14:        if (existingRole) {
15:            logging.warn('"Super Editor" role already exists, skipping');
16:            return;

ghost/core/core/server/data/migrations/versions/5.40/2023-03-21-18-52-add-self-serve-integration.js
15:        if (existingIntegration) {
16:            logging.warn('Integration already exists, skipping');
17:            return;

ghost/core/core/server/data/migrations/versions/6.23/2026-03-26-05-50-35-insert-default-email-design-settings-row.js
13:        if (existing) {
14:            logging.warn('Default email_design_settings row already exists, skipping');
15:            return;

ghost/core/core/server/data/migrations/versions/5.3/2022-07-06-09-26-add-ghost-explore-integration-api-key.js
39:        if (existingKey) {
40:            logging.warn('Admin API key already exists');
41:            return;

ghost/core/core/server/data/migrations/versions/5.100/2024-11-06-04-45-15-add-activitypub-integration.js
17:        if (existing) {
18:            logging.warn('Found existing Ghost ActivityPub integration');
19:            return;

ghost/core/core/server/data/migrations/versions/5.3/2022-07-06-07-58-add-ghost-explore-integration-role.js
14:        if (existingRole) {
15:            logging.warn('"Ghost Explore Integration" role already exists, skipping');
16:            return;

ghost/core/core/server/data/migrations/versions/5.3/2022-07-06-09-17-add-ghost-explore-integration.js
15:        if (existingIntegration) {
16:            logging.warn('Integration already exists, skipping');
17:            return;


await knex('email_design_settings').insert({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i'd meant to bring this up yesterday: would it make sense to seed these styles from the first active newsletter (avoiding saying "default" lol) if it exists?

one spot it could get clunky is if there's something like a header image in the newsletter, it might not make sense in the welcome email.

i think seeding like how you have it is probably right but just wanted to bring it up before we ship this piece. A "copy design from newsletter" button would probably be a better way to smooth out the experience for people that do want stuff the same or want to save most of the effort of copying colors etc.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The risk of this, I think, is if someone already has a welcome email set up, they might be surprised to see its design change. But you could make the argument the other way too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yeah I think I agree that this is probably right as-is, to avoid surprises with the welcome email design changing without any user intervention.

A "copy design from newsletter" button would probably be a better way to smooth out the experience for people that do want stuff the same or want to save most of the effort of copying colors etc.

This is a good idea though, might be something we can fit in before GA

id: (new ObjectID()).toHexString(),
slug: DEFAULT_SLUG,
background_color: 'light',
header_background_color: 'transparent',
show_header_title: true,
button_color: 'accent',
button_corners: 'rounded',
button_style: 'fill',
link_color: 'accent',
link_style: 'underline',
body_font_category: 'sans_serif',
title_font_category: 'sans_serif',
title_font_weight: 'bold',
image_corners: 'square',
show_badge: true,
created_at: knex.raw('current_timestamp')
});
},
async function down(knex) {
logging.info('Deleting default email_design_settings row');

await knex('email_design_settings').where({slug: DEFAULT_SLUG}).del();
}
);
8 changes: 8 additions & 0 deletions ghost/core/core/server/data/schema/fixtures/fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@
}
]
},
{
"name": "EmailDesignSetting",
"entries": [
{
"slug": "default-automated-email"
}
]
},
{
"name": "Tag",
"entries": [
Expand Down
27 changes: 27 additions & 0 deletions ghost/core/core/server/models/email-design-setting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const ghostBookshelf = require('./base');
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

note: creating the model is required for the fixtures to work

yarn knex-migrator init doesn't run all the migrations in the versions directory, so we have to add any default data like this in the migration (for existing sites) and the fixtures (for new sites). The fixture manager depends on the model, hence including it in this PR.


const EmailDesignSetting = ghostBookshelf.Model.extend({
tableName: 'email_design_settings',

defaults() {
return {
background_color: 'light',
header_background_color: 'transparent',
show_header_title: true,
button_color: 'accent',
button_corners: 'rounded',
button_style: 'fill',
link_color: 'accent',
link_style: 'underline',
body_font_category: 'sans_serif',
title_font_category: 'sans_serif',
title_font_weight: 'bold',
image_corners: 'square',
show_badge: true
};
}
});

module.exports = {
EmailDesignSetting: ghostBookshelf.model('EmailDesignSetting', EmailDesignSetting)
};
2 changes: 1 addition & 1 deletion ghost/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ghost",
"version": "6.24.0",
"version": "6.25.0-rc.0",
"description": "The professional publishing platform",
"author": "Ghost Foundation",
"homepage": "https://ghost.org",
Expand Down
2 changes: 1 addition & 1 deletion ghost/core/test/unit/server/data/schema/integrity.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const validateRouteSettings = require('../../../../../core/server/services/route
describe('DB version integrity', function () {
// Only these variables should need updating
const currentSchemaHash = 'c193998d02d13e8b85162315d938772c';
const currentFixturesHash = '4dcbd7b52bc9ce23e6f5f1673118ba73';
const currentFixturesHash = '987b256376357b65fb0d9ef7137c0b00';
const currentSettingsHash = 'a102b80d2ab0cd92325ed007c94d7da6';
const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01';

Expand Down
Loading