Skip to content

Commit f3f28eb

Browse files
committed
dev-routing: make 4000 API-only and keep SPA on 4001
1 parent b55ee50 commit f3f28eb

6 files changed

Lines changed: 103 additions & 22 deletions

File tree

app.rb

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,21 @@
1313

1414
module Html2rss
1515
module Web
16+
DEFAULT_HEADERS = {
17+
'X-Content-Type-Options' => 'nosniff',
18+
'X-XSS-Protection' => '1; mode=block',
19+
'X-Frame-Options' => 'SAMEORIGIN',
20+
'X-Permitted-Cross-Domain-Policies' => 'none',
21+
'Referrer-Policy' => 'strict-origin-when-cross-origin',
22+
'Permissions-Policy' => 'geolocation=(), microphone=(), camera=()',
23+
'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains; preload',
24+
'Cross-Origin-Embedder-Policy' => 'require-corp',
25+
'Cross-Origin-Opener-Policy' => 'same-origin',
26+
'Cross-Origin-Resource-Policy' => 'same-origin',
27+
'X-DNS-Prefetch-Control' => 'off',
28+
'X-Download-Options' => 'noopen'
29+
}.freeze
30+
1631
##
1732
# Roda app serving RSS feeds via html2rss
1833
class App < Roda
@@ -72,20 +87,7 @@ def development? = self.class.development?
7287
end
7388
# rubocop:enable Metrics/BlockLength
7489

75-
plugin :default_headers, {
76-
'X-Content-Type-Options' => 'nosniff',
77-
'X-XSS-Protection' => '1; mode=block',
78-
'X-Frame-Options' => 'SAMEORIGIN',
79-
'X-Permitted-Cross-Domain-Policies' => 'none',
80-
'Referrer-Policy' => 'strict-origin-when-cross-origin',
81-
'Permissions-Policy' => 'geolocation=(), microphone=(), camera=()',
82-
'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains; preload',
83-
'Cross-Origin-Embedder-Policy' => 'require-corp',
84-
'Cross-Origin-Opener-Policy' => 'same-origin',
85-
'Cross-Origin-Resource-Policy' => 'same-origin',
86-
'X-DNS-Prefetch-Control' => 'off',
87-
'X-Download-Options' => 'noopen'
88-
}
90+
plugin :default_headers, DEFAULT_HEADERS
8991

9092
plugin :json_parser
9193
plugin :static,
@@ -104,9 +106,20 @@ def development? = self.class.development?
104106

105107
route do |r|
106108
r.public
109+
r.root do
110+
if development?
111+
render_development_api_landing(r)
112+
else
113+
render_index_page(r)
114+
end
115+
end
107116

108117
Routes::ApiV1.call(r) ||
109-
Routes::FeedPages.call(r, index_renderer: ->(router_ctx) { render_index_page(router_ctx) })
118+
Routes::FeedPages.call(
119+
r,
120+
index_renderer: ->(router_ctx) { render_index_page(router_ctx) },
121+
serve_spa: !development?
122+
)
110123
end
111124

112125
private
@@ -115,6 +128,11 @@ def render_index_page(router)
115128
router.response['Content-Type'] = 'text/html'
116129
File.exist?(FRONTEND_INDEX_PATH) ? File.read(FRONTEND_INDEX_PATH) : FALLBACK_HTML
117130
end
131+
132+
def render_development_api_landing(router)
133+
router.response['Content-Type'] = 'text/html'
134+
DevelopmentLandingPage::HTML
135+
end
118136
end
119137
end
120138
end
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# frozen_string_literal: true
2+
3+
module Html2rss
4+
module Web
5+
##
6+
# Static HTML shown on the API origin during development.
7+
module DevelopmentLandingPage
8+
HTML = <<~HTML
9+
<!DOCTYPE html>
10+
<html>
11+
<head>
12+
<title>html2rss-web API</title>
13+
<meta name="viewport" content="width=device-width,initial-scale=1">
14+
<meta name="robots" content="noindex,nofollow">
15+
</head>
16+
<body>
17+
<h1>html2rss-web API (development)</h1>
18+
<p>API metadata: <a href="/api/v1"><code>/api/v1</code></a></p>
19+
<p>Frontend SPA: <a href="http://127.0.0.1:4001/">http://127.0.0.1:4001/</a></p>
20+
</body>
21+
</html>
22+
HTML
23+
end
24+
end
25+
end

app/web/routes/feed_pages.rb

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,19 @@ module FeedPages
1212
class << self
1313
# @param router [Roda::RodaRequest]
1414
# @param index_renderer [#call]
15+
# @param serve_spa [Boolean]
1516
# @return [void]
1617
# rubocop:disable Metrics/MethodLength
17-
def call(router, index_renderer:)
18-
router.root do
19-
index_renderer.call(router)
20-
end
21-
18+
def call(router, index_renderer:, serve_spa: true)
2219
router.get do
2320
feed_name = requested_feed_name(router)
2421
next if feed_name.empty?
2522

26-
if spa_app_path?(feed_name)
23+
if spa_app_path?(feed_name) && serve_spa
2724
index_renderer.call(router)
2825
next
2926
end
27+
next if spa_app_path?(feed_name)
3028
next if feed_name.include?('.') && !feed_name.end_with?('.json', '.xml', '.rss')
3129

3230
RequestTarget.mark!(router, RequestTarget::FEED)

docs/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Welcome! This is the canonical source of truth for contributing to `html2rss-web
1818
`html2rss-web` converts arbitrary websites into RSS 2.0 feeds.
1919

2020
- **Backend**: Ruby + Roda under the `Html2rss::Web` namespace.
21-
- **Frontend**: Preact + Vite, built into `frontend/dist` and served at `/`.
21+
- **Frontend**: Preact + Vite, built into `frontend/dist` and served at `/` in production.
2222
- **Feed extraction**: Delegated to the `html2rss` gem.
2323
- **Distribution**: Docker Compose / Dev Container first.
2424

@@ -59,6 +59,12 @@ Running the app directly on the host is not supported.
5959
| `pnpm run test:run` | Unit tests (Vitest). |
6060
| `pnpm run test:contract`| Contract tests with MSW. |
6161

62+
Development routing defaults:
63+
64+
- `http://127.0.0.1:4000` is API-only in development (`/api/v1` metadata and API endpoints).
65+
- `http://127.0.0.1:4001` is the canonical frontend SPA entrypoint in development.
66+
- Vite keeps proxying `/api` and `/rss.xsl` to `:4000` so frontend code can use same-origin-style paths.
67+
6268
---
6369

6470
## Contract-Driven Development Loop

spec/html2rss/web/app_integration_spec.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,19 @@
8686
expect(last_response.status).to eq(404)
8787
expect(last_response.body).to eq('')
8888
end
89+
90+
it 'returns not found for SPA app routes in development mode', :aggregate_failures do
91+
ClimateControl.modify('RACK_ENV' => 'development') do
92+
get '/create'
93+
expect(last_response.status).to eq(404)
94+
95+
get '/token'
96+
expect(last_response.status).to eq(404)
97+
98+
get '/result/generated-token'
99+
expect(last_response.status).to eq(404)
100+
end
101+
end
89102
end
90103

91104
describe 'GET /api/v1/feeds/:token' do # rubocop:disable RSpec/MultipleMemoizedHelpers

spec/html2rss/web/app_spec.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,34 @@ def app = described_class
8787
get '/'
8888

8989
expect(last_response).to be_ok
90+
expect(last_response.body).not_to include('html2rss-web API (development)')
9091
expect(last_response.headers['Content-Security-Policy']).to include("default-src 'none'")
9192
expect(last_response.headers['Content-Security-Policy']).to include("script-src 'self'")
9293
expect(last_response.headers['Content-Security-Policy']).to include("style-src 'self'")
9394
expect(last_response.headers['Content-Security-Policy']).not_to include("'unsafe-inline'")
9495
expect(last_response.headers['Strict-Transport-Security']).to include('max-age=31536000')
9596
end
9697

98+
it 'serves an API-only landing page on root in development', :aggregate_failures do
99+
ClimateControl.modify('RACK_ENV' => 'development') do
100+
get '/'
101+
end
102+
103+
expect(last_response).to be_ok
104+
expect(last_response.body).to include('html2rss-web API (development)')
105+
expect(last_response.body).to include('/api/v1')
106+
expect(last_response.body).to include('http://127.0.0.1:4001/')
107+
end
108+
109+
it 'does not render SPA app routes in development' do
110+
ClimateControl.modify('RACK_ENV' => 'development') do
111+
get '/create'
112+
end
113+
114+
expect(last_response.status).to eq(404)
115+
expect(last_response.body).to eq('')
116+
end
117+
97118
it 'does not serve the removed legacy frontend entrypoint' do
98119
get '/frontend/index.html'
99120

0 commit comments

Comments
 (0)