feat(route): add Le Monde & Le Monde in English (#22175)

* feat(route): add Le Monde route

* feat(route): add Le Monde in English route

Add English version of Le Monde RSS route with 40+ categories
covering World, Europe, France, Economy, Culture, Sports, etc.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(route/le-monde): remove unnecessary cache.tryGet wrapper

cache.tryGet was wrapping a synchronous callback with no async work,
making it a no-op. The items are already fully parsed from the RSS
feed, so direct assignment is sufficient.

* fix: address review comments for Le Monde route

- Rename namespace from le-monde/le-monde-en to lemonde
- Remove unnecessary .first() calls on RSS elements
- Remove default ofetch responseType option
- Use static cheerio import instead of dynamic import
- Extract <category> tags into category field

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: sort imports to satisfy oxlint

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: update example paths to match lemonde namespace

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: trigger CI to re-test with updated route paths

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: remove non-existent <content> element fallback from image URL

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: mlkgrnt <mlkgrnt@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Clementina Lam Choi
2026-06-06 23:16:17 +08:00
committed by GitHub
co-authored by Claude Opus 4.7 mlkgrnt
parent cae03809c0
commit 6886265f1b
3 changed files with 362 additions and 0 deletions
+217
View File
@@ -0,0 +1,217 @@
import { load } from 'cheerio';
import type { Route } from '@/types';
import { ViewType } from '@/types';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
const ROOT_URL = 'https://www.lemonde.fr/en';
const feedMap: Record<string, string> = {
'': `${ROOT_URL}/rss/une.xml`,
// World
international: `${ROOT_URL}/international/rss_full.xml`,
americas: `${ROOT_URL}/americas/rss_full.xml`,
'united-kingdom': `${ROOT_URL}/united-kingdom/rss_full.xml`,
'united-states': `${ROOT_URL}/united-states/rss_full.xml`,
africa: `${ROOT_URL}/le-monde-africa/rss_full.xml`,
'asia-pacific': `${ROOT_URL}/asia-and-pacific/rss_full.xml`,
'middle-east': `${ROOT_URL}/middle-east/rss_full.xml`,
// Europe
europe: `${ROOT_URL}/europe/rss_full.xml`,
// France
politics: `${ROOT_URL}/politics/rss_full.xml`,
'police-and-justice': `${ROOT_URL}/police-and-justice/rss_full.xml`,
education: `${ROOT_URL}/education/rss_full.xml`,
'french-delights': `${ROOT_URL}/french-delights/rss_full.xml`,
// Environment
environment: `${ROOT_URL}/environment/rss_full.xml`,
// Economy
economy: `${ROOT_URL}/economy/rss_full.xml`,
'world-economy': `${ROOT_URL}/world-economy/rss_full.xml`,
'french-economy': `${ROOT_URL}/french-economy/rss_full.xml`,
// M Magazine
'm-le-mag': `${ROOT_URL}/m-le-mag/rss_full.xml`,
lifestyle: `${ROOT_URL}/lifestyle/rss_full.xml`,
fashion: `${ROOT_URL}/fashion/rss_full.xml`,
food: `${ROOT_URL}/food/rss_full.xml`,
travel: `${ROOT_URL}/travel/rss_full.xml`,
// Culture
culture: `${ROOT_URL}/culture/rss_full.xml`,
arts: `${ROOT_URL}/arts/rss_full.xml`,
cinema: `${ROOT_URL}/cinema/rss_full.xml`,
music: `${ROOT_URL}/music/rss_full.xml`,
books: `${ROOT_URL}/books/rss_full.xml`,
// Global Issues
'global-issues': `${ROOT_URL}/global-issues/rss_full.xml`,
// Pixels
pixels: `${ROOT_URL}/pixels/rss_full.xml`,
'artificial-intelligence': `${ROOT_URL}/artificial-intelligence/rss_full.xml`,
'social-media': `${ROOT_URL}/social-media/rss_full.xml`,
// Sports
sports: `${ROOT_URL}/sports/rss_full.xml`,
football: `${ROOT_URL}/football/rss_full.xml`,
rugby: `${ROOT_URL}/rugby/rss_full.xml`,
tennis: `${ROOT_URL}/tennis/rss_full.xml`,
cycling: `${ROOT_URL}/cycling/rss_full.xml`,
basketball: `${ROOT_URL}/basketball/rss_full.xml`,
// Science & Health
science: `${ROOT_URL}/science/rss_full.xml`,
health: `${ROOT_URL}/health/rss_full.xml`,
intimacy: `${ROOT_URL}/intimacy/rss_full.xml`,
// Others
'les-decodeurs': `${ROOT_URL}/les-decodeurs/rss_full.xml`,
'our-times': `${ROOT_URL}/our-times/rss_full.xml`,
obituaries: `${ROOT_URL}/obituaries/rss_full.xml`,
religion: `${ROOT_URL}/religion/rss_full.xml`,
// Opinion
opinion: `${ROOT_URL}/opinion/rss_full.xml`,
editorials: `${ROOT_URL}/editorials/rss_full.xml`,
columns: `${ROOT_URL}/columns/rss_full.xml`,
'op-eds': `${ROOT_URL}/op-eds/rss_full.xml`,
};
export const route: Route = {
path: '/en/:category?',
categories: ['traditional-media'],
example: '/lemonde/en',
view: ViewType.Articles,
parameters: {
category: {
description: 'Category slug, see table below. Defaults to homepage.',
default: '',
},
},
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: [
{
source: ['lemonde.fr/en/:category'],
target: '/en/:category',
},
],
name: 'News (English)',
maintainers: ['mlkgrnt'],
handler,
description: `| Category | Description |
| ----------------------- | ------------------------- |
| (empty) | Homepage / Top stories |
| international | World – International |
| americas | World – Americas |
| united-kingdom | World – United Kingdom |
| united-states | World – United States |
| africa | World – Africa |
| asia-pacific | World – Asia Pacific |
| middle-east | World – Middle East |
| europe | Europe |
| politics | France – French Politics |
| police-and-justice | France – French Justice |
| education | France – French Education |
| french-delights | France – French Delights |
| environment | Environment |
| economy | Economy |
| world-economy | Economy – World Economy |
| french-economy | Economy – French Economy |
| m-le-mag | M Magazine |
| lifestyle | M Magazine – Lifestyle |
| fashion | M Magazine – Fashion |
| food | M Magazine – Food |
| travel | M Magazine – Travel |
| culture | Culture |
| arts | Culture – Art |
| cinema | Culture – Cinema |
| music | Culture – Music |
| books | Culture – Books |
| global-issues | Global Issues |
| pixels | Pixels |
| artificial-intelligence | Pixels – AI |
| social-media | Pixels – Social Media |
| sports | Sports |
| football | Sports – Football |
| rugby | Sports – Rugby |
| tennis | Sports – Tennis |
| cycling | Sports – Cycling |
| basketball | Sports – Basketball |
| science | Science |
| health | Health |
| intimacy | Intimacy |
| les-decodeurs | Les Décodeurs |
| our-times | Our Times |
| obituaries | Obituaries |
| religion | Religion |
| opinion | Opinion |
| editorials | Opinion – Editorials |
| columns | Opinion – Columns |
| op-eds | Opinion – Op-Eds |`,
};
async function handler(ctx) {
const category = ctx.req.param('category') ?? '';
const feedUrl = feedMap[category];
if (!feedUrl) {
throw new Error(`Invalid category: ${category}`);
}
const xml = await ofetch(feedUrl);
const $ = load(xml, { xml: true });
const channel = $('channel');
const feedTitle = channel.children('title').text();
const feedLink = channel.children('link').text() || ROOT_URL;
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20;
const items = $('item')
.toArray()
.slice(0, limit)
.map((el) => {
const item = $(el);
const link = item.children('link').text() || item.children('guid').text();
const title = item.children('title').text();
const pubDateText = item.children('pubDate').text();
const summary = item.children('description').text();
const categoryList = item
.children('category')
.toArray()
.map((c) => $(c).text())
.filter(Boolean);
const imageUrl = item.find(String.raw`media\:content`).attr('url');
const imageCredit = item.find(String.raw`media\:credit`).text();
let description = '';
if (imageUrl) {
description += `<img src="${imageUrl}" />`;
}
if (imageCredit) {
description += `<p><em>${imageCredit}</em></p>`;
}
if (summary) {
description += summary;
}
return {
title,
link,
pubDate: pubDateText ? parseDate(pubDateText) : undefined,
description,
category: categoryList,
guid: link,
};
})
.filter((item) => item.link);
return {
title: feedTitle || 'Le Monde in English',
link: feedLink,
description: feedTitle || 'Le Monde in English',
language: 'en',
item: items,
};
}
+138
View File
@@ -0,0 +1,138 @@
import { load } from 'cheerio';
import type { Route } from '@/types';
import { ViewType } from '@/types';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
const ROOT_URL = 'https://www.lemonde.fr';
const feedMap: Record<string, string> = {
'': `${ROOT_URL}/rss/une.xml`,
international: `${ROOT_URL}/international/rss_full.xml`,
politique: `${ROOT_URL}/politique/rss_full.xml`,
economie: `${ROOT_URL}/economie/rss_full.xml`,
societe: `${ROOT_URL}/societe/rss_full.xml`,
culture: `${ROOT_URL}/culture/rss_full.xml`,
sport: `${ROOT_URL}/sport/rss_full.xml`,
planete: `${ROOT_URL}/planete/rss_full.xml`,
pixels: `${ROOT_URL}/pixels/rss_full.xml`,
sciences: `${ROOT_URL}/sciences/rss_full.xml`,
idees: `${ROOT_URL}/idees/rss_full.xml`,
sante: `${ROOT_URL}/sante/rss_full.xml`,
em: `${ROOT_URL}/em/rss_full.xml`,
'en-continu': `${ROOT_URL}/en-continu/rss_full.xml`,
decodeurs: `${ROOT_URL}/decodeurs/rss_full.xml`,
};
export const route: Route = {
path: '/:category?',
categories: ['traditional-media'],
example: '/lemonde',
view: ViewType.Articles,
parameters: {
category: {
description: 'Category slug, see table below. Defaults to homepage.',
default: '',
},
},
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: [
{
source: ['lemonde.fr/:category'],
target: '/:category',
},
],
name: 'News',
maintainers: ['mlkgrnt'],
handler,
description: `| Category | Description |
| ------------- | ---------------------- |
| (empty) | Homepage / Top stories |
| international | International |
| politique | Politics |
| economie | Economy |
| societe | Society |
| culture | Culture |
| sport | Sports |
| planete | Environment |
| pixels | Tech / Digital |
| sciences | Sciences |
| idees | Opinions |
| sante | Health |
| em | M le mag |
| en-continu | Live / Breaking |
| decodeurs | Fact-checking |`,
};
async function handler(ctx) {
const category = ctx.req.param('category') ?? '';
const feedUrl = feedMap[category];
if (!feedUrl) {
throw new Error(`Invalid category: ${category}`);
}
const xml = await ofetch(feedUrl);
const $ = load(xml, { xml: true });
const channel = $('channel');
const feedTitle = channel.children('title').text();
const feedLink = channel.children('link').text() || ROOT_URL;
const limit = ctx.req.query('limit') ? Number.parseInt(ctx.req.query('limit'), 10) : 20;
const items = $('item')
.toArray()
.slice(0, limit)
.map((el) => {
const item = $(el);
const link = item.children('link').text() || item.children('guid').text();
const title = item.children('title').text();
const pubDateText = item.children('pubDate').text();
const summary = item.children('description').text();
const categoryList = item
.children('category')
.toArray()
.map((c) => $(c).text())
.filter(Boolean);
const imageUrl = item.find(String.raw`media\:content`).attr('url');
const imageCredit = item.find(String.raw`media\:credit`).text();
let description = '';
if (imageUrl) {
description += `<img src="${imageUrl}" />`;
}
if (imageCredit) {
description += `<p><em>${imageCredit}</em></p>`;
}
if (summary) {
description += summary;
}
return {
title,
link,
pubDate: pubDateText ? parseDate(pubDateText) : undefined,
description,
category: categoryList,
guid: link,
};
})
.filter((item) => item.link);
return {
title: feedTitle || 'Le Monde',
link: feedLink,
description: feedTitle || 'Le Monde',
language: 'fr',
item: items,
};
}
+7
View File
@@ -0,0 +1,7 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Le Monde',
url: 'lemonde.fr',
lang: 'fr',
};