Skip to content

Sync with upstream Ghost v6.21.0#55

Open
madewithlove-machine-user wants to merge 69 commits intomainfrom
chore/sync-v6.21.0
Open

Sync with upstream Ghost v6.21.0#55
madewithlove-machine-user wants to merge 69 commits intomainfrom
chore/sync-v6.21.0

Conversation

@madewithlove-machine-user
Copy link
Copy Markdown

Syncing fork to upstream release v6.21.0.

kevinansfield and others added 30 commits March 2, 2026 15:55
fixes https://linear.app/ghost/issue/ONC-1510

Sites using a subdirectory/proxy setup (e.g. `example.com/blog/ghost/`) could not sign out via the UI — the sign-out handler redirected to `/ghost` instead of `/blog/ghost/`, resulting in a "Cannot GET /ghost" error.

- Replaced hardcoded `/ghost/api/admin/session` fetch URL with `getGhostPaths().apiRoot` which derives the correct subdirectory from the current URL
- Replaced hardcoded `/ghost` redirect with `getGhostPaths().adminRoot` so the post-signout redirect respects the subdirectory
ref TryGhost#26640

The subdirectory signout bug was caused by hardcoded /ghost/ paths that
don't account for subdirectory installations. This adds a custom lint
rule to catch string literals and template literals starting with
/ghost/ in src files, guiding developers to use getGhostPaths() instead.
This PR adds i18n ("t") wrapping for the private.hbs, pagination.hbs,
and content-cta.hbs templates.

Note that content-cta required a little bit of a logic restructuring to
make them more translatable. (Substituting in "page"/"post" was more
likely to be a-grammatical or confusing vs splitting out two version of
the string.

At the present time, translations will be pulled from the
theme/locales/xx.json file, with fallback first to en, then to the key.
Thus, no effects on page render are expected until we add translations.

A separate PR will handle a shared set of translations so that each
theme doesn't need to reproduce the work of translating these shared
strings.

Note: I needed to update tests to make the 't' helper available.
Additionally, since we have two different paths for t (based on
feature-flagging the i18n/i18next swap), I did a restructure to test
both routes.



<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Low behavioral risk but moderate template-rendering risk: multiple
shared frontend templates now rely on `t` helper bindings and changed
string composition, which could affect output if helpers/locales aren’t
registered or interpolation is wrong.
> 
> **Overview**
> Adds theme i18n support to core frontend templates by wrapping
user-facing strings in `{{t}}` across `private.hbs`, `pagination.hbs`,
and the paywall `content-cta.hbs` (including parameterized `Page {page}
of {totalPages}` and tier-list interpolation).
> 
> Refactors `content-cta.hbs` copy to split page vs post variants for
better translation grammar, and updates tests to register `t` plus
verify translations/fallbacks for both legacy `themeI18n` and new
`themeI18next` paths via a new `i18n-test-utils` harness and expanded
locale fixtures (en/de).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
c5bfec9. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Steve Larson <9larsons@gmail.com>
no-issue

Moved URL decoding and path normalization to a single place at the
top of the request handler, rather than having each helper function
decode independently. isDeniedFile and isAllowedFile are now pure
functions that operate on an already-decoded path. Extracted denylist
and allowlist constants to module level for clarity.
closes https://linear.app/ghost/issue/NY-1097

This change should have no user impact. It's just a cleanup.
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
|
[eslint-plugin-playwright](https://redirect.github.com/mskelton/eslint-plugin-playwright)
| [`2.7.1` →
`2.8.0`](https://renovatebot.com/diffs/npm/eslint-plugin-playwright/2.7.1/2.8.0)
|
![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-plugin-playwright/2.8.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-plugin-playwright/2.7.1/2.8.0?slim=true)
|

---

### Release Notes

<details>
<summary>mskelton/eslint-plugin-playwright
(eslint-plugin-playwright)</summary>

###
[`v2.8.0`](https://redirect.github.com/mskelton/eslint-plugin-playwright/releases/tag/v2.8.0)

[Compare
Source](https://redirect.github.com/mskelton/eslint-plugin-playwright/compare/v2.7.1...v2.8.0)

##### Bug Fixes

- Add missing test coverage and fix several minor bugs
([#&TryGhost#8203;434](https://redirect.github.com/mskelton/eslint-plugin-playwright/issues/434))
([e3398ec](https://redirect.github.com/mskelton/eslint-plugin-playwright/commit/e3398ec61da52de205e7c9af2896633357769f74))
- **missing-playwright-await:** Handle spread elements
([df30163](https://redirect.github.com/mskelton/eslint-plugin-playwright/commit/df3016323819f7bc335fd1841971dccc2ae64f51)),
closes
[#&TryGhost#8203;430](https://redirect.github.com/mskelton/eslint-plugin-playwright/issues/430)
- **missing-playwright-await:** Support more promise edge cases
([b4cdcbd](https://redirect.github.com/mskelton/eslint-plugin-playwright/commit/b4cdcbd010a2b4dfc7ee14ab5bdc655897389f19))

##### Features

- Auto-detect `test.extend()` fixtures and import aliases
([#&TryGhost#8203;432](https://redirect.github.com/mskelton/eslint-plugin-playwright/issues/432))
([8b22ee7](https://redirect.github.com/mskelton/eslint-plugin-playwright/commit/8b22ee7b1f7823d81bafda82e240dd51106726dd))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - Only on Sunday and Saturday ( * * * * 0,6 ), Between 12:00
AM and 12:59 PM, only on Monday ( * 0-12 * * 1 ) in timezone Etc/UTC.

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Never, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/TryGhost/Ghost).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My40My4yIiwidXBkYXRlZEluVmVyIjoiNDMuNDMuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
no ref

This types-only change should have no user impact.
no ref
- added yarn dev:mailgun script to bypass mailpit when needing to use a
real service for testing trx emails
towards https://linear.app/ghost/issue/NY-1101
ref TryGhost/Koenig#1750

You can now paste URLs and use the bookmark card in the welcome email
editor.

https://github.com/user-attachments/assets/ca7d2be9-488c-4433-b679-8beccdcb1714

Co-authored-by: Steve Larson <9larsons@gmail.com>
ref https://linear.app/ghost/issue/NY-1114/
- added card styles to member email editor to support image, button,
callout, etc cards
ref https://linear.app/ghost/issue/NY-1109/

- added use link suggestions hook in framework
- added adapter hook in settings for ease
- added link suggestions functionality to welcome email editor

This PR adds link suggestions similar to the editor implementation. It's
largely a rewrite of the hooks used there that'll later be used for all
of admin.

![](https://github.com/user-attachments/assets/c53bd82d-eb0a-4bf4-bb30-c2114ad6cf13)

Co-authored-by: Evan Hahn <evan@ghost.org>
…26641)

no ref

When someone signs up with a [Feedbin] email address, we now have a link
to take them straight to the app. (Feedbin is an RSS reader that has
custom email inboxes you can use to read everything all in one place.)

Note that Feedbin doesn't have an Android app, so we use the regular
desktop link for both.

[Feedbin]: https://feedbin.com/

Co-authored-by: Cathy Sarisky <42299862+cathysarisky@users.noreply.github.com>
## Summary

- Removed @cmraible and @ibalosh from `/e2e/` code ownership
- Added @EvanHahn to `**/tinybird/` code ownership

## Why?

The `/e2e/` package is changed in a lot of pull requests. Having 3 people tagged for review on any PR that touched the e2e package isn't ideal, because it makes the ownership unclear - which of the 3 of us will actually review each PR? Adding a single owner eliminates this confusion.

The tinybird files are a different case - these change rarely, so it makes sense to have the whole team as owners — not necessarily so we will all review each PR, but so we all at least _see_ any PR that changes these files, even if retroactively.
…st#26655)

refs https://linear.app/tryghost/issue/ONC-1518

## Summary
- Added `labels-manager` service with shared paginated label cache,
debounced server-side search, and `sortLabels`/`findBySlug` helpers —
all label dropdowns share one cache instead of each fetching
independently
- Rewrote `gh-member-label-input` to lazy-load labels on dropdown open
with infinite scroll and automatic server-side/client-side search toggle
based on whether all labels have been loaded
- Converted `gh-member-single-label-input` from `OneWaySelect` to
`PowerSelect` with search and infinite scroll support
- Added `power-select-options-with-scroll` component for infinite-scroll
pagination in `PowerSelect` dropdowns (segment select, recipient select)
- Added cache invalidation to the shared label cache:
`addLabel()`/`removeLabel()` for targeted mutations (member save, label
delete), `reset()` for full reload on `refreshData`

---------

Co-authored-by: Steve Larson <9larsons@gmail.com>
ref https://linear.app/ghost/issue/ONC-1521/

We should follow best practice to not trust any server-provided content,
even when we're sanitizing input on the server.

- Added `escapeHtml()` for template interpolations in reader iframe
srcdoc
- Added DOMPurify sanitization for all `dangerouslySetInnerHTML` usages
no issue

This should prevent the flakiness we've seen where the domain tests take
too long to run. Instead of using the ORM layer, we're bulk inserting /
deleting using the knex layer. I wouldn't want to do this in the source,
but in tests this feels like a suitable optimisation to check bulk cases
quickly.
Ref
https://linear.app/ghost/issue/DES-1293/add-back-button-escape-hatch-on-email-failure-screen

There was previously no way to escape the email failed to send modal
without hitting escape. This just keeps the close button that's visible
on the pre-publish step.

| Header | After |
|--------|--------|
| <img width="1440" height="1024" alt="localhost_2368_ghost_ (16)"
src="https://github.com/user-attachments/assets/15145a24-eb47-4504-8d4c-7d12968abc24"
/> | <img width="1440" height="1024" alt="localhost_2368_ghost_ (15)"
src="https://github.com/user-attachments/assets/9b452941-ddbe-4f03-9561-ccbbdf4899fb"
/> |

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Template-only UI logic changes to button visibility/wiring in the
publish modal, with no data or auth impact.
> 
> **Overview**
> Ensures the publish flow header always shows the **Close** button
whenever the modal isn’t complete, including when an `emailErrorMessage`
is present, so users can exit the email-failure screen without using
Escape.
> 
> The **Preview** button is now suppressed while an email error is
shown, and the email-error step receives `@close` so it can trigger the
same exit behavior.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
8d62d09. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
no ref

This contains some changes to support additions within the Koenig repo:
- embed cards
- youtube cards
- restricting width for image cards

Youtube embeds are still having some issues with the renderer.
closes
https://linear.app/ghost/issue/NY-1106/create-feature-flag-for-email-settings-customization

## Summary
- Adds a new `welcomeEmailDesignCustomization` private feature flag to
enable toggling welcome email design customization on/off
- Adds the flag to the labs settings UI under private features
- Updates the config API snapshot to include the new flag

## Test plan
- [x] Unit tests pass (`ghost/core/test/unit/shared/labs.test.js` — 10
passing)
- [x] Config API e2e snapshot updated and passing
- [X] Toggle visible in Labs → Private features when developer
experiments is enabled
…logic (TryGhost#26543)

ref https://linear.app/ghost/issue/BER-3338

Both functions independently computed discount windows using the same
three data paths (trial periods, Stripe coupon data, and legacy offer
duration fallback). This extracts the shared computation into a single
`getDiscountWindow` utility so the two callers stay in sync as the
discount model evolves.
…ryGhost#26672)

closes https://linear.app/ghost/issue/BER-3406

- previously, we expected the /tiers endpoint to always have
tier-related data to render retention offer popups
- however, archived tiers are not returned as part of the `/tiers`
endpoint
- with this fix, we directly get necessary data from the member
subscription object
ref TryGhost#26416

Fixed `SharedStorage` deprecation warning
Added label picker with inline editing to members filter

ref https://linear.app/tryghost/issue/BER-3342/

The members list label filter now uses a custom label picker built with Popover + Command (cmdk) from Shade. Labels can be created, renamed, and deleted directly from both the filter dropdown and the bulk-action modals (add/remove label), without leaving the members page.
Fixed DST drift in domain warming integration test clock

no ref
The test used setDate(), which diverged from elapsed-24h day math around DST and caused flaky mysql8 CI failures.
troyciesco and others added 28 commits March 4, 2026 21:31
no ref

- a test for labs ensures that config values take precedence over GA
keys. However, the test referenced the `announcementBar` flag was
removed in TryGhost#26196. That means this
test was looking at a state that wasn't really true
- instead, we can check for the first item in the `GA_KEYS` list. If
there's nothing in the list, the test can be skipped.
- a few other tests had this same issue, so this PR updates them as well
- there was a test for alpha flags that wasn't actually doing anything,
so it's deleted
no ref

I wrote this package so I should review changes to it.
closes https://linear.app/ghost/issue/NY-1125

- added new `usePinturaConfig` hook to admin

Co-authored-by: Evan Hahn <evan@ghost.org>
closes https://linear.app/ghost/issue/BER-3393

Aligned the copy for once offers with the repeating / free months
offers:
- Before "$4.00 - Next payment"
- After: "$4.00/month - Ends {date}"
ref https://linear.app/ghost/issue/BER-3388/design-bugsimprovements

- Fixed empty price gap in retention offer screen for free month discounts — the price div was rendering even when not applicable, causing a large visual gap
- Fixed title overlapping with back/close buttons in non-full-size retention offer
- Improved responsive layout for the offer list table
- Added `aria-label` to the icon-only filter button in retention offers
ref
https://linear.app/ghost/issue/BER-3348/portal-paid-section-doesnt-indicate-subscription-is-canceled

When a member cancels their subscription, the Portal account page looked
nearly identical to an active subscription — the only signal was a small
plain-text line that was easy to miss.
                  
- Replaced the plain cancellation text with an accent-colored banner
containing the expiry date and "Continue subscription" button
  - Added a CANCELED badge next to the tier name in the plan row
  - Added i18n keys for the new copy
  - Added unit tests for both the banner and badge

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> UI-only changes to account display plus new tests/i18n strings; no
backend logic or sensitive flows modified.
> 
> **Overview**
> Improves the Portal account UI for *canceled-but-still-active* paid
subscriptions by replacing the easy-to-miss expiry notice with a
prominent brand-accented banner showing the expiry date and a **Continue
subscription** CTA.
> 
> Adds a `Canceled` badge next to the tier/plan name, introduces styling
for the new banner/badge, and includes i18n keys plus new unit tests
covering the banner, expiry date rendering, and badge visibility.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
39b77fe. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…ngs (TryGhost#26696)

closes https://linear.app/ghost/issue/BER-3392
closes https://linear.app/ghost/issue/BER-3218

- added max. length display title / description fields
- added inline validation for floating numbers for free-months /
duration-in-months fields


---------

Co-authored-by: Sodbileg Gansukh <sodbileg.gansukh@gmail.com>
The canary job dispatched to Ghost-Moya's deploy.yml which cloned Ghost
from source and built a Docker image from scratch. trigger_cd now
handles all cases by dispatching to cd.yml which extends the pre-built
GHCR image instead.

trigger_cd supports the deploy-to-staging label on PRs, matching the
old canary behaviour: PRs from org members or renovate[bot] with the
label deploy to staging via cd.yml. Main builds dispatch with
dry_run=false and should_rollout=true. Regular PRs remain dry-run only.

deploy_admin remains enabled and unchanged — it continues to update
production admin-forward via deploy-admin.yml.
ref https://linear.app/tryghost/issue/BER-3380/

When bulk-unsubscribing members, the modal now lets you choose between
unsubscribing from all newsletters or selecting specific ones via an
inline searchable picker. When only one newsletter is active, the modal
shows a simple confirmation instead.
…ts (TryGhost#26699)

refs https://linear.app/tryghost/issue/ONC-1529

- The paginated label loading refactor (TryGhost#26655) missed adding server-side search to `gh-members-recipient-select` and `gh-members-segment-select`
- Users with many labels had to manually scroll through all pages before search could find labels not yet loaded client-side
- Added conditional server-side search (via `labelsManager.searchLabelsTask`) to both components, matching the
pattern already used by `gh-member-label-input`
- Tiers, statuses, and offers are still filtered client-side since they're always fully loaded
- Fixed labels found via search result not being selectable in some cases

---------

Co-authored-by: Steve Larson <9larsons@gmail.com>
closes
https://linear.app/ghost/issue/BER-3413/account-page-date-mismatch-between-discount-end-billing-period

- When a member redeems a 1-month free offer, Stripe sets the discount
start date to now and end date to now + 1 month, i.e. anchors them to
the redemption date, not the billing date
- In Portal, to avoid confusion, we want to anchor the `"- Ends {date}"`
to a billing date to avoid confusion
- For example, today is 5 Mar 2026 and my subscription renews on 3 Apr
2026. I hit cancel, redeem a free-month offer that applies to my next
payment. I expect to see `"$0.00/month - Ends 3 Apr 2026"`, not
`"$0.00/month - Ends 5 Apr 2026"`
…6647)

closes https://linear.app/ghost/issue/ONC-1516

- When multiple `DELETE /members/{id}` requests target the same member concurrently, a race condition causes Bookshelf to throw "No Rows Deleted" errors that bubble up as 500s to Sentry
- The `findOne` check passes for both requests (member exists), but by the time the second request's `destroy()` runs, the member is already gone
- Fixed by passing `require: false` to Bookshelf's `destroy()` so it treats a missing row as a no-op — the member is already deleted, which is the desired outcome
ref f9f5f72

*This change should have no user impact.*

The `announcementBar` feature flag has been enabled since 2023. We can
remove the flag and assume it's always on.
closes https://linear.app/ghost/issue/BER-3336

The fallback to `sub.offer` was added as a transitional measure while
core and admin could be out of sync during CD. Both are now fully
deployed with `offer_redemptions` always populated, making the fallback
dead code
Ref https://linear.app/ghost/issue/BER-3408/design-clean-up

- Hid bulk actions for 0 selected members
- Used text-foreground for columns with values for open rate
ref https://linear.app/ghost/issue/ENG-1326/

Ember's dynamic asset loading (lazy-loader, admin-x components, Koenig
editor, user avatars) previously relied on a build-time `cdnUrl` config
to construct absolute CDN URLs. This required piping GHOST_CDN_URL
through environment.js into an encoded meta tag.

Instead, derive the asset base at runtime from the Ember script's own
URL — the same principle ES modules use for relative imports. If the
script loaded from a CDN (via index.html sed rewrite), dynamic loads
inherit the CDN origin. If local, they inherit the local path.

New `assetBase()` utility replaces 6 duplicated cdnUrl conditionals.
Also fixes the vite-ember-assets build to read from ghost/admin/dist.
fixes https://linear.app/ghost/issue/ONC-1512/

The contributor layout used `h-full overflow-auto` on the `<main>`
element, with the EmberRoot child also using `h-full`. This constrained
the Ember content to exactly the scroll container's height, so
`.gh-viewport`'s `overflow: hidden` clipped the posts list with no way
to scroll.

Changed the contributor layout to use the same flex column pattern as
the non-contributor `SidebarInset` layout — a `flex flex-col
overflow-y-auto` container with a `flex-1` wrapper for children. This
allows the content to grow beyond the container via `min-height: auto`,
enabling scroll.
…ryGhost#26716)

ref https://linear.app/ghost/issue/ONC-1512/

- Fixed the "Posts" link in the contributor user menu pointing to `/` instead of `/posts`, causing navigation to the wrong route
ref https://ghost.slack.com/archives/C018EKC56JF/p1771527734858719

The existing JWT is used for identity and only contains the email
address. It has a TTL of 10 minutes which is reasonable for the use case
but little else.

We have another use case where integrations often want to validate the
source, but are left with no option but to hit the members Admin API
endpoint with an Admin API key because the email isn't sufficient to
know if the member has access to a tier, is paid, isn't specific to
email (which could change), etc., all of which are used for
integrations.

This endpoint would still require the members session cookie and has the
following props, e.g.:
```
 sub: darylmayer880479@example.com
 scope: members:entitlements:read
 member_uuid: 629c5190-f068-4f49-b533-00d7477a7aed
 paid: false
 active_tier_ids: []
 jti: present
 iat: 1771555398
 exp: 1771555518
 aud/iss: http://localhost:2368/members/api
 Lifetime: 120s (2 minutes)
```
ref https://linear.app/ghost/issue/ONC-1533/

- replaced regex IP matching with a WHATWG URL parser normalization step and octet matching
ref https://linear.app/ghost/issue/ONC-1533/

The `gotOpts` passed to `metascraper-logo-favicon` only included a
User-Agent header, meaning its internal requests (e.g. favicon probes
via `reachable-url`) did not inherit the request hooks configured on
`externalRequest`. Fixed by using `got.mergeOptions` to clone the full
`externalRequest.defaults.options` (including hooks) and merge our
User-Agent override on top
broccoli-asset-rev rewrites string literals in compiled JS at build time,
prepending the CDN origin and fingerprint hash. The assetBase() utility
introduced in TryGhost#26555 always prepends a non-empty base URL, which caused
double-prefixing when the URL had already been made absolute by
broccoli-asset-rev. Added prefixAssetUrl() helper that skips prefixing
when the URL is already absolute, and updated all call sites.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.