feat(snapshot): share placements for the post page - #6556
Open
tomeredlich wants to merge 27 commits into
Open
Conversation
Adds a Storybook page that draws the post page from its production components — PostSourceInfo, PostActions, PostContentWidget and MobilePostFloatingBar — at desktop, tablet and mobile, showing what ships today and the one snapshot placement we want. Today the post menu opens with 'Share via' and the engagement bar ends with a labeled 'Copy'; below laptop PostHeaderActions is hidden, so on tablet the only share action on the screen is that Copy. The post has a real per-post OG image, so snapshot is limited to the text-selection bar, where a highlighted line has no OG of its own. Carries the Snapshot icon it needs; no production surface changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The page argued for one snapshot placement — the text-selection bar — without showing what pressing it produces, so the trade-off against Copy link had to be taken on trust. The selection category now ends with the 1080x1080 card the button exports, drawn from the real HighlightTextSnapshotCard (#6544) at a third of its size and seeded with the highlighted line on the page above it. Carries the four card files that page needs from #6544 — the frame, the seeded gradient, the quote truncation and the highlight card itself — plus the .snapshot-copy utility they rely on. No production surface renders them yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#6556 argued the case for one snapshot placement and drew what it exports; this makes it real. Selecting a line of a post now raises a floating bar with copy link, copy text and Snapshot, and Snapshot hands back the 1080x1080 PNG of the quote — clipboard first, download where the clipboard refuses images. Behind `snapshot_selection_share`, which defaults to off, and only on the post page: the decision on the modal was to leave it alone. `useConditionalFeature` evaluates on post pages only, so nothing else logs an exposure. The capture pipeline and the button come from #6544 (snapdom, captureShareImage, SnapshotButton). New here: `useTextSelection`, which reports a selection only while it lives inside the post body, and the bar itself. The selection commits on the trailing edge of the drag rather than behind a drag flag — a pointerup lost to a release outside the window would otherwise strand the toolbar. /dev/snapshot carries the review surface: the live bar, the card at every quote length, and the real capture output. Unflagged there, so it can be reviewed without GrowthBook, and blocked on the canonical production hosts. Testing: shared 376 suites / 2652 tests, webapp 81 / 644, extension 6 / 52. Drag-selected a quote in a browser and captured from the bar: clipboard on a real press, download plus an "Image saved" toast on a scripted one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The version on this branch was a trimmed copy: two categories out of six. The page on #6544 carries the whole post-page argument, and the missing four are the load-bearing ones — the share strip, end-of-thread band and post-upvote prompt; the audit showing seven post types breaking seven different ways; the poll recommendation, which is the one type where snapshot beats a link outright; and the modal and sticky nav the audit had missed. Brings surfaceChrome along, since the page is drawn with its ShareSheet and its three real viewport widths rather than the older device rail, and keeps the exported-image variant next to the selection bar it belongs to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Storybook page argues for nine placements; only the selection bar could actually be pressed. /dev/snapshot now carries all of them with working controls rather than pictures of them: copy summary, the selection bar, the share strip, the end-of-thread band, the post-upvote prompt, both poll snapshots and the sticky nav copy-link. Every copy button copies for real and every Snapshot rasterizes the card it would share. Two new payloads this needed: - PollSnapshotCard — the poll recommendation had no card. The winner is drawn in the accent and the rest stay quiet, so the answer reads before the bars. - CopySummaryButton (#6350) — headline, TLDR and link as three paragraphs, so a paste into a thread is a usable message rather than a bare URL. Both copy call sites now catch a rejected clipboard write. `navigator.clipboard` refuses outright when the document is not focused, and the shared hook awaits it without a catch, so the press produced no toast, no fallback and an unhandled rejection. It now says the clipboard was blocked. Only the selection bar is wired to the real post page; the rest are placement mocks with real controls, which is what the review needs before any of them is committed to a surface. Testing: shared 377 suites / 2654 tests, webapp 81 / 644. Pressed every control in a browser — real clicks copy (OS clipboard changes), blocked clipboard shows the error toast, both poll Snapshots and both capture buttons return 1080² PNGs (quote 423 KB, poll 378 KB). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every placement the Storybook page argues for is now production code on the surface it belongs to, each behind its own flag defaulting to off: - post_copy_summary — CopySummaryButton runs into the end of the TLDR in PostContent, evaluated only where a summary exists. - post_share_prompts — DiscussionShareRow under the comment composer in PostEngagements, already shipping in the discussion panel but never on the post page, and EndOfThreadShare after the thread. - poll_snapshot — PollSnapshotButton under the poll bars and on the post-vote prompt in PollPostContent, evaluated only on polls that have votes. - post_nav_copy_link — a copy-link icon in PostNavigation, gated on isFixedNavigation so the inline navigation is untouched. The post-upvote prompt needed nothing: PostContentShare already renders "Should anyone else see this post?" with a copy-link input the moment you upvote. The page proposed a placement production had already built. pollSnapshotFromPost derives each bar from the options' own counts rather than numPollVotes, which counts voters and would leave the bars adding up to something other than the poll. /dev/snapshot now mounts the real components instead of imitations. `/dev/*` short-circuits to a QueryClient-only tree in _app, so the page stands in for the auth and log contexts the share components reach for — signed out, no squads, logging swallowed — and names the flag behind each placement. Testing: shared 379 suites / 2661 tests, webapp 81 / 644, extension 6 / 52. New specs cover the poll payload maths, the band's empty-thread case, the copy payload and both flag states for copy summary. Pressed every control in a browser: both poll snapshots capture, the real share row and band render. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lint_shared runs eslint over the whole package with --max-warnings 0, and this file was written after the last per-file fix pass, so a prettier break reached CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The placements were production code on the real surfaces but invisible where they most needed reviewing. A preview deployment runs NODE_ENV=production against the production API, so `isDevelopment` is false and GrowthBook's dev tools are off: there is no way to open a flag from the browser, and every placement defaulted to off. useSharePlacement wraps the flag: on, when the flag says so, or unconditionally on a *.preview.app.daily.dev host. The host is read after mount rather than during render, because the server cannot know what it will be served as and disagreeing would break hydration. The committed defaults stay false, so merging still changes nothing for anyone on app.daily.dev and the rollout remains a GrowthBook decision. Only the branch preview opens itself, and a surface that opted out with shouldEvaluate stays out there too. Testing: shared 380 suites / 2665 tests, webapp 81 / 644. New spec covers the production host with the flag on and off, the preview host with no flag, and the opted-out surface on a preview. Package lint clean for shared and webapp. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found on the preview: the icon was missing on a real post that plainly had a summary. Anonymous visitors get in-content AdSense, and then the post page renders the summary itself through `renderSummarySegments` — ad slots between the parts — so PostContent's own paragraph, the only branch carrying the button, never runs. Every signed-out visitor saw nothing. The segmented branch cannot take the icon inline the way the plain one does, since the parts belong to the page's render prop, so it trails the block instead. Testing: shared 380 suites / 2667 tests, webapp 81 / 644, package lint clean. Two new specs cover the segmented summary with the flag on and off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four placements were invisible, and they had one cause: they were wired to PostContent and PostEngagements, which the redesigned post page does not render. It draws PostFocusCard and PostDiscussionPanel instead, and the post_redesign flag decides which layout a reader gets — so the work landed on the layout half the audience does not see. The two that did work were the ones that did not depend on it: the share strip, which the discussion panel already shipped, and the poll, which is not redesign-eligible and is always classic. - PostFocusCard now carries the selection bar, scoped to the card so a quote can only come from the post's own body, and copy summary on both summary variants — the plain paragraph and the truncated video one. - PostDiscussionPanel carries the end-of-thread band, and PostContentShare, which is where the classic page's post-upvote prompt comes from. The prompt also never fired from the post page at all: PostContentShare waits on usePostActions, and only feed card buttons raised 'upvote'. Both post-page bars — PostActions and FocusCardActionBar — now raise it when they upvote, behind post_share_prompts so the prompt's existing behaviour is unchanged wherever the flag is off. Testing: shared 380 suites / 2667 tests, webapp 81 / 644, extension 6 / 52, package lint clean for shared and webapp. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two reasons it was missing from the modal. The gate was `isPostPage`, so the classic modal never mounted it — I had read the page's "no snapshot on the post page or modal" as covering selected text too, when selected text was the exception it carved out. And the bar is portaled to the body at z-popup while the modal overlay is z-modal, so even once mounted it rendered behind the modal the quote was selected in. The bar is now page and modal both, at z-max: a reader highlights a line wherever they happen to be reading it, and on desktop that is mostly the modal. Copy summary is still page-only — say the word and it can follow. Also covers the redesigned layout with specs: PostFocusCard is what the post_redesign flag serves, and the placements were wired to the classic layout first. Testing: shared 380 suites / 2673 tests, webapp 81 / 644, package lint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nk with it Design pass on the three prompts: - The floating bar leads with Snapshot, labelled and solid, with copy link and copy text as the quiet pair beside it. It is the reason the bar exists and it was the last, smallest thing in the row. - "Should anyone else see this post?" drops the bordered widget and the link in an input for a copy-link button with the icon trailing. There is one thing to do with that link, and an input asks to be read first. Behind post_share_prompts, so the shipping prompt is unchanged until rollout. - The end-of-thread band drops its box for the same reason, reads "Send it to someone who'd have opinions" instead of counting comments, and offers a solid copy-link button. It still only appears where a thread exists. A copied snapshot now carries the post link as text/plain beside the PNG, so a rich composer takes the image and a plain one takes the link. Browsers that refuse two representations in one item still get the image. Testing: shared 381 suites / 2676 tests, webapp 81 / 644, extension 6 / 52, package lint clean. New spec covers the clipboard payload, its single-item fallback and the no-link case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- The poll card carried its status and count as a footer, while the product puts that line under the question and above the options. It now reads the same way, and says what the product says: "Voting open · 1.3K votes · Today", or "Voting ended · N total votes" once it closes. The source keeps the bottom line as attribution. - The upvote prompt moves out of the discussion panel and under the action bar that raises it — the classic page has always had it there, and on the focus layout it had ended up below the comment composer. - Both prompts now read as a pair: title, subtext, and a solid Copy link with the icon trailing. - Both primary buttons are ButtonV2. Testing: shared 381 suites / 2677 tests, webapp 81 / 644, package lint clean. The poll spec now freezes the clock rather than reading it: postDateFormat answers "Now" inside the first minute, so a real clock decided the assertion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tomeredlich
marked this pull request as draft
September 2, 2026 06:50
tomeredlich
added a commit
that referenced
this pull request
Sep 2, 2026
Three controls inside the brief, all on `briefing_share_controls`: - The post page's highlight bar (#6556), scoped to the body so a selection in the header or the preferences card never raises it. - A copy icon per bullet and paragraph, carrying the text and the brief link, like #6350's copy-summary icon. - A Snapshot on the Must know heading that captures its bullets as one card, with the claim before the colon as each line. The body is a single `<Markdown content={contentHtml} />`, so none of these can be declared in JSX: briefBodyBlocks reads the blocks back out of the rendered DOM — which is also the most faithful source, being what the reader is looking at — and the controls are portalled into them. The end band takes #6349's borderless layout with the icon on the right, copy unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tomeredlich
added a commit
that referenced
this pull request
Sep 2, 2026
Three controls inside the brief itself: - The post page's highlight bar (#6556), scoped to a body container so a selection in the header or the preferences card never raises it. - A copy icon per bullet and paragraph, carrying the text and the brief link, after #6350's copy-summary icon. Each one is labelled with what it copies, since a brief carries a column of them. - A Snapshot on the Must know heading that captures its bullets as one card, with the claim before the colon as each line. None of these can be declared in JSX: the body is a single `<Markdown content={contentHtml} />`, so briefBodyBlocks reads the blocks back out of the rendered DOM — which is also the most faithful source, being what the reader is looking at — and the controls are portalled into them. Hosts are marked and swept before each run, because Markdown re-renders on its own for hover cards and the image modal. The closing band is #6369's ShareBand with the briefing's copy, so it cannot drift from the end-of-conversation band it was asked to match. All of it sits behind `briefing_share_controls`, default false. Mockup-to-eng-pass: 1 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…o what ships The copy confirmation was a green check of its own invention. It is now the upvote button's glyph, colour and spin, so the two gestures that mean "that worked" look alike. The Storybook page also carried the pre-change design of every surface and the placements we decided against — the menu-and-sheet route, the seven-post-type audit, the poll before snapshot, the modal and sticky nav as they ship. A review page for a change should show the change, so only the five flagged placements remain, each labelled with its flag. The scaffolding those sections needed goes with them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tomeredlich
added a commit
that referenced
this pull request
Sep 3, 2026
Three controls inside the brief itself: - The post page's highlight bar (#6556), scoped to a body container so a selection in the header or the preferences card never raises it. - A copy icon per bullet and paragraph, carrying the text and the brief link, after #6350's copy-summary icon. Each one is labelled with what it copies, since a brief carries a column of them. - A Snapshot on the Must know heading that captures its bullets as one card, with the claim before the colon as each line. None of these can be declared in JSX: the body is a single `<Markdown content={contentHtml} />`, so briefBodyBlocks reads the blocks back out of the rendered DOM — which is also the most faithful source, being what the reader is looking at — and the controls are portalled into them. Hosts are marked and swept before each run, because Markdown re-renders on its own for hover cards and the image modal. The closing band is #6369's ShareBand with the briefing's copy, so it cannot drift from the end-of-conversation band it was asked to match. All of it sits behind `briefing_share_controls`, default false. Mockup-to-eng-pass: 1 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tton The split control already answers a copy with the avocado arrow spinning in. The copy buttons around it — the selection bar's link and text, copy summary, and the sticky nav's link — answered with nothing, so the same gesture landed differently depending on which control the reader reached for. CopyStateIcon now takes the glyph it rests on. Left at its default it is the copy sheets, as the split button has it; the two link controls rest on the link, so a row of icons still reads at a glance. The confirmation itself is unchanged, and the 1s window comes from useCopy, which already tracked it. Testing: shared 383 suites / 2684 tests, webapp 81 / 644, package lint clean. New spec covers the resting state, the landed state and a custom idle glyph. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tomeredlich
added a commit
that referenced
this pull request
Sep 3, 2026
Three controls inside the brief itself: - The post page's highlight bar (#6556), scoped to a body container so a selection in the header or the preferences card never raises it. - A copy icon per bullet and paragraph, carrying the text and the brief link, after #6350's copy-summary icon. Each one is labelled with what it copies, since a brief carries a column of them. - A Snapshot on the Must know heading that captures its bullets as one card, with the claim before the colon as each line. None of these can be declared in JSX: the body is a single `<Markdown content={contentHtml} />`, so briefBodyBlocks reads the blocks back out of the rendered DOM — which is also the most faithful source, being what the reader is looking at — and the controls are portalled into them. Hosts are marked and swept before each run, because Markdown re-renders on its own for hover cards and the image modal. The closing band is #6369's ShareBand with the briefing's copy, so it cannot drift from the end-of-conversation band it was asked to match. All of it sits behind `briefing_share_controls`, default false. Mockup-to-eng-pass: 1 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#6570 extends CopyStateIcon for the same reason — a copy control that rests on a link glyph — and lands on a green VIcon that fades in, with the resting glyph passed as `icon`. This branch had an avocado UpvoteIcon spinning 360° through a `copy-confirm` keyframe, under a prop called `idle`. Both PRs touch the same file, so this takes #6570's component and spec verbatim and renames the call sites. The keyframe goes with the spin: nothing references it once the check lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bar held the coordinates the quote had when it was made. Scrolling left it behind — hundreds of pixels from the highlight, down beside the composer — because the only scroll listener was on the window, and the post page scrolls an inner container. A scroll event does not bubble, so the window never heard it. Listening on the document in the capture phase catches whichever ancestor actually scrolls, in the page and in the modal alike. A ResizeObserver covers the other way the quote moves without a scroll: expanding a truncated summary reflows everything under it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The link now sits beside the ⋯ in PostHeaderActions, which every post type renders — article, share, freeform, welcome, collection, video and poll — on the page, in the modal and in the sticky nav. One placement rather than one per content component, since that cluster is the only thing all of them have in common. It replaces the sticky-nav-only button: PostNavigation draws this same cluster, so keeping both put two link icons side by side in the sticky bar. The flag loses its `nav` with the placement — `post_nav_copy_link` becomes `post_copy_link`, which is safe to rename because it has never been turned on. Testing: shared 384 suites / 2693 tests, webapp 81 / 644, package lint clean. The spec walks all seven post types, the flag-off case, and the position relative to the menu. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
typecheck_strict_changed went red: PostHeaderActionsProps requires contextMenuId and the spec left it out. I typechecked before writing that spec and never again after, so only CI saw it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s one The link went into PostHeaderActions, and the redesigned post page does not render that component — PostFocusCard builds its own header and reaches for PostMenuOptions directly, as do CollectionPostHeaderActions, FocusCardActionBar and EngagementRail. So the button existed and was invisible on the layout most readers get. PostMenuOptions is the one control every header has in common, which is what "next to the three dots" actually means. The link now renders there, just before the menu trigger, and every header inherits it. Testing: shared 384 suites / 2693 tests, webapp 81 / 644, package lint clean, typecheck against origin/main as CI runs it. The spec moves with the code and still walks all seven post types. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A collection has no summary — CollectionPostContent renders contentHtml through Markdown — so the copy that trails a TLDR everywhere else had nothing to attach to and the body ended with no way to lift a passage out. Each paragraph now ends with its own copy. The body is sanitized HTML written straight into the DOM, so there is no JSX to hang a button off: every paragraph gets one empty span appended and the control is portalled into it, which leaves the markup the renderer's and the button React's. A MutationObserver covers the sanitize finishing after the first render, and a signature of the paragraph text stops the spans it appends from feeding it back to itself. Paragraphs under 80 characters are left alone; a caption is not worth a control. Behind post_copy_summary, the flag already covering "copy the text of this post". Testing: shared 385 suites / 2696 tests, webapp 81 / 644, package lint clean, typecheck against origin/main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tomeredlich
added a commit
that referenced
this pull request
Sep 6, 2026
Three controls inside the brief itself: - The post page's highlight bar (#6556), scoped to a body container so a selection in the header or the preferences card never raises it. - A copy icon per bullet and paragraph, carrying the text and the brief link, after #6350's copy-summary icon. Each one is labelled with what it copies, since a brief carries a column of them. - A Snapshot on the Must know heading that captures its bullets as one card, with the claim before the colon as each line. None of these can be declared in JSX: the body is a single `<Markdown content={contentHtml} />`, so briefBodyBlocks reads the blocks back out of the rendered DOM — which is also the most faithful source, being what the reader is looking at — and the controls are portalled into them. Hosts are marked and swept before each run, because Markdown re-renders on its own for hover cards and the image modal. The closing band is #6369's ShareBand with the briefing's copy, so it cannot drift from the end-of-conversation band it was asked to match. All of it sits behind `briefing_share_controls`, default false. Mockup-to-eng-pass: 1 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tomeredlich
marked this pull request as ready for review
September 6, 2026 06:10
This was referenced Sep 6, 2026
The quote card was a size ladder inside a fixed square: short highlights set at 72px, long ones stepped down to 40 and the frame padded whatever was left with gradient. #6544 replaced that with one copy size in a frame that grows with the passage, so a shared image no longer changes character with its length, and pulls the attribution back to just the source. Carries over SnapshotFrame's grow/wide modes, SnapshotCredit, and the measured capture options — a growing card cannot be captured at an assumed 1080 square without letterboxing a long passage or padding a short one. The poll card keeps its square, since bar charts need it, and picks up the same credit block. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A link the backend could not attribute is given a real source record named "unknown", handle "unknown", over a squad placeholder avatar. In the post header that passes as chrome; printed as the sole credit on an image someone sends out it reads as a bug — and the credit became the only attribution when the card dropped the domain line. Credit the domain instead, which is the one thing an unattributed link still carries, and nobody at all when there is not even that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What ships
Every share placement the Storybook post-page page argues for, wired into the surfaces it names, each behind a flag defaulting to off:
PostContent,PostFocusCardsnapshot_selection_sharePostContent,PostFocusCardpost_copy_summaryCollectionPostContent,PostFocusCardpost_copy_summaryPostEngagements,PostDiscussionPanelpost_share_promptsPollPostContentpoll_snapshotPostMenuOptionspost_copy_linkSelecting a line raises a bar whose Snapshot rasterizes the quote into a 1080² PNG and puts it on the clipboard with the tracked link. Polls get the same treatment for their result. Everything that copies a link resolves the short, campaign-carrying URL at press time, the way
usePostContentandDiscussionShareRowalready do, and confirms with the green check from #6570.Placements are on for
*.preview.app.daily.devregardless of GrowthBook (useSharePlacement), because a preview runs as production against the production API and has no way to open a flag from the browser. Merging changes nothing for anyone on app.daily.dev.Two things a reviewer should look at first
Vendored from a closed PR.
ShareBand,ShareActions,SplitShareButtonandCopyStateIconcome from #6369, which is closed, and #6378, which merged into it. They exist on no branch that ships, so they are carried here. If that stack revives,EndOfThreadShareandPostContentShareshould collapse into itsEndOfConversationShareand these copies should go./dev/snapshotis an internal review surface for all of the above, unflagged so it can be judged without GrowthBook. Host-gated off app.daily.dev,noindex.Not in this PR
The mobile single-tap share path still awaits the shortener before
navigator.share; that lives inuseShareOrCopyLinkand is fixed standalone in #6564.Events
SharePoston every copy and share, with the provider and origin. Nothing new.Experiment
Five flags, all defaulting to off. No experiment is being started here — this is the code the experiments would enable.
Testing
typecheck-strict-changedagainstorigin/main.Review links
Every placement opens by itself on
*.preview.app.daily.dev(useSharePlacement, above), so none of this needs GrowthBook.Features/Snapshot/Surfaces/Post page: https://storybook-git-snapshot-post-page-variations-dailydotdev.vercel.app/?path=/story/features-snapshot-surfaces-post-page--variationsPreview domain
https://snapshot-post-page-variations.preview.app.daily.dev