Skip to content

Proof of concept newsletter mode - #50680

Closed
davemart-in wants to merge 44 commits into
trunkfrom
try/newsletter-mode-v0
Closed

davemart-in wants to merge 44 commits into
trunkfrom
try/newsletter-mode-v0

Conversation

@davemart-in

@davemart-in davemart-in commented Jul 20, 2026 •

Copy link
Copy Markdown
Contributor

Important

Not for merge. This branch is a proof of concept, built as a cheap way to
test the idea end-to-end in one place. It is not intended to ship as-is.

The work will be broken out into a series of smaller, reviewable PRs. A P2 post
will lay out that breakdown for feedback — please hold detailed code review
until then. This PR is here to look at and click through, not to approve.

Newsletter Mode (experimental spike)

Adds an opt-in, focused Newsletter Mode for the unified Newsletter page. When enabled, it declutters the wp-admin left nav down to the newsletter surfaces — Dashboard, Subscribers, Settings, Comments, Write, Monetize — with a "Newsletters" header and one-click exit back to wp-admin.

Proposed changes

An opt-in, focused Newsletter Mode for the unified Newsletter page, plus the first pass at a Newsletter Dashboard.

Mode flag and opt-in

  • Internal flag, off by default, behind the jetpack_newsletter_mode_available filter plus a per-site option — so nothing here is reachable unless it is deliberately switched on.
  • Per-site toggle in Newsletter → Settings, backed by a package-owned REST namespace (jetpack-newsletter/v1) rather than the shared settings whitelist.

Focused workspace

  • Declutters the wp-admin left nav down to the newsletter surfaces, with a "Newsletters" header and one-click exit.
  • Keeps nav state correct across the surfaces that can only be resolved server side (Posts, Comments) and client side (Subscribers, Settings), and fixes the admin title and body class that decluttering would otherwise break.

Newsletter Dashboard (new mode-only page)

  • Two views — an onboarding view and a stats view — temporarily toggled with mod+J or a ?view= query arg.
  • Onboarding: a four-task getting-started checklist with per-user completion tracking, a one-time intro modal, and a share modal.
  • Stats: subscriber growth chart, email performance, and a recent posts table, reading real data.

Supporting changes

  • jetpack-mu-wpcom: a new wpcom_write_back_destinations filter so the Write editor's back button can return to the Newsletter page. Registered vetted destinations only — an arbitrary return URL via query string is deliberately not supported. Includes a fallback for environments without that filter.
  • stats-admin: proxy the email overview stats resource.
  • Newsletter Settings: a Newsletter identity section for editing site title and tagline without leaving the page (independent of Newsletter Mode).
  • Subscribers: deep links that open Add Subscribers on the tab the link promised.

Does this pull request change what data or activity we track or use?

Yes — one new Tracks event:

  • jetpack_subscribers_share_site, recorded when someone shares the newsletter from the share modal. Has a type property recording the share method (copy, web-share, or the social service name).

The Newsletter identity section records the existing jetpack_newsletter_section_save event, which five other settings sections already use — no new event there.

Testing instructions

Newsletter Mode is off by default and has no UI until it is made available.

  1. Make the mode available on your test site:
    add_filter( 'jetpack_newsletter_mode_available', '__return_true' );
  2. Go to Jetpack → Newsletter → Settings and switch Newsletter Mode on.
  3. On page refresh, the left nav should collapse to the newsletter surfaces, with a "Newsletters" header and an exit link at the foot. Confirm the exit link returns you to the normal wp-admin menu.
  4. Open the Dashboard item. You should land on the onboarding view with the getting-started checklist. Ticking a row should persist across a reload.
  5. Switch to the stats view with ?view=stats on the Dashboard URL (or mod+J), and confirm the subscriber chart, email performance, and recent posts render.
  6. Check the nav stays correctly highlighted as you move between Subscribers, Settings, Posts and Comments.

Turning the toggle back off, or removing the filter, should restore the normal Newsletter page and wp-admin menu with no trace of the mode.

@davemart-in davemart-in self-assigned this Jul 20, 2026
@davemart-in
davemart-in marked this pull request as draft July 20, 2026 20:52
@github-actions

github-actions Bot commented Jul 20, 2026 •

Copy link
Copy Markdown
Contributor

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack or WordPress.com Site Helper), and enable the try/newsletter-mode-v0 branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack try/newsletter-mode-v0
bin/jetpack-downloader test jetpack-mu-wpcom-plugin try/newsletter-mode-v0

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions github-actions Bot added [Package] Newsletter [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ labels Jul 20, 2026
@github-actions

github-actions Bot commented Jul 20, 2026 •

Copy link
Copy Markdown
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!


Jetpack plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.

@jp-launch-control

jp-launch-control Bot commented Jul 20, 2026 •

Copy link
Copy Markdown

Code Coverage Summary

Coverage changed in 8 files. Only the first 5 are listed here.

File Coverage Δ% Δ Uncovered
projects/packages/newsletter/src/settings/newsletter-settings.tsx 78/171 (45.61%) -0.79% 11 💔
projects/plugins/jetpack/load-jetpack.php 0/52 (0.00%) 0.00% 1 ❤️‍🩹
projects/packages/jetpack-mu-wpcom/src/features/write/write.php 577/664 (86.90%) 0.02% 0 💚
projects/packages/newsletter/routes/dashboard/stage.tsx 26/27 (96.30%) 0.14% 0 💚
projects/packages/newsletter/src/class-settings.php 84/199 (42.21%) 0.59% 0 💚

16 files are newly checked for coverage. Only the first 5 are listed here.

File Coverage
projects/packages/newsletter/src/class-mode.php 271/489 (55.42%) 💚
projects/packages/newsletter/routes/home/stats/recent-posts.tsx 17/22 (77.27%) 💚
projects/packages/newsletter/routes/home/stats/use-subscriber-stats.ts 57/70 (81.43%) 💚
projects/packages/newsletter/src/settings/sections/newsletter-identity-section.tsx 16/19 (84.21%) 💚
projects/packages/newsletter/routes/home/stage.tsx 20/22 (90.91%) 💚

Full summary · PHP report · JS report

If appropriate, add one of these labels to override the failing coverage check: Covered by non-unit tests Use to ignore the Code coverage requirement check when E2Es or other non-unit tests cover the code Coverage tests to be added later Use to ignore the Code coverage requirement check when tests will be added in a follow-up PR I don't care about code coverage for this PR Use this label to ignore the check for insufficient code coveage.

Comment on lines +259 to +271
.jetpack-newsletter-home__granularity-option {
border-radius: 0;
padding: var(--wpds-dimension-padding-sm) var(--wpds-dimension-padding-lg);

+ .jetpack-newsletter-home__granularity-option {
border-inline-start: var(--wpds-border-width-xs) solid var(--wpds-color-stroke-surface-neutral-weak);
}

&.is-selected {
background-color: var(--wp-admin-theme-color, #3858e9);
color: #fff;
}
}

@simison simison Jul 31, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could we avoid these Button component overrides? They will introduce bugs in future since you're relying on existing CSS and structure not change.

Alternatively you have "unstyled" variation for Button if needed, but I bet there's a component variation already you can use or adjust the design. :-)

const fields = useMemo< Field< RecentPost >[] >(
() => [
{
id: 'media',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There's type "media" in Fields as well which gives you bunch of stuff for free.

</Text>
{ item.date && (
<Text variant="body-sm" className="jetpack-newsletter-home__muted">
{ item.date }

@simison simison Jul 31, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not sure (depends on API) but you might need localizing the date for user's or site's datetime settings.

Could also be on its own row with date type which then give you all the stuff for free, if it works for the design?

Comment thread projects/packages/newsletter/routes/home/stats/recent-posts.tsx
Comment on lines +123 to +131
{
id: 'recipients',
label: __( 'Recipients', 'jetpack-newsletter' ),
getValue: ( { item }: { item: RecentPost } ) => item.recipients ?? 0,
render: ( { item }: { item: RecentPost } ) => (
<span>{ item.recipients === null ? EMPTY_VALUE : String( item.recipients ) }</span>
),
enableSorting: false,
},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can just use type: 'number' or integer for values like these to simplify and help with number format i18n for thousans etc. Docs

Comment on lines +308 to +325
// A link wearing the primary button's clothes — see the note at its markup
// for why it is not a `Button`. Uses the admin theme colour, like the rest of
// the Newsletter Mode chrome.
.jetpack-newsletter-home__no-posts-cta {
background-color: var(--wp-admin-theme-color, #3858e9);
border-radius: var(--wpds-border-radius-sm);
color: #fff;
display: inline-block;
margin-block-start: var(--wpds-dimension-gap-md);
padding: var(--wpds-dimension-padding-sm) var(--wpds-dimension-padding-lg);
text-decoration: none;

&:hover,
&:focus {
color: #fff;
}
}
}

@simison simison Jul 31, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks pretty hacky. 😅 Also mixes static color, --wpds, and --wp-admin tokens which is really messy.

Let's just use LinkButton I just made for this purpose. Depends on the version bump (#50509), but for a prototype, it's fine to update packages just in the Newsletter package locally.

Comment on lines +292 to +296
.jetpack-newsletter-home__post-title {
display: flex;
flex-direction: column;
gap: var(--wpds-dimension-gap-xs);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can replace this with Stack component.

Comment on lines +348 to +353
.jetpack-newsletter-intro__body {
display: flex;
flex-direction: column;
// The container owns the outer padding now.
gap: var(--wpds-dimension-gap-md);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can replace this with Stack component.

Comment on lines +359 to +363
.jetpack-newsletter-intro__title {
font-size: 20px;
line-height: 1.3;
margin: 0;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's use Text component to set typography consistently.

}

.jetpack-newsletter-intro__cta {
margin-block-start: var(--wpds-dimension-gap-lg);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Needing margin here looks like could be solved just by using Stack. Possibly same with width: 100%.

Comment on lines +100 to +101
apiRoot={ getSiteData()?.rest_root }
apiNonce={ getSiteData()?.rest_nonce }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You should need this apiRoot/apiNonce setter here and something is wrong if you do. :-)

Comment on lines +1 to +11
// `Tabs.Tab` sizes its box to its label, so the focus ring lands hard against
// the word with nothing between the two. A little inline padding gives the ring
// room to breathe; the tabs keep their own spacing from the list's gap, so the
// row reads the same.
.jetpack-newsletter__add-subscribers-tabs {

[role="tab"] {
padding-inline: var(--wpds-dimension-padding-xs);
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this a problem elsewhere too in Tabs used for Jetpack admin pages, so a local fix for just Jetpack isn't something we should have?

// the URL field doesn't stretch. Paired with `.components-modal__frame` — the
// element `Modal` puts our class on — because WP's own frame width rules
// are single-class too, so this shouldn't depend on stylesheet order.
.components-modal__frame.jetpack-newsletter-share {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can't really rely on internal class like .components-modal__frame as stable API, it can change at any time. You can use modal's own size props to pick a size. You could also use Dialog which is newer component and if I remember right also more size options.

Comment on lines +42 to +43
* Ported from Calypso's `packages/launchpad/src/action-components/share-site-modal`,
* with three deliberate differences: the WhatsApp host is chosen per

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

More of a product decision but it was always odd to me we have two parallel "social sharing" implementations; this and Jetpack Social.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Better convert png to a more optimal webp file to save some bytes.

href={ getNewsletterModeScriptData()?.writeUrl ?? 'post-new.php' }
>
{ __( 'Write your first post', 'jetpack-newsletter' ) }
</a>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Noted in another comment that this was a bit hacky solution. :-)

https://github.com/Automattic/jetpack/pull/50680/changes#r3689431104

while it actually takes you somewhere. Styled to match instead. */ }
<a
className="jetpack-newsletter-home__no-posts-cta"
href={ getNewsletterModeScriptData()?.writeUrl ?? 'post-new.php' }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

BTW! in WP Build routes you can transition to post editor without doing full refresh. You calso get "return" links for free then. Test site editor's Pages list for example to see how smooth it is.

Check Pages list implementation in Gutenberg and Canvas settings for Router.

Comment on lines +1 to +7
import {
Button,
Modal,
__experimentalHStack as HStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis
__experimentalInputControl as InputControl, // eslint-disable-line @wordpress/no-unsafe-wp-apis
__experimentalVStack as VStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis
} from '@wordpress/components';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Recommend swapping to Button, Dialog and Stack from @wordpress/ui instead of using the old components.

Comment on lines +158 to +162
<SocialLogo
className="jetpack-newsletter-share__service-icon"
size={ 24 }
icon={ link.service }
/>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You could implement the icon just with icon prop for @wordpress/components Button, or with <Button.Icon /> for @wordpress/ui Button. Then you don't need the manual styling to handle it, which isn't super sustainable, as the button's own styles can change and the logo suddenly wouldn't fit/look good. Official icon handles it.

href={ link.href }
label={ link.title }
target="_blank"
rel="noopener noreferrer"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No need rel anymore for modern browsers. Gutenberg's ExternalLink and Link don't use it anymore either.

className="jetpack-newsletter-share__service"
href={ link.href }
label={ link.title }
target="_blank"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For _blank opening things you'd need the ↗

LinkButton is prolly the best component here.

@simison

simison commented Jul 31, 2026

Copy link
Copy Markdown
Member

I would recommend looking deeper into WP Build for providing the sidebar, route transitions, and opening the editor and navigating back to Newsletter dash.

Check examples from Gutenberg for site editor and how smoothly it works when navigating around and how it replaces the classic WP Admin.

It'll provide you SPA experience for focused UIs like these, provide you with the sidebar as well. Avoids lots of boilerplate, natively supported by WP.

*
* @return The empty state.
*/
const NoPosts = (): JSX.Element => (

@simison simison Jul 31, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Take a look at EmptyState component, would that work here?

You can even just pass it to DataViews with empty prop (docs)

Comment on lines +107 to +110
// The mode's own nav is the frame here, so the Jetpack footer would be
// out of place. This page only ever renders inside the mode, so it needs
// no condition — unlike the Newsletter page, which is shared.
showFooter={ false }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Product decision, of course, but I'd include the Footer. Matt directly gave feedback about Jetpack that it was looking really random (page content widths weren't consistent, headers were all over the place, and some pages had footer sand some didn't).

I don't understand the "out of place" design argument either.

Comment on lines +40 to +48
/**
* Which view to open on.
*
* `?view=` wins over the remembered choice: it is the reliable way in — see the
* shortcut below — and it makes either state a shareable link.
*
* @return The view to render first.
*/
const getInitialView = (): DashboardView => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's better to move these kind of logic to router file instead of stage. You can do redirects there before anything renders, while at stage it's pretty late in the flow.

<div className="jetpack-newsletter-mode-page">
{ /* Shown over whichever view is up — it introduces the mode, not a view. */ }
<IntroModal />
{ view === 'stats' ? <StatsView /> : <OnboardingView /> }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You should use the router and separate routes for these. Now you've kinda implemented your own inside the router, which is a bit of a funky solution and unnecessary.

@davemart-in

Copy link
Copy Markdown
Contributor Author

Closing out in favor of Dotcom-only approach. See #50973

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement Changes to an existing feature — removing, adding, or changing parts of it [Focus] Newsletter [mu wpcom Feature] Write Newsletters [Package] Jetpack mu wpcom WordPress.com Features [Package] Newsletter Settings [Package] Newsletter [Package] Stats Admin [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ [Pri] Low [Tests] Includes Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants