Skip to content

Commit da8025c

Browse files
authored
refactor: from astro to vite+preact & radically simple (#877)
* refactor: replace Astro frontend with Vite and Preact * fix: simplify result actions and surface feed errors * fix: accept escaped public feed tokens * fix: label the result feed url field * refactor: redesign frontend ui system * style: refine frontend typography and visual noise * chore: drop accidental typecheck gate changes * chore: add frontend typecheck quick-check * ci: add frontend guardrail checks * refactor: harden demo contract and simplify ui states * chore: remove dead frontend components * style: prettier * chore: refresh generated openapi client * chore: update npm deps * Refactor feed creation UX flow * Tighten feed result surface * Refine create flow support surfaces * Refine feed creation workspace UX * Simplify feed creation page layout * Align result screen with simplified create flow * Refine feed preview and utility surfaces * Refine token prompt interaction flow * Polish feed result presentation * Update frontend contract test for new flow * Expose json feed URLs and tighten recovery flow
1 parent fc5be2a commit da8025c

66 files changed

Lines changed: 4256 additions & 8920 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/copilot-instructions.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
## Overview
44

55
- Ruby web app that converts websites into RSS 2.0 feeds.
6-
- Built with **Roda** backend + **Astro** frontend, using the **html2rss** gem (+ `html2rss-configs`).
7-
- **Frontend:** Modern Astro-based UI with component architecture, served alongside Ruby backend.
6+
- Built with **Roda** backend + **Preact** frontend, using the **html2rss** gem (+ `html2rss-configs`).
7+
- **Frontend:** Vite-built Preact UI, served alongside Ruby backend.
88

99
## Documentation website of core dependencies
1010

@@ -14,10 +14,10 @@ Search these pages before using them. Find examples, plugins, UI components, and
1414

1515
1. https://roda.jeremyevans.net/documentation.html
1616

17-
### Astro & Starlight
17+
### Preact & Vite
1818

19-
1. https://docs.astro.build/en/getting-started/
20-
2. https://starlight.astro.build/getting-started/
19+
1. https://preactjs.com/guide/v10/getting-started/
20+
2. https://vite.dev/guide/
2121

2222
### html2rss
2323

@@ -40,8 +40,8 @@ Fix rubocop `RSpec/MultipleExpectations` adding rspec tag `:aggregate_failures`.
4040
- ✅ Validate all inputs. Pass outbound requests through **SSRF filter**.
4141
- ✅ Add caching headers where appropriate (`Rack::Cache`).
4242
- ✅ Errors: friendly messages for users, detailed logging internally.
43-
-**Frontend**: Use Astro components in `frontend/src/`. Keep it simple.
44-
-**CSS**: Use frontend styles provided by Astro Starlight.
43+
-**Frontend**: Use Preact components in `frontend/src/`. Keep it simple.
44+
-**CSS**: Use the app-owned frontend styles in `frontend/src/styles/`.
4545
-**Specs**: RSpec for Ruby, build tests for frontend.
4646
- ✅ When a spec needs to tweak environment variables, wrap the example in `ClimateControl.modify` so state is restored automatically.
4747

.github/workflows/ci.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ jobs:
8888
- name: Verify generated OpenAPI client is up to date
8989
run: npm run openapi:verify
9090

91+
- name: Typecheck frontend
92+
run: npm run typecheck
93+
9194
- name: Check formatting
9295
run: npm run format:check
9396

@@ -127,7 +130,7 @@ jobs:
127130
run: npm ci
128131
working-directory: frontend
129132

130-
- name: Build frontend
133+
- name: Build frontend static assets
131134
run: npm run build
132135
working-directory: frontend
133136

@@ -162,7 +165,7 @@ jobs:
162165
run: npm ci
163166
working-directory: frontend
164167

165-
- name: Build frontend
168+
- name: Build frontend static assets
166169
run: npm run build
167170
working-directory: frontend
168171

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,6 @@ USER html2rss
7777

7878
COPY --from=builder /usr/local/bundle /usr/local/bundle
7979
COPY --chown=$USER:$USER . /app
80-
COPY --from=frontend-builder --chown=$USER:$USER /app/frontend/dist ./public/frontend
80+
COPY --from=frontend-builder --chown=$USER:$USER /app/public/frontend ./public/frontend
8181

8282
CMD ["bundle", "exec", "puma", "-C", "./config/puma.rb"]

Makefile

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
.PHONY: help test lint lint-js lint-ruby lintfix lintfix-js lintfix-ruby setup dev clean frontend-setup ready yard-verify-public-docs openapi openapi-verify openapi-client openapi-client-verify openapi-lint openapi-lint-redocly openapi-lint-spectral openai-lint-spectral
3+
.PHONY: help test lint lint-js lint-ruby lintfix lintfix-js lintfix-ruby setup dev clean frontend-setup check-frontend quick-check ready yard-verify-public-docs openapi openapi-verify openapi-client openapi-client-verify openapi-lint openapi-lint-redocly openapi-lint-spectral openai-lint-spectral
44

55
# Default target
66
help: ## Show this help message
@@ -28,7 +28,7 @@ dev: ## Start development server with live reload
2828
dev-ruby: ## Start Ruby server only
2929
@bin/dev-ruby
3030

31-
dev-frontend: ## Start Astro dev server only
31+
dev-frontend: ## Start frontend dev server only
3232
@cd frontend && npm run dev
3333

3434
test: ## Run all tests (Ruby + Frontend)
@@ -47,6 +47,10 @@ test-frontend-unit: ## Run frontend unit tests only
4747
test-frontend-contract: ## Run frontend contract tests only
4848
@cd frontend && npm run test:contract
4949

50+
check-frontend: ## Run frontend typecheck, format, and test checks
51+
$(MAKE) lint-js
52+
$(MAKE) test-frontend
53+
5054

5155
lint: lint-ruby lint-js ## Run all linters (Ruby + Frontend) - errors when issues found
5256
@echo "All linting complete!"
@@ -59,6 +63,8 @@ lint-ruby: ## Run Ruby linter (RuboCop) - errors when issues found
5963
@echo "Ruby linting complete!"
6064

6165
lint-js: ## Run JavaScript/Frontend linter (Prettier) - errors when issues found
66+
@echo "Running TypeScript typecheck..."
67+
@cd frontend && npm run typecheck
6268
@echo "Running Prettier format check..."
6369
@cd frontend && npm run format:check
6470
@echo "JavaScript linting complete!"
@@ -76,10 +82,15 @@ lintfix-js: ## Auto-fix JavaScript/Frontend linting issues
7682
@cd frontend && npm run format
7783
@echo "JavaScript lintfix complete!"
7884

79-
ready: ## Pre-commit gate (RuboCop + RSpec)
85+
quick-check: ## Fast local checks (Ruby lint/docs + frontend format/typecheck)
86+
@echo "Running quick checks..."
87+
$(MAKE) lint-ruby
88+
$(MAKE) lint-js
89+
@echo "Quick checks complete!"
90+
91+
ready: ## Pre-commit gate (quick checks + RSpec)
8092
@echo "Running pre-commit checks..."
81-
bundle exec rubocop -F
82-
bundle exec rake yard:verify_public_docs
93+
$(MAKE) quick-check
8394
bundle exec rspec
8495
@echo "Pre-commit checks complete!"
8596

@@ -111,7 +122,7 @@ openai-lint-spectral: openapi-lint-spectral ## Alias for openapi-lint-spectral
111122

112123
clean: ## Clean temporary files
113124
@rm -rf tmp/rack-cache-* coverage/
114-
@cd frontend && rm -rf dist/ .astro/ node_modules/
125+
@cd frontend && rm -rf dist/ node_modules/
115126
@echo "Clean complete!"
116127

117128
frontend-setup: ## Setup frontend dependencies

README.md

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# html2rss-web
44

5-
html2rss-web converts arbitrary websites into RSS 2.0 feeds with a slim Ruby backend and an Astro-powered frontend.
5+
html2rss-web converts arbitrary websites into RSS 2.0 feeds with a slim Ruby backend and a Preact frontend.
66

77
## Links
88

@@ -12,25 +12,16 @@ html2rss-web converts arbitrary websites into RSS 2.0 feeds with a slim Ruby bac
1212

1313
## Highlights
1414

15-
- Responsive Astro interface with gallery and custom feed creation.
15+
- Responsive Preact interface for demo, sign-in, conversion, and result flows.
1616
- Automatic source discovery with token-scoped permissions.
1717
- Signed public feed URLs that work in standard RSS readers.
1818
- Built-in SSRF defences, input validation, and HMAC-protected tokens.
1919

2020
## Architecture
2121

2222
- **Backend:** Ruby + Roda, backed by the `html2rss` gem for extraction.
23-
- **Frontend:** Astro static site with progressive enhancement.
23+
- **Frontend:** Preact app built with Vite into `public/frontend`.
2424
- **Distribution:** Docker Compose by default; other deployments require manual wiring.
25-
26-
## Documentation
27-
28-
In-repo docs live under `frontend/src/content/docs/` and are published by Astro.
29-
30-
- [Configuration Guide](frontend/src/content/docs/configuration.md)
31-
- [Security Guide](frontend/src/content/docs/security.md)
32-
- [REST API v1](frontend/src/content/docs/api/v1.md)
33-
- [Testing Overview](frontend/src/content/docs/testing.md)
3425
- [v2 Migration Guide](docs/migrations/v2.md)
3526

3627
## REST API Snapshot
@@ -73,17 +64,17 @@ bundle exec rspec
7364
make openapi
7465
```
7566

76-
Dev URLs: Ruby app at `http://localhost:4000`, Astro dev server at `http://localhost:4001`.
67+
Dev URLs: Ruby app at `http://localhost:4000`, frontend dev server at `http://localhost:4001`.
7768

7869
## Make Targets
7970

8071
| Command | Purpose |
8172
| -------------------- | ------------------------------------------------------- |
8273
| `make help` | List available shortcuts. |
8374
| `make setup` | Install Ruby and Node dependencies. |
84-
| `make dev` | Run Ruby (port 4000) and Astro (port 4001) dev servers. |
75+
| `make dev` | Run Ruby (port 4000) and frontend (port 4001) dev servers. |
8576
| `make dev-ruby` | Start only the Ruby server. |
86-
| `make dev-frontend` | Start only the Astro dev server (port 4001). |
77+
| `make dev-frontend` | Start only the frontend dev server (port 4001). |
8778
| `make test` | Run Ruby and frontend test suites. |
8879
| `make test-ruby` | Run Ruby specs. |
8980
| `make test-frontend` | Run frontend unit and contract tests. |
@@ -105,8 +96,8 @@ The OpenAPI file is generated from Ruby request specs only.
10596

10697
| Command | Purpose |
10798
| ----------------------- | --------------------------------------------- |
108-
| `npm run dev` | Astro dev server with hot reload (port 4001). |
109-
| `npm run build` | Production build. |
99+
| `npm run dev` | Vite dev server with hot reload (port 4001). |
100+
| `npm run build` | Build static assets into `public/frontend`. |
110101
| `npm run test:run` | Unit tests (Vitest). |
111102
| `npm run test:contract` | Contract tests with MSW. |
112103

app/api/v1/feeds.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require 'html2rss/url'
4+
35
require_relative 'contract'
46
require_relative 'feeds/create_feed'
57
require_relative 'feeds/show_feed'
@@ -33,7 +35,9 @@ def create(request)
3335
# @param url [String]
3436
# @return [String, nil]
3537
def extract_site_title(url)
36-
CreateFeed.extract_site_title(url)
38+
Html2rss::Url.for_channel(url).channel_titleized
39+
rescue StandardError
40+
nil
3741
end
3842
end
3943
end

app/api/v1/feeds/create_feed.rb

Lines changed: 16 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
require 'time'
44
require 'json'
5-
require 'html2rss/url'
65

76
require_relative '../../../auth'
87
require_relative '../../../auto_source'
@@ -18,12 +17,11 @@ module Api
1817
module V1
1918
module Feeds
2019
##
21-
# Creates stable feed records from authenticated API requests.
22-
#
23-
# The implementation intentionally keeps parsing, authorization, and
24-
# normalization in a single boundary object so callers can rely on one
25-
# predictable contract instead of coordinating multiple services.
20+
# Creates stable feed records from authenticated API requests with one predictable boundary contract.
2621
module CreateFeed
22+
FEED_ATTRIBUTE_KEYS =
23+
%i[id name url strategy feed_token public_url json_public_url created_at updated_at].freeze
24+
2725
class << self
2826
# Creates a feed and returns a normalized API success payload.
2927
#
@@ -41,41 +39,19 @@ def call(request)
4139
raise
4240
end
4341

44-
# Extracts a best-effort human-readable title from the URL.
45-
#
46-
# @param url [String] target source URL.
47-
# @return [String, nil] inferred title or nil when unavailable.
48-
def extract_site_title(url)
49-
Html2rss::Url.for_channel(url).channel_titleized
50-
rescue StandardError
51-
nil
52-
end
53-
5442
private
5543

56-
# Enforces feature availability at the API edge to fail fast.
57-
#
58-
# @return [void]
5944
def ensure_auto_source_enabled!
6045
raise ForbiddenError, Contract::MESSAGES[:auto_source_disabled] unless AutoSource.enabled?
6146
end
6247

63-
# Resolves the authenticated account from the request.
64-
#
65-
# @param request [Rack::Request]
66-
# @return [Hash{Symbol=>Object}] authenticated account attributes.
6748
def require_account(request)
6849
account = Auth.authenticate(request)
6950
raise UnauthorizedError, 'Authentication required' unless account
7051

7152
account
7253
end
7354

74-
# Validates and normalizes feed creation parameters.
75-
#
76-
# @param params [Hash{String=>Object}] merged request parameters.
77-
# @param account [Hash{Symbol=>Object}] authenticated account.
78-
# @return [Html2rss::Web::BoundaryModels::FeedCreateParams]
7955
def build_create_params(params, account)
8056
url = params['url'].to_s.strip
8157
raise BadRequestError, 'URL parameter is required' if url.empty?
@@ -89,10 +65,6 @@ def build_create_params(params, account)
8965
)
9066
end
9167

92-
# Normalizes a strategy value while preserving a default path.
93-
#
94-
# @param raw_strategy [String, nil]
95-
# @return [String]
9668
def normalize_strategy(raw_strategy)
9769
strategy = raw_strategy.to_s.strip
9870
strategy = default_strategy if strategy.empty?
@@ -112,24 +84,13 @@ def default_strategy
11284
Html2rss::RequestService.default_strategy_name.to_s
11385
end
11486

115-
# Shapes feed attributes into the stable API schema.
116-
#
117-
# @param feed_data [Html2rss::Web::BoundaryModels::FeedMetadata, Hash{Symbol=>Object}] feed record.
118-
# @return [Hash{Symbol=>Object}] response-safe feed attributes.
11987
def feed_attributes(feed_data)
120-
typed_feed = feed_data.is_a?(BoundaryModels::FeedMetadata) ? feed_data : BoundaryModels::FeedMetadata.new(**feed_data)
12188
timestamp = Time.now.iso8601
89+
typed_feed = feed_metadata(feed_data)
12290

123-
typed_feed.to_h.merge(
124-
created_at: timestamp,
125-
updated_at: timestamp
126-
).slice(:id, :name, :url, :strategy, :public_url, :created_at, :updated_at)
91+
typed_feed_attributes(typed_feed, timestamp).slice(*FEED_ATTRIBUTE_KEYS)
12792
end
12893

129-
# Parses params with optional JSON body override.
130-
#
131-
# @param request [Rack::Request]
132-
# @return [Hash{String=>Object}] merged request params.
13394
def request_params(request)
13495
return request.params unless json_request?(request)
13596

@@ -145,15 +106,11 @@ def request_params(request)
145106
raise BadRequestError, 'Invalid JSON payload'
146107
end
147108

148-
# @param request [Rack::Request]
149-
# @return [Boolean] whether request body should be parsed as JSON.
150109
def json_request?(request)
151110
content_type = request.env['CONTENT_TYPE'].to_s
152111
content_type.include?('application/json')
153112
end
154113

155-
# @param request [Rack::Request]
156-
# @return [Array<(Html2rss::Web::BoundaryModels::FeedCreateParams, Object)>]
157114
def build_feed_from_request(request)
158115
account = require_account(request)
159116
ensure_auto_source_enabled!
@@ -165,8 +122,6 @@ def build_feed_from_request(request)
165122
[params, feed_data]
166123
end
167124

168-
# @param params [Html2rss::Web::BoundaryModels::FeedCreateParams]
169-
# @return [void]
170125
def emit_create_success(params)
171126
Observability.emit(
172127
event_name: 'feed.create',
@@ -176,8 +131,6 @@ def emit_create_success(params)
176131
)
177132
end
178133

179-
# @param error [StandardError]
180-
# @return [void]
181134
def emit_create_failure(error)
182135
Observability.emit(
183136
event_name: 'feed.create',
@@ -186,6 +139,16 @@ def emit_create_failure(error)
186139
level: :warn
187140
)
188141
end
142+
143+
def feed_metadata(feed_data)
144+
return feed_data if feed_data.is_a?(BoundaryModels::FeedMetadata)
145+
146+
BoundaryModels::FeedMetadata.new(**feed_data)
147+
end
148+
149+
def typed_feed_attributes(typed_feed, timestamp)
150+
typed_feed.to_h.merge(created_at: timestamp, updated_at: timestamp)
151+
end
189152
end
190153
end
191154
end

0 commit comments

Comments
 (0)