diff --git a/apps/ui/src/components/site-overview-view/cards.module.css b/apps/ui/src/components/site-overview-view/cards.module.css
index ba012b05e5..6086792916 100644
--- a/apps/ui/src/components/site-overview-view/cards.module.css
+++ b/apps/ui/src/components/site-overview-view/cards.module.css
@@ -37,6 +37,20 @@
color: var(--wpds-color-fg-content-neutral);
}
+.headerActions {
+ display: flex;
+ align-items: center;
+ gap: var(--wpds-dimension-padding-sm);
+}
+
+.headerMeta {
+ font-size: var(--wpds-typography-font-size-xs);
+ line-height: var(--wpds-typography-line-height-xs);
+ color: var(--wpds-color-fg-content-neutral-weak);
+ white-space: nowrap;
+ cursor: default;
+}
+
.empty {
margin: 0;
font-size: var(--wpds-typography-font-size-sm);
@@ -66,6 +80,20 @@
min-width: 0;
}
+.rowTitle {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ font-size: var(--wpds-typography-font-size-sm);
+ line-height: var(--wpds-typography-line-height-sm);
+ color: var(--wpds-color-fg-content-neutral);
+}
+
+.rowTitleExpired {
+ color: var(--wpds-color-fg-content-neutral-weak);
+ text-decoration: line-through;
+}
+
.rowMeta {
overflow: hidden;
white-space: nowrap;
@@ -114,6 +142,10 @@
color: var(--wpds-color-fg-content-warning);
}
+.expired {
+ color: var(--wpds-color-fg-content-error);
+}
+
.itemIcon {
display: grid;
place-items: center;
diff --git a/apps/ui/src/components/site-overview-view/index.test.tsx b/apps/ui/src/components/site-overview-view/index.test.tsx
index cac9618843..34c59753be 100644
--- a/apps/ui/src/components/site-overview-view/index.test.tsx
+++ b/apps/ui/src/components/site-overview-view/index.test.tsx
@@ -6,6 +6,7 @@ import { useAgenticFeatures } from '@/data/queries/use-agentic-features';
import { useAuthUser, useLogin } from '@/data/queries/use-auth-user';
import { useConnectedWpcomSites } from '@/data/queries/use-connected-wpcom-sites';
import { useExistingCustomDomains } from '@/data/queries/use-create-site-helpers';
+import { usePublishPreviewSite } from '@/data/queries/use-preview-site';
import { useSiteStorageUsage } from '@/data/queries/use-site-storage-usage';
import { useSiteThumbnail } from '@/data/queries/use-site-thumbnail';
import {
@@ -19,6 +20,7 @@ import {
useUpdateSite,
useXdebugEnabledSite,
} from '@/data/queries/use-sites';
+import { useSnapshots, useSnapshotUsage } from '@/data/queries/use-snapshots';
import { usePullSiteFromLive, usePushSiteToLive } from '@/data/queries/use-sync-site';
import { useUserPreferences } from '@/data/queries/use-user-preferences';
import { useWordPressVersions, useWpVersion } from '@/data/queries/use-wordpress-versions';
@@ -105,6 +107,15 @@ vi.mock( '@/data/queries/use-connected-wpcom-sites', () => ( {
useConnectedWpcomSites: vi.fn(),
} ) );
+vi.mock( '@/data/queries/use-preview-site', () => ( {
+ usePublishPreviewSite: vi.fn(),
+} ) );
+
+vi.mock( '@/data/queries/use-snapshots', () => ( {
+ useSnapshots: vi.fn(),
+ useSnapshotUsage: vi.fn(),
+} ) );
+
vi.mock( '@/data/queries/use-create-site-helpers', () => ( {
useExistingCustomDomains: vi.fn(),
} ) );
@@ -163,6 +174,9 @@ const useConnectorMock = vi.mocked( useConnector, { partial: true } );
const useAgenticFeaturesMock = vi.mocked( useAgenticFeatures );
const useAuthUserMock = vi.mocked( useAuthUser, { partial: true } );
const useConnectedWpcomSitesMock = vi.mocked( useConnectedWpcomSites, { partial: true } );
+const usePublishPreviewSiteMock = vi.mocked( usePublishPreviewSite, { partial: true } );
+const useSnapshotsMock = vi.mocked( useSnapshots, { partial: true } );
+const useSnapshotUsageMock = vi.mocked( useSnapshotUsage, { partial: true } );
const useLoginMock = vi.mocked( useLogin, { partial: true } );
const useExistingCustomDomainsMock = vi.mocked( useExistingCustomDomains, { partial: true } );
const useCopySiteMock = vi.mocked( useCopySite, { partial: true } );
@@ -197,6 +211,7 @@ describe( 'SiteOverviewView', () => {
const copyText = vi.fn().mockResolvedValue( undefined );
const pullSiteFromLive = vi.fn();
const pushSiteToLive = vi.fn();
+ const publishPreviewSite = vi.fn();
const onTabChange = vi.fn();
const connectorStub = ( openInOS = true ) => ( {
@@ -244,6 +259,12 @@ describe( 'SiteOverviewView', () => {
} );
useLoginMock.mockReturnValue( { isPending: false, mutate: vi.fn() } );
useConnectedWpcomSitesMock.mockReturnValue( { data: [], isLoading: false } );
+ useSnapshotsMock.mockReturnValue( { data: [] } );
+ useSnapshotUsageMock.mockReturnValue( { data: undefined } );
+ usePublishPreviewSiteMock.mockReturnValue( {
+ isPending: false,
+ mutate: publishPreviewSite,
+ } );
usePullSiteFromLiveMock.mockReturnValue( { mutate: pullSiteFromLive } );
usePushSiteToLiveMock.mockReturnValue( { mutate: pushSiteToLive } );
useIsSiteSyncingMock.mockReturnValue( { push: false, pull: false } );
@@ -423,6 +444,49 @@ describe( 'SiteOverviewView', () => {
);
} );
+ it( 'shows preview expiry and account usage', () => {
+ useSnapshotsMock.mockReturnValue( {
+ data: [
+ {
+ url: 'demo-preview.wp.build',
+ atomicSiteId: 1,
+ localSiteId: 'site-1',
+ date: Date.now() - 5 * 24 * 60 * 60 * 1000,
+ },
+ ],
+ } );
+ useSnapshotUsageMock.mockReturnValue( {
+ data: { siteCount: 2, siteLimit: 10, siteCreationBlocked: false },
+ } );
+
+ renderView();
+
+ expect( screen.getByText( 'demo-preview.wp.build' ) ).toBeVisible();
+ expect( screen.getByText( 'Expires in 2 days' ) ).toBeVisible();
+ expect( screen.getByText( '2 of 10' ) ).toBeVisible();
+ } );
+
+ it( 'publishes a preview site from its empty state', () => {
+ renderView();
+
+ fireEvent.click( screen.getByRole( 'button', { name: 'Publish a preview site' } ) );
+
+ expect( publishPreviewSite ).toHaveBeenCalledWith( { siteId: 'site-1' } );
+ } );
+
+ it( 'explains that preview publishing requires sign-in', () => {
+ useAuthUserMock.mockReturnValue( { data: null } );
+
+ renderView();
+
+ expect(
+ screen.getByText( 'Sign in to publish a preview site and share your work.' )
+ ).toBeVisible();
+ expect(
+ screen.queryByRole( 'button', { name: 'Publish a preview site' } )
+ ).not.toBeInTheDocument();
+ } );
+
it( 'keeps the browser action available without a cached thumbnail', () => {
useSiteThumbnailMock.mockReturnValue( { data: null } );
diff --git a/apps/ui/src/components/site-overview-view/index.tsx b/apps/ui/src/components/site-overview-view/index.tsx
index 1b02bcc906..73a47d2bd8 100644
--- a/apps/ui/src/components/site-overview-view/index.tsx
+++ b/apps/ui/src/components/site-overview-view/index.tsx
@@ -36,6 +36,7 @@ import { useTrafficLightSpace } from '@/hooks/use-traffic-light-space';
import { AboutSection } from './about-section';
import { ConnectionsSection } from './connections-section';
import { CardSectionDivider, OverviewCard } from './overview-card';
+import { PreviewSitesSection } from './preview-sites-section';
import styles from './style.module.css';
import type { SiteSettingsTabId } from '@/components/site-settings-view';
import type { SiteDetails } from '@/data/core';
@@ -272,6 +273,8 @@ function SiteOverviewBody( {