Skip to content

feat: blog RSS feed, plaintext (.txt) posts, and a hidden CLI blog reader#3471

Open
josephfarina wants to merge 2 commits into
mainfrom
navi/feat/blog-rss-and-txt
Open

feat: blog RSS feed, plaintext (.txt) posts, and a hidden CLI blog reader#3471
josephfarina wants to merge 2 commits into
mainfrom
navi/feat/blog-rss-and-txt

Conversation

@josephfarina

@josephfarina josephfarina commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What

Three additive read surfaces for the blog, with no change to how posts are authored or stored:

  1. /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).
  2. /blog/<slug>.txt — the raw Markdown body of a post as text/plain. Append .txt to any post URL to get clean source with no HTML.
  3. A hidden CLI blog command — reads the blog through the published feed: astryx blog lists posts from /rss.xml (and prints the feed + per-post .txt URLs), astryx blog <slug> fetches that post's .txt alternate.

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

  • RSS feed (app/rss.xml/route.ts): iterates blogPosts, emits <item>s with the human <link>, <guid>, description, category, authors, <pubDate>, and an <atom:link rel="alternate" type="text/plain"> pointing at the .txt URL. All interpolated values (including URLs) are XML-escaped. Autodiscovery <link> added to the site head via metadata.alternates.
  • Plaintext (app/blog/txt/[slug]/route.ts): a short header + the post's Markdown body as text/plain. A dynamic segment can't carry a static extension (a bare [slug] page swallows foo.txt — verified empirically), so the public URL /blog/:slug.txt is a next.config rewrite to /blog/txt/:slug. Lookup is exact-match against the in-memory registry — the slug never touches the filesystem.
  • CLI (api/blog.mjs, commands/blog.mjs): blog() fetches /rss.xml, parses items, and for a single post fetches its .txt alternate. Registered hidden, so it does not appear in --help or the manifest — only reachable by calling astryx blog directly.

Security

The CLI is the one component consuming untrusted input (network XML), so it's hardened accordingly:

  • Canonical origin only. The feed URL is derived from a hardcoded site origin (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.
  • SSRF guard (defense-in-depth). A post's .txt URL comes from feed content, so before fetching it the CLI asserts it lives on the canonical origin. A tampered feed pointing at 169.254.169.254 or localhost is refused (covered by a test).
  • Timeout + size cap. Every fetch has a 15s AbortController timeout and a 5 MB response cap.
  • No XXE / injection. The feed parser is regex-based (no XML entity expansion); the .txt route is text/plain with no HTML.
  • Adds ERR_UNKNOWN_POST and ERR_FETCH_FAILED error codes.

Testing

  • CLI blog API: 7 tests (list w/ feed URL, read-via-.txt, case-insensitive slug, unknown-post error, feed-fetch failure, SSRF cross-origin refusal, always-canonical-feed), fetch stubbed. All pass.
  • Manifest + error-code drift guards: pass; confirmed blog stays out of --help/manifest and that blog --json is cleanly rejected.
  • Live e2e against the running docsite (core built): /rss.xml returns valid RSS 2.0 (application/rss+xml), /blog/<slug>.txt returns the body as text/plain via the rewrite, unknown slugs and path-traversal attempts 404.

Notes

  • Changeset included (@astryxdesign/cli patch).
  • The docsite renders server-side, so the route handlers run at request time.

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.
@vercel

vercel Bot commented Jul 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview, Comment Jul 2, 2026 10:57pm

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Jul 2, 2026
…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.
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

🧪 Sandbox Preview

View Sandbox for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

No new or modified components detected.

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.6KB 0B

Accessibility Audit

Status: No accessibility violations detected.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

github-actions Bot added a commit that referenced this pull request Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant