Skip to content

Commit b7f4b05

Browse files
committed
🐛 Fixed 404 when navigating between post stats and editor
1 parent 3ecb770 commit b7f4b05

File tree

4 files changed

+64
-15
lines changed

4 files changed

+64
-15
lines changed

apps/stats/src/views/Stats/Overview/components/latest-post.tsx

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@ const LatestPost: React.FC<LatestPostProps> = ({
4646

4747
const metricClassName = 'group mr-2 flex flex-col gap-1.5 hover:cursor-pointer';
4848

49+
// Determine if we should navigate to editor instead of post analytics
50+
// Posts with no email data (never sent as newsletter) have no analytics when webAnalytics and membersTrackSources are disabled
51+
// Posts that were emailed (email-only OR published+sent) have newsletter stats, so they should go to analytics
52+
const hasEmailData = Boolean(latestPostStats?.email);
53+
const shouldGoToEditor = !hasEmailData &&
54+
!appSettings?.analytics.webAnalytics &&
55+
!appSettings?.analytics.membersTrackSources;
56+
57+
const getPostDestination = (postId: string) => {
58+
if (shouldGoToEditor) {
59+
return `/editor/post/${postId}`;
60+
}
61+
return `/posts/analytics/${postId}`;
62+
};
63+
4964
return (
5065
<Card className='group/card bg-gradient-to-tr from-muted/40 to-muted/0 to-50%' data-testid='latest-post'>
5166
<CardHeader>
@@ -99,7 +114,7 @@ const LatestPost: React.FC<LatestPostProps> = ({
99114
<div className='flex grow flex-col items-start justify-center self-stretch'>
100115
<div className='text-lg font-semibold leading-tighter tracking-tight hover:cursor-pointer hover:opacity-75' onClick={() => {
101116
if (!isLoading && latestPostStats) {
102-
navigate(`/posts/analytics/${latestPostStats.id}`, {crossApp: true});
117+
navigate(getPostDestination(latestPostStats.id), {crossApp: true});
103118
}
104119
}}>
105120
{latestPostStats.title}
@@ -136,13 +151,22 @@ const LatestPost: React.FC<LatestPostProps> = ({
136151
className={latestPostStats.email_only ? 'w-full' : ''}
137152
variant='outline'
138153
onClick={() => {
139-
navigate(`/posts/analytics/${latestPostStats.id}`, {crossApp: true});
154+
navigate(getPostDestination(latestPostStats.id), {crossApp: true});
140155
}}
141156
>
142-
<LucideIcon.ChartNoAxesColumn />
143-
<span className='hidden md:!visible md:!block'>
144-
{!latestPostStats.email_only ? 'Analytics' : 'Post analytics' }
145-
</span>
157+
{shouldGoToEditor ? (
158+
<>
159+
<LucideIcon.Pen />
160+
<span className='hidden md:!visible md:!block'>Edit post</span>
161+
</>
162+
) : (
163+
<>
164+
<LucideIcon.ChartNoAxesColumn />
165+
<span className='hidden md:!visible md:!block'>
166+
{!latestPostStats.email_only ? 'Analytics' : 'Post analytics'}
167+
</span>
168+
</>
169+
)}
146170
</Button>
147171
</div>
148172
</div>

apps/stats/src/views/Stats/Overview/components/top-posts.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,19 @@ const TopPosts: React.FC<TopPostsProps> = ({
7171

7272
const metricClass = 'flex items-center justify-end gap-1 rounded-md px-2 py-1 font-mono text-gray-800 hover:bg-muted-foreground/10 group-hover:text-foreground';
7373

74+
// Helper to determine navigation destination based on post type and analytics settings
75+
// Posts with no email data (never sent as newsletter) have no analytics when webAnalytics and membersTrackSources are disabled
76+
// Posts that were emailed (email-only OR published+sent) have newsletter stats, so they should go to analytics
77+
const getPostDestination = (postId: string, hasEmailData: boolean) => {
78+
const analyticsDisabled = !appSettings?.analytics.webAnalytics &&
79+
!appSettings?.analytics.membersTrackSources;
80+
81+
if (analyticsDisabled && !hasEmailData) {
82+
return `/editor/post/${postId}`;
83+
}
84+
return `/posts/analytics/${postId}`;
85+
};
86+
7487
return (
7588
<Card className='group/card w-full lg:col-span-2' data-testid='top-posts-card'>
7689
<CardHeader>
@@ -89,7 +102,7 @@ const TopPosts: React.FC<TopPostsProps> = ({
89102
return (
90103
<div key={post.post_id} className='group relative flex w-full items-start justify-between gap-5 border-t border-border/50 py-4 before:absolute before:-inset-x-4 before:inset-y-0 before:z-0 before:hidden before:rounded-md before:bg-accent before:opacity-80 before:content-[""] first:!border-border hover:cursor-pointer hover:border-transparent hover:before:block md:items-center dark:before:bg-accent/50 [&+div]:hover:border-transparent'>
91104
<div className='z-10 flex min-w-[160px] grow items-start gap-4 md:items-center lg:min-w-[320px]' onClick={() => {
92-
navigate(`/posts/analytics/${post.post_id}`, {crossApp: true});
105+
navigate(getPostDestination(post.post_id, post.sent_count !== null), {crossApp: true});
93106
}}>
94107
{post.feature_image ?
95108
<div className='hidden aspect-[16/10] w-[80px] shrink-0 rounded-sm bg-cover bg-center sm:!visible sm:!block lg:w-[100px]' style={{

ghost/admin/app/routes/lexical-editor.js

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,25 @@ export default AuthenticatedRoute.extend({
1717
},
1818

1919
setupController(controller, model, transition) {
20-
if (transition.from?.name?.startsWith('posts-x') && transition.to?.name !== 'lexical-editor.new') {
21-
// Extract the analytics path from window.location.href to preserve the exact tab
22-
let currentUrl = window.location.href;
23-
// Convert editor URL back to analytics URL and extract just the hash portion
24-
let analyticsUrl = currentUrl.replace('/editor/', '/').replace(/\/edit$/, '');
25-
let hashMatch = analyticsUrl.match(/#(.+)/);
26-
controller.fromAnalytics = hashMatch ? hashMatch[1] : 'posts-x';
20+
if (transition.to?.name === 'lexical-editor.new') {
21+
return;
22+
}
23+
24+
if (transition.from?.name?.includes('posts-x')) {
25+
// Came from post analytics - reconstruct the full analytics URL including tab
26+
let postId = transition.from?.parent?.params?.post_id || transition.from?.params?.post_id;
27+
if (!postId) {
28+
// Fallback: extract post ID from the editor URL hash
29+
let hashMatch = window.location.hash.match(/\/editor\/\w+\/([a-f0-9]+)/);
30+
postId = hashMatch?.[1] || model?.id;
31+
}
32+
let sub = transition.from?.params?.sub;
33+
controller.fromAnalytics = sub
34+
? `/posts/analytics/${postId}/${sub}`
35+
: `/posts/analytics/${postId}`;
36+
} else if (transition.from?.name?.includes('stats-x')) {
37+
// Came from stats overview
38+
controller.fromAnalytics = '/analytics';
2739
}
2840
},
2941

ghost/admin/tests/acceptance/editor-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,7 @@ describe('Acceptance: Editor', function () {
620620
expect(
621621
find('[data-test-breadcrumb]').getAttribute('href'),
622622
'breadcrumb link'
623-
).to.equal(`#posts-x`);
623+
).to.equal(`#/posts/analytics/${post.id}`);
624624
});
625625

626626
it('does not render analytics breadcrumb for a new post', async function () {

0 commit comments

Comments
 (0)