feat: blog RSS feed, plaintext (.txt) posts, and a hidden CLI blog reader#3471
Open
josephfarina wants to merge 2 commits into
Open
feat: blog RSS feed, plaintext (.txt) posts, and a hidden CLI blog reader#3471josephfarina wants to merge 2 commits into
josephfarina wants to merge 2 commits into
Conversation
Adds an RSS 2.0 feed at /rss.xml and a plaintext variant of every post at /blog/<slug>.txt, then teaches the CLI to read the blog through the feed. Nothing about how posts are authored or stored changes; these are additive read surfaces. - /rss.xml: RSS feed generated from the same blog registry that drives the routes, so it never drifts. Each item carries the human post URL and an atom:link alternate pointing at the plaintext variant. - /blog/<slug>.txt: the post's raw Markdown body as text/plain. Served via a next.config rewrite to /blog/txt/[slug] (a dynamic segment can't carry a static extension on its own). - RSS autodiscovery <link> added to the site head. - CLI 'blog' command (hidden from --help and the manifest): fetches /rss.xml to list posts and reads a post via its .txt alternate. The CLI never touches blog source files; it consumes the public feed like any reader. - Adds ERR_UNKNOWN_POST and ERR_FETCH_FAILED error codes.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…ngeset - Remove the --site override: the CLI always reads the canonical site feed (the site is the source of truth; no user-supplied URL to follow). - Add packages/cli/src/lib/site.mjs as the single place the CLI holds the site origin. - fetchText: 15s timeout via AbortController + 5MB response cap. - assertCanonicalOrigin: a post's plaintext URL from feed content must live on the canonical origin (defense-in-depth SSRF guard). - Surface the RSS feed URL in the command output (list header + per-post .txt URLs) so an agent can hit the feed directly. - RSS route: XML-escape interpolated URLs. - Add changeset.
Contributor
PR Analysis Report📚 Storybook PreviewView Storybook for this PR 🧪 Sandbox PreviewView Sandbox for this PR No new or modified components detected. Bundle Size Summary
Accessibility AuditStatus: No accessibility violations detected. Generated by PR Enrichment workflow | Storybook | Sandbox | View full report |
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
Three additive read surfaces for the blog, with no change to how posts are authored or stored:
/rss.xml— an RSS 2.0 feed of every post, generated from the same blog registry that drives the blog routes (so it can't drift)./blog/<slug>.txt— the raw Markdown body of a post astext/plain. Append.txtto any post URL to get clean source with no HTML.blogcommand — reads the blog through the published feed:astryx bloglists posts from/rss.xml(and prints the feed + per-post.txtURLs),astryx blog <slug>fetches that post's.txtalternate.Why
Posts stay exactly where they are. The CLI doesn't reach into the repo or bundle content; it consumes the public feed like any reader would, so the blog's structure can change freely without touching the CLI. The feed advertises each post's plaintext URL, so an agent can go from "list posts" to "read this one as text" using only public URLs.
How it works
app/rss.xml/route.ts): iteratesblogPosts, emits<item>s with the human<link>,<guid>, description, category, authors,<pubDate>, and an<atom:link rel="alternate" type="text/plain">pointing at the.txtURL. All interpolated values (including URLs) are XML-escaped. Autodiscovery<link>added to the site head viametadata.alternates.app/blog/txt/[slug]/route.ts): a short header + the post's Markdown body astext/plain. A dynamic segment can't carry a static extension (a bare[slug]page swallowsfoo.txt— verified empirically), so the public URL/blog/:slug.txtis anext.configrewrite to/blog/txt/:slug. Lookup is exact-match against the in-memory registry — the slug never touches the filesystem.api/blog.mjs,commands/blog.mjs):blog()fetches/rss.xml, parses items, and for a single post fetches its.txtalternate. Registeredhidden, so it does not appear in--helpor the manifest — only reachable by callingastryx blogdirectly.Security
The CLI is the one component consuming untrusted input (network XML), so it's hardened accordingly:
packages/cli/src/lib/site.mjs). There is no--site/user-supplied URL — the site is the source of truth and the CLI can never be pointed at an arbitrary host..txtURL comes from feed content, so before fetching it the CLI asserts it lives on the canonical origin. A tampered feed pointing at169.254.169.254orlocalhostis refused (covered by a test).AbortControllertimeout and a 5 MB response cap..txtroute istext/plainwith no HTML.ERR_UNKNOWN_POSTandERR_FETCH_FAILEDerror codes.Testing
fetchstubbed. All pass.blogstays out of--help/manifest and thatblog --jsonis cleanly rejected./rss.xmlreturns valid RSS 2.0 (application/rss+xml),/blog/<slug>.txtreturns the body astext/plainvia the rewrite, unknown slugs and path-traversal attempts 404.Notes
@astryxdesign/clipatch).