Repository navigation
feat(blog): browser handoff for reading, newsletter subscribe - #172
Merged
Merged
Conversation
Implements #121 and #122 together — #121 is the decision that determines what #122 can even be, so splitting them would have meant shipping a decision with no consequence. #121's answer: browser handoff. There is NO public blog-read endpoint. Re-verified at build time — /api/blog, /api/blog/posts and /api/blog/list all return 404, and the only other blog routes in the spec are /api/admin/blog*, which are admin-only and cookieAuth. So there is nothing to render in-app, and scraping the website's HTML is not a substitute for an API. "Open the blog in your browser" uses the same OS-browser pattern as OAuth linking and "Manage account on the web". #122: POST /api/blog/subscribe takes {email} and is double opt-in. The server validates the address — `not-an-email` returns 400 {"error":"A valid email is required","code":"bad_request"} — verified live with an obviously-invalid value, so no message was sent to anyone. The success envelope is deliberately unverified (confirming it would mean mailing a real address), so nothing is parsed from it. The UI states the double opt-in explicitly. A user who subscribes and is told nothing will assume they are subscribed, and they are not until they click the emailed link. Unsubscribe is deliberately NOT a button, and that is the interesting finding: both GET and POST /api/blog/unsubscribe take only a `?token=` query parameter, and that token comes from the email footer (the POST form is the RFC-8058 one-click target). An app that knows only the user's address therefore CANNOT unsubscribe them. #122 asked for "unsubscribe available in the same place"; that is not implementable, so the panel says where the link actually lives instead of offering a control that cannot work. UnsubscribeFromBlogAsync exists for a caller that genuinely holds a token, with a comment saying not to surface it as "unsubscribe me". Email is pre-filled from the account address, with a cheap client-side shape check so an obviously-malformed value doesn't cost a round trip. The server stays authoritative. Additive only: a new InterlinedApiClient.Blog.cs partial plus a self-contained BlogPanel control with its own ViewModel. SettingsView and SettingsViewModel are being reworked across several open PRs (#132/#134/#167/#170), so hosting is a one-line add afterward rather than a conflict now. Build green in Debug and Release. Closes #121 Closes #122 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 16, 2026
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.
Closes #121
Closes #122
One PR for both, deliberately: #121 is the decision that determines what #122 can be, so splitting them would ship a decision with no consequence.
#121's answer: browser handoff
There is no public blog-read endpoint. Re-verified at build time:
The only other blog routes in the spec are
/api/admin/blog*— admin-only andcookieAuth. So there is nothing to render in-app, and scraping the website's HTML is not a substitute for an API. "Open the blog in your browser" uses the same OS-browser pattern as OAuth linking and "Manage account on the web".#122: subscribe works; unsubscribe is not implementable
POST /api/blog/subscribetakes{email}and is double opt-in. The server validates the address:Verified with an obviously-invalid value, so no message was sent to anyone. The success envelope is deliberately unverified — confirming it would mean mailing a real address — so nothing is parsed from it.
The UI states the double opt-in explicitly. A user who subscribes and is told nothing will assume they're done, and they aren't until they click the emailed link.
Unsubscribe is deliberately not a button
Both
GETandPOST /api/blog/unsubscribetake only a?token=query parameter, and that token comes from the email footer (thePOSTform is the RFC-8058 one-click target). An app that knows only the user's address therefore cannot unsubscribe them.#122 asked for "unsubscribe available in the same place". That isn't possible, so the panel says where the link actually lives rather than offering a control that can't work — the same judgement as #128's push toggle and #160's mutual chips.
UnsubscribeFromBlogAsyncexists for a caller that genuinely holds a token, with a comment saying not to surface it as "unsubscribe me".Additive only
A new
InterlinedApiClient.Blog.cspartial plus a self-containedBlogPanelcontrol owning its own ViewModel.SettingsView/SettingsViewModelare being reworked across four open PRs (#132/#134/#167/#170), so hosting is a one-line add afterward rather than a conflict now:Honest note on priority
This was the lowest-value item in the backlog and I said so when filing it — a read-and-subscribe surface the browser already serves well. It's here for completeness. The subscribe control is the only part that does something the browser doesn't already do better.
dotnet build -c Debug— green.dotnet build -c Release— green.🤖 Generated with Claude Code