Skip to content

Commit 438d9f6

Browse files
authored
fix(web): harden feed reader fallback and rss rendering (#944)
## Summary - harden feed responder and RSS rendering behavior for reader compatibility and fallback safety - add/adjust RSS XSL and shared public UI assets used by feed presentation - add regression coverage for responder and RSS XSL behavior - refresh docs where behavior/auth boundaries changed This pull request introduces several improvements to the feed rendering experience, both visually and functionally, along with some internal refactoring and documentation updates. The most significant changes are a major redesign of the RSS feed XSL template to enhance the UI, the addition of a script to improve feed reader link handling, and a refactor in the feed responder logic. There are also updates to documentation for clarity and accuracy. **Feed UI and UX improvements:** * Major redesign of the `public/rss.xsl` template to provide a more modern, accessible, and informative feed presentation. This includes a new hero section with icons, improved layout, additional metadata (such as updated/published stamps), and visual signals for item quality (summary, image, tags, byline). The CSS has been extensively updated to support these changes. [[1]](diffhunk://#diff-498feb1537333a67915485f92ad9c4b9b7748289e9ac06394a0207cca746e928L2-R2) [[2]](diffhunk://#diff-498feb1537333a67915485f92ad9c4b9b7748289e9ac06394a0207cca746e928R13) [[3]](diffhunk://#diff-498feb1537333a67915485f92ad9c4b9b7748289e9ac06394a0207cca746e928L30-R82) [[4]](diffhunk://#diff-498feb1537333a67915485f92ad9c4b9b7748289e9ac06394a0207cca746e928L135-R224) [[5]](diffhunk://#diff-498feb1537333a67915485f92ad9c4b9b7748289e9ac06394a0207cca746e928R238-R247) [[6]](diffhunk://#diff-498feb1537333a67915485f92ad9c4b9b7748289e9ac06394a0207cca746e928L171-R275) [[7]](diffhunk://#diff-498feb1537333a67915485f92ad9c4b9b7748289e9ac06394a0207cca746e928R290-R326) [[8]](diffhunk://#diff-498feb1537333a67915485f92ad9c4b9b7748289e9ac06394a0207cca746e928R412-R415) [[9]](diffhunk://#diff-498feb1537333a67915485f92ad9c4b9b7748289e9ac06394a0207cca746e928L285-R425) [[10]](diffhunk://#diff-498feb1537333a67915485f92ad9c4b9b7748289e9ac06394a0207cca746e928R434-R459) * Improved item summary rendering and tag/author/image detection in the feed template, with new logic for stripping tags and decoding HTML entities for better display of content. [[1]](diffhunk://#diff-498feb1537333a67915485f92ad9c4b9b7748289e9ac06394a0207cca746e928L345-R549) [[2]](diffhunk://#diff-498feb1537333a67915485f92ad9c4b9b7748289e9ac06394a0207cca746e928R412-R415) **Feed reader integration:** * Added `public/feed-reader-link.js`, which dynamically sets the "Open in feed reader" link to use the `feed:` protocol for the current page if not already set, improving compatibility with feed readers. The script is now included in the XSL template. [[1]](diffhunk://#diff-d6ebecc8aac3ec170ae683f1b42e69352a5a202d9561c057ef597054d5efcd08R1-R10) [[2]](diffhunk://#diff-498feb1537333a67915485f92ad9c4b9b7748289e9ac06394a0207cca746e928R13) [[3]](diffhunk://#diff-498feb1537333a67915485f92ad9c4b9b7748289e9ac06394a0207cca746e928R290-R326) **Backend code refactoring:** * Refactored the feed responder logic in `app/web/feeds/responder.rb` by extracting request resolution into a new `resolve_request` method and improving result emission with `emit_response_result`, making the code more modular and readable. [[1]](diffhunk://#diff-7fd0a6c7dcb1e744c836bdcd3739eb3f86fd54b92ce2674c7730064c217fccc2L15-R17) [[2]](diffhunk://#diff-7fd0a6c7dcb1e744c836bdcd3739eb3f86fd54b92ce2674c7730064c217fccc2R26-R58) **Documentation and developer experience:** * Updated `docs/README.md` to clarify make targets, add new OpenAPI verification/linting commands, and switch frontend instructions from npm to pnpm for consistency. * Improved `docs/architecture.md` to clarify feed routing, authentication, and source resolution logic, with more precise descriptions of static and token-backed feeds. **Dependency cleanup:** * Removed the unused `ssrf_filter` gem from the `Gemfile`.
1 parent 6dfa1a9 commit 438d9f6

12 files changed

Lines changed: 628 additions & 110 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
# Ignore rack cache
3636
/tmp/rack-cache-*
3737

38-
3938
# Ignore frontend build output and tooling caches
4039
/frontend/dist/
4140
/frontend/node_modules/
@@ -54,3 +53,4 @@
5453

5554
.yardoc
5655
frontend/.astro
56+
.pnpm-store

Gemfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ gem 'parallel'
1616
gem 'rack-cache'
1717
gem 'rack-timeout'
1818
gem 'roda'
19-
gem 'ssrf_filter'
2019
gem 'zeitwerk'
2120

2221
gem 'puma', require: false

Gemfile.lock

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,6 @@ GEM
339339
simplecov_json_formatter (~> 0.1)
340340
simplecov-html (0.13.2)
341341
simplecov_json_formatter (0.1.4)
342-
ssrf_filter (1.4.0)
343342
stackprof (0.2.28)
344343
stringio (3.2.0)
345344
thor (1.5.0)
@@ -394,7 +393,6 @@ DEPENDENCIES
394393
ruby-lsp
395394
sentry-ruby
396395
simplecov
397-
ssrf_filter
398396
stackprof
399397
vcr
400398
webmock
@@ -526,7 +524,6 @@ CHECKSUMS
526524
simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
527525
simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
528526
simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
529-
ssrf_filter (1.4.0) sha256=64634f7955836808244d9ce65800af682803000b8eb15a5d573df25ab3c5422b
530527
stackprof (0.2.28) sha256=4ec2ace02f386012b40ca20ef80c030ad711831f59511da12e83b34efb0f9a04
531528
stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
532529
thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73

app/web/feeds/responder.rb

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,9 @@ class << self
1212
# @param identifier [String]
1313
# @return [String] serialized feed body.
1414
def call(request:, target_kind:, identifier:)
15-
feed_request = Request.call(request:, target_kind:, identifier:)
16-
resolved_source = SourceResolver.call(feed_request)
17-
result = Service.call(resolved_source)
18-
normalized_identifier = feed_request.feed_name || identifier
15+
feed_request, resolved_source, result = resolve_request(request:, target_kind:, identifier:)
1916
body = write_response(response: request.response, representation: feed_request.representation, result:)
20-
21-
emit_result(target_kind:, identifier: normalized_identifier, resolved_source:, result:)
17+
emit_response_result(target_kind:, identifier:, feed_request:, resolved_source:, result:)
2218
body
2319
rescue StandardError => error
2420
emit_failure(target_kind:, identifier:, error:)
@@ -27,6 +23,39 @@ def call(request:, target_kind:, identifier:)
2723

2824
private
2925

26+
# @param request [Rack::Request]
27+
# @param target_kind [Symbol]
28+
# @param identifier [String]
29+
# @return [Array<(Html2rss::Web::Feeds::Contracts::Request, Html2rss::Web::Feeds::Contracts::ResolvedSource, Html2rss::Web::Feeds::Contracts::RenderResult)>]
30+
def resolve_request(request:, target_kind:, identifier:)
31+
feed_request = Request.call(request:, target_kind:, identifier:)
32+
resolved_source = SourceResolver.call(feed_request)
33+
result = Service.call(resolved_source)
34+
[feed_request, resolved_source, result]
35+
end
36+
37+
# @param feed_request [Html2rss::Web::Feeds::Contracts::Request]
38+
# @param identifier [String]
39+
# @return [String]
40+
def normalized_identifier(feed_request, identifier)
41+
feed_request.feed_name || identifier
42+
end
43+
44+
# @param target_kind [Symbol]
45+
# @param identifier [String]
46+
# @param feed_request [Html2rss::Web::Feeds::Contracts::Request]
47+
# @param resolved_source [Html2rss::Web::Feeds::Contracts::ResolvedSource]
48+
# @param result [Html2rss::Web::Feeds::Contracts::RenderResult]
49+
# @return [void]
50+
def emit_response_result(target_kind:, identifier:, feed_request:, resolved_source:, result:)
51+
emit_result(
52+
target_kind:,
53+
identifier: normalized_identifier(feed_request, identifier),
54+
resolved_source:,
55+
result:
56+
)
57+
end
58+
3059
# @param response [Rack::Response]
3160
# @param representation [Symbol]
3261
# @param result [Html2rss::Web::Feeds::Contracts::RenderResult]

docs/README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,23 @@ Running the app directly on the host is not supported.
4141
| ------------------------------ | ---------------------------------------------------------- |
4242
| `make setup` | Install Ruby and Node dependencies. |
4343
| `make dev` | Run Ruby (port 4000) and frontend (port 4001) dev servers. |
44-
| `make ready` | Full pre-flight check (Lint + Test + OpenAPI + Zeitwerk). |
44+
| `make ready` | Pre-commit gate: `make quick-check` + `bundle exec rspec`. |
4545
| `make test` | Run Ruby and frontend test suites. |
4646
| `make lint` | Run all linters. |
4747
| `make yard-verify-public-docs` | Enforce typed YARD docs for public methods in `app/`. |
4848
| `make openapi` | Regenerate `public/openapi.yaml` from request specs. |
49+
| `make openapi-verify` | Verify generated OpenAPI and frontend client artifacts are current. |
50+
| `make openapi-lint` | Lint OpenAPI with Redocly + Spectral. |
4951

50-
### Frontend npm Scripts
52+
### Frontend pnpm Scripts
5153

5254
| Command | Purpose |
5355
| ----------------------- | -------------------------------------------- |
54-
| `npm run dev` | Vite dev server with hot reload (port 4001). |
55-
| `npm run build` | Build static assets into `frontend/dist/`. |
56-
| `npm run test:run` | Unit tests (Vitest). |
57-
| `npm run test:contract` | Contract tests with MSW. |
56+
| `pnpm run dev` | Vite dev server with hot reload (port 4001). |
57+
| `pnpm run build` | Build static assets into `frontend/dist/`. |
58+
| `pnpm run lint` | Run ESLint across the frontend workspace. |
59+
| `pnpm run test:run` | Unit tests (Vitest). |
60+
| `pnpm run test:contract`| Contract tests with MSW. |
5861

5962
---
6063

docs/architecture.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,21 @@ flowchart TD
2121

2222
Requests enter via `app.rb` and are dispatched to `app/web/routes/`.
2323

24-
- **API v1**: Authenticated via `app/web/security/auth.rb`.
25-
- **Public Feeds**: Validated via HMAC tokens in `app/web/security/feed_token.rb`.
24+
- **Static feed pages (`/<feed_name>`)**: Routed by `app/web/routes/feed_pages.rb` and resolved as `target_kind: :static`.
25+
- Source: static config in `config/feeds.yml` (via `LocalConfig.find`).
26+
- Auth boundary: no feed token required on this route.
27+
- Failure mode: unknown feed names fail at static config lookup.
28+
- **Token-backed feed reads (`/api/v1/feeds/:token`)**: Routed by `app/web/routes/api_v1/feed_routes.rb` and resolved as `target_kind: :token`.
29+
- Token scope: `FeedAccess.authorize_feed_token!` validates signature/expiry and re-checks account URL access.
30+
- Constraint: disabled when AutoSource is off (`ForbiddenError` from `SourceResolver.ensure_auto_source_enabled!`).
31+
- **Feed creation (`POST /api/v1/feeds`)**: Authenticated via bearer token in `app/web/security/auth.rb`; this endpoint mints feed tokens for subsequent token-backed reads.
2632

2733
### 2. Resolution
2834

29-
The `Html2rss::Web::Feeds::SourceResolver` determines where the feed configuration comes from:
35+
The `Html2rss::Web::Feeds::SourceResolver` determines where feed configuration comes from based on route target:
3036

31-
- **Static**: Pre-defined in `config/feeds.yml`.
32-
- **Dynamic**: Generated on-the-fly via the `/api/v1/feeds` endpoint (AutoSource).
37+
- **Static (`target_kind: :static`)**: Pre-defined in `config/feeds.yml`.
38+
- **Token (`target_kind: :token`)**: Generated from validated feed token payload + AutoSource globals.
3339

3440
### 3. Fetching & Rendering
3541

public/feed-reader-link.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
document.addEventListener('DOMContentLoaded', function () {
2+
var readerLink = document.querySelector('[data-feed-reader-link]');
3+
if (!readerLink) return;
4+
var currentHref = readerLink.getAttribute('href');
5+
if (currentHref && currentHref !== '#') return;
6+
7+
var pageHref = window.location.href;
8+
var feedHref = pageHref.indexOf('feed:') === 0 ? pageHref : 'feed:' + pageHref;
9+
readerLink.setAttribute('href', feedHref);
10+
});

0 commit comments

Comments
 (0)