From 6886265f1b3d798be3369a9afd409fb8e47d5575 Mon Sep 17 00:00:00 2001 From: Clementina Lam Choi <109236912+mlkgrnt@users.noreply.github.com> Date: Sat, 6 Jun 2026 23:16:17 +0800 Subject: [PATCH] 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 * 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 tags into category field Co-Authored-By: Claude Opus 4.7 * fix: sort imports to satisfy oxlint Co-Authored-By: Claude Opus 4.7 * fix: update example paths to match lemonde namespace Co-Authored-By: Claude Opus 4.7 * chore: trigger CI to re-test with updated route paths Co-Authored-By: Claude Opus 4.7 * fix: remove non-existent element fallback from image URL Co-Authored-By: Claude Opus 4.7 --------- Co-authored-by: mlkgrnt Co-authored-by: Claude Opus 4.7 --- lib/routes/lemonde/en.ts | 217 ++++++++++++++++++++++++++++++++ lib/routes/lemonde/index.ts | 138 ++++++++++++++++++++ lib/routes/lemonde/namespace.ts | 7 ++ 3 files changed, 362 insertions(+) create mode 100644 lib/routes/lemonde/en.ts create mode 100644 lib/routes/lemonde/index.ts create mode 100644 lib/routes/lemonde/namespace.ts diff --git a/lib/routes/lemonde/en.ts b/lib/routes/lemonde/en.ts new file mode 100644 index 0000000000..54c5ec1c27 --- /dev/null +++ b/lib/routes/lemonde/en.ts @@ -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 = { + '': `${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 += ``; + } + if (imageCredit) { + description += `

${imageCredit}

`; + } + 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, + }; +} diff --git a/lib/routes/lemonde/index.ts b/lib/routes/lemonde/index.ts new file mode 100644 index 0000000000..99aaaa814a --- /dev/null +++ b/lib/routes/lemonde/index.ts @@ -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 = { + '': `${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 += ``; + } + if (imageCredit) { + description += `

${imageCredit}

`; + } + 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, + }; +} diff --git a/lib/routes/lemonde/namespace.ts b/lib/routes/lemonde/namespace.ts new file mode 100644 index 0000000000..6ff477b0be --- /dev/null +++ b/lib/routes/lemonde/namespace.ts @@ -0,0 +1,7 @@ +import type { Namespace } from '@/types'; + +export const namespace: Namespace = { + name: 'Le Monde', + url: 'lemonde.fr', + lang: 'fr', +};