Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,16 @@ gh release create vYY.MM.N --verify-tag --generate-notes --title vYY.MM.N

## Configuration

| Setting | Description | Type | Default |
| ------------------- | -------------------------------------------------------------------- | -------- | -------------------------------------- |
| `bypass_cors` | Route the RSS feed URL through the CORS proxy | optional | `true` |
| `cache_interval` | How often (in seconds) to refresh the feed | required | `1800` |
| `display_errors` | Show errors on screen for debugging purposes | optional | `false` |
| `override_locale` | Override the locale used for date formatting (e.g. `en`, `fr`, `de`) | optional | `en` |
| `override_timezone` | Override the timezone for date display (e.g. `Europe/London`) | optional | system timezone |
| `rss_title` | Source label displayed on each card | required | `BBC News` |
| `rss_url` | URL of the RSS feed to display | required | `http://feeds.bbci.co.uk/news/rss.xml` |
| Setting | Description | Type | Default |
| ------------------- | ------------------------------------------------------------------------------------------------------------ | -------- | -------------------------------------- |
| `bypass_cors` | Route the RSS feed URL through the CORS proxy | optional | `true` |
| `cache_interval` | How often (in seconds) to refresh the feed | required | `1800` |
| `display_errors` | Show errors on screen for debugging purposes | optional | `false` |
| `mode` | Layout: `headlines` (lead story with image, plus a list) or `list` (headlines only, for maximum readability) | optional | `headlines` |
| `override_locale` | Override the locale used for date formatting (e.g. `en`, `fr`, `de`) | optional | `en` |
| `override_timezone` | Override the timezone for date display (e.g. `Europe/London`) | optional | system timezone |
| `rss_title` | Source name shown in the header | required | `BBC News` |
| `rss_url` | URL of the RSS feed to display | required | `http://feeds.bbci.co.uk/news/rss.xml` |

## Example Feeds

Expand Down
41 changes: 30 additions & 11 deletions src/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,26 +104,45 @@ describe('getTitleSizeClass', () => {
})

describe('resizeImageUrl', () => {
const BBC =
const ACE =
'https://ichef.bbci.co.uk/ace/standard/240/cpsprodpb/50a9/live/ece0.jpg'
const bbcAt = (width: number) =>
`https://ichef.bbci.co.uk/ace/standard/${width}/cpsprodpb/50a9/live/ece0.jpg`

test('rounds up to a width the BBC image CDN serves', () => {
const aceAt = (size: number) =>
`https://ichef.bbci.co.uk/ace/standard/${size}/cpsprodpb/50a9/live/ece0.jpg`
const NEWS = 'https://ichef.bbci.co.uk/news/240/cpsprodpb/50a9/live/ece0.jpg'
const newsAt = (size: number) =>
`https://ichef.bbci.co.uk/news/${size}/cpsprodpb/50a9/live/ece0.jpg`
const IC = 'https://ichef.bbci.co.uk/images/ic/240x135/p0l7jnbt.jpg'
const icAt = (size: string) =>
`https://ichef.bbci.co.uk/images/ic/${size}/p0l7jnbt.jpg`

test('rounds /ace/ URLs up to a width the CDN serves', () => {
// 1080 itself is a 404 on the CDN
expect(resizeImageUrl(BBC, 1080)).toBe(bbcAt(1280))
expect(resizeImageUrl(BBC, 480)).toBe(bbcAt(480))
expect(resizeImageUrl(BBC, 270)).toBe(bbcAt(480))
expect(resizeImageUrl(BBC, 120)).toBe(bbcAt(240))
expect(resizeImageUrl(ACE, 1080)).toBe(aceAt(1280))
expect(resizeImageUrl(ACE, 480)).toBe(aceAt(480))
expect(resizeImageUrl(ACE, 270)).toBe(aceAt(480))
expect(resizeImageUrl(ACE, 120)).toBe(aceAt(240))
expect(resizeImageUrl(ACE, 4096)).toBe(aceAt(2048))
})

test('skips the widths /news/ URLs do not serve', () => {
// 1280 is a 404 on this shape
expect(resizeImageUrl(NEWS, 1080)).toBe(newsAt(1536))
expect(resizeImageUrl(NEWS, 270)).toBe(newsAt(480))
expect(resizeImageUrl(NEWS, 4096)).toBe(newsAt(2048))
})

test('caps at the largest served width', () => {
expect(resizeImageUrl(BBC, 4096)).toBe(bbcAt(2048))
test('resizes /images/ic/ URLs by their width-by-height segment', () => {
expect(resizeImageUrl(IC, 1080)).toBe(icAt('1280x720'))
expect(resizeImageUrl(IC, 270)).toBe(icAt('480x270'))
// 2048x1152 is a 403, so this shape tops out lower
expect(resizeImageUrl(IC, 4096)).toBe(icAt('1920x1080'))
})

test('leaves other feeds untouched', () => {
const nyt = 'https://static01.nyt.com/images/2026/11/03/240/photo.jpg'
expect(resizeImageUrl(nyt, 1080)).toBe(nyt)
const otherBbc = 'https://ichef.bbci.co.uk/onesport/cps/240/photo.jpg'
expect(resizeImageUrl(otherBbc, 1080)).toBe(otherBbc)
expect(resizeImageUrl('', 1080)).toBe('')
})
})
45 changes: 36 additions & 9 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,44 @@ export function getTitleSizeClass(title: string): string {
}

// BBC feeds only advertise 240px thumbnails, which blur on a screen-sized lead
// story. Their image CDN serves other widths from the same path: any width up
// to 999, but above that only a fixed set (1080, for one, is a 404). Round up
// to a width it is known to serve. Other feeds' URLs are left untouched.
const BBC_IMAGE_WIDTH = /^(https?:\/\/ichef\.bbci\.co\.uk\/[^/]+\/[^/]+\/)\d+\//
const BBC_WIDTHS = [240, 480, 640, 976, 1280, 1536, 2048]
// story. Their image CDN serves the same image at other sizes, but each URL
// shape accepts its own set (/ace/ 404s on 1080, /news/ on 1280, /images/ic/
// 403s on 2048x1152), so round up to a size each shape is known to serve.
// Other feeds' URLs are left untouched.
const BBC_IMAGE_SHAPES: { pattern: RegExp; sizes: string[] }[] = [
{
// https://ichef.bbci.co.uk/ace/standard/240/cpsprodpb/...
pattern: /^(https?:\/\/ichef\.bbci\.co\.uk\/ace\/[^/]+\/)\d+\//,
sizes: ['240', '480', '640', '976', '1280', '1536', '2048'],
},
{
// https://ichef.bbci.co.uk/news/240/cpsprodpb/...
pattern: /^(https?:\/\/ichef\.bbci\.co\.uk\/news\/)\d+\//,
sizes: ['240', '480', '640', '976', '1536', '2048'],
},
{
// https://ichef.bbci.co.uk/images/ic/240x135/p0l7jnbt.jpg (video items)
pattern: /^(https?:\/\/ichef\.bbci\.co\.uk\/images\/ic\/)\d+x\d+\//,
sizes: [
'240x135',
'480x270',
'640x360',
'976x549',
'1280x720',
'1536x864',
'1920x1080',
],
},
]

export function resizeImageUrl(url: string, width: number): string {
const snapped =
BBC_WIDTHS.find((candidate) => candidate >= width) ??
BBC_WIDTHS[BBC_WIDTHS.length - 1]
return url.replace(BBC_IMAGE_WIDTH, `$1${snapped}/`)
const shape = BBC_IMAGE_SHAPES.find(({ pattern }) => pattern.test(url))
if (!shape) return url
const { pattern, sizes } = shape
const size =
sizes.find((candidate) => parseInt(candidate, 10) >= width) ??
sizes[sizes.length - 1]
return url.replace(pattern, `$1${size}/`)
}

export function stripHtml(html: string): string {
Expand Down
Loading