Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# Docker Compose profiles to enable
## Run `docker compose config --profiles` to see all available profiles
## See https://docs.docker.com/compose/how-tos/profiles/ for more information
COMPOSE_PROFILES=ghost
# COMPOSE_PROFILES=stripe

# Debug level to pass to Ghost
# DEBUG=
Expand Down
6 changes: 6 additions & 0 deletions .github/scripts/dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,12 @@ async function handleStripe() {
}
debug('at least one command provided');

debug('resetting nx');
await exec("yarn nx reset --onlyDaemon");
debug('nx reset');
await exec("yarn nx daemon --start");
debug('nx daemon started');

console.log(`Running projects: ${commands.map(c => chalk.green(c.name)).join(', ')}`);

debug('creating concurrently promise');
Expand Down
1 change: 1 addition & 0 deletions apps/admin/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default defineConfig({
"process.env.DEBUG": false, // Shim env var utilized by the @tryghost/nql package
},
server: {
port: 5174,
host: true,
allowedHosts: true
},
Expand Down
20 changes: 20 additions & 0 deletions compose.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ services:
- ghost-dev-media:/home/ghost/ghost/core/content/media
- ghost-dev-files:/home/ghost/ghost/core/content/files
- ghost-dev-logs:/home/ghost/ghost/core/content/logs
- shared-config:/mnt/shared-config:ro
environment:
NODE_ENV: development
NODE_TLS_REJECT_UNAUTHORIZED: "0"
Expand Down Expand Up @@ -98,6 +99,9 @@ services:
condition: service_healthy
mailpit:
condition: service_healthy
stripe:
condition: service_healthy
required: false
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://localhost:2368',{redirect:'manual'}).then(r=>process.exit(r.status<500?0:1)).catch(()=>process.exit(1))"]
timeout: 5s
Expand All @@ -123,10 +127,26 @@ services:
ghost-dev:
condition: service_healthy

stripe:
image: stripe/stripe-cli:latest
container_name: ghost-dev-stripe
entrypoint: ["/entrypoint.sh"]
profiles: ["stripe"]
volumes:
- ./docker/stripe/entrypoint.sh:/entrypoint.sh:ro
- shared-config:/mnt/shared-config
environment:
- GHOST_URL=http://ghost-dev:2368
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY:-}
healthcheck:
test: ["CMD", "test", "-f", "/mnt/shared-config/.env.stripe"]
interval: 1s
retries: 120

volumes:
mysql-data:
redis-data:
shared-config:
ghost-dev-data:
ghost-dev-images:
ghost-dev-media:
Expand Down
1 change: 1 addition & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ services:
- ./docker/stripe/entrypoint.sh:/entrypoint.sh:ro
- shared-config:/mnt/shared-config
environment:
- GHOST_URL=http://server:2368
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY:-}
- STRIPE_PUBLISHABLE_KEY=${STRIPE_PUBLISHABLE_KEY:-}
- STRIPE_ACCOUNT_ID=${STRIPE_ACCOUNT_ID:-}
Expand Down
2 changes: 1 addition & 1 deletion docker/dev-gateway/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ RUN caddy add-package github.com/caddyserver/transform-encoder

# Default proxy targets (can be overridden via environment variables)
ENV GHOST_BACKEND=ghost-dev:2368 \
ADMIN_DEV_SERVER=host.docker.internal:5173 \
ADMIN_DEV_SERVER=host.docker.internal:5174 \
ADMIN_LIVE_RELOAD_SERVER=host.docker.internal:4200 \
PORTAL_DEV_SERVER=host.docker.internal:4175 \
COMMENTS_DEV_SERVER=host.docker.internal:7173 \
Expand Down
6 changes: 3 additions & 3 deletions docker/dev-gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The Caddy reverse proxy container:
Caddy uses environment variables (set in `compose.dev.yaml`) to configure proxy targets:

- `GHOST_BACKEND` - Ghost container hostname (e.g., `ghost-dev:2368`)
- `ADMIN_DEV_SERVER` - React admin dev server (e.g., `host.docker.internal:5173`)
- `ADMIN_DEV_SERVER` - React admin dev server (e.g., `host.docker.internal:5174`)
- `ADMIN_LIVE_RELOAD_SERVER` - Ember live reload WebSocket (e.g., `host.docker.internal:4200`)
- `PORTAL_DEV_SERVER` - Portal dev server (e.g., `host.docker.internal:4175`)
- `COMMENTS_DEV_SERVER` - Comments UI (e.g., `host.docker.internal:7173`)
Expand Down Expand Up @@ -48,8 +48,8 @@ The Caddyfile defines these routing rules:
| `/ghost/assets/signup-form/*` | Signup dev server (port 6174) | Signup form widget |
| `/ghost/assets/sodo-search/*` | Search dev server (port 4178) | Search widget (JS + CSS) |
| `/ghost/assets/announcement-bar/*` | Announcement dev server (port 4177) | Announcement widget |
| `/ghost/assets/*` | Admin dev server (port 5173) | Other admin assets (fallback) |
| `/ghost/*` | Admin dev server (port 5173) | Admin interface |
| `/ghost/assets/*` | Admin dev server (port 5174) | Other admin assets (fallback) |
| `/ghost/*` | Admin dev server (port 5174) | Admin interface |
| Everything else | Ghost backend | Main Ghost application |

**Note:** All port numbers listed are the host ports where dev servers run by default.
12 changes: 12 additions & 0 deletions docker/ghost-dev/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ else
echo "WARNING: Tinybird not enabled: .env.tinybird file not found at /mnt/shared-config/.env.tinybird" >&2
fi


# Configure Stripe webhook secret
if [ -f /mnt/shared-config/.env.stripe ]; then
source /mnt/shared-config/.env.stripe
if [ -n "${STRIPE_WEBHOOK_SECRET:-}" ]; then
export WEBHOOK_SECRET="$STRIPE_WEBHOOK_SECRET"
echo "Stripe webhook secret configured successfully"
else
echo "WARNING: Stripe webhook secret not found in shared config"
fi
fi

# Execute the CMD
exec "$@"

4 changes: 2 additions & 2 deletions docker/stripe/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ else
fi

# Start stripe listen in the background
echo "Starting Stripe webhook listener forwarding to http://server:2368/members/webhooks/stripe/"
stripe listen --forward-to http://server:2368/members/webhooks/stripe/ --api-key "${STRIPE_SECRET_KEY}" &
echo "Starting Stripe webhook listener forwarding to ${GHOST_URL}/members/webhooks/stripe/"
stripe listen --forward-to ${GHOST_URL}/members/webhooks/stripe/ --api-key "${STRIPE_SECRET_KEY}" &
child=$!

# Wait for the child process
Expand Down
4 changes: 2 additions & 2 deletions e2e/data-factory/factories/automated-email-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class AutomatedEmailFactory extends Factory<Partial<AutomatedEmail>, Auto
status: 'active',
name: 'Welcome Email (Free)',
slug: 'member-welcome-email-free',
subject: 'Welcome to {{site.title}}!',
subject: 'Welcome to {site_title}!',
lexical: JSON.stringify(this.defaultLexicalContent()),
sender_name: null,
sender_email: null,
Expand All @@ -45,7 +45,7 @@ export class AutomatedEmailFactory extends Factory<Partial<AutomatedEmail>, Auto
type: 'paragraph',
children: [{
type: 'text',
text: 'Welcome to {{site.title}}!'
text: 'Welcome to {site_title}!'
}]
}],
direction: null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const {createAddColumnMigration} = require('../../utils');

module.exports = createAddColumnMigration('offers', 'redemption_type', {
type: 'string',
maxlength: 50,
nullable: false,
defaultTo: 'signup'
});
1 change: 1 addition & 0 deletions ghost/core/core/server/data/schema/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ module.exports = {
duration_in_months: {type: 'integer', nullable: true},
portal_title: {type: 'string', maxlength: 191, nullable: true},
portal_description: {type: 'string', maxlength: 2000, nullable: true},
redemption_type: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'signup', validations: {isIn: [['signup', 'retention']]}},
created_at: {type: 'dateTime', nullable: false},
updated_at: {type: 'dateTime', nullable: true}
},
Expand Down
4 changes: 4 additions & 0 deletions ghost/core/core/server/models/offer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ const Offer = ghostBookshelf.Model.extend({
actionsCollectCRUD: true,
actionsResourceType: 'offer',

defaults: {
redemption_type: 'signup'
},

product() {
return this.belongsTo('Product', 'product_id', 'id');
}
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ const juice = require('juice');
const lexicalLib = require('../../lib/lexical');
const errors = require('@tryghost/errors');
const {MESSAGES} = require('./constants');
const {wrapReplacementStrings} = require('../koenig/render-utils/replacement-strings');

const REPLACEMENT_REGEX = /%%\{(\w+?)(?:,? *"(.*?)")?\}%%/g;
const UNMATCHED_TOKEN_REGEX = /%%\{.*?\}%%/g;

class MemberWelcomeEmailRenderer {
#wrapperTemplate;
Expand All @@ -18,6 +22,60 @@ class MemberWelcomeEmailRenderer {
this.#wrapperTemplate = this.Handlebars.compile(wrapperSource);
}

/**
* Builds replacement token definitions for member and site data
* @param {Object} options
* @param {Object} options.member - Member data
* @param {string} [options.member.name] - Member's full name
* @param {string} [options.member.email] - Member's email address
* @param {Object} options.siteSettings - Site settings
* @param {string} options.siteSettings.title - Site title
* @param {string} options.siteSettings.url - Site URL
* @returns {{id: string, getValue: () => string|undefined}[]}
*/
#buildReplacementDefinitions({member, siteSettings}) {
return [
{id: 'first_name', getValue: () => {
const name = member.name?.trim();
if (!name) {
return undefined;
}
return name.split(/\s+/)[0];
}},
{id: 'name', getValue: () => member.name},
{id: 'email', getValue: () => member.email},
{id: 'site_title', getValue: () => siteSettings.title},
{id: 'site_url', getValue: () => siteSettings.url}
];
}

/**
* Applies replacement tokens to a string
* Supports fallback values: {first_name, "friend"} renders "friend" if name is empty
* @param {Object} options
* @param {string} options.text - The text to process (content body or subject line)
* @param {Object} options.member - Member data
* @param {Object} options.siteSettings - Site settings
* @param {boolean} [options.escapeHtml=false] - Whether to HTML-escape replaced values
* @returns {string}
*/
#applyReplacements({text, member, siteSettings, escapeHtml = false}) {
const definitions = this.#buildReplacementDefinitions({member, siteSettings});
let processed = wrapReplacementStrings(text);

processed = processed.replace(REPLACEMENT_REGEX, (match, property, fallback) => {
const def = definitions.find(d => d.id === property);
if (def) {
const raw = def.getValue();
const resolved = raw || fallback || '';
return escapeHtml ? this.Handlebars.Utils.escapeExpression(resolved) : resolved;
}
return match;
});

return processed.replace(UNMATCHED_TOKEN_REGEX, '');
}

/**
* Renders a member welcome email
* @param {Object} options
Expand All @@ -38,31 +96,15 @@ class MemberWelcomeEmailRenderer {
});
}

const memberName = member.name || 'there';
const firstName = memberName.split(' ')[0];
const contentWithReplacements = this.#applyReplacements({text: content, member, siteSettings, escapeHtml: true});
const subjectWithReplacements = this.#applyReplacements({text: subject, member, siteSettings, escapeHtml: false});

const templateData = {
site: {
title: siteSettings.title,
url: siteSettings.url
},
member: {
name: memberName,
email: member.email || '',
firstname: firstName
},
const html = this.#wrapperTemplate({
content: contentWithReplacements,
subject: subjectWithReplacements,
siteTitle: siteSettings.title,
siteUrl: siteSettings.url,
accentColor: siteSettings.accentColor
};

const contentWithReplacements = this.Handlebars.compile(content)(templateData);
const subjectWithReplacements = this.Handlebars.compile(subject)(templateData);

const html = this.#wrapperTemplate({
...templateData,
content: contentWithReplacements,
subject: subjectWithReplacements
});

const inlinedHtml = juice(html, {inlinePseudoElements: true, removeStyleTags: true});
Expand Down
2 changes: 1 addition & 1 deletion ghost/core/test/integration/jobs/process-outbox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe('Process Outbox Job', function () {
status: 'active',
name: 'Free Member Welcome Email',
slug: MEMBER_WELCOME_EMAIL_SLUGS.free,
subject: 'Welcome to {{site.title}}',
subject: 'Welcome to {site_title}',
lexical,
created_at: new Date()
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('Member Welcome Emails Integration', function () {
status: 'active',
name: 'Free Member Welcome Email',
slug: MEMBER_WELCOME_EMAIL_SLUGS.free,
subject: 'Welcome to {{site.title}}',
subject: 'Welcome to {site_title}',
lexical,
created_at: new Date()
});
Expand All @@ -52,7 +52,7 @@ describe('Member Welcome Emails Integration', function () {
status: 'active',
name: 'Paid Member Welcome Email',
slug: MEMBER_WELCOME_EMAIL_SLUGS.paid,
subject: 'Welcome paid member to {{site.title}}',
subject: 'Welcome paid member to {site_title}',
lexical,
created_at: new Date()
});
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 @@ -35,7 +35,7 @@ const validateRouteSettings = require('../../../../../core/server/services/route
*/
describe('DB version integrity', function () {
// Only these variables should need updating
const currentSchemaHash = '840147221764a9de1b3807e3abb61df2';
const currentSchemaHash = '5d8ff12fd51267005bff200abc420bb0';
const currentFixturesHash = 'c583f33910bb84a70847303b323be2db';
const currentSettingsHash = '64007c832bc27eefbe4d0ed3f888eeab';
const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01';
Expand Down
Loading
Loading