Skip to content

Commit 755eef1

Browse files
committed
Improve mobile footer flow and degraded-feed messaging
1 parent 07304bc commit 755eef1

10 files changed

Lines changed: 151 additions & 15 deletions

File tree

app/web/rendering/feed_notice_text.rb

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,17 @@ module Web
66
# Shared copy helpers for rendered feed warnings and fallback documents.
77
module FeedNoticeText
88
EMPTY_FEED_DESCRIPTION_TEMPLATE = <<~DESC
9-
Unable to extract content from %<url>s using the %<strategy>s strategy.
10-
The site may rely on JavaScript, block automated requests, or expose a structure that needs a different parser.
9+
We could not extract entries from %<url>s right now.
10+
The source may block automated requests, require dynamic rendering, or be temporarily unavailable.
1111
DESC
1212

1313
EMPTY_FEED_ITEM_TEMPLATE = <<~DESC
1414
No entries were extracted from %<url>s.
15-
Possible causes:
16-
- JavaScript-heavy site (try the browserless strategy)
17-
- Anti-bot protection
18-
- Complex or changing markup
19-
- Site blocking automated requests
2015
21-
Try another strategy or reach out to the site owner.
16+
What you can do:
17+
- Try again in a few moments
18+
- Open the original page to confirm content is available
19+
- Reach out to the site owner if access is restricted
2220
DESC
2321

2422
class << self

app/web/rendering/json_feed_builder.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def build_single_item(item)
7575
# @return [Hash{Symbol=>String}]
7676
def empty_feed_item(url)
7777
{
78-
title: 'Content Extraction Failed',
78+
title: 'Preview unavailable for this source',
7979
content_text: FeedNoticeText.empty_feed_item(url: url),
8080
url: url
8181
}

app/web/rendering/xml_builder.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def build_empty_feed_warning(url:, strategy:, site_title: nil)
5353
build_single_item_feed(
5454
title: FeedNoticeText.empty_feed_title(site_title),
5555
description: FeedNoticeText.empty_feed_description(url: url, strategy: strategy),
56-
item: { title: 'Content Extraction Failed', description: FeedNoticeText.empty_feed_item(url: url),
56+
item: { title: 'Preview unavailable for this source', description: FeedNoticeText.empty_feed_item(url: url),
5757
link: url },
5858
link: url
5959
)

frontend/src/__tests__/ResultDisplay.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,30 @@ describe('ResultDisplay', () => {
102102
});
103103
});
104104

105+
it('keeps feed-ready actions visible while showing preview degradation warning copy', async () => {
106+
render(
107+
<ResultDisplay
108+
result={{
109+
...mockResult,
110+
readinessPhase: 'feed_ready',
111+
preview: { items: [], error: 'Preview unavailable right now.', isLoading: false },
112+
}}
113+
workflowState="ready"
114+
onCreateAnother={mockOnCreateAnother}
115+
onRetryReadiness={mockOnRetryReadiness}
116+
/>
117+
);
118+
119+
await waitFor(() => {
120+
expect(screen.getByText('Feed ready')).toBeInTheDocument();
121+
expect(
122+
screen.getByText('Feed link is usable, but preview content could not be verified yet.')
123+
).toBeInTheDocument();
124+
expect(screen.getByRole('link', { name: 'Open feed' })).toBeInTheDocument();
125+
expect(screen.getByText('Preview unavailable right now.')).toBeInTheDocument();
126+
});
127+
});
128+
105129
it('keeps result shell visible while readiness check is in progress', async () => {
106130
render(
107131
<ResultDisplay

frontend/src/components/ResultDisplay.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export function ResultDisplay({
3232
const canManuallyRetryReadiness =
3333
readinessPhase === 'feed_not_ready_yet' || readinessPhase === 'preview_unavailable';
3434
const isReadinessCheckInProgress = readinessPhase === 'link_created' && preview.isLoading;
35+
const hasPreviewDegradation = readinessPhase === 'feed_ready' && Boolean(preview.error);
3536
const showReadinessAction = canManuallyRetryReadiness || isReadinessCheckInProgress;
3637
const previewItems = showAllPreviewItems ? preview.items : preview.items.slice(0, 3);
3738
const hasMorePreviewItems = preview.items.length > 3;
@@ -83,6 +84,11 @@ export function ResultDisplay({
8384
<h1 class="result-title ui-display-title">{statusTitle}</h1>
8485
<p class="result-meta layout-rail-copy">{feed.name}</p>
8586
<p class="field-help">{statusMessage}</p>
87+
{hasPreviewDegradation && (
88+
<p class="field-help field-help--warning">
89+
Feed link is usable, but preview content could not be verified yet.
90+
</p>
91+
)}
8692
</div>
8793
</div>
8894
<div class="result-hero__actions ui-hero__actions">

frontend/src/styles/main.css

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
}
1212

1313
.page-main {
14+
min-height: 100%;
15+
display: flex;
16+
flex-direction: column;
1417
width: 100%;
1518
max-width: var(--layout-page-max-width);
1619
margin: 0 auto;
@@ -191,6 +194,10 @@
191194
color: var(--danger);
192195
}
193196

197+
.field-help--warning {
198+
color: var(--text-body);
199+
}
200+
194201
.input {
195202
width: 100%;
196203
min-width: 0;
@@ -693,7 +700,7 @@ a:focus-visible {
693700

694701
@media (width < 48rem) {
695702
.page-shell {
696-
--footer-nav-reserve: calc(12rem + env(safe-area-inset-bottom, 0px));
703+
--footer-nav-reserve: 0px;
697704
}
698705

699706
.page-main,
@@ -761,15 +768,29 @@ a:focus-visible {
761768
}
762769

763770
.app-footer {
764-
padding-top: var(--space-3);
765-
background: linear-gradient(180deg, transparent 0%, rgb(var(--color-rgb-black) / 76%) 46%);
771+
position: static;
772+
inset-inline: auto;
773+
bottom: auto;
774+
z-index: auto;
775+
margin-top: auto;
776+
padding: var(--space-5) var(--space-3) calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
777+
border-top: 1px solid var(--border-subtle);
778+
background: transparent;
779+
backdrop-filter: none;
766780
}
767781

768782
.utility-strip__items {
769783
grid-auto-flow: row;
770784
grid-auto-columns: 1fr;
771785
justify-items: center;
772-
gap: var(--space-1);
786+
gap: calc(var(--space-1) * 0.9);
787+
}
788+
789+
.utility-link,
790+
.utility-button {
791+
color: var(--text-faint);
792+
font-size: var(--font-size-00);
793+
opacity: 0.92;
773794
}
774795
}
775796

public/rss.xsl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@
361361
</div>
362362
</section>
363363

364-
<xsl:if test="rss/channel/title = 'Error' or rss/channel/item[1]/title = 'Content Extraction Failed'">
364+
<xsl:if test="rss/channel/title = 'Error' or contains(rss/channel/title, 'Content Extraction Issue') or rss/channel/item[1]/title = 'Content Extraction Failed' or rss/channel/item[1]/title = 'Preview unavailable for this source'">
365365
<section class="feed-notice ui-card ui-card--notice ui-card--padded layout-rail-reading" aria-label="Feed status">
366366
<p>
367367
<xsl:call-template name="clean-text">
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require_relative '../../../app'
5+
6+
RSpec.describe Html2rss::Web::FeedNoticeText do
7+
describe '.empty_feed_item' do
8+
subject(:message) { described_class.empty_feed_item(url: 'https://example.com/articles') }
9+
10+
it 'includes actionable product guidance' do
11+
expect(message).to include('What you can do:')
12+
expect(message).to include('Try again in a few moments')
13+
end
14+
15+
it 'does not mention hidden strategy controls' do
16+
expect(message).not_to include('browserless strategy')
17+
expect(message).not_to include('Try another strategy')
18+
end
19+
end
20+
end
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require_relative '../../../app'
5+
6+
RSpec.describe Html2rss::Web::JsonFeedBuilder do
7+
describe '.build_empty_feed_warning' do
8+
subject(:payload) do
9+
JSON.parse(
10+
described_class.build_empty_feed_warning(
11+
url: 'https://example.com/articles',
12+
strategy: 'faraday',
13+
site_title: 'Example Site'
14+
)
15+
)
16+
end
17+
18+
it 'uses updated channel description copy' do
19+
expect(payload.fetch('description')).to include('We could not extract entries')
20+
expect(payload.fetch('description')).not_to include('different parser')
21+
end
22+
23+
it 'uses updated item title and content text' do
24+
first_item = payload.fetch('items').first
25+
expect(first_item.fetch('title')).to eq('Preview unavailable for this source')
26+
expect(first_item.fetch('content_text')).to include('What you can do:')
27+
end
28+
29+
it 'does not mention hidden strategy controls in item text' do
30+
first_item = payload.fetch('items').first
31+
expect(first_item.fetch('content_text')).not_to include('browserless strategy')
32+
end
33+
end
34+
end
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require 'nokogiri'
5+
require_relative '../../../app'
6+
7+
RSpec.describe Html2rss::Web::XmlBuilder do
8+
describe '.build_empty_feed_warning' do
9+
subject(:xml_doc) do
10+
xml = described_class.build_empty_feed_warning(
11+
url: 'https://example.com/articles',
12+
strategy: 'faraday',
13+
site_title: 'Example Site'
14+
)
15+
Nokogiri::XML(xml)
16+
end
17+
18+
it 'uses updated channel description copy' do
19+
description = xml_doc.at_xpath('//channel/description').text
20+
expect(description).to include('We could not extract entries')
21+
expect(description).not_to include('different parser')
22+
end
23+
24+
it 'uses updated item title and content text' do
25+
expect(xml_doc.at_xpath('//item/title').text).to eq('Preview unavailable for this source')
26+
expect(xml_doc.at_xpath('//item/description').text).to include('What you can do:')
27+
end
28+
29+
it 'does not mention hidden strategy controls in item text' do
30+
expect(xml_doc.at_xpath('//item/description').text).not_to include('browserless strategy')
31+
end
32+
end
33+
end

0 commit comments

Comments
 (0)