Skip to content

feat(blog): browser handoff for reading, newsletter subscribe - #172

Merged
Adron merged 2 commits into
mainfrom
issue-121-122-blog
Sep 24, 2026
Merged

Adron merged 2 commits into
mainfrom
issue-121-122-blog

Conversation

@Adron

@Adron Adron commented Sep 16, 2026

Copy link
Copy Markdown
Member

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:

/api/blog        -> 404
/api/blog/posts  -> 404
/api/blog/list   -> 404

The only other blog routes in the spec are /api/admin/blog* — 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: subscribe works; unsubscribe is not implementable

POST /api/blog/subscribe takes {email} and is double opt-in. The server validates the address:

POST /api/blog/subscribe {"email":"not-an-email"}
  -> 400 {"error":"A valid email is required","code":"bad_request"}

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 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 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. UnsubscribeFromBlogAsync exists 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.cs partial plus a self-contained BlogPanel control owning its own ViewModel. SettingsView/SettingsViewModel are being reworked across four open PRs (#132/#134/#167/#170), so hosting is a one-line add afterward rather than a conflict now:

<local:BlogPanel Margin="0,0,0,12"/>

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

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>
@Adron
Adron merged commit 2ef72ee into main Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Blog: subscribe and unsubscribe from the newsletter Blog: decide between an in-app reader and a browser handoff

1 participant