Skip to content

Sync with upstream Ghost v6.21.2#56

Open
madewithlove-machine-user wants to merge 99 commits intomainfrom
chore/sync-v6.21.2
Open

Sync with upstream Ghost v6.21.2#56
madewithlove-machine-user wants to merge 99 commits intomainfrom
chore/sync-v6.21.2

Conversation

@madewithlove-machine-user
Copy link
Copy Markdown

Syncing fork to upstream release v6.21.2.

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.
jloh and others added 28 commits March 6, 2026 11:41
no ref

- The picture element is widely supported now and lets the browser choose the best supported format for the browser in question
- Testing to see how often browsers will actually pick a different image
  format when using the new card
towards https://linear.app/ghost/issue/NY-1140

This should have no user impact but makes an upcoming change easier.
ref
https://linear.app/ghost/issue/DES-1275/mobile-newsletter-font-size-inconsistent-between-designs

Some bookmark cards exceeded Gmail mobile viewport width, which made
Gmail auto-scale the whole email.
Thumbnail sizing now uses proportional width (33%) with a desktop cap
(180px) to preserve desktop layout.
ref TryGhost#26397

- The picture element has now been released but didn't have a Private UI toggle which this adds
Admin deployment to production is now handled by Ghost-Moya's
cd-admin.yml, triggered via the cd.yml orchestrator on every main
merge. The deploy_admin job (which built admin with GHOST_CDN_URL,
uploaded an artifact, and dispatched to deploy-admin.yml in Moya)
is no longer needed.
Dedicated workflow triggered by version tag pushes (v*). Builds admin
and Docker images with semver tags (v5.100.1, 5.100.1, 5.100, latest)
using docker/metadata-action's native type=semver patterns, then
dispatches Moya CD with the version tag as ref so Daisy gets a proper
version label. Validates tag matches package.json before building.

Separate from ci.yml which handles branch/PR pushes with sha/branch
tags. Both fire on release (grunt publish --follow-tags pushes commit
and tag atomically) — CD is idempotent so the duplicate is harmless.
The core-image-sha-tag output was defined in both ci.yml and release.yml
but never consumed by any downstream job. The step that extracted it and
the output declaration are both removed.
…yGhost#26733)

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

The `{{cancel_link}}` theme helper's default `continueLabel` was
"Continue subscription", while Portal was updated to use "Resume
subscription" in TryGhost#26679.
                  
- Updated the default `continueLabel` from "Continue subscription" to
"Resume subscription"
  - Updated the corresponding unit test
…t#26738)

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

- retention offer codes now use Uint32Array instead of Uint16Array,
generating 8-character hex codes instead of 4-character ones, increasing
the code space from ~65K to ~4.3 billion possible values
- also extracted the offer name generation logic and added unit tests,
to make sure we stay under the 40-char max. limit imposed by Stripe on
coupon names


---------

Co-authored-by: Kevin Ansfield <kevin@ghost.org>
…26735)

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

When a member cancelled via the retention offer path (Portal popup), the
theme page's cancel button didn't update because no page reload
occurred. The `cancelSubscription` and `applyOffer` actions now return
`reloadOnPopupClose: true`, and the App component reloads the page when
the popup closes with that flag set. The flag is cleared on each
`openPopup` to prevent stale reloads.
…t#26737)

no ref

The `/members/api/member/offers/` endpoint was returning the full admin
DTO but some of the fields were not are not needed by Portal. Added a
`PublicOfferDTO` and `toPublicDTO` mapper that returns only the fields
the public endpoint requires
ref https://linear.app/tryghost/issue/BER-3423/

Scheduled nightly cleanup for ghost, ghost-core, and ghost-development
container packages. Deletes versions older than 14 days while preserving
semver-tagged releases, latest/main/cache-main tags, and a minimum of
10 versions per package. Uses GITHUB_TOKEN with packages:write — no PAT
needed since the packages are published from this repo.
no ref

This types-only change should have no user impact.
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [mysql2](https://sidorares.github.io/node-mysql2/docs)
([source](https://redirect.github.com/sidorares/node-mysql2)) |
[`3.18.0` →
`3.18.1`](https://renovatebot.com/diffs/npm/mysql2/3.18.0/3.18.1) |
![age](https://developer.mend.io/api/mc/badges/age/npm/mysql2/3.18.1?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/mysql2/3.18.0/3.18.1?slim=true)
|

---

### Release Notes

<details>
<summary>sidorares/node-mysql2 (mysql2)</summary>

###
[`v3.18.1`](https://redirect.github.com/sidorares/node-mysql2/blob/HEAD/Changelog.md#3181-2026-02-25)

[Compare
Source](https://redirect.github.com/sidorares/node-mysql2/compare/v3.18.0...v3.18.1)

##### Bug Fixes

- **types:** ensure optional params in `query` and `execute` methods
([#&TryGhost#8203;4123](https://redirect.github.com/sidorares/node-mysql2/issues/4123))
([3f4bbca](https://redirect.github.com/sidorares/node-mysql2/commit/3f4bbca38e8dcab4344841653cd26493b44a84f3))

</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 these
updates 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>
The GHCR push guard now checks repository_owner instead of an exact
repository match, allowing same-org private forks (e.g. Ghost-Security)
to push Docker images to GHCR under their own namespace. Image names
are derived from the repository name so each fork gets a distinct
GHCR path (ghost-core vs ghost-security-core).

Admin and server artifacts are now always uploaded regardless of GHCR
push strategy, so any fork's CI run produces consumable build outputs.

The trigger_cd job passes source_repo in the dispatch payload so
Ghost-Moya can resolve SHAs, artifacts, and commit statuses against
the correct source repository.
closes
https://linear.app/ghost/issue/BER-3413/account-page-date-mismatch-between-discount-end-billing-period

- In Stripe, discount dates are anchored around the redemption date, not
the billing date. However, we're interested in whether the next
payment(s) are discounted. This aligns the next payment object discount
end date with the last payment with a discount.
- Example:
    - Bob's billing cycle is from Mar 3 to April 3
    - Bob redeems a retention offer on Mar 1, that is repeating 3 months
    - Stripe will set discount start to Mar 1 and discount end to Jun 1
- Bob's next 3 payments will be discounted: Mar 3, Apr 3 and May 3, and
we want to display "Ends May 3" (not "Ends Jun 1")
…ts list (TryGhost#26726)

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

The template for the posts list only contained feature image display for drafts.

- matched published feature image template to the draft template image
ref https://linear.app/ghost/issue/ONC-1533/

After normalizing expanded IPv6 forms, re-check for IPv4-mapped
addresses to ensure consistent private IP detection across all
notation variants.
…yGhost#26751)

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

The /files/upload endpoint now determines the MIME type from the
file extension server-side rather than trusting the client-provided
value, preventing content type spoofing on S3/GCS storage backends.
…t#26750)

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

The filter field validation was only checking the last segment of
dot-separated keys. Updated to check all segments so that compound
keys are handled correctly. Also added restricted field filtering
to the Admin API users endpoint, and renamed the utility module
to better reflect its broader usage across API layers.
ref https://linear.app/ghost/issue/ONC-1533/

Ensured that the IP address validated during the DNS check is the same
one used for the actual TCP connection by installing a custom dnsLookup
on request options. Added tests for the new validation layer.
https://linear.app/ghost/issue/GVA-647/

The signin endpoint now returns a consistent 201 response regardless
of whether the provided email matches an existing member. If no member
is found, no email is sent. This aligns signin behavior with the
existing signup flow.
…st#26748)

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

- when a member signs ups with a repeating 1 month offer, the next
payment is not discounted
- therefore, they should be able to redeem a retention offer
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.